Skip to content

Commit

Permalink
StatsHouse UI: update transport dashboard
Browse files Browse the repository at this point in the history
  • Loading branch information
vauweb committed Dec 15, 2024
1 parent 3c5b361 commit a33cd23
Show file tree
Hide file tree
Showing 19 changed files with 336 additions and 180 deletions.
51 changes: 11 additions & 40 deletions statshouse-ui/src/admin/AdminDashControl/AdminDashControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
// 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 React, { useCallback, useEffect, useId, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useStatsHouse } from 'store2';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { apiDashboardListFetch } from '../../api/dashboardsList';
import { apiDashboardFetch, DashboardInfo } from '../../api/dashboard';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { selectApiDashboardList, useApiDashboardList } from '../../api/dashboardsList';
import { apiDashboardSave, DashboardInfo, useApiDashboard } from '../../api/dashboard';
import cn from 'classnames';
import { useIntersectionObserver } from '../../hooks';
import { isObject, SearchFabric, toNumber } from '../../common/helpers';
Expand All @@ -17,7 +17,6 @@ import { arrToObj, getDefaultParams, toTreeObj, treeParamsObjectValueSymbol } fr
import { GET_PARAMS } from '../../api/enum';
import { loadDashboard } from '../../store2/urlStore/loadDashboard';
import { Link, useSearchParams } from 'react-router-dom';
import { saveDashboard } from '../../store2/urlStore';
import { Button } from '../../components/UI';
import { ReactComponent as SVGFloppy } from 'bootstrap-icons/icons/floppy.svg';
import { produce } from 'immer';
Expand All @@ -26,32 +25,6 @@ import { fmtInputDateTime } from '../../view/utils2';
const versionsList = [0, 1, 2, 3];
const actualVersion = 3;

function useDashboardList() {
return useQuery({
queryKey: ['/api/dashboards-list'],
queryFn: async () => {
const { error, response } = await apiDashboardListFetch();
if (error) {
throw error;
}
return response?.data.dashboards ?? [];
},
});
}
function useDashboardInfo(id: number, enabled: boolean = true) {
return useQuery({
queryKey: ['/api/dashboard', id],
enabled,
retry: false,
queryFn: async () => {
const { error, response } = await apiDashboardFetch({ id: id.toString() });
if (error) {
throw error;
}
return response?.data;
},
});
}
function useUpdateDashboard() {
const queryClient = useQueryClient();
return useMutation({
Expand All @@ -64,7 +37,7 @@ function useUpdateDashboard() {
if (errorLoad) {
throw errorLoad;
}
const { response, error } = await saveDashboard(saveParams);
const { response, error } = await apiDashboardSave(saveParams);

if (error) {
throw error;
Expand All @@ -84,7 +57,7 @@ export function AdminDashControl() {
const searchVersionValue = useMemo(() => toNumber(searchParams.get('v')), [searchParams]);
const searchOldValue = useMemo(() => !!searchParams.get('o'), [searchParams]);
const [autoUpdate, setAutoUpdate] = useState(false);
const dashboardList = useDashboardList();
const dashboardList = useApiDashboardList(selectApiDashboardList);
const [dashVersions, setDashVersions] = useState<Record<string, number>>({});

const filterList = useMemo(() => {
Expand Down Expand Up @@ -233,10 +206,11 @@ function DashItem({ item, setDashVersions, autoUpdate }: DashItemProps) {
}
}, [visible]);
const queryClient = useQueryClient();
const dashboardInfo = useDashboardInfo(item.id, visibleBool);
const dashboardInfo = useApiDashboard(item.id.toString(), undefined, undefined, visibleBool);
const updateDashboard = useUpdateDashboard();
const dashVersionKey = dashboardInfo.data?.dashboard.version;
const dashVersion = useMemo(() => getVersion(dashboardInfo.data), [dashboardInfo]);
const data = dashboardInfo.data?.data;
const dashVersionKey = data?.dashboard.version;
const dashVersion = useMemo(() => getVersion(data), [data]);
const needUpdate = dashVersion > -1 && dashVersion < actualVersion;

useEffect(() => {
Expand Down Expand Up @@ -277,10 +251,7 @@ function DashItem({ item, setDashVersions, autoUpdate }: DashItemProps) {
<Link to={`/view?id=${item.id}`} target="_blank">
{dashVersionKey}
</Link>{' '}
<span>
{dashboardInfo.data?.dashboard.update_time &&
fmtInputDateTime(new Date(dashboardInfo.data?.dashboard.update_time * 1000))}
</span>
<span>{data?.dashboard.update_time && fmtInputDateTime(new Date(data?.dashboard.update_time * 1000))}</span>
</div>
<div>{item.name}</div>
<div className="text-body-tertiary">{item.description}</div>
Expand Down
151 changes: 150 additions & 1 deletion statshouse-ui/src/api/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,20 @@
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

import { GET_PARAMS } from './enum';
import { apiFetch } from './api';
import { apiFetch, ApiFetchResponse, ExtendedError } from './api';
import {
CancelledError,
QueryClient,
UndefinedInitialDataOptions,
useMutation,
UseMutationOptions,
useQuery,
useQueryClient,
} from '@tanstack/react-query';
import { queryClient } from '../common/queryClient';
import { QueryParams, urlEncode } from '../url2';
import { dashboardMigrateSaveToOld } from '../store2/urlStore/dashboardMigrate';
import { toNumber } from '../common/helpers';

const ApiDashboardEndpoint = '/api/dashboard';

Expand Down Expand Up @@ -61,3 +74,139 @@ export async function apiDashboardSaveFetch(params: ApiDashboardPost, keyRequest
keyRequest,
});
}

export function getDashboardOptions<T = ApiDashboard>(
dashboardId: string,
dashboardVersion?: string
): UndefinedInitialDataOptions<ApiDashboard, ExtendedError, T, [string, ApiDashboardGet]> {
const fetchParams: ApiDashboardGet = { [GET_PARAMS.dashboardID]: dashboardId };
if (dashboardVersion != null) {
fetchParams[GET_PARAMS.dashboardApiVersion] = dashboardVersion;
}
return {
queryKey: [ApiDashboardEndpoint, fetchParams],
queryFn: async ({ signal }) => {
const { response, error } = await apiDashboardFetch(fetchParams, signal);
if (error) {
throw error;
}
if (!response) {
throw new ExtendedError('empty response');
}
return response;
},
placeholderData: (previousData, previousQuery) => previousData,
};
}

export async function apiDashboard(dashboardId: string, dashboardVersion?: string) {
const result: ApiFetchResponse<ApiDashboard> = { ok: false, status: 0 };
try {
result.response = await queryClient.fetchQuery(getDashboardOptions<ApiDashboard>(dashboardId, dashboardVersion));
result.ok = true;
} catch (error) {
result.status = ExtendedError.ERROR_STATUS_UNKNOWN;
if (error instanceof ExtendedError) {
result.error = error;
result.status = error.status;
} else if (error instanceof CancelledError) {
result.error = new ExtendedError(error, ExtendedError.ERROR_STATUS_ABORT);
result.status = ExtendedError.ERROR_STATUS_ABORT;
} else {
result.error = new ExtendedError(error);
}
}
return result;
}

export function useApiDashboard<T = ApiDashboard>(
dashboardId: string,
dashboardVersion?: string,
select?: (response?: ApiDashboard) => T,
enabled: boolean = true
) {
const options = getDashboardOptions<ApiDashboard>(dashboardId, dashboardVersion);
return useQuery({ ...options, select, enabled });
}
export function getDashboardSaveFetchParams(params: QueryParams, remove?: boolean): DashboardInfo {
const searchParams = urlEncode(params);
const oldDashboardParams = dashboardMigrateSaveToOld(params);
oldDashboardParams.dashboard.data.searchParams = searchParams;
const dashboardParams: DashboardInfo = {
dashboard: {
name: params.dashboardName,
description: params.dashboardDescription,
version: params.dashboardVersion ?? 0,
dashboard_id: toNumber(params.dashboardId) ?? undefined,
data: {
...oldDashboardParams.dashboard.data,
searchParams,
},
},
};
if (remove) {
dashboardParams.delete_mark = true;
}
return dashboardParams;
}

export function getDashboardSaveOptions(
queryClient: QueryClient,
remove?: boolean
): UseMutationOptions<ApiDashboard, Error, QueryParams, unknown> {
return {
retry: false,
mutationFn: async (params: QueryParams) => {
const dashboardParams: DashboardInfo = getDashboardSaveFetchParams(params, remove);
const { response, error } = await apiDashboardSaveFetch(dashboardParams);
if (error) {
throw error;
}
if (!response) {
throw new ExtendedError('empty response');
}
return response;
},
onSuccess: (data, params) => {
if (data.data.dashboard.dashboard_id) {
const fetchParams: ApiDashboardGet = { [GET_PARAMS.dashboardID]: data.data.dashboard.dashboard_id.toString() };
if (data.data.dashboard.version != null) {
fetchParams[GET_PARAMS.dashboardApiVersion] = data.data.dashboard.version.toString();
}
queryClient.setQueryData([ApiDashboardEndpoint, fetchParams], data);
}
},
};
}

export async function apiDashboardSave(params: QueryParams, remove?: boolean): Promise<ApiFetchResponse<ApiDashboard>> {
const options = getDashboardSaveOptions(queryClient, remove);
const result: ApiFetchResponse<ApiDashboard> = { ok: false, status: 0 };
try {
result.response = await options.mutationFn?.(params);
if (result.response) {
result.ok = true;
options.onSuccess?.(result.response, params, undefined);
} else {
result.error = new ExtendedError('empty response');
result.status = ExtendedError.ERROR_STATUS_UNKNOWN;
}
} catch (error) {
result.status = ExtendedError.ERROR_STATUS_UNKNOWN;
if (error instanceof ExtendedError) {
result.error = error;
result.status = error.status;
} else if (error instanceof CancelledError) {
result.error = new ExtendedError(error, ExtendedError.ERROR_STATUS_ABORT);
result.status = ExtendedError.ERROR_STATUS_ABORT;
} else {
result.error = new ExtendedError(error);
}
}
return result;
}

export function useApiDashboardSave(remove?: boolean) {
const queryClient = useQueryClient();
return useMutation(getDashboardSaveOptions(queryClient, remove));
}
57 changes: 56 additions & 1 deletion statshouse-ui/src/api/dashboardsList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
// 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 } from './api';
import { apiFetch, ApiFetchResponse, ExtendedError } from './api';
import { CancelledError, UndefinedInitialDataOptions, useQuery } from '@tanstack/react-query';
import { queryClient } from '../common/queryClient';

const ApiDashboardListEndpoint = '/api/dashboards-list';

Expand All @@ -28,3 +30,56 @@ export type DashboardShortInfo = {
export async function apiDashboardListFetch(keyRequest?: unknown) {
return await apiFetch<ApiDashboardList>({ url: ApiDashboardListEndpoint, keyRequest });
}

export function getDashboardListOptions<T = ApiDashboardList>(): UndefinedInitialDataOptions<
ApiDashboardList,
ExtendedError,
T,
[string]
> {
return {
queryKey: [ApiDashboardListEndpoint],
queryFn: async ({ signal }) => {
const { response, error } = await apiDashboardListFetch(signal);
if (error) {
throw error;
}
if (!response) {
throw new ExtendedError('empty response');
}
return response;
},
placeholderData: (previousData, previousQuery) => previousData,
};
}

export async function apiDashboardList(): Promise<ApiFetchResponse<ApiDashboardList>> {
const result: ApiFetchResponse<ApiDashboardList> = { ok: false, status: 0 };
try {
result.response = await queryClient.fetchQuery(getDashboardListOptions<ApiDashboardList>());
result.ok = true;
} catch (error) {
result.status = ExtendedError.ERROR_STATUS_UNKNOWN;
if (error instanceof ExtendedError) {
result.error = error;
result.status = error.status;
} else if (error instanceof CancelledError) {
result.error = new ExtendedError(error, ExtendedError.ERROR_STATUS_ABORT);
result.status = ExtendedError.ERROR_STATUS_ABORT;
} else {
result.error = new ExtendedError(error);
}
}
return result;
}

export function useApiDashboardList<T = ApiDashboardList>(select?: (response?: ApiDashboardList) => T) {
const options = getDashboardListOptions();
return useQuery({
...options,
select,
});
}
export function selectApiDashboardList(response?: ApiDashboardList) {
return response?.data.dashboards ?? [];
}
22 changes: 22 additions & 0 deletions statshouse-ui/src/hooks/useDebounceValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright 2022 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 { useEffect, useState } from 'react';

export function useDebounceValue<T>(value: T, delay: number = 200) {
const [debounceValue, setDebounceValue] = useState<T>(value);

useEffect(() => {
const timout = setTimeout(() => {
setDebounceValue(() => value);
}, delay);
return () => {
clearTimeout(timout);
};
}, [delay, value]);

return debounceValue;
}
7 changes: 4 additions & 3 deletions statshouse-ui/src/store/dashboardList/dashboardListStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

import { useErrorStore } from '../errors';
import { apiDashboardListFetch, DashboardShortInfo } from '../../api/dashboardsList';
import { apiDashboardList, DashboardShortInfo } from '../../api/dashboardsList';
import { createStore } from '../createStore';
import { ExtendedError } from '../../api/api';

export type DashboardListStore = {
list: DashboardShortInfo[];
Expand All @@ -27,13 +28,13 @@ export const useDashboardListStore = createStore<DashboardListStore>((setState)
setState((state) => {
state.loading = true;
});
const { response, error } = await apiDashboardListFetch('dashboardListState');
const { response, error } = await apiDashboardList();
if (response) {
setState((state) => {
state.list = response.data.dashboards ?? [];
});
}
if (error) {
if (error && error.status !== ExtendedError.ERROR_STATUS_ABORT) {
errorRemove = useErrorStore.getState().addError(error);
}
setState((state) => {
Expand Down
Loading

0 comments on commit a33cd23

Please sign in to comment.