diff --git a/statshouse-ui/src/admin/api/saveMetric.ts b/statshouse-ui/src/admin/api/saveMetric.ts index 3992f5c71..5b0548ffe 100644 --- a/statshouse-ui/src/admin/api/saveMetric.ts +++ b/statshouse-ui/src/admin/api/saveMetric.ts @@ -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 { IBackendKind, IBackendMetric, IMetric } from '../models/metric'; +import { isNotNil } from '@/common/helpers'; +import { IBackendKind, IBackendMetric, IKind, IMetric, ITag } from '../models/metric'; +import { freeKeyPrefix } from '@/url2'; export function saveMetric(metric: IMetric) { const body: IBackendMetric = { @@ -77,3 +79,55 @@ export function resetMetricFlood(metricName: string) { } }); } + +export const fetchMetric = async (url: string) => { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch: ${url}`); + } + return response.json(); +}; + +export const fetchAndProcessMetric = async (url: string) => { + const { + data: { metric }, + } = await fetchMetric(url); + + const tags_draft: ITag[] = Object.entries(metric.tags_draft ?? {}) + .map(([, t]) => t as ITag) + .filter(isNotNil); + tags_draft.sort((a, b) => (b.name < a.name ? 1 : b.name === a.name ? 0 : -1)); + + return { + id: metric.metric_id === undefined ? 0 : metric.metric_id, + name: metric.name, + description: metric.description, + kind: (metric.kind.endsWith('_p') ? metric.kind.replace('_p', '') : metric.kind) as IKind, + stringTopName: metric.string_top_name === undefined ? '' : metric.string_top_name, + stringTopDescription: metric.string_top_description === undefined ? '' : metric.string_top_description, + weight: metric.weight === undefined ? 1 : metric.weight, + resolution: metric.resolution === undefined ? 1 : metric.resolution, + visible: metric.visible === undefined ? false : metric.visible, + withPercentiles: metric.kind.endsWith('_p'), + tags: metric.tags.map((tag: ITag, index: number) => ({ + name: tag.name === undefined || tag.name === `key${index}` ? '' : tag.name, + alias: tag.description === undefined ? '' : tag.description, + customMapping: tag.value_comments + ? Object.entries(tag.value_comments).map(([from, to]) => ({ + from, + to, + })) + : [], + isRaw: tag.raw, + raw_kind: tag.raw_kind, + })), + tags_draft, + tagsSize: metric.tags.length, + pre_key_tag_id: metric.pre_key_tag_id && freeKeyPrefix(metric.pre_key_tag_id), + pre_key_from: metric.pre_key_from, + metric_type: metric.metric_type, + version: metric.version, + group_id: metric.group_id, + fair_key_tag_ids: metric.fair_key_tag_ids, + }; +}; diff --git a/statshouse-ui/src/admin/pages/FormPage.tsx b/statshouse-ui/src/admin/pages/FormPage.tsx index 841a58fbc..c01f3b7fa 100644 --- a/statshouse-ui/src/admin/pages/FormPage.tsx +++ b/statshouse-ui/src/admin/pages/FormPage.tsx @@ -4,29 +4,32 @@ // 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 * as React from 'react'; -import { useEffect, useMemo, useState } from 'react'; -import { Link, useNavigate, useParams } from 'react-router-dom'; -import { IBackendMetric, IKind, IMetric, ITag, ITagAlias } from '../models/metric'; +import { Dispatch, useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import { Link, useNavigate, useParams, useSearchParams } from 'react-router-dom'; +import { IBackendMetric, IKind, IMetric, ITagAlias } from '../models/metric'; import { MetricFormValuesContext, MetricFormValuesStorage } from '../storages/MetricFormValues'; import { ReactComponent as SVGTrash } from 'bootstrap-icons/icons/trash.svg'; -import { resetMetricFlood, saveMetric } from '../api/saveMetric'; import { IActions } from '../storages/MetricFormValues/reducer'; import { useStore } from '@/store'; import { RawValueKind } from '@/view/api'; -import { freeKeyPrefix } from '@/url/queryParams'; import { METRIC_TYPE, METRIC_TYPE_DESCRIPTION, MetricType } from '@/api/enum'; import { maxTagsSize } from '@/common/settings'; import { Button } from '@/components/UI'; import { ReactComponent as SVGPlusLg } from 'bootstrap-icons/icons/plus-lg.svg'; import { ReactComponent as SVGDashLg } from 'bootstrap-icons/icons/dash-lg.svg'; -import { isNotNil, toNumber } from '@/common/helpers'; +import { toNumber } from '@/common/helpers'; import { dequal } from 'dequal/lite'; import { produce } from 'immer'; import { TagDraft } from './TagDraft'; import { formatInputDate } from '@/view/utils2'; import { Select } from '@/components/Select'; +import { fetchAndProcessMetric, resetMetricFlood, saveMetric } from '../api/saveMetric'; +import { StickyTop } from '@/components2/StickyTop'; +import { queryClient } from '@/common/queryClient'; +import { API_HISTORY } from '@/api/history'; +import { HistoryList } from '@/components2/HistoryList'; + const { clearMetricsMeta } = useStore.getState(); const METRIC_TYPE_KEYS: MetricType[] = Object.values(METRIC_TYPE) as MetricType[]; @@ -34,77 +37,114 @@ const METRIC_TYPE_KEYS: MetricType[] = Object.values(METRIC_TYPE) as MetricType[ export function FormPage(props: { yAxisSize: number; adminMode: boolean }) { const { yAxisSize, adminMode } = props; const { metricName } = useParams(); - const [initMetric, setInitMetric] = React.useState | null>(null); - React.useEffect(() => { - fetch(`/api/metric?s=${metricName}`) - .then<{ data: { metric: IBackendMetric } }>((res) => res.json()) - .then(({ data: { metric } }) => { - const tags_draft: ITag[] = Object.entries(metric.tags_draft ?? {}) - .map(([, t]) => t) - .filter(isNotNil); - tags_draft.sort((a, b) => (b.name < a.name ? 1 : b.name === a.name ? 0 : -1)); + + const [searchParams] = useSearchParams(); + const historicalMetricVersion = useMemo(() => searchParams.get('mv'), [searchParams]); + + const [initMetric, setInitMetric] = useState | null>(null); + const [isShowHistory, setIsShowHistory] = useState(false); + + const isHistoricalMetric = useMemo( + () => !!initMetric?.version && !!historicalMetricVersion && initMetric.version !== Number(historicalMetricVersion), + [initMetric?.version, historicalMetricVersion] + ); + + const loadMetric = useCallback(async () => { + try { + if (initMetric?.version && initMetric?.id && historicalMetricVersion) { + const currentMetric = await fetchAndProcessMetric(`/api/metric?s=${metricName}`); + const historicalMetricData = await fetchAndProcessMetric( + `/api/metric?id=${initMetric.id}&ver=${historicalMetricVersion}` + ); + setInitMetric({ - id: metric.metric_id === undefined ? 0 : metric.metric_id, - name: metric.name, - description: metric.description, - kind: (metric.kind.endsWith('_p') ? metric.kind.replace('_p', '') : metric.kind) as IKind, - stringTopName: metric.string_top_name === undefined ? '' : metric.string_top_name, - stringTopDescription: metric.string_top_description === undefined ? '' : metric.string_top_description, - weight: metric.weight === undefined ? 1 : metric.weight, - resolution: metric.resolution === undefined ? 1 : metric.resolution, - visible: metric.visible === undefined ? false : metric.visible, - withPercentiles: metric.kind.endsWith('_p'), - tags: metric.tags.map((tag: ITag, index) => ({ - name: tag.name === undefined || tag.name === `key${index}` ? '' : tag.name, // now API sends undefined for canonical names, but this can change in the future, so we keep the code - alias: tag.description === undefined ? '' : tag.description, - customMapping: tag.value_comments - ? Object.entries(tag.value_comments).map(([from, to]) => ({ - from, - to, - })) - : [], - isRaw: tag.raw, - raw_kind: tag.raw_kind, - })), - tags_draft, - tagsSize: metric.tags.length, - pre_key_tag_id: metric.pre_key_tag_id && freeKeyPrefix(metric.pre_key_tag_id), - pre_key_from: metric.pre_key_from, - metric_type: metric.metric_type, - version: metric.version, - group_id: metric.group_id, - fair_key_tag_ids: metric.fair_key_tag_ids, + ...historicalMetricData, + version: currentMetric.version || historicalMetricData.version, }); - }); - }, [metricName]); + } else { + const metricData = await fetchAndProcessMetric(`/api/metric?s=${metricName}`); + setInitMetric(metricData); + } + } catch (_) {} + }, [historicalMetricVersion, initMetric?.id, initMetric?.version, metricName]); + + useEffect(() => { + if (metricName) { + loadMetric(); + } + }, [metricName, loadMetric]); // update document title - React.useEffect(() => { + useEffect(() => { document.title = `${metricName + ': edit'} — StatsHouse`; }, [metricName]); + const handleShowHistory = () => { + setIsShowHistory(true); + }; + + const handleShowEdit = () => { + setIsShowHistory(false); + }; + + const onVersionClick = useCallback(() => { + handleShowEdit?.(); + }, []); + + const mainPath = useMemo(() => `/admin/edit/${metricName}?mv=`, [metricName]); + return (
-
- {metricName} - <> - : edit - - view - - -
+ +
+
+
+ {metricName} + + : edit + + + history + + + view + +
+
+ + {isHistoricalMetric && ( +
+ Historical version +
+ )} +
+
+ {metricName && !initMetric ? (
Loading...
+ ) : isShowHistory && initMetric?.id ? ( + ) : ( )} @@ -119,10 +159,10 @@ const kindConfig = [ { label: 'Mixed', value: 'mixed' }, ]; -export function EditForm(props: { isReadonly: boolean; adminMode: boolean }) { - const { isReadonly, adminMode } = props; - const { values, dispatch } = React.useContext(MetricFormValuesContext); - const { onSubmit, isRunning, error, success } = useSubmit(values, dispatch); +export function EditForm(props: { isReadonly: boolean; adminMode: boolean; isHistoricalMetric: boolean }) { + const { isReadonly, adminMode, isHistoricalMetric } = props; + const { values, dispatch } = useContext(MetricFormValuesContext); + const { onSubmit, isRunning, error, success } = useSubmit(values, dispatch, isHistoricalMetric); const { onSubmitFlood, isRunningFlood, errorFlood, successFlood } = useSubmitResetFlood(values.name); const preKeyFromString = useMemo( () => (values.pre_key_from ? formatInputDate(values.pre_key_from) : ''), @@ -829,15 +869,15 @@ function AliasField(props: { ); } -function useSubmit(values: IMetric, dispatch: React.Dispatch) { - const [isRunning, setRunning] = React.useState(false); - const [error, setError] = React.useState(null); - const [success, setSuccess] = React.useState(null); +function useSubmit(values: IMetric, dispatch: Dispatch, isHistoricalMetric: boolean) { + const [isRunning, setRunning] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(null); const { metricName } = useParams(); const navigate = useNavigate(); - const onSubmit = React.useCallback(() => { + const onSubmit = () => { setError(null); setSuccess(null); setRunning(true); @@ -850,7 +890,10 @@ function useSubmit(values: IMetric, dispatch: React.Dispatch) { .then<{ data: { metric: IBackendMetric } }>((res) => res) .then((r) => { dispatch({ version: r.data.metric.version }); - if (metricName !== r.data.metric.name) { + + if (metricName !== r.data.metric.name || isHistoricalMetric) { + const queryId = values.id.toString(); + queryClient.refetchQueries({ queryKey: [API_HISTORY, queryId] }); navigate(`/admin/edit/${r.data.metric.name}`); } }) @@ -859,7 +902,7 @@ function useSubmit(values: IMetric, dispatch: React.Dispatch) { setRunning(false); clearMetricsMeta(values.name); }); - }, [dispatch, metricName, navigate, values]); + }; return { isRunning, @@ -870,11 +913,11 @@ function useSubmit(values: IMetric, dispatch: React.Dispatch) { } function useSubmitResetFlood(metricName: string) { - const [isRunningFlood, setRunningFlood] = React.useState(false); - const [errorFlood, setErrorFlood] = React.useState(null); - const [successFlood, setSuccessFlood] = React.useState(null); + const [isRunningFlood, setRunningFlood] = useState(false); + const [errorFlood, setErrorFlood] = useState(null); + const [successFlood, setSuccessFlood] = useState(null); - const onSubmitFlood = React.useCallback(() => { + const onSubmitFlood = useCallback(() => { setErrorFlood(null); setSuccessFlood(null); setRunningFlood(true); diff --git a/statshouse-ui/src/admin/storages/MetricFormValues/index.tsx b/statshouse-ui/src/admin/storages/MetricFormValues/index.tsx index 48fd19a42..5b7b0e618 100644 --- a/statshouse-ui/src/admin/storages/MetricFormValues/index.tsx +++ b/statshouse-ui/src/admin/storages/MetricFormValues/index.tsx @@ -4,18 +4,18 @@ // 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 * as React from 'react'; import { IMetric } from '../../models/metric'; import { IActions, initialValues, reducer } from './reducer'; +import React, { Dispatch, FC, ReactNode, useEffect, useMemo, useReducer } from 'react'; interface IMetricFormValuesProps { initialMetric?: Partial; - children?: React.ReactNode; + children?: ReactNode; } interface IMetricFormValuesContext { values: IMetric; - dispatch: React.Dispatch; + dispatch: Dispatch; } // eslint-disable-next-line react-refresh/only-export-components @@ -24,11 +24,16 @@ export const MetricFormValuesContext = React.createContext {}, }); -export const MetricFormValuesStorage: React.FC = (props) => { +export const MetricFormValuesStorage: FC = (props) => { const { initialMetric, children } = props; - // eslint-disable-next-line react-hooks/exhaustive-deps - const initValues = React.useMemo(() => ({ ...initialValues, ...initialMetric }), []); - const [values, dispatch] = React.useReducer(reducer, initValues); + + const initValues = useMemo(() => ({ ...initialValues, ...initialMetric }), [initialMetric]); + + const [values, dispatch] = useReducer(reducer, initValues); + + useEffect(() => { + dispatch({ type: 'reset', newState: initValues }); + }, [initValues, initialMetric]); return {children}; }; diff --git a/statshouse-ui/src/admin/storages/MetricFormValues/reducer.ts b/statshouse-ui/src/admin/storages/MetricFormValues/reducer.ts index 36b6b5eab..f592f7565 100644 --- a/statshouse-ui/src/admin/storages/MetricFormValues/reducer.ts +++ b/statshouse-ui/src/admin/storages/MetricFormValues/reducer.ts @@ -35,13 +35,18 @@ export type IActions = | { type: 'preSortKey'; key: string } | { type: 'group_id'; key: string } | { type: 'move_draft'; pos: number; tag: Partial; tag_key: string } - | { type: 'fair_key_tag_ids'; value?: string[] | null }; + | { type: 'fair_key_tag_ids'; value?: string[] | null } + | { type: 'reset'; newState?: IMetric }; export function reducer(state: IMetric, data: IActions): IMetric { if (!('type' in data)) { return { ...state, ...data }; } + if (data.type === 'reset') { + return { ...initialValues, ...(data.newState || {}) }; + } + if (data.type === 'numTags') { const valueAsNumber = Math.min(Math.max(1, Number(data.num)), maxTagsSize); diff --git a/statshouse-ui/src/api/dashboard.ts b/statshouse-ui/src/api/dashboard.ts index 0e85be6b1..5b262e540 100644 --- a/statshouse-ui/src/api/dashboard.ts +++ b/statshouse-ui/src/api/dashboard.ts @@ -19,6 +19,7 @@ import { queryClient } from '@/common/queryClient'; import { type QueryParams, urlEncode } from '@/url2'; import { dashboardMigrateSaveToOld } from '@/store2/urlStore/dashboardMigrate'; import { toNumber } from '@/common/helpers'; +import { API_HISTORY } from './history'; const ApiDashboardEndpoint = '/api/dashboard'; @@ -57,6 +58,7 @@ export type DashboardMetaInfo = { name: string; description: string; version?: number; + current_version?: number; update_time?: number; deleted_time?: number; data: Record; @@ -83,16 +85,27 @@ export function getDashboardOptions( 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'); } + + if (dashboardVersion != null) { + const { response: resCurrent } = await apiDashboardFetch({ [GET_PARAMS.dashboardID]: dashboardId }); + + if (resCurrent) { + response.data.dashboard.current_version = resCurrent.data.dashboard.version; + } + } + return response; }, placeholderData: (previousData) => previousData, @@ -128,15 +141,18 @@ export function useApiDashboard( const options = getDashboardOptions(dashboardId, dashboardVersion); return useQuery({ ...options, select, enabled }); } -export function getDashboardSaveFetchParams(params: QueryParams, remove?: boolean): DashboardInfo { +export function getDashboardSaveFetchParams(params: QueryParams, remove?: boolean, copy?: boolean): DashboardInfo { const searchParams = urlEncode(params); + const oldDashboardParams = dashboardMigrateSaveToOld(params); oldDashboardParams.dashboard.data.searchParams = searchParams; + const version = params.dashboardCurrentVersion || params.dashboardVersion; + const dashboardParams: DashboardInfo = { dashboard: { name: params.dashboardName, description: params.dashboardDescription, - version: params.dashboardVersion ?? 0, + version: version ?? 0, dashboard_id: toNumber(params.dashboardId) ?? undefined, data: { ...oldDashboardParams.dashboard.data, @@ -147,17 +163,23 @@ export function getDashboardSaveFetchParams(params: QueryParams, remove?: boolea if (remove) { dashboardParams.delete_mark = true; } + if (copy) { + delete dashboardParams.dashboard.dashboard_id; + delete dashboardParams.dashboard.version; + } + return dashboardParams; } export function getDashboardSaveOptions( queryClient: QueryClient, - remove?: boolean + remove?: boolean, + copy?: boolean ): UseMutationOptions { return { retry: false, mutationFn: async (params: QueryParams) => { - const dashboardParams: DashboardInfo = getDashboardSaveFetchParams(params, remove); + const dashboardParams: DashboardInfo = getDashboardSaveFetchParams(params, remove, copy); const { response, error } = await apiDashboardSaveFetch(dashboardParams); if (error) { throw error; @@ -168,19 +190,28 @@ export function getDashboardSaveOptions( return response; }, onSuccess: (data) => { - if (data.data.dashboard.dashboard_id) { - const fetchParams: ApiDashboardGet = { [GET_PARAMS.dashboardID]: data.data.dashboard.dashboard_id.toString() }; + const dashboardId = data.data.dashboard.dashboard_id?.toString(); + if (dashboardId) { + const baseParams = { [GET_PARAMS.dashboardID]: dashboardId }; + const fetchParams: ApiDashboardGet = baseParams; if (data.data.dashboard.version != null) { fetchParams[GET_PARAMS.dashboardApiVersion] = data.data.dashboard.version.toString(); } queryClient.setQueryData([ApiDashboardEndpoint, fetchParams], data); + + queryClient.invalidateQueries({ queryKey: [ApiDashboardEndpoint, baseParams] }); + queryClient.refetchQueries({ queryKey: [API_HISTORY, dashboardId] }); } }, }; } -export async function apiDashboardSave(params: QueryParams, remove?: boolean): Promise> { - const options = getDashboardSaveOptions(queryClient, remove); +export async function apiDashboardSave( + params: QueryParams, + remove?: boolean, + copy?: boolean +): Promise> { + const options = getDashboardSaveOptions(queryClient, remove, copy); const result: ApiFetchResponse = { ok: false, status: 0 }; try { result.response = await options.mutationFn?.(params); diff --git a/statshouse-ui/src/api/history.ts b/statshouse-ui/src/api/history.ts new file mode 100644 index 000000000..949fd7f4c --- /dev/null +++ b/statshouse-ui/src/api/history.ts @@ -0,0 +1,52 @@ +// 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 { useQuery } from '@tanstack/react-query'; +import { apiFetch } from './api'; + +export const API_HISTORY = '/api/history'; + +export type ApiHistory = { + data: GetHistoryListResponse; +}; + +export type GetHistoryListResponse = { + events: HistoryShortInfo[]; +}; + +export type HistoryShortInfo = { + metadata: HistoryShortInfoMetadata; + version: number; + update_time?: number; +}; + +export type HistoryShortInfoMetadata = { + user_email: string; + user_name: string; + user_ref: string; +}; + +export async function apHistoryListFetch(id: string, keyRequest?: unknown) { + const url = `/api/history?id=${id}`; + + return await apiFetch({ url: url, keyRequest }); +} + +export function useHistoryList(id: string) { + return useQuery({ + enabled: !!id, + queryKey: [API_HISTORY, id], + queryFn: async () => { + const { response, error } = await apHistoryListFetch(id); + + if (error) { + throw error; + } + + return response?.data.events; + }, + }); +} diff --git a/statshouse-ui/src/components2/Dashboard/Dashboard.tsx b/statshouse-ui/src/components2/Dashboard/Dashboard.tsx index 2bfef47c8..52c665467 100644 --- a/statshouse-ui/src/components2/Dashboard/Dashboard.tsx +++ b/statshouse-ui/src/components2/Dashboard/Dashboard.tsx @@ -4,7 +4,7 @@ // 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 { memo, useCallback } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import { useStatsHouseShallow } from '@/store2'; import { DashboardName } from './DashboardName'; import { DashboardHeader } from './DashboardHeader'; @@ -19,6 +19,8 @@ import { useLinkPlot } from '@/hooks/useLinkPlot'; import { useGlobalLoader } from '@/store2/plotQueryStore'; import { useTvModeStore } from '@/store2/tvModeStore'; import { ErrorMessages } from '@/components/ErrorMessages'; +import { produce } from '~immer/dist/immer'; +import { HistoryList } from '../HistoryList'; export type DashboardProps = { className?: string; @@ -37,6 +39,8 @@ export const Dashboard = memo(function Dashboard({ className }: DashboardProps) setDashboardLayoutEdit, isDashboard, saveDashboard, + dashboardId, + setParams, } = useStatsHouseShallow( useCallback( ({ @@ -45,6 +49,7 @@ export const Dashboard = memo(function Dashboard({ className }: DashboardProps) dashboardLayoutEdit, setDashboardLayoutEdit, saveDashboard, + setParams, }) => ({ tabNum, isEmbed, @@ -55,22 +60,37 @@ export const Dashboard = memo(function Dashboard({ className }: DashboardProps) setDashboardLayoutEdit, isDashboard: dashboardId != null, saveDashboard, + dashboardId, + setParams, }), [] ) ); - const onSaveDashboard = useCallback(() => { - saveDashboard().then(() => { + const onSaveDashboard = async () => { + const dashResponse = await saveDashboard(); + if (dashResponse) { + setParams( + produce((params) => { + params.dashboardCurrentVersion = undefined; + }) + ); setDashboardLayoutEdit(false); - }); - }, [saveDashboard, setDashboardLayoutEdit]); + } + }; const dashboardLink = useLinkPlot('-1', true); const dashboardSettingLink = useLinkPlot('-2', true); + const dashboardHistoryLink = useLinkPlot('-3', true); const isPlot = +tabNum > -1; + const onVersionClick = useCallback(() => { + setDashboardLayoutEdit?.(false); + }, [setDashboardLayoutEdit]); + + const mainPath = useMemo(() => `/view?id=${dashboardId}&dv=`, [dashboardId]); + return (
{!!dashboardName && !isEmbed && !tvModeEnable && } @@ -88,6 +108,13 @@ export const Dashboard = memo(function Dashboard({ className }: DashboardProps) Setting +
  • + {dashboardId && ( + + History + + )} +
  • @@ -119,6 +146,9 @@ export const Dashboard = memo(function Dashboard({ className }: DashboardProps) )} {tabNum === '-2' && } + {tabNum === '-3' && dashboardId && ( + + )}
    ); }); diff --git a/statshouse-ui/src/components2/Dashboard/DashboardName.tsx b/statshouse-ui/src/components2/Dashboard/DashboardName.tsx index e5a30f206..a9858e575 100644 --- a/statshouse-ui/src/components2/Dashboard/DashboardName.tsx +++ b/statshouse-ui/src/components2/Dashboard/DashboardName.tsx @@ -4,51 +4,104 @@ // 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 { memo } from 'react'; -import { useWindowSize } from '@/hooks/useWindowSize'; -import cn from 'classnames'; +import { memo, useMemo, useState } from 'react'; import { Tooltip } from '@/components/UI'; import { DashboardNameTitle } from './DashboardNameTitle'; import { useStatsHouseShallow } from '@/store2'; import css from '../style.module.css'; import { MarkdownRender } from '@/components2/Plot/PlotView/MarkdownRender'; +import { produce } from 'immer'; +import { StickyTop } from '../StickyTop'; +import { HistoricalButton } from '../HistoricalButton/HistoricalButton'; export const DashboardName = memo(function DashboardName() { - const { dashboardName, dashboardDescription } = useStatsHouseShallow( - ({ params: { dashboardName, dashboardDescription } }) => ({ dashboardName, dashboardDescription }) + const { + dashboardName, + dashboardDescription, + saveParams, + saveDashboard, + setParams, + dashboardVersion, + dashboardCurrentVersion, + } = useStatsHouseShallow( + ({ + params: { dashboardName, dashboardDescription, dashboardVersion, dashboardCurrentVersion }, + saveParams, + saveDashboard, + setParams, + }) => ({ + dashboardName, + dashboardDescription, + saveParams, + saveDashboard, + setParams, + dashboardVersion, + dashboardCurrentVersion, + }) + ); + + const [dropdown, setDropdown] = useState(false); + + const isHistoricalDashboard = useMemo( + () => !!dashboardVersion && !!dashboardCurrentVersion && dashboardCurrentVersion !== dashboardVersion, + [dashboardVersion, dashboardCurrentVersion] ); - const scrollY = useWindowSize((s) => s.scrollY > 16); if (!dashboardName) { return null; } + + const onDashboardSave = async (copy?: boolean) => { + const dashResponse = await saveDashboard(copy); + if (dashResponse) { + setParams( + produce((params) => { + params.dashboardCurrentVersion = undefined; + }) + ); + setDropdown(false); + } + }; + + const isDashNamesEqual = dashboardName === saveParams.dashboardName; + return ( -
    - } - hover - horizontal="left" - > -
    - {dashboardName} - {!!dashboardDescription && ':'} -
    - {!!dashboardDescription && ( -
    - , - }} - unwrapDisallowed - > - {dashboardDescription} - + +
    + } + hover + horizontal="left" + > +
    + {dashboardName} + {!!dashboardDescription && ':'}
    + {!!dashboardDescription && ( +
    + , + }} + unwrapDisallowed + > + {dashboardDescription} + +
    + )} +
    + {isHistoricalDashboard && ( + )} - -
    +
    + ); }); diff --git a/statshouse-ui/src/components2/Dashboard/DashboardSettings/DashboardSettings.tsx b/statshouse-ui/src/components2/Dashboard/DashboardSettings/DashboardSettings.tsx index 333ffd124..ad3ccf985 100644 --- a/statshouse-ui/src/components2/Dashboard/DashboardSettings/DashboardSettings.tsx +++ b/statshouse-ui/src/components2/Dashboard/DashboardSettings/DashboardSettings.tsx @@ -1,3 +1,9 @@ +// Copyright 2024 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 { memo } from 'react'; import { DashboardInfo } from './DashboardInfo'; import { DashboardVariable } from './DashboardVariable'; @@ -9,12 +15,10 @@ export type DashboardSettingsProps = { export const DashboardSettings = memo(function DashboardSettings() { return (
    -
    -
    - -
    -
    {}
    +
    +
    +
    {}
    ); }); diff --git a/statshouse-ui/src/components2/Dashboard/DashboardSettings/index.ts b/statshouse-ui/src/components2/Dashboard/DashboardSettings/index.ts index 19d2b4b91..957728c68 100644 --- a/statshouse-ui/src/components2/Dashboard/DashboardSettings/index.ts +++ b/statshouse-ui/src/components2/Dashboard/DashboardSettings/index.ts @@ -1 +1,7 @@ +// Copyright 2024 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/. + export * from './DashboardSettings'; diff --git a/statshouse-ui/src/components2/Dashboard/DashboardVariablesBadge.tsx b/statshouse-ui/src/components2/Dashboard/DashboardVariablesBadge.tsx index a828734e4..14bd954dd 100644 --- a/statshouse-ui/src/components2/Dashboard/DashboardVariablesBadge.tsx +++ b/statshouse-ui/src/components2/Dashboard/DashboardVariablesBadge.tsx @@ -1,3 +1,9 @@ +// Copyright 2024 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 { formatTagValue } from '../../view/api'; import React from 'react'; import { MetricMetaTag } from '../../api/metric'; diff --git a/statshouse-ui/src/components2/Dashboard/DashboardVariablesBadgeByKey.tsx b/statshouse-ui/src/components2/Dashboard/DashboardVariablesBadgeByKey.tsx index 2b2a1f997..77284928b 100644 --- a/statshouse-ui/src/components2/Dashboard/DashboardVariablesBadgeByKey.tsx +++ b/statshouse-ui/src/components2/Dashboard/DashboardVariablesBadgeByKey.tsx @@ -1,3 +1,9 @@ +// Copyright 2024 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 { VariableKey } from '@/url2'; import { useStatsHouseShallow } from '@/store2'; import { useVariableListStore } from '@/store2/variableList'; diff --git a/statshouse-ui/src/components2/HistoricalButton/HistoricalButton.tsx b/statshouse-ui/src/components2/HistoricalButton/HistoricalButton.tsx new file mode 100644 index 000000000..b2a270278 --- /dev/null +++ b/statshouse-ui/src/components2/HistoricalButton/HistoricalButton.tsx @@ -0,0 +1,56 @@ +// Copyright 2024 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 { POPPER_HORIZONTAL, POPPER_VERTICAL, Tooltip } from '@/components/UI'; +import { memo, useRef } from 'react'; +import { SaveButton } from '../SaveButton'; +import { useOnClickOutside } from '@/hooks'; +import { ReactComponent as SVGCloudArrowUp } from 'bootstrap-icons/icons/cloud-arrow-up.svg'; + +interface HistoricalButtonProps { + isDashNamesEqual: boolean; + onDashboardSave: (arg?: boolean) => void; + dropdown: boolean; + setDropdown: (arg: boolean) => void; +} + +export const HistoricalButton = memo(function HistoricalButton({ + isDashNamesEqual, + onDashboardSave, + dropdown, + setDropdown, +}: HistoricalButtonProps) { + const refDropButton = useRef(null); + + useOnClickOutside(refDropButton, () => { + setDropdown(false); + }); + + const onShow = () => { + setDropdown(!dropdown); + }; + + return ( + } + > + + + Historical version + + + ); +}); diff --git a/statshouse-ui/src/components2/HistoricalButton/index.tsx b/statshouse-ui/src/components2/HistoricalButton/index.tsx new file mode 100644 index 000000000..b47a8d7ff --- /dev/null +++ b/statshouse-ui/src/components2/HistoricalButton/index.tsx @@ -0,0 +1,7 @@ +// Copyright 2024 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/. + +export * from './HistoricalButton'; diff --git a/statshouse-ui/src/components2/HistoryList/HistoryLink.tsx b/statshouse-ui/src/components2/HistoryList/HistoryLink.tsx new file mode 100644 index 000000000..7290ea243 --- /dev/null +++ b/statshouse-ui/src/components2/HistoryList/HistoryLink.tsx @@ -0,0 +1,33 @@ +import { memo } from 'react'; +import { Link } from 'react-router-dom'; + +import { ReactComponent as SVGBoxArrowUpRight } from 'bootstrap-icons/icons/box-arrow-up-right.svg'; +import { HistoryShortInfo } from '@/api/history'; + +export type IHistoryLink = { + mainPath: string; + event?: HistoryShortInfo; + timeChange?: string | 0 | undefined; + onVersionClick?: () => void; +}; + +export const HistoryLink = memo(function HistoryLink({ mainPath, event, timeChange, onVersionClick }: IHistoryLink) { + const path = `${mainPath}${event?.version}`; + + return ( + +
    +
  • + {`time: ${timeChange || 'unknown'}`} + {`name: ${event?.metadata?.user_email || 'unknown'}`} + {`version: ${event?.version || 'unknown'}`} +
  • + +
    + + ); +}); diff --git a/statshouse-ui/src/components2/HistoryList/HistoryList.tsx b/statshouse-ui/src/components2/HistoryList/HistoryList.tsx new file mode 100644 index 000000000..36b2bdc28 --- /dev/null +++ b/statshouse-ui/src/components2/HistoryList/HistoryList.tsx @@ -0,0 +1,43 @@ +import { memo } from 'react'; +import { HistorySpinner } from './HistorySpinner'; + +import { HistoryLink } from './HistoryLink'; +import { HistoryShortInfo, useHistoryList } from '@/api/history'; +import { fmtInputDateTime } from '@/view/utils2'; + +export type IHistoryList = { + id: string; + onVersionClick: () => void; + mainPath: string; +}; + +export const HistoryList = memo(function HistoryList({ id, onVersionClick, mainPath }: IHistoryList) { + const { data, isLoading, isError } = useHistoryList(id); + + if (isLoading) return ; + if (isError) return
    Error loading history data
    ; + + return ( +
    +
      + {data?.length ? ( + data.map((event: HistoryShortInfo, index: number) => { + const timeChange = event?.update_time && fmtInputDateTime(new Date(event.update_time * 1000)); + return ( +
      + +
      + ); + }) + ) : ( +
      no history
      + )} +
    +
    + ); +}); diff --git a/statshouse-ui/src/components2/HistoryList/HistorySpinner.tsx b/statshouse-ui/src/components2/HistoryList/HistorySpinner.tsx new file mode 100644 index 000000000..e6e8146e8 --- /dev/null +++ b/statshouse-ui/src/components2/HistoryList/HistorySpinner.tsx @@ -0,0 +1,9 @@ +import { memo } from 'react'; + +export const HistorySpinner = memo(function HistorySpinner() { + return ( +
    + +
    + ); +}); diff --git a/statshouse-ui/src/components2/HistoryList/index.ts b/statshouse-ui/src/components2/HistoryList/index.ts new file mode 100644 index 000000000..a9199d4f8 --- /dev/null +++ b/statshouse-ui/src/components2/HistoryList/index.ts @@ -0,0 +1,7 @@ +// 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/. + +export * from './HistoryList'; diff --git a/statshouse-ui/src/components2/LeftMenu/LeftMenu.tsx b/statshouse-ui/src/components2/LeftMenu/LeftMenu.tsx index 1095b0487..c60a0b622 100644 --- a/statshouse-ui/src/components2/LeftMenu/LeftMenu.tsx +++ b/statshouse-ui/src/components2/LeftMenu/LeftMenu.tsx @@ -73,7 +73,7 @@ export function LeftMenu({ className }: LeftMenuProps) { ); const isView = location.pathname.indexOf('view') > -1; const isSettings = location.pathname.indexOf('settings') > -1; - const isDash = tabNum === '-1' || tabNum === '-2'; + const isDash = tabNum === '-1' || tabNum === '-2' || tabNum === '-3'; const onSetTheme = useCallback((event: React.MouseEvent) => { const value = toTheme(event.currentTarget.getAttribute('data-value')); if (value) { diff --git a/statshouse-ui/src/components2/SaveButton/SaveButton.tsx b/statshouse-ui/src/components2/SaveButton/SaveButton.tsx new file mode 100644 index 000000000..4277d81d9 --- /dev/null +++ b/statshouse-ui/src/components2/SaveButton/SaveButton.tsx @@ -0,0 +1,31 @@ +// Copyright 2024 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 { Button, Tooltip } from '@/components/UI'; +import { memo } from 'react'; + +interface SaveButtonProps { + isDashNamesEqual: boolean; + onDashboardSave: (arg?: boolean) => void; +} + +export const SaveButton = memo(function SaveButton({ isDashNamesEqual, onDashboardSave }: SaveButtonProps) { + return ( +
    + + + + + + + +
    + ); +}); diff --git a/statshouse-ui/src/components2/SaveButton/index.tsx b/statshouse-ui/src/components2/SaveButton/index.tsx new file mode 100644 index 000000000..d1f189466 --- /dev/null +++ b/statshouse-ui/src/components2/SaveButton/index.tsx @@ -0,0 +1,7 @@ +// Copyright 2024 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/. + +export { SaveButton } from './SaveButton'; diff --git a/statshouse-ui/src/components2/StickyTop/StickyTop.tsx b/statshouse-ui/src/components2/StickyTop/StickyTop.tsx new file mode 100644 index 000000000..a28cde637 --- /dev/null +++ b/statshouse-ui/src/components2/StickyTop/StickyTop.tsx @@ -0,0 +1,24 @@ +// Copyright 2024 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 { useWindowSize } from '@/hooks/useWindowSize'; +import cn from 'classnames'; +import { memo } from 'react'; +import { HTMLAttributes, ReactNode } from '~@types/react'; + +interface StickyTopProps extends HTMLAttributes { + children: ReactNode; +} + +export const StickyTop = memo(function StickyTop({ children, ...props }: StickyTopProps) { + const scrollY = useWindowSize((s) => s.scrollY > 16); + + return ( +
    + {children} +
    + ); +}); diff --git a/statshouse-ui/src/components2/StickyTop/index.ts b/statshouse-ui/src/components2/StickyTop/index.ts new file mode 100644 index 000000000..8e5c557ed --- /dev/null +++ b/statshouse-ui/src/components2/StickyTop/index.ts @@ -0,0 +1,7 @@ +// Copyright 2024 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/. + +export * from './StickyTop'; diff --git a/statshouse-ui/src/store2/helpers/updateTitle.ts b/statshouse-ui/src/store2/helpers/updateTitle.ts index 6179be7d3..1aeec382e 100644 --- a/statshouse-ui/src/store2/helpers/updateTitle.ts +++ b/statshouse-ui/src/store2/helpers/updateTitle.ts @@ -10,6 +10,9 @@ export function updateTitle({ params: { tabNum, plots, dashboardName }, plotsDat case '-2': document.title = `Dashboard setting — ${pageTitle}`; break; + case '-3': + document.title = `Dashboard history — ${pageTitle}`; + break; default: { const fullName = getMetricFullName(plots[tabNum], plotsData[tabNum]); document.title = `${fullName} — ${pageTitle}`; diff --git a/statshouse-ui/src/store2/urlStore/loadDashboard.ts b/statshouse-ui/src/store2/urlStore/loadDashboard.ts index d0d4d882b..cebf4772c 100644 --- a/statshouse-ui/src/store2/urlStore/loadDashboard.ts +++ b/statshouse-ui/src/store2/urlStore/loadDashboard.ts @@ -10,6 +10,7 @@ import { debug } from '@/common/debug'; import { apiDashboard } from '@/api/dashboard'; import { readDataDashboard } from './readDataDashboard'; import { ExtendedError } from '../../api/api'; +import { useErrorStore } from '@/store/errors'; export function getDashboardId(urlTree: TreeParamsObject) { return urlTree[GET_PARAMS.dashboardID]?.[treeParamsObjectValueSymbol]?.[0]; @@ -34,6 +35,9 @@ export async function loadDashboard( } if (response) { dashboardParams = readDataDashboard(response.data, defaultParams); + if (!dashboardParams.dashboardId || !dashboardParams.dashboardVersion) { + useErrorStore.getState().addError(new Error('The dashboard does not exist. Displayed the default dashboards')); + } } } return { params: dashboardParams }; diff --git a/statshouse-ui/src/store2/urlStore/readDataDashboard.ts b/statshouse-ui/src/store2/urlStore/readDataDashboard.ts index 5cf7c64ce..2a3f75bff 100644 --- a/statshouse-ui/src/store2/urlStore/readDataDashboard.ts +++ b/statshouse-ui/src/store2/urlStore/readDataDashboard.ts @@ -9,5 +9,6 @@ export function readDataDashboard(data: DashboardInfo, defaultParams = getDefaul dashboardName: data.dashboard.name, dashboardDescription: data.dashboard.description, dashboardVersion: data.dashboard.version, + dashboardCurrentVersion: data.dashboard.current_version, }); } diff --git a/statshouse-ui/src/store2/urlStore/urlStore.ts b/statshouse-ui/src/store2/urlStore/urlStore.ts index d73389d9c..0dee73cb5 100644 --- a/statshouse-ui/src/store2/urlStore/urlStore.ts +++ b/statshouse-ui/src/store2/urlStore/urlStore.ts @@ -45,7 +45,7 @@ import { filterVariableByPlot } from '../helpers/filterVariableByPlot'; import { fixMessageTrouble } from '@/url/fixMessageTrouble'; import { isNotNil } from '../../common/helpers'; import { getUrlObject } from '../../common/getUrlObject'; -import { apiDashboardSave } from '../../api/dashboard'; +import { ApiDashboard, apiDashboardSave } from '../../api/dashboard'; import { ExtendedError } from '../../api/api'; export type UrlStore = { @@ -74,7 +74,7 @@ export type UrlStore = { setDashboardGroup(groupKey: GroupKey, next: ProduceUpdate): void; setNextDashboardSchemePlot(nextScheme: { groupKey: GroupKey; plots: PlotKey[] }[]): void; autoSearchVariable(): Promise>; - saveDashboard(): Promise; + saveDashboard(copy?: boolean): Promise; removeDashboard(): Promise; removeVariableLinkByPlotKey(plotKey: PlotKey): void; }; @@ -99,7 +99,7 @@ export const urlStore: StoreSlice = (setState, getSta if (res.error != null && res.error.status !== ExtendedError.ERROR_STATUS_ABORT) { useErrorStore.getState().addError(res.error); } - if (s.params.tabNum === '-2') { + if (s.params.tabNum === '-2' || s.params.tabNum === '-3') { s.dashboardLayoutEdit = true; } }); @@ -231,7 +231,7 @@ export const urlStore: StoreSlice = (setState, getSta }); if (!status) { setUrlStore((s) => { - if (s.params.tabNum === '-2') { + if (s.params.tabNum === '-2' || s.params.tabNum === '-3') { s.params.tabNum = '-1'; } }); @@ -312,18 +312,21 @@ export const urlStore: StoreSlice = (setState, getSta async autoSearchVariable() { return getAutoSearchVariable(getState); }, - async saveDashboard() { - const { response, error } = await apiDashboardSave(getState().params); + async saveDashboard(copy?: boolean) { + const { response, error } = await apiDashboardSave(getState().params, false, copy); if (error && error.status !== ExtendedError.ERROR_STATUS_ABORT) { useErrorStore.getState().addError(error); } if (response) { const saveParams = readDataDashboard(response.data); + setUrlStore((store) => { store.saveParams = saveParams; store.params.dashboardVersion = saveParams.dashboardVersion; store.params.dashboardId = saveParams.dashboardId; }); + + return response; } }, async removeDashboard() { diff --git a/statshouse-ui/src/url2/queryParams.ts b/statshouse-ui/src/url2/queryParams.ts index 7ef4c9c3c..fce7eabef 100644 --- a/statshouse-ui/src/url2/queryParams.ts +++ b/statshouse-ui/src/url2/queryParams.ts @@ -93,6 +93,7 @@ export type QueryParams = { dashboardName: string; dashboardDescription: string; dashboardVersion?: number; + dashboardCurrentVersion?: number; timeRange: TimeRange; eventFrom: number; timeShifts: number[]; diff --git a/statshouse-ui/src/url2/urlDecode.ts b/statshouse-ui/src/url2/urlDecode.ts index 6a301ad09..a3fd49577 100644 --- a/statshouse-ui/src/url2/urlDecode.ts +++ b/statshouse-ui/src/url2/urlDecode.ts @@ -41,6 +41,7 @@ export function urlDecode( plotKeys.push('0'); } }); + const global = urlDecodeGlobalParam(searchParams, defaultParams); const timeRange = urlDecodeTimeRange(searchParams, defaultParams); const plots = widgetsParamsDecode( @@ -48,6 +49,7 @@ export function urlDecode( uniqueArray([...plotKeys, ...defaultParams.orderPlot]), defaultParams ); + const groups = urlDecodeGroups(searchParams, uniqueArray([...groupKeys, ...defaultParams.orderGroup]), defaultParams); const variables = urlDecodeVariables( searchParams, @@ -81,6 +83,7 @@ export function urlDecodeGlobalParam( | 'dashboardName' | 'dashboardDescription' | 'dashboardVersion' + | 'dashboardCurrentVersion' > { const rawLive = searchParams[GET_PARAMS.metricLive]?.[treeParamsObjectValueSymbol]?.[0]; const rawTheme = searchParams[GET_PARAMS.theme]?.[treeParamsObjectValueSymbol]?.[0]; @@ -104,6 +107,7 @@ export function urlDecodeGlobalParam( dashboardName: rawDashboardName ?? defaultParams.dashboardName, dashboardDescription: rawDashboardDescription ?? defaultParams.dashboardDescription, dashboardVersion: rawDashboardVersion ?? defaultParams.dashboardVersion, + dashboardCurrentVersion: defaultParams.dashboardCurrentVersion, }; } diff --git a/statshouse-ui/src/url2/urlEncode.ts b/statshouse-ui/src/url2/urlEncode.ts index 9756b1e95..de5326a2d 100644 --- a/statshouse-ui/src/url2/urlEncode.ts +++ b/statshouse-ui/src/url2/urlEncode.ts @@ -51,9 +51,10 @@ export function urlEncodeGlobalParam( if (defaultParams.dashboardDescription !== params.dashboardDescription) { paramArr.push([GET_PARAMS.dashboardDescription, params.dashboardDescription]); } - if (defaultParams.dashboardVersion !== params.dashboardVersion && params.dashboardVersion) { + if (params.dashboardVersion) { paramArr.push([GET_PARAMS.dashboardVersion, params.dashboardVersion.toString()]); } + return paramArr; } @@ -95,6 +96,7 @@ export function urlEncodeGroups( if (!dequal(defaultParams.orderGroup, params.orderGroup)) { paramArr.push([GET_PARAMS.orderGroup, params.orderGroup.join(orderGroupSplitter)]); } + return paramArr; }