diff --git a/.gitignore b/.gitignore index 28855f18..5d9d5a75 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .devcontainer/dev.env .DS_Store +.vscode web/cypress/screenshots/ web/cypress/export-env.sh web/screenshots/ diff --git a/config/perses-dashboards.patch.json b/config/perses-dashboards.patch.json index 5420606a..2b90e7ac 100644 --- a/config/perses-dashboards.patch.json +++ b/config/perses-dashboards.patch.json @@ -8,7 +8,7 @@ "exact": false, "path": ["/multicloud/monitoring/v2/dashboards"], "component": { - "$codeRef": "DashboardsPage" + "$codeRef": "DashboardListPage" } } } @@ -36,7 +36,7 @@ "properties": { "exact": false, "path": ["/monitoring/v2/dashboards"], - "component": { "$codeRef": "DashboardsPage" } + "component": { "$codeRef": "DashboardListPage" } } } }, @@ -64,7 +64,7 @@ "properties": { "exact": false, "path": ["/virt-monitoring/v2/dashboards"], - "component": { "$codeRef": "DashboardsPage" } + "component": { "$codeRef": "DashboardListPage" } } } }, @@ -83,5 +83,41 @@ "insertAfter": "dashboards-virt" } } + }, + { + "op": "add", + "path": "/extensions/1", + "value": { + "type": "console.page/route", + "properties": { + "exact": false, + "path": ["/monitoring/v2/dashboards/view"], + "component": { "$codeRef": "DashboardPage" } + } + } + }, + { + "op": "add", + "path": "/extensions/1", + "value": { + "type": "console.page/route", + "properties": { + "exact": false, + "path": ["/virt-monitoring/v2/dashboards/view"], + "component": { "$codeRef": "DashboardPage" } + } + } + }, + { + "op": "add", + "path": "/extensions/1", + "value": { + "type": "console.page/route", + "properties": { + "exact": false, + "path": ["/multicloud/monitoring/v2/dashboards/view"], + "component": { "$codeRef": "DashboardPage" } + } + } } ] diff --git a/web/package.json b/web/package.json index 53bf8b5b..056d4a4c 100644 --- a/web/package.json +++ b/web/package.json @@ -153,7 +153,8 @@ "displayName": "OpenShift console monitoring plugin", "description": "This plugin adds the monitoring UI to the OpenShift web console", "exposedModules": { - "DashboardsPage": "./components/dashboards/perses/dashboard-page", + "DashboardListPage": "./components/dashboards/perses/dashboard-list-page", + "DashboardPage": "./components/dashboards/perses/dashboard-page", "LegacyDashboardsPage": "./components/dashboards/legacy/legacy-dashboard-page", "SilencesPage": "./components/alerting/SilencesPage", "SilencesDetailsPage": "./components/alerting/SilencesDetailsPage", diff --git a/web/src/components/dashboards/perses/dashoard-app.tsx b/web/src/components/dashboards/perses/dashboard-app.tsx similarity index 100% rename from web/src/components/dashboards/perses/dashoard-app.tsx rename to web/src/components/dashboards/perses/dashboard-app.tsx diff --git a/web/src/components/dashboards/perses/dashboard-layout.tsx b/web/src/components/dashboards/perses/dashboard-layout.tsx new file mode 100644 index 00000000..eecaf68d --- /dev/null +++ b/web/src/components/dashboards/perses/dashboard-layout.tsx @@ -0,0 +1,45 @@ +import React, { ReactNode } from 'react'; +import { DashboardEmptyState } from './emptystates/DashboardEmptyState'; +import { DashboardSkeleton } from './dashboard-skeleton'; +import { CombinedDashboardMetadata } from './hooks/useDashboardsData'; +import { ProjectBar } from './project/ProjectBar'; +import { PersesWrapper } from './PersesWrapper'; +import { Overview } from '@openshift-console/dynamic-plugin-sdk'; + +export interface DashboardLayoutProps { + activeProject: string | null; + setActiveProject: (project: string | null) => void; + activeProjectDashboardsMetadata: CombinedDashboardMetadata[]; + changeBoard: (boardName: string) => void; + dashboardName: string; + children: ReactNode; +} + +export const DashboardLayout: React.FC = ({ + activeProject, + setActiveProject, + activeProjectDashboardsMetadata, + changeBoard, + dashboardName, + children, +}) => { + return ( + <> + + + {activeProjectDashboardsMetadata?.length === 0 ? ( + + ) : ( + + {children} + + )} + + + ); +}; diff --git a/web/src/components/dashboards/perses/dashboard-list-page.tsx b/web/src/components/dashboards/perses/dashboard-list-page.tsx new file mode 100644 index 00000000..4a1b50c2 --- /dev/null +++ b/web/src/components/dashboards/perses/dashboard-list-page.tsx @@ -0,0 +1,26 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { type FC } from 'react'; +import { QueryParamProvider } from 'use-query-params'; +import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5'; +import { DashboardList } from './dashboard-list'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + retry: 0, + }, + }, +}); + +const DashboardListPage: FC = () => { + return ( + + + + + + ); +}; + +export default DashboardListPage; diff --git a/web/src/components/dashboards/perses/dashboard-list.tsx b/web/src/components/dashboards/perses/dashboard-list.tsx new file mode 100644 index 00000000..fc79fe52 --- /dev/null +++ b/web/src/components/dashboards/perses/dashboard-list.tsx @@ -0,0 +1,249 @@ +import React, { ReactNode, useMemo, type FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import { DashboardLayout } from './dashboard-layout'; +import { useDashboardsData } from './hooks/useDashboardsData'; + +import { Pagination } from '@patternfly/react-core'; +import { DataView } from '@patternfly/react-data-view/dist/dynamic/DataView'; +import { DataViewFilters } from '@patternfly/react-data-view/dist/dynamic/DataViewFilters'; +import { + DataViewTable, + DataViewTh, + DataViewTr, +} from '@patternfly/react-data-view/dist/dynamic/DataViewTable'; +import { DataViewTextFilter } from '@patternfly/react-data-view/dist/dynamic/DataViewTextFilter'; +import { DataViewToolbar } from '@patternfly/react-data-view/dist/dynamic/DataViewToolbar'; +import { useDataViewFilters, useDataViewSort } from '@patternfly/react-data-view'; +import { useDataViewPagination } from '@patternfly/react-data-view/dist/dynamic/Hooks'; +import { ThProps } from '@patternfly/react-table'; +import { Link, useSearchParams } from 'react-router-dom-v5-compat'; + +import { getDashboardUrl, usePerspective } from '../../hooks/usePerspective'; + +const perPageOptions = [ + { title: '10', value: 10 }, + { title: '20', value: 20 }, +]; + +interface DashboardName { + link: ReactNode; + label: string; +} + +interface DashboardRow { + name: DashboardName; + project: string; + created: string; + modified: string; +} + +interface DashboardRowFilters { + name?: string; + 'project-filter'?: string; +} + +const sortDashboardData = ( + data: DashboardRow[], + sortBy: keyof DashboardRow | undefined, + direction: 'asc' | 'desc' | undefined, +): DashboardRow[] => + sortBy && direction + ? [...data].sort((a, b) => { + const aValue = sortBy === 'name' ? a.name.label : a[sortBy]; + const bValue = sortBy === 'name' ? b.name.label : b[sortBy]; + + if (direction === 'asc') { + return aValue < bValue ? -1 : aValue > bValue ? 1 : 0; + } else { + return aValue > bValue ? -1 : aValue < bValue ? 1 : 0; + } + }) + : data; + +interface DashboardsTableProps { + persesDashboards: Array<{ + metadata?: { + name?: string; + project?: string; + createdAt?: string; + updatedAt?: string; + }; + }>; + persesDashboardsLoading: boolean; + activeProject: string | null; +} + +const DashboardsTable: React.FunctionComponent = ({ + persesDashboards, + persesDashboardsLoading, + activeProject, +}) => { + const { t } = useTranslation(process.env.I18N_NAMESPACE); + + const { perspective } = usePerspective(); + const dashboardBaseURL = getDashboardUrl(perspective); + + const [searchParams, setSearchParams] = useSearchParams(); + const { sortBy, direction, onSort } = useDataViewSort({ searchParams, setSearchParams }); + + const { filters, onSetFilters, clearAllFilters } = useDataViewFilters({ + initialFilters: { name: '', 'project-filter': '' }, + searchParams, + setSearchParams, + }); + const pagination = useDataViewPagination({ perPage: perPageOptions[0].value }); + const { page, perPage } = pagination; + + const DASHBOARD_COLUMNS = useMemo( + () => [ + { label: t('Dashboard'), key: 'name' as keyof DashboardRow, index: 0 }, + { label: t('Project'), key: 'project' as keyof DashboardRow, index: 1 }, + { label: t('Created on'), key: 'created' as keyof DashboardRow, index: 2 }, + { label: t('Last Modified'), key: 'modified' as keyof DashboardRow, index: 3 }, + ], + [t], + ); + const sortByIndex = useMemo(() => { + return DASHBOARD_COLUMNS.findIndex((item) => item.key === sortBy); + }, [DASHBOARD_COLUMNS, sortBy]); + + const getSortParams = (columnIndex: number): ThProps['sort'] => ({ + sortBy: { + index: sortByIndex, + direction, + defaultDirection: 'asc', + }, + onSort: (_event, index, direction) => onSort(_event, DASHBOARD_COLUMNS[index].key, direction), + columnIndex, + }); + + const tableColumns: DataViewTh[] = DASHBOARD_COLUMNS.map((column, index) => ({ + cell: t(column.label), + props: { sort: getSortParams(index) }, + })); + + const tableRows: DashboardRow[] = useMemo(() => { + if (persesDashboardsLoading) { + return []; + } + return persesDashboards.map((board) => { + const metadata = board?.metadata; + const dashboardsParams = `?dashboard=${metadata?.name}&project=${metadata?.project}`; + const dashboardName: DashboardName = { + link: ( + + {metadata?.name} + + ), + label: metadata?.name || '', + }; + + return { + name: dashboardName, + project: board?.metadata?.project || '', + created: board?.metadata?.createdAt || '', + modified: board?.metadata?.updatedAt || '', + }; + }); + }, [dashboardBaseURL, persesDashboards, persesDashboardsLoading]); + + const filteredData = useMemo( + () => + tableRows.filter( + (item) => + (!filters.name || + item.name?.label?.toLocaleLowerCase().includes(filters.name?.toLocaleLowerCase())) && + (!filters['project-filter'] || + item.project + ?.toLocaleLowerCase() + .includes(filters['project-filter']?.toLocaleLowerCase())) && + (!activeProject || item.project === activeProject), + ), + [filters, tableRows, activeProject], + ); + + const sortedAndFilteredData = useMemo( + () => sortDashboardData(filteredData, sortBy as keyof DashboardRow, direction), + [filteredData, sortBy, direction], + ); + + const pageRows: DataViewTr[] = useMemo( + () => + sortedAndFilteredData + .slice((page - 1) * perPage, (page - 1) * perPage + perPage) + .map(({ name, project, created, modified }) => [name.link, project, created, modified]), + [page, perPage, sortedAndFilteredData], + ); + + const PaginationTool = () => { + return ( + + ); + }; + + return ( + + } + filters={ + onSetFilters(values)} values={filters}> + + + + } + /> + + } /> + + ); +}; + +export const DashboardList: FC = () => { + const { + activeProjectDashboardsMetadata, + changeBoard, + dashboardName, + setActiveProject, + activeProject, + persesDashboards, + combinedInitialLoad, + } = useDashboardsData(); + + return ( + + + + ); +}; diff --git a/web/src/components/dashboards/perses/dashboard-page.tsx b/web/src/components/dashboards/perses/dashboard-page.tsx index cf88e5fa..c1dae5d5 100644 --- a/web/src/components/dashboards/perses/dashboard-page.tsx +++ b/web/src/components/dashboards/perses/dashboard-page.tsx @@ -1,175 +1,109 @@ -import { Overview } from '@openshift-console/dynamic-plugin-sdk'; -import { - QueryClient, - QueryClientProvider, - useMutation, - UseMutationResult, - useQueryClient, -} from '@tanstack/react-query'; -import { useCallback, type FC } from 'react'; +import type { FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useSearchParams } from 'react-router-dom-v5-compat'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryParamProvider } from 'use-query-params'; import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5'; -import { LoadingInline } from '../../console/console-shared/src/components/loading/LoadingInline'; -import { PersesWrapper } from './PersesWrapper'; -import { DashboardSkeleton } from './dashboard-skeleton'; -import { DashboardEmptyState } from './emptystates/DashboardEmptyState'; -import { ProjectEmptyState } from './emptystates/ProjectEmptyState'; +import { DashboardLayout } from './dashboard-layout'; import { useDashboardsData } from './hooks/useDashboardsData'; -import { ProjectBar } from './project/ProjectBar'; -import { - DashboardResource, - EphemeralDashboardResource, - getResourceExtendedDisplayName, -} from '@perses-dev/core'; -import { getCSRFToken } from '@openshift-console/dynamic-plugin-sdk/lib/utils/fetch/console-fetch-utils'; -import { useSnackbar } from '@perses-dev/components'; -import buildURL from './perses/url-builder'; -import { OCPDashboardApp } from './dashoard-app'; -import { t } from 'i18next'; - -const resource = 'dashboards'; -const HTTPMethodPUT = 'PUT'; -const HTTPHeader: Record = { - 'Content-Type': 'application/json', - Accept: 'application/json', -}; +import { ProjectEmptyState } from './emptystates/ProjectEmptyState'; +import { LoadingInline } from '../../console/console-shared/src/components/loading/LoadingInline'; +import { OCPDashboardApp } from './dashboard-app'; const queryClient = new QueryClient({ defaultOptions: { queries: { + retry: false, refetchOnWindowFocus: false, - retry: 0, }, }, }); -async function updateDashboard(entity: DashboardResource): Promise { - const url = buildURL({ - resource: resource, - project: entity.metadata.project, - name: entity.metadata.name, - }); - - const response = await fetch(url, { - method: HTTPMethodPUT, - headers: { - ...HTTPHeader, - 'X-CSRFToken': getCSRFToken(), - }, - body: JSON.stringify(entity), - }); - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - - return response.json(); -} - -function useUpdateDashboardMutation(): UseMutationResult< - DashboardResource, - Error, - DashboardResource -> { - const queryClient = useQueryClient(); +const DashboardPage_: FC = () => { + const { t } = useTranslation(process.env.I18N_NAMESPACE); + const [searchParams] = useSearchParams(); - return useMutation({ - mutationKey: [resource], - mutationFn: (dashboard) => { - return updateDashboard(dashboard); - }, - onSuccess: () => { - return queryClient.invalidateQueries({ queryKey: [resource] }); - }, - }); -} - -const MonitoringDashboardsPage_: FC = () => { const { - changeBoard, activeProjectDashboardsMetadata, - combinedInitialLoad, - activeProject, - setActiveProject, + changeBoard, dashboardName, + setActiveProject, + activeProject, + combinedInitialLoad, } = useDashboardsData(); - const updateDashboardMutation = useUpdateDashboardMutation(); - const { successSnackbar, exceptionSnackbar } = useSnackbar(); + // Get dashboard and project from URL parameters + const urlDashboard = searchParams.get('dashboard'); + const urlProject = searchParams.get('project'); - const handleDashboardSave = useCallback( - (data: DashboardResource | EphemeralDashboardResource) => { - if (data.kind !== 'Dashboard') { - throw new Error('Invalid kind'); - } - return updateDashboardMutation.mutateAsync(data, { - onSuccess: (updatedDashboard: DashboardResource) => { - successSnackbar( - `Dashboard ${getResourceExtendedDisplayName( - updatedDashboard, - )} has been successfully updated`, - ); - return updatedDashboard; - }, - onError: (err) => { - exceptionSnackbar(err); - throw err; - }, - }); - }, - [exceptionSnackbar, successSnackbar, updateDashboardMutation], - ); + // Set active project if provided in URL + if (urlProject && urlProject !== activeProject) { + setActiveProject(urlProject); + } + + // Change dashboard if provided in URL + if (urlDashboard && urlDashboard !== dashboardName) { + changeBoard(urlDashboard); + } if (combinedInitialLoad) { return ; } - if (!activeProject) { - // If we have loaded all of the requests fully and there are no projects, then - return ; // empty state + if (activeProjectDashboardsMetadata?.length === 0) { + return ; + } + + // Find the dashboard that matches either the URL parameter or the current dashboardName + const targetDashboardName = urlDashboard || dashboardName; + const currentDashboard = activeProjectDashboardsMetadata.find( + (d) => d.name === targetDashboardName, + ); + + if (!currentDashboard) { + return ( +
+

{t('Dashboard not found')}

+

+ {t('The dashboard "{{name}}" was not found in project "{{project}}".', { + name: targetDashboardName, + project: activeProject || urlProject, + })} +

+
+ ); } return ( - <> - - - {activeProjectDashboardsMetadata.length === 0 ? ( - - ) : ( - - - - - - )} - - + + + ); }; -const MonitoringDashboardsPageWrapper: FC = () => { +const DashboardPage: React.FC = () => { return ( - + ); }; -export default MonitoringDashboardsPageWrapper; +export default DashboardPage; diff --git a/web/src/components/dashboards/perses/dashboard-skeleton.tsx b/web/src/components/dashboards/perses/dashboard-skeleton.tsx index f185f8c8..9acb78d5 100644 --- a/web/src/components/dashboards/perses/dashboard-skeleton.tsx +++ b/web/src/components/dashboards/perses/dashboard-skeleton.tsx @@ -3,19 +3,66 @@ import { memo } from 'react'; import { Helmet } from 'react-helmet'; import { useTranslation } from 'react-i18next'; -import { PageSection, Split, SplitItem, Title } from '@patternfly/react-core'; +import { PageSection, Stack, StackItem, Title } from '@patternfly/react-core'; import { CombinedDashboardMetadata } from './hooks/useDashboardsData'; +import { Breadcrumb, BreadcrumbItem } from '@patternfly/react-core'; +import { getDashboardsListUrl, usePerspective } from '../../hooks/usePerspective'; +import { StringParam, useQueryParam } from 'use-query-params'; +import { QueryParams } from '../../query-params'; +import { useNavigate } from 'react-router-dom-v5-compat'; + +import { chart_color_blue_100, chart_color_blue_300 } from '@patternfly/react-tokens'; +import { usePatternFlyTheme } from '../../hooks/usePatternflyTheme'; + +const DashboardBreadCrumb: React.FunctionComponent = () => { + const { t } = useTranslation(process.env.I18N_NAMESPACE); + + const { perspective } = usePerspective(); + const [dashboardName] = useQueryParam(QueryParams.Dashboard, StringParam); + const { theme } = usePatternFlyTheme(); + const navigate = useNavigate(); + + const handleDashboardsClick = () => { + navigate(getDashboardsListUrl(perspective)); + }; + + const patternflyBlue100 = chart_color_blue_100.value; + + const patternflyBlue300 = chart_color_blue_300.value; + + const linkColor = theme == 'dark' ? patternflyBlue100 : patternflyBlue300; + + return ( + + + {t('Dashboards')} + + {dashboardName && {dashboardName}} + + ); +}; + const HeaderTop: FC = memo(() => { const { t } = useTranslation(process.env.I18N_NAMESPACE); return ( - - + + + + + {t('Dashboards')} {t('View and manage dashboards.')} - - + + ); }); diff --git a/web/src/components/dashboards/perses/hooks/useDashboardsData.ts b/web/src/components/dashboards/perses/hooks/useDashboardsData.ts index ca700d6a..a0d3fa66 100644 --- a/web/src/components/dashboards/perses/hooks/useDashboardsData.ts +++ b/web/src/components/dashboards/perses/hooks/useDashboardsData.ts @@ -1,11 +1,11 @@ -import { useMemo, useCallback, useEffect } from 'react'; +import { useMemo, useCallback } from 'react'; import { DashboardResource } from '@perses-dev/core'; import { useNavigate } from 'react-router-dom-v5-compat'; import { StringParam, useQueryParam } from 'use-query-params'; import { getAllQueryArguments } from '../../../console/utils/router'; import { useBoolean } from '../../../hooks/useBoolean'; -import { getDashboardsUrl, usePerspective } from '../../../hooks/usePerspective'; +import { getDashboardUrl, usePerspective } from '../../../hooks/usePerspective'; import { QueryParams } from '../../../query-params'; import { useActiveProject } from '../project/useActiveProject'; import { usePerses } from './usePerses'; @@ -61,6 +61,9 @@ export const useDashboardsData = () => { // Retrieve dashboard metadata for the currently selected project const activeProjectDashboardsMetadata = useMemo(() => { + if (!activeProject) { + return combinedDashboardsMetadata; + } return combinedDashboardsMetadata.filter((combinedDashboardMetadata) => { return combinedDashboardMetadata.project === activeProject; }); @@ -75,34 +78,28 @@ export const useDashboardsData = () => { const queryArguments = getAllQueryArguments(); const params = new URLSearchParams(queryArguments); - params.set(QueryParams.Project, activeProject); + + let projectToUse = activeProject; + if (!activeProject) { + const dashboardMetadata = combinedDashboardsMetadata.find((item) => item.name === newBoard); + projectToUse = dashboardMetadata?.project; + } + + if (projectToUse) { + params.set(QueryParams.Project, projectToUse); + } params.set(QueryParams.Dashboard, newBoard); - let url = getDashboardsUrl(perspective); + let url = getDashboardUrl(perspective); url = `${url}?${params.toString()}`; if (newBoard !== dashboardName) { navigate(url, { replace: true }); } }, - [perspective, dashboardName, navigate, activeProject], + [perspective, dashboardName, navigate, activeProject, combinedDashboardsMetadata], ); - // If a dashboard hasn't been selected yet, or if the current project doesn't have a - // matching board name then display the board present in the URL parameters or the first - // board in the dropdown list - useEffect(() => { - const metadataMatch = activeProjectDashboardsMetadata.find((activeProjectDashboardMetadata) => { - return ( - activeProjectDashboardMetadata.project === activeProject && - activeProjectDashboardMetadata.name === dashboardName - ); - }); - if (!dashboardName || !metadataMatch) { - changeBoard(activeProjectDashboardsMetadata?.[0]?.name); - } - }, [dashboardName, changeBoard, activeProject, activeProjectDashboardsMetadata]); - return { persesAvailable, persesProjectsLoading, diff --git a/web/src/components/dashboards/perses/project/ProjectBar.tsx b/web/src/components/dashboards/perses/project/ProjectBar.tsx index 1e046597..1d785a7d 100644 --- a/web/src/components/dashboards/perses/project/ProjectBar.tsx +++ b/web/src/components/dashboards/perses/project/ProjectBar.tsx @@ -1,21 +1,34 @@ import type { SetStateAction, Dispatch, FC } from 'react'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { KEYBOARD_SHORTCUTS } from './utils'; +import { getDashboardsListUrl, usePerspective } from '../../../hooks/usePerspective'; import ProjectDropdown from './ProjectDropdown'; export type ProjectBarProps = { - setActiveProject: Dispatch>; - activeProject: string; + setActiveProject: Dispatch>; + activeProject: string | null; }; export const ProjectBar: FC = ({ setActiveProject, activeProject }) => { + const navigate = useNavigate(); + const { perspective } = usePerspective(); + return (
{ - setActiveProject(newProject); + const params = new URLSearchParams(); + if (newProject === '') { + setActiveProject(null); + } else { + params.set('project', newProject); + setActiveProject(newProject); + } + const url = `${getDashboardsListUrl(perspective)}?${params.toString()}`; + navigate(url); }} - selected={activeProject} + selected={activeProject || ''} shortCut={KEYBOARD_SHORTCUTS.focusNamespaceDropdown} />
diff --git a/web/src/components/dashboards/perses/project/ProjectDropdown.tsx b/web/src/components/dashboards/perses/project/ProjectDropdown.tsx index 490bc291..d79bc0fe 100644 --- a/web/src/components/dashboards/perses/project/ProjectDropdown.tsx +++ b/web/src/components/dashboards/perses/project/ProjectDropdown.tsx @@ -116,13 +116,15 @@ const ProjectMenu: React.FC<{ const optionItems = useMemo(() => { const items = persesProjects.map((item) => { const { name } = item.metadata; - return { title: item?.spec?.display?.name ?? name, key: name }; + const title = item?.spec?.display?.name ?? name ?? ''; + return { title, key: name ?? '' }; }); - if (!items.some((option) => option.key === selected)) { + if (selected && !items.some((option) => option.key === selected)) { items.push({ title: selected, key: selected }); // Add current project if it isn't included } items.sort((a, b) => alphanumericCompare(a.title, b.title)); + items.unshift({ title: 'All Projects', key: '' }); return items; }, [persesProjects, selected]); @@ -207,7 +209,7 @@ const ProjectDropdown: React.FC = ({ const selectedProject = persesProjects.find( (persesProject) => persesProject.metadata.name === selected, ); - const title = selectedProject?.spec?.display?.name ?? t('Dashboards'); + const title = selectedProject?.spec?.display?.name ?? t('All Projects'); return (
diff --git a/web/src/components/dashboards/perses/project/useActiveProject.tsx b/web/src/components/dashboards/perses/project/useActiveProject.tsx index ddaf13cc..5643ada8 100644 --- a/web/src/components/dashboards/perses/project/useActiveProject.tsx +++ b/web/src/components/dashboards/perses/project/useActiveProject.tsx @@ -11,7 +11,7 @@ import { QueryParams } from '../../../query-params'; import { StringParam, useQueryParam } from 'use-query-params'; export const useActiveProject = () => { - const [activeProject, setActiveProject] = useState(''); + const [activeProject, setActiveProject] = useState(null); const [activeNamespace, setActiveNamespace] = useActiveNamespace(); const { perspective } = usePerspective(); const { persesProjects, persesProjectsLoading } = usePerses(); @@ -24,7 +24,6 @@ export const useActiveProject = () => { // Sync the state and the URL param useEffect(() => { - // If data and url hasn't been set yet, default to legacy dashboard (for now) if (!activeProject && projectFromUrl) { setActiveProject(projectFromUrl); return; @@ -32,14 +31,10 @@ export const useActiveProject = () => { if (persesProjectsLoading) { return; } - if (!activeProject && !projectFromUrl) { - // set to first project - setActiveProject(persesProjects[0]?.metadata?.name); - return; - // If activeProject isn't set yet, but the url is, then load from url - } // If the url and the data is out of sync, follow the data - setProject(activeProject); + if (activeProject) { + setProject(activeProject); + } }, [ projectFromUrl, activeProject, diff --git a/web/src/components/dashboards/perses/project/utils.ts b/web/src/components/dashboards/perses/project/utils.ts index 16e3817c..4a0352ea 100644 --- a/web/src/components/dashboards/perses/project/utils.ts +++ b/web/src/components/dashboards/perses/project/utils.ts @@ -1,5 +1,8 @@ export const alphanumericCompare = (a: string, b: string): number => { - return a.localeCompare(b, undefined, { + const safeA = a || ''; + const safeB = b || ''; + + return safeA.localeCompare(safeB, undefined, { numeric: true, sensitivity: 'base', }); diff --git a/web/src/components/hooks/usePerspective.tsx b/web/src/components/hooks/usePerspective.tsx index e609b4cf..0f97b39e 100644 --- a/web/src/components/hooks/usePerspective.tsx +++ b/web/src/components/hooks/usePerspective.tsx @@ -294,7 +294,20 @@ export const getLegacyDashboardsUrl = ( } }; -export const getDashboardsUrl = (perspective: Perspective) => { +export const getDashboardUrl = (perspective: Perspective) => { + switch (perspective) { + case 'virtualization-perspective': + return `/virt-monitoring/v2/dashboards/view`; + case 'admin': + return `/monitoring/v2/dashboards/view`; + case 'acm': + return `/multicloud/monitoring/v2/dashboards/view`; + default: + return ''; + } +}; + +export const getDashboardsListUrl = (perspective: Perspective) => { switch (perspective) { case 'virtualization-perspective': return `/virt-monitoring/v2/dashboards`;