From 0fd7dfeb2ac2b40b1f46d820d8980da07b5abdca Mon Sep 17 00:00:00 2001 From: Michael Mandrus <41969079+mmandrus@users.noreply.github.com> Date: Tue, 8 Apr 2025 13:14:17 +0100 Subject: [PATCH] CloudMigrations: Implement table sorting in the UI (#103061) * implement sorting * swagger gen * minor fixes * clean up param reading * add todo * add errors only prop * codegen stuff * fix copy paste error * forgot the api gen * implement sorting on the frontend * remove unused enums * cleanup * remove tests that are obe * fix merge * change page length based on checked box * cleanup * fix test * i18n-extract * var name tweaks * use switch and conditional rendering instead of checkbox * i18n-extract * use initial sort * fix type * fix layout * fix bugs * make betterer happy * fix var names --- go.work.sum | 4 +- pkg/services/cloudmigration/api/api.go | 4 ++ .../onprem/MigrationSummary.tsx | 31 +++++++++-- .../features/migrate-to-cloud/onprem/Page.tsx | 55 +++++++++++++++++-- .../onprem/ResourcesTable.test.tsx | 1 + .../onprem/ResourcesTable.tsx | 21 +++++-- public/locales/en-US/grafana.json | 1 + 7 files changed, 99 insertions(+), 18 deletions(-) diff --git a/go.work.sum b/go.work.sum index 6eaabd350d2..8eaab29f53d 100644 --- a/go.work.sum +++ b/go.work.sum @@ -675,6 +675,8 @@ github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQ github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blevesearch/bleve/v2 v2.4.4-0.20250319135056-b82baf10b205 h1:u6DQJ1k4FKwRNtsrVhIRQenNdtz31way7/LgWCluFzA= +github.com/blevesearch/bleve/v2 v2.4.4-0.20250319135056-b82baf10b205/go.mod h1:nSmFOQ7M264rKoM3jf63Gl2G+ylCgZGovPgL6ZEQYzU= github.com/blevesearch/bleve_index_api v1.1.12/go.mod h1:PbcwjIcRmjhGbkS/lJCpfgVSMROV6TRubGGAODaK1W8= github.com/blevesearch/bleve_index_api v1.2.1/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0= github.com/blevesearch/go-faiss v1.0.23/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk= @@ -1089,7 +1091,6 @@ github.com/grafana/alerting v0.0.0-20250310104713-16b885f1c79e/go.mod h1:HfvjmU3 github.com/grafana/alerting v0.0.0-20250403153742-418bc7118d05 h1:hMzOzI/S0nkZt0nUqpfAa4Rdb+YL8z8oG3pl4Jb31h8= github.com/grafana/alerting v0.0.0-20250403153742-418bc7118d05/go.mod h1:K3YAJumchx5EEZItGv4D3pCv/Ux796hmoOibP/p/eYk= github.com/grafana/authlib v0.0.0-20250123104008-e99947858901/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= -github.com/grafana/authlib v0.0.0-20250325095148-d6da9c164a7d/go.mod h1:PBtQaXwkFu4BAt2aXsR7w8p8NVpdjV5aJYhqRDei9Us= github.com/grafana/authlib/types v0.0.0-20250120144156-d6737a7dc8f5/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM= github.com/grafana/authlib/types v0.0.0-20250314102521-a77865c746c0/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= @@ -1114,7 +1115,6 @@ github.com/grafana/grafana/pkg/build v0.0.0-20250227105625-8f465f124924/go.mod h github.com/grafana/grafana/pkg/build v0.0.0-20250227163402-d78c646f93bb/go.mod h1:Vw0LdoMma64VgIMVpRY3i0D156jddgUGjTQBOcyeF3k= github.com/grafana/grafana/pkg/semconv v0.0.0-20250121113133-e747350fee2d/go.mod h1:tfLnBpPYgwrBMRz4EXqPCZJyCjEG4Ev37FSlXnocJ2c= github.com/grafana/grafana/pkg/storage/unified/apistore v0.0.0-20250121113133-e747350fee2d/go.mod h1:CXpwZ3Mkw6xVlGKc0SqUxqXCP3Uv182q6qAQnLaLxRg= -github.com/grafana/grafana/pkg/storage/unified/apistore v0.0.0-20250317130411-3f270d1de043/go.mod h1:usON2sfgh4qjGs4GLhH6+PL7Q6g5ezOP6M/9vOeHpAM= github.com/grafana/prometheus-alertmanager v0.25.1-0.20240930132144-b5e64e81e8d3 h1:6D2gGAwyQBElSrp3E+9lSr7k8gLuP3Aiy20rweLWeBw= github.com/grafana/prometheus-alertmanager v0.25.1-0.20240930132144-b5e64e81e8d3/go.mod h1:YeND+6FDA7OuFgDzYODN8kfPhXLCehcpxe4T9mdnpCY= github.com/grafana/prometheus-alertmanager v0.25.1-0.20250331083058-4563aec7a975 h1:4/BZkGObFWZf4cLbE2Vqg/1VTz67Q0AJ7LHspWLKJoQ= diff --git a/pkg/services/cloudmigration/api/api.go b/pkg/services/cloudmigration/api/api.go index 4bcfe484921..ce8b3ec8045 100644 --- a/pkg/services/cloudmigration/api/api.go +++ b/pkg/services/cloudmigration/api/api.go @@ -370,6 +370,10 @@ func (cma *CloudMigrationAPI) GetSnapshot(c *contextmodel.ReqContext) response.R col := getQueryCol(c.Query("resultSortColumn"), cloudmigration.SortColumnID) order := getQueryOrder(c.Query("resultSortOrder"), cloudmigration.SortOrderAsc) errorsOnly := c.QueryBool("errorsOnly") + // Don't allow the user to reverse-sort by ID + if col == cloudmigration.SortColumnID && order == cloudmigration.SortOrderDesc { + order = cloudmigration.SortOrderAsc + } q := cloudmigration.GetSnapshotsQuery{ SnapshotUID: snapshotUid, diff --git a/public/app/features/migrate-to-cloud/onprem/MigrationSummary.tsx b/public/app/features/migrate-to-cloud/onprem/MigrationSummary.tsx index 25207d6a760..270cc8729a6 100644 --- a/public/app/features/migrate-to-cloud/onprem/MigrationSummary.tsx +++ b/public/app/features/migrate-to-cloud/onprem/MigrationSummary.tsx @@ -1,4 +1,4 @@ -import { Box, Button, Space, Stack, Text } from '@grafana/ui'; +import { Box, Button, Switch, Space, Stack, Text } from '@grafana/ui'; import { Trans, t } from 'app/core/internationalization'; import { formatDate } from 'app/core/internationalization/dates'; @@ -23,6 +23,10 @@ interface MigrationSummaryProps { onUploadSnapshot: () => void; showRebuildSnapshot: boolean; + + onHighlightErrors: () => void; + isHighlightErrors: boolean; + showOnlyErrorsSwitch: boolean; } const DATE_FORMAT: Intl.DateTimeFormatOptions = { @@ -47,6 +51,10 @@ export function MigrationSummary(props: MigrationSummaryProps) { onUploadSnapshot, showRebuildSnapshot, + + isHighlightErrors, + onHighlightErrors, + showOnlyErrorsSwitch, } = props; const totalCount = snapshot?.stats?.total ?? 0; @@ -54,6 +62,8 @@ export function MigrationSummary(props: MigrationSummaryProps) { const successCount = snapshot?.stats?.statuses?.['OK'] ?? 0; const warningCount = snapshot?.stats?.statuses?.['WARNING'] ?? 0; + const switchLabel = t('migrate-to-cloud.summary.show-errors', 'Only view errors'); + return ( - - {errorCount} - - {successCount + warningCount} + + + {errorCount} + + {showOnlyErrorsSwitch && ( + + + + {switchLabel} + + + )} + + + {session.slug} diff --git a/public/app/features/migrate-to-cloud/onprem/Page.tsx b/public/app/features/migrate-to-cloud/onprem/Page.tsx index 1efd44e7489..bd2cd0ccf89 100644 --- a/public/app/features/migrate-to-cloud/onprem/Page.tsx +++ b/public/app/features/migrate-to-cloud/onprem/Page.tsx @@ -65,10 +65,15 @@ const SHOULD_POLL_STATUSES: Array = [ const SNAPSHOT_REBUILD_STATUSES: Array = ['PENDING_UPLOAD', 'FINISHED', 'ERROR', 'UNKNOWN']; const SNAPSHOT_BUILDING_STATUSES: Array = ['INITIALIZING', 'CREATING']; const SNAPSHOT_UPLOADING_STATUSES: Array = ['UPLOADING', 'PENDING_PROCESSING', 'PROCESSING']; +const SNAPSHOT_RESOURCES_HAVE_ERROR_STATUSES: Array = [ + 'PROCESSING', + 'PENDING_PROCESSING', + 'FINISHED', +]; const PAGE_SIZE = 50; -function useGetLatestSnapshot(sessionUid?: string, page = 1) { +function useGetLatestSnapshot(sessionUid?: string, page = 1, sortParams?: SortParams, showErrors = false) { const [shouldPoll, setShouldPoll] = useState(false); const listResult = useGetShapshotListQuery( @@ -78,7 +83,15 @@ function useGetLatestSnapshot(sessionUid?: string, page = 1) { const getSnapshotQueryArgs = sessionUid && lastItem?.uid - ? { uid: sessionUid, snapshotUid: lastItem.uid, resultLimit: PAGE_SIZE, resultPage: page } + ? { + uid: sessionUid, + snapshotUid: lastItem.uid, + resultLimit: PAGE_SIZE, + resultPage: page, + resultSortColumn: sortParams?.column ? sortParams.column : undefined, + resultSortOrder: sortParams?.order, + errorsOnly: showErrors, + } : skipToken; const snapshotResult = useGetSnapshotQuery(getSnapshotQueryArgs, { @@ -113,11 +126,20 @@ function useGetLatestSnapshot(sessionUid?: string, page = 1) { }; } +interface SortParams { + column: string; + order: string | undefined; +} + export const Page = () => { const [disconnectModalOpen, setDisconnectModalOpen] = useState(false); - const session = useGetLatestSession(); const [page, setPage] = useState(1); - const snapshot = useGetLatestSnapshot(session.data?.uid, page); + const [sortParams, setSortParams] = useState({ + column: '', + order: undefined, + }); + const [highlightErrors, setHighlightErrors] = useState(false); + const [performCreateSnapshot, createSnapshotResult] = useCreateSnapshotMutation(); const [performUploadSnapshot, uploadSnapshotResult] = useUploadSnapshotMutation(); const [performCancelSnapshot, cancelSnapshotResult] = useCancelSnapshotMutation(); @@ -125,6 +147,16 @@ export const Page = () => { const { currentData: localPlugins = [] } = useGetLocalPluginListQuery(); + const session = useGetLatestSession(); + const snapshot = useGetLatestSnapshot(session.data?.uid, page, sortParams, highlightErrors); + const numPages = Math.ceil( + (highlightErrors ? snapshot?.data?.stats?.statuses?.['ERROR'] || 0 : snapshot?.data?.stats?.total || 0) / PAGE_SIZE + ); + useEffect(() => { + if (numPages > 0 && page > numPages) { + setPage(numPages); + } + }, [numPages, page]); useNotifySuccessful(snapshot.data); const sessionUid = session.data?.uid; @@ -147,6 +179,7 @@ export const Page = () => { const showUploadSnapshot = !snapshot.isError && (status === 'PENDING_UPLOAD' || SNAPSHOT_UPLOADING_STATUSES.includes(status)); const showRebuildSnapshot = SNAPSHOT_REBUILD_STATUSES.includes(status); + const showOnlyErrorsSwitch = SNAPSHOT_RESOURCES_HAVE_ERROR_STATUSES.includes(status); const error = getError({ snapshot: snapshot.data, @@ -210,6 +243,9 @@ export const Page = () => { uploadSnapshotIsLoading={uploadSnapshotResult.isLoading || SNAPSHOT_UPLOADING_STATUSES.includes(status)} onUploadSnapshot={handleUploadSnapshot} showRebuildSnapshot={showRebuildSnapshot} + onHighlightErrors={() => setHighlightErrors(!highlightErrors)} + isHighlightErrors={highlightErrors} + showOnlyErrorsSwitch={showOnlyErrorsSwitch} /> )} @@ -245,8 +281,17 @@ export const Page = () => { resources={snapshot.data.results} localPlugins={localPlugins} onChangePage={setPage} - numberOfPages={Math.ceil((snapshot?.data?.stats?.total || 0) / PAGE_SIZE)} + numberOfPages={numPages} page={page} + onChangeSort={(a) => { + const order = a.sortBy[0]?.desc === undefined ? undefined : a.sortBy[0]?.desc ? 'desc' : 'asc'; + if (sortParams.column !== a.sortBy[0]?.id || order !== sortParams.order) { + setSortParams({ + column: a.sortBy[0]?.id, + order: order, + }); + } + }} /> diff --git a/public/app/features/migrate-to-cloud/onprem/ResourcesTable.test.tsx b/public/app/features/migrate-to-cloud/onprem/ResourcesTable.test.tsx index 8528f02f884..9a0eb47313c 100644 --- a/public/app/features/migrate-to-cloud/onprem/ResourcesTable.test.tsx +++ b/public/app/features/migrate-to-cloud/onprem/ResourcesTable.test.tsx @@ -20,6 +20,7 @@ function render(props: Partial) { rtlRender( {}} onChangePage={() => {}} numberOfPages={10} page={0} diff --git a/public/app/features/migrate-to-cloud/onprem/ResourcesTable.tsx b/public/app/features/migrate-to-cloud/onprem/ResourcesTable.tsx index 946cdf6383a..71d4419952b 100644 --- a/public/app/features/migrate-to-cloud/onprem/ResourcesTable.tsx +++ b/public/app/features/migrate-to-cloud/onprem/ResourcesTable.tsx @@ -1,6 +1,6 @@ import { useCallback, useMemo, useState } from 'react'; -import { InteractiveTable, Pagination, Stack } from '@grafana/ui'; +import { InteractiveTable, Pagination, Stack, Column, type FetchDataFunc } from '@grafana/ui'; import { LocalPlugin } from '../../plugins/admin/types'; import { MigrateDataResponseItemDto } from '../api'; @@ -17,12 +17,13 @@ export interface ResourcesTableProps { page: number; numberOfPages: number; onChangePage: (page: number) => void; + onChangeSort: FetchDataFunc; } -const columns = [ - { id: 'name', header: 'Name', cell: NameCell }, - { id: 'type', header: 'Type', cell: TypeCell }, - { id: 'status', header: 'Status', cell: StatusCell }, +const columns: Array> = [ + { id: 'name', header: 'Name', cell: NameCell, sortType: 'alphanumeric' }, + { id: 'resource_type', header: 'Type', cell: TypeCell, sortType: 'alphanumeric' }, + { id: 'status', header: 'Status', cell: StatusCell, sortType: 'alphanumeric' }, ]; export function ResourcesTable({ @@ -30,8 +31,10 @@ export function ResourcesTable({ localPlugins, numberOfPages = 0, onChangePage, + onChangeSort, page = 1, }: ResourcesTableProps) { + const initialSortBy = useMemo(() => [{ id: 'resource_type', desc: true }], []); const [focusedResource, setfocusedResource] = useState(); const handleShowDetailsModal = useCallback((resource: ResourceTableItem) => { @@ -53,7 +56,13 @@ export function ResourcesTable({ return ( <> - r.refId} /> + r.refId} + fetchData={onChangeSort} + initialSortBy={initialSortBy} + > diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 324acae10ef..b04d5b40e5e 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -5427,6 +5427,7 @@ "errored-resource-count": "Errors", "page-loading": "Loading...", "rebuild-snapshot": "Rebuild snapshot", + "show-errors": "Only view errors", "snapshot-date": "Snapshot timestamp", "snapshot-not-created": "Not yet created", "start-migration": "Build snapshot",