Skip to content

Commit

Permalink
StatsHouse UI: update transport
Browse files Browse the repository at this point in the history
  • Loading branch information
vauweb committed Dec 12, 2024
1 parent 53e4692 commit 36ab583
Show file tree
Hide file tree
Showing 19 changed files with 576 additions and 109 deletions.
5 changes: 2 additions & 3 deletions statshouse-ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
// import { Admin } from './admin/Admin';
// import { ViewPage } from './view/ViewPage';
import { currentAccessInfo } from './common/access';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { QueryClientProvider } from '@tanstack/react-query';
import { queryClient } from './common/queryClient';
// import { DashboardListView } from './view/DashboardListView';
// import { SettingsPage } from './view/Settings/SettingsPage';
// import { GroupPage } from './view/Settings/GroupPage';
Expand All @@ -29,8 +30,6 @@ const View2Page = React.lazy(() => import('./view2/ViewPage'));
const Core = React.lazy(() => import('./view2/Core'));
const yAxisSize = 54; // must be synced with .u-legend padding-left

const queryClient = new QueryClient();

function App() {
const ai = currentAccessInfo();
return (
Expand Down
87 changes: 68 additions & 19 deletions statshouse-ui/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,47 @@ export type ApiFetchResponse<R> = {
response?: R;
status: number;
ok: boolean;
error?: Error;
error?: ExtendedError;
};

const abortControllers: Map<unknown, AbortController> = new Map();
export class ApiAbortController extends AbortController {
static abortControllers: Map<unknown, AbortController> = new Map();

timer?: NodeJS.Timeout;
keyRequest?: unknown;

constructor(keyRequest?: unknown, timeout: number = 300000) {
super();
this.keyRequest = keyRequest;
this.signal.addEventListener('abort', this.remove);
const abortFn = () => {
this.abort();
};

this.timer = timeout ? setTimeout(abortFn, timeout) : undefined;

let outerSignal: AbortSignal | undefined;
if (keyRequest instanceof AbortController) {
outerSignal = keyRequest.signal;
}

if (keyRequest instanceof AbortSignal) {
outerSignal = keyRequest;
}

outerSignal?.addEventListener('abort', abortFn);
if (keyRequest) {
ApiAbortController.abortControllers.get(keyRequest)?.abort();
ApiAbortController.abortControllers.set(keyRequest, this);
}
}

remove() {
this.timer && clearTimeout(this.timer);
this.signal?.removeEventListener('abort', this.remove);
ApiAbortController.abortControllers.delete(this.keyRequest);
}
}

export async function apiFetch<R = unknown, G = ApiFetchParam, P = ApiFetchParam>({
url,
Expand All @@ -46,14 +83,8 @@ export async function apiFetch<R = unknown, G = ApiFetchParam, P = ApiFetchParam
body = JSON.stringify(post);
}
keyRequest ??= fullUrl;
let controller: AbortController;
if (keyRequest instanceof AbortController) {
controller = keyRequest;
} else {
controller = new AbortController();
}
abortControllers.get(keyRequest)?.abort();
abortControllers.set(keyRequest, controller);

const controller = new ApiAbortController(keyRequest);

try {
const resp = await fetch(fullUrl, {
Expand All @@ -65,31 +96,29 @@ export async function apiFetch<R = unknown, G = ApiFetchParam, P = ApiFetchParam
result.status = resp.status;
if (resp.headers.get('Content-Type') !== 'application/json') {
const text = await resp.text();
result.error = new Error(`${resp.status}: ${text.substring(0, 255)}`);
result.error = new ExtendedError(`${resp.status}: ${text.substring(0, 255)}`, resp.status);
} else {
const json = (await resp.json()) as R;
if (resp.ok) {
result.ok = true;
result.response = json;
} else if (hasOwn(json, 'error') && typeof json.error === 'string') {
result.error = new Error(json.error);
result.error = new ExtendedError(json.error, resp.status);
}
}
} catch (error) {
result.status = -1;
result.status = ExtendedError.ERROR_STATUS_UNKNOWN;
if (error instanceof Error) {
if (error.name !== 'AbortError') {
result.error = error;
result.error = new ExtendedError(error);
} else {
result.status = -2;
result.error = new ExtendedError(error, ExtendedError.ERROR_STATUS_ABORT);
result.status = ExtendedError.ERROR_STATUS_ABORT;
}
} else {
result.error = new Error('Fetch error');
result.error = new ExtendedError('Fetch error');
}
}
if (result.status !== -2) {
abortControllers.delete(keyRequest);
}
return result;
}

Expand All @@ -110,3 +139,23 @@ export async function apiFirstFetch<R = unknown, G = ApiFetchParam, P = ApiFetch
firstFetchMap.delete(params.keyRequest);
return result;
}

export class ExtendedError extends Error {
static readonly ERROR_STATUS_UNKNOWN = -1;
static readonly ERROR_STATUS_ABORT = -2;

status: number = ExtendedError.ERROR_STATUS_UNKNOWN;

constructor(message: string | Error | unknown, status: number = ExtendedError.ERROR_STATUS_UNKNOWN) {
super(message instanceof Error ? '' : typeof message === 'string' ? message : 'unknown error');
if (message instanceof Error) {
Object.assign(this, message);
} else if (typeof message !== 'string') {
this.cause = message;
}
this.status = status;
}
toString() {
return `${this.status}: ${this.message.substring(0, 255)}`;
}
}
82 changes: 82 additions & 0 deletions statshouse-ui/src/api/badges.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright 2023 V Kontakte LLC
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

import { apiFetch, ExtendedError } from './api';
import { ApiQueryGet } from './query';
import { PlotParams, QueryParams } from '../url2';
import { useLiveModeStore } from '../store2/liveModeStore';
import { useMemo } from 'react';
import { getLoadPlotUrlParams } from '../store2/plotDataStore/loadPlotData';
import { useQuery, UseQueryResult } from '@tanstack/react-query';

export const ApiBadgesEndpoint = '/api/badges';

/**
* Response endpoint api/query
*/
export type ApiBadges = {
data: BadgesResponse;
};

export type BadgesResponse = {
sampling_factor_src: number;
sampling_factor_agg: number;
receive_errors: number;
receive_warnings: number;
mapping_errors: number;
};

export async function apiBadgesFetch(params: ApiQueryGet, keyRequest?: unknown) {
return await apiFetch<ApiBadges>({ url: ApiBadgesEndpoint, get: params, keyRequest });
}

export function useApiBadges<T = ApiBadges>(
plot: PlotParams,
params: QueryParams,
select?: (response?: ApiBadges) => T,
enabled: boolean = true
): UseQueryResult<T, ExtendedError> {
const interval = useLiveModeStore(({ interval, status }) => (status ? interval : undefined));

const priority = useMemo(() => {
if (plot?.id === params.tabNum) {
return 1;
}
return enabled ? 2 : 3;
}, [enabled, params.tabNum, plot?.id]);

const keyParams = useMemo(() => {
if (!plot?.id) {
return null;
}
return getLoadPlotUrlParams(plot?.id, params);
}, [params, plot?.id]);

const fetchParams = useMemo(() => {
if (!plot?.id) {
return null;
}
return getLoadPlotUrlParams(plot?.id, params, interval, false, priority);
}, [interval, params, plot?.id, priority]);

return useQuery({
enabled,
select,
queryKey: [ApiBadgesEndpoint, keyParams],
queryFn: async ({ signal }) => {
if (!fetchParams) {
throw new ExtendedError('no request params');
}
const { response, error, status } = await apiBadgesFetch(fetchParams, signal);
if (error) {
throw error;
}

return response;
},
placeholderData: (previousData, previousQuery) => previousData,
});
}
56 changes: 54 additions & 2 deletions statshouse-ui/src/api/metric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

import { GET_PARAMS, MetricMetaKind, MetricMetaTagRawKind } from './enum';
import { apiFetch } from './api';
import { apiFetch, ApiFetchResponse, ExtendedError } from './api';
import { UndefinedInitialDataOptions, useQuery, UseQueryResult } from '@tanstack/react-query';
import { queryClient } from '../common/queryClient';

const ApiMetricEndpoint = '/api/metric';
export const ApiMetricEndpoint = '/api/metric';
/**
* Response endpoint api/metric
*/
Expand Down Expand Up @@ -67,3 +69,53 @@ export type MetricMetaTag = {
export async function apiMetricFetch(params: ApiMetricGet, keyRequest?: unknown) {
return await apiFetch<ApiMetric>({ url: ApiMetricEndpoint, get: params, keyRequest });
}

export function getMetricOptions<T = ApiMetric>(
metricName: string,
enabled: boolean = true
): UndefinedInitialDataOptions<ApiMetric | undefined, ExtendedError, T, [string, string]> {
return {
enabled,
queryKey: [ApiMetricEndpoint, metricName],
queryFn: async ({ signal }) => {
if (!metricName) {
throw new ExtendedError('no metric name');
}
const { response, error, status } = await apiMetricFetch({ [GET_PARAMS.metricName]: metricName }, signal);
if (error) {
throw error;
}
return response;
},
};
}

export async function apiMetric<T = ApiMetric>(metricName: string): Promise<ApiFetchResponse<T>> {
const result: ApiFetchResponse<T> = { ok: false, status: 0 };

try {
const { queryKey, queryFn } = getMetricOptions(metricName);
result.response = await queryClient.fetchQuery({ queryKey, queryFn });
result.ok = true;
} catch (error) {
result.status = ExtendedError.ERROR_STATUS_UNKNOWN;
if (error instanceof ExtendedError) {
result.error = error;
result.status = error.status;
} else {
result.error = new ExtendedError(error);
}
}
return result;
}

export function useApiMetric<T = ApiMetric>(
metricName: string,
select?: (response?: ApiMetric) => T,
enabled: boolean = true
): UseQueryResult<T, ExtendedError> {
return useQuery({
...getMetricOptions(metricName, enabled),
select,
});
}
Loading

0 comments on commit 36ab583

Please sign in to comment.