diff --git a/ui/packages/tidb-dashboard-for-op/cypress/integration/topsql/topsql.spec.ts b/ui/packages/tidb-dashboard-for-op/cypress/integration/topsql/topsql.spec.ts index deed2e5180..3f25d31a0f 100644 --- a/ui/packages/tidb-dashboard-for-op/cypress/integration/topsql/topsql.spec.ts +++ b/ui/packages/tidb-dashboard-for-op/cypress/integration/topsql/topsql.spec.ts @@ -154,6 +154,57 @@ skipOn(Cypress.env('TIDB_VERSION') !== 'latest', () => { }) }) + describe('URL state', () => { + it('updates url when dropdown filters change', () => { + setCustomTimeRange( + '2022-01-12 00:00:00{enter}2022-01-12 05:00:00{enter}' + ) + cy.wait('@getTopsqlSummary') + + cy.getByTestId('instance-selector').click() + cy.contains('.ant-select-item-option', '127.0.0.1:20160').click() + cy.wait('@getTopsqlSummary') + + cy.getByTestId('limit_select').click() + cy.contains('[data-e2e="limit_option"]', 'Limit 20').click() + cy.wait('@getTopsqlSummary') + + cy.getByTestId('group_select').click() + cy.contains('[data-e2e="group_option"]', 'By Region').click() + cy.wait('@getTopsqlSummary') + + cy.getByTestId('order_by_select').click() + cy.getByTestId('order_by_option_logical_io_bytes').click() + cy.wait('@getTopsqlSummary') + + cy.location('search').should('include', 'instance=127.0.0.1%3A20160') + cy.location('search').should('include', 'instance_type=tikv') + cy.location('search').should('include', 'limit=20') + cy.location('search').should('include', 'group_by=region') + cy.location('search').should('include', 'order_by=logical_io') + }) + + it('uses url params to restore dropdown filters', () => { + cy.window().then((win) => win.sessionStorage.clear()) + cy.visit( + `${this.uri.topsql}?from=1641916800&to=1641934800&instance=127.0.0.1%3A20160&instance_type=tikv&limit=20&group_by=region&order_by=logical_io` + ) + cy.wait('@getTopsqlSummary') + cy.wait('@getTopsqlConfig') + + cy.getByTestId('instance-selector').should( + 'contain', + 'tikv - 127.0.0.1:20160' + ) + cy.getByTestId('limit_select').should('contain', 'Limit 20') + cy.getByTestId('group_select').should('contain', 'By Region') + cy.getByTestId('order_by_select').should( + 'contain', + 'Order By Logical IO' + ) + }) + }) + describe('Refresh', () => { it('click refresh button with the recent x time range, fetch the recent x time range data', () => { cy.getByTestId('timerange-selector').click() diff --git a/ui/packages/tidb-dashboard-lib/src/apps/TopSQL/pages/List/List.tsx b/ui/packages/tidb-dashboard-lib/src/apps/TopSQL/pages/List/List.tsx index 399ac79fa6..bdff696e07 100644 --- a/ui/packages/tidb-dashboard-lib/src/apps/TopSQL/pages/List/List.tsx +++ b/ui/packages/tidb-dashboard-lib/src/apps/TopSQL/pages/List/List.tsx @@ -23,7 +23,7 @@ import { SettingOutlined } from '@ant-design/icons' import { useTranslation } from 'react-i18next' -import { useMount, useSessionStorage } from 'react-use' +import { useSessionStorage } from 'react-use' import { useMemoizedFn } from 'ahooks' import { sortBy } from 'lodash' import formatSql from '@lib/utils/sqlFormatter' @@ -96,6 +96,85 @@ const toTimeRangeValue: typeof _toTimeRangeValue = (v) => { return _toTimeRangeValue(v, v?.type === 'recent' ? RECENT_RANGE_OFFSET : 0) } +type TopSQLQueryParams = { + instance: string + instance_type: string + limit: number + group_by: AggLevel + order_by: OrderBy +} + +const isSameInstance = ( + prev: TopsqlInstanceItem | null | undefined, + next: TopsqlInstanceItem | null | undefined +) => + prev?.instance === next?.instance && + prev?.instance_type === next?.instance_type + +const findInstance = ( + instances: TopsqlInstanceItem[], + instanceName?: string, + instanceType?: string +) => { + if (!instanceName) { + return null + } + + if (instanceType) { + return ( + instances.find( + (item) => + item.instance === instanceName && item.instance_type === instanceType + ) ?? null + ) + } + + return instances.find((item) => item.instance === instanceName) ?? null +} + +const resolveSelectedInstance = ( + instances: TopsqlInstanceItem[], + instanceName: string, + instanceType: string, + storedInstance: TopsqlInstanceItem | null | undefined +) => { + const instanceFromUrl = findInstance(instances, instanceName, instanceType) + if (instanceFromUrl) { + return instanceFromUrl + } + + if (instanceName && instanceType) { + return { + instance: instanceName, + instance_type: instanceType + } + } + + const instanceFromStorage = findInstance( + instances, + storedInstance?.instance, + storedInstance?.instance_type + ) + + return instanceFromStorage || storedInstance || instances[0] || null +} + +const normalizeLimit = (value: number) => { + return LIMITS.includes(value) ? value : LIMITS[0] +} + +const normalizeGroupBy = (value: string) => { + return GROUP.includes(value as AggLevel) + ? (value as AggLevel) + : AggLevel.Query +} + +const normalizeOrderBy = (value: string) => { + return Object.values(OrderBy).includes(value as OrderBy) + ? (value as OrderBy) + : OrderBy.CpuTime +} + export function TopSQLList() { const ctx = useContext(TopSQLContext) const { t } = useTranslation() @@ -103,19 +182,20 @@ export function TopSQLList() { const { topSQLConfig, isConfigLoading, updateConfig, haveHistoryData } = useTopSQLConfig() const [showSettings, setShowSettings] = useState(false) - const [instance, setInstance] = useSessionStorage( - 'topsql.instance', - null - ) - const { queryParams, setQueryParams } = useQueryParams<{ - instance: string - }>({ - instance: '' + const [storedInstance, setStoredInstance] = + useSessionStorage('topsql.instance', null) + const { queryParams, setQueryParams } = useQueryParams({ + instance: '', + instance_type: '', + limit: LIMITS[0], + group_by: AggLevel.Query, + order_by: OrderBy.CpuTime }) const { timeRange, setTimeRange } = useURLTimeRange() - const [limit, setLimit] = useState(5) - const [groupBy, setGroupBy] = useState(AggLevel.Query) - const [orderBy, setOrderBy] = useState(OrderBy.CpuTime) + const limit = useMemo( + () => normalizeLimit(queryParams.limit), + [queryParams.limit] + ) const [timeWindowSize, setTimeWindowSize] = useState(0) const containerRef = useRef(null) const computeTimeWindowSize = useMemoizedFn( @@ -132,6 +212,49 @@ export function TopSQLList() { return finalWindowSize } ) + const { + instances, + isLoading: isInstancesLoading, + fetchInstances + } = useInstances(timeRange) + const instance = useMemo( + () => + resolveSelectedInstance( + instances, + queryParams.instance, + queryParams.instance_type, + storedInstance + ), + [instances, queryParams.instance, queryParams.instance_type, storedInstance] + ) + const groupBy = useMemo(() => { + if (ctx?.cfg.showGroupBy !== true || instance?.instance_type !== 'tikv') { + return AggLevel.Query + } + const group = normalizeGroupBy(queryParams.group_by) + if (group === AggLevel.Region && ctx?.cfg.showGroupByRegion !== true) { + return AggLevel.Query + } + return group + }, [ + ctx?.cfg.showGroupBy, + ctx?.cfg.showGroupByRegion, + instance?.instance_type, + queryParams.group_by + ]) + const orderBy = useMemo(() => { + if (ctx?.cfg.showOrderBy !== true) { + return OrderBy.CpuTime + } + const order = normalizeOrderBy(queryParams.order_by) + if ( + instance?.instance_type !== 'tikv' && + order === OrderBy.LogicalIoBytes + ) { + return OrderBy.CpuTime + } + return order + }, [ctx?.cfg.showOrderBy, instance?.instance_type, queryParams.order_by]) const { topSQLData, isLoading: isDataLoading, @@ -145,11 +268,6 @@ export function TopSQLList() { computeTimeWindowSize ) const isLoading = isConfigLoading || isDataLoading - const { - instances, - isLoading: isInstancesLoading, - fetchInstances - } = useInstances(timeRange) const { data: tikvNetworkIoCollection, isLoading: isTikvNetworkIoCollectionLoading, @@ -157,6 +275,36 @@ export function TopSQLList() { } = useClientRequest(ctx!.ds.topsqlTikvNetworkIoCollectionGet, { immediate: false }) + const syncSelectedInstance = useMemoizedFn( + (nextInstances: TopsqlInstanceItem[]) => { + const nextInstance = resolveSelectedInstance( + nextInstances, + queryParams.instance, + queryParams.instance_type, + storedInstance + ) + + if (!nextInstance) { + return null + } + + if (!isSameInstance(storedInstance, nextInstance)) { + setStoredInstance(nextInstance) + } + + if ( + queryParams.instance !== nextInstance.instance || + queryParams.instance_type !== nextInstance.instance_type + ) { + setQueryParams({ + instance: nextInstance.instance!, + instance_type: nextInstance.instance_type! + }) + } + + return nextInstance + } + ) const handleBrushEnd: BrushEndListener = useCallback( (v: BrushEvent) => { @@ -183,31 +331,6 @@ export function TopSQLList() { [setTimeRange] ) - const fetchInstancesAndSelectInstance = useMemoizedFn(async () => { - const instances = await fetchInstances(timeRange) - const instanceFromURL = queryParams.instance - - if (instanceFromURL) { - const instance = instances.find( - (instance) => instance.instance === instanceFromURL - ) - if (instance) { - setInstance(instance) - return - } - } - - // Select the first instance if there not instance selected - if (!!instance) { - return - } - setInstance(instances[0]) - }) - - useMount(() => { - fetchInstancesAndSelectInstance() - }) - const chartRef = useRef(null) // only for clinic @@ -222,6 +345,36 @@ export function TopSQLList() { return infos.join(' | ') }, [ctx?.cfg.orgName, ctx?.cfg.clusterName]) + useEffect(() => { + syncSelectedInstance(instances) + }, [instances, syncSelectedInstance]) + + useEffect(() => { + const nextParams: Partial = {} + + if (queryParams.limit !== limit) { + nextParams.limit = limit + } + if (queryParams.group_by !== groupBy) { + nextParams.group_by = groupBy + } + if (queryParams.order_by !== orderBy) { + nextParams.order_by = orderBy + } + + if (Object.keys(nextParams).length > 0) { + setQueryParams(nextParams) + } + }, [ + groupBy, + limit, + orderBy, + queryParams.group_by, + queryParams.limit, + queryParams.order_by, + setQueryParams + ]) + const shouldCheckNetworkIoCollection = canOpenSettings && instance?.instance_type === 'tikv' && @@ -289,24 +442,23 @@ export function TopSQLList() { { - setInstance(inst) - if (!!inst?.instance) { - setQueryParams({ instance: inst.instance }) + const nextParams: Partial = { + instance: inst.instance!, + instance_type: inst.instance_type! + } + + if (inst.instance_type !== 'tikv') { + nextParams.group_by = AggLevel.Query + if (orderBy === OrderBy.LogicalIoBytes) { + nextParams.order_by = OrderBy.CpuTime + } } + + setStoredInstance(inst) + setQueryParams(nextParams) if (inst) { telemetry.finishSelectInstance(inst?.instance_type!) } - // only group by sql when instance is not tikv - if (inst?.instance_type !== 'tikv') { - setGroupBy(AggLevel.Query) - } - // Reset orderBy if current selection is not supported by new instance type - if ( - inst?.instance_type !== 'tikv' && - orderBy === OrderBy.LogicalIoBytes - ) { - setOrderBy(OrderBy.CpuTime) - } }} instances={instances} disabled={isLoading || isInstancesLoading} @@ -334,7 +486,7 @@ export function TopSQLList() { setQueryParams({ group_by: value })} data-e2e="group_select" > {GROUP.filter((item) => { @@ -369,7 +521,7 @@ export function TopSQLList() {