Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { useDataViewSort } from '@patternfly/react-data-view/dist/dynamic/Hooks'
import RhUiEllipsisVerticalFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-ellipsis-vertical-fill-icon';
import { ActionsColumn } from '@patternfly/react-table';
import type { DashboardResource } from '@perses-dev/client';
import { type FC, memo, type ReactNode, useCallback, useMemo, useState } from 'react';
import { type FC, memo, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link, useSearchParams } from 'react-router';

Expand All @@ -31,10 +31,14 @@ import {
import { useDashboardsData } from '@/features/perses-dashboards/hooks/useDashboardsData';
import { usePersesDashboardAccess } from '@/features/perses-dashboards/hooks/usePersesDashboardAccess';
import { DashboardListFrame } from '@/features/perses-dashboards/pages/dashboard-list-page/DashboardListFrame';
import {
type DashboardRow,
type DashboardRowNameLink,
sortDashboardData,
} from '@/features/perses-dashboards/pages/dashboard-list-page/sort-dashboards';
import { useTableColumns } from '@/shared/components/table/hooks/useTableColumns';
import { rowFilter, useTableFilters } from '@/shared/components/table/hooks/useTableFilters';
import { useTablePagination } from '@/shared/components/table/hooks/useTablePagination';
import { directedSort, localeCompareSort } from '@/shared/components/table/sort-utils';
import {
TableFilter,
TableFilterOption,
Expand Down Expand Up @@ -131,62 +135,12 @@ const DashboardActionsCell = memo(

DashboardActionsCell.displayName = 'DashboardActionsCell';

interface DashboardRowNameLink {
link: ReactNode;
label: string;
}

interface DashboardRow {
name: DashboardRowNameLink;
tags: ReactNode;
project: string;
created: ReactNode;
modified: ReactNode;
// Raw values for sorting
createdAt?: string;
updatedAt?: string;
// Reference to original dashboard data
dashboard: DashboardResource;
}

interface DashboardRowFilters {
name?: string;
'project-filter'?: string;
tags?: string;
}

const sortDashboardData = (
data: DashboardRow[],
sortBy: string | undefined,
direction: 'asc' | 'desc' | undefined,
): DashboardRow[] => {
if (!sortBy || !direction) {
return data;
}
if (sortBy === rowFilter('name')) {
return [...data].sort((a, b) => localeCompareSort(a.name.label, b.name.label, direction));
}
if (sortBy === rowFilter('project')) {
return [...data].sort((a, b) => localeCompareSort(a.project, b.project, direction));
}
if (sortBy === rowFilter('created')) {
return [...data].sort((a, b) => localeCompareSort(a.createdAt, b.createdAt, direction));
}
if (sortBy === rowFilter('modified')) {
return [...data].sort((a, b) => localeCompareSort(a.updatedAt, b.updatedAt, direction));
}
if (sortBy === rowFilter('tags')) {
return [...data].sort((a, b) =>
directedSort(
(a.dashboard.metadata?.tags?.length || 0) - (b.dashboard.metadata?.tags?.length || 0),
direction,
),
);
}

return data;
};

interface DashboardsTableProps {
persesDashboards: DashboardResource[];
persesDashboardsLoading: boolean;
Expand Down Expand Up @@ -216,7 +170,19 @@ const DashboardsTable: FC<DashboardsTableProps> = ({

const columnKeys = useMemo(
() => [
{ label: t('Dashboard'), key: rowFilter('name') },
{ label: t('Display Name'), key: rowFilter('name') },
{
label: t('Name'),
key: rowFilter('id'),
props: {
info: {
tooltip: t(
'This is the immutable, unique identifier for the dashboard. It cannot be changed after the dashboard is created.',
),
ariaLabel: t('More information on Dashboard ID'),
},
},
},
{ label: t('Project'), key: rowFilter('project') },
{ label: t('Tags'), key: rowFilter('tags') },
{ label: t('Created on'), key: rowFilter('created') },
Expand All @@ -233,7 +199,8 @@ const DashboardsTable: FC<DashboardsTableProps> = ({
}
return persesDashboards.map((board) => {
const metadata = board?.metadata;
const displayName = board?.spec?.display?.name || metadata?.name;
const id = metadata?.name;
const displayName = board?.spec?.display?.name || id;
const dashboardsParams = `?dashboard=${metadata?.name}&project=${metadata?.project}`;
const dashboardName: DashboardRowNameLink = {
link: (
Expand Down Expand Up @@ -263,6 +230,7 @@ const DashboardsTable: FC<DashboardsTableProps> = ({

return {
name: dashboardName,
id: id,
project: board?.metadata?.project || '',
tags: dashboardTags,
created: <Timestamp timestamp={metadata?.createdAt} />,
Expand Down Expand Up @@ -331,8 +299,9 @@ const DashboardsTable: FC<DashboardsTableProps> = ({
const pageRows: DataViewTr[] = useMemo(() => {
return sortedAndFilteredData
.slice((page - 1) * perPage, (page - 1) * perPage + perPage)
.map(({ name, project, tags, created, modified, dashboard }) => [
.map(({ name, id, project, tags, created, modified, dashboard }) => [
name.link,
id,
project,
tags,
created,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import type { DashboardResource } from '@perses-dev/client';

import {
type DashboardRow,
sortDashboardData,
} from '@/features/perses-dashboards/pages/dashboard-list-page/sort-dashboards';

const makeDashboard = (tags: string[]): DashboardResource =>
({ metadata: { tags } }) as unknown as DashboardResource;

const makeRow = (overrides: Partial<DashboardRow> & { id: string }): DashboardRow => ({
name: { link: null, label: overrides.id },
tags: null,
project: '',
created: null,
modified: null,
dashboard: makeDashboard([]),
...overrides,
});

const rowA = makeRow({
id: 'a-dashboard',
name: { link: null, label: 'Charlie' },
project: 'project-b',
createdAt: '2026-01-03T00:00:00Z',
updatedAt: '2026-01-01T00:00:00Z',
dashboard: makeDashboard(['one']),
});

const rowB = makeRow({
id: 'b-dashboard',
name: { link: null, label: 'Alpha' },
project: 'project-a',
createdAt: '2026-01-01T00:00:00Z',
updatedAt: '2026-01-03T00:00:00Z',
dashboard: makeDashboard(['one', 'two', 'three']),
});

const rowC = makeRow({
id: 'c-dashboard',
name: { link: null, label: 'Bravo' },
project: 'project-c',
createdAt: '2026-01-02T00:00:00Z',
updatedAt: '2026-01-02T00:00:00Z',
dashboard: makeDashboard(['one', 'two']),
});

const rows = [rowA, rowB, rowC];

describe('sortDashboardData', () => {
it('returns the data unchanged when sortBy is undefined', () => {
expect(sortDashboardData(rows, undefined, 'asc')).toBe(rows);
});

it('returns the data unchanged when direction is undefined', () => {
expect(sortDashboardData(rows, 'row-filter-name', undefined)).toBe(rows);
});

it('returns the data unchanged for an unrecognized sortBy', () => {
expect(sortDashboardData(rows, 'row-filter-unknown', 'asc')).toBe(rows);
});

it('sorts by name ascending and descending', () => {
const asc = sortDashboardData(rows, 'row-filter-name', 'asc');
expect(asc.map((r) => r.name.label)).toEqual(['Alpha', 'Bravo', 'Charlie']);

const desc = sortDashboardData(rows, 'row-filter-name', 'desc');
expect(desc.map((r) => r.name.label)).toEqual(['Charlie', 'Bravo', 'Alpha']);
});

it('sorts by id', () => {
const asc = sortDashboardData(rows, 'row-filter-id', 'asc');
expect(asc.map((r) => r.id)).toEqual(['a-dashboard', 'b-dashboard', 'c-dashboard']);
});

it('sorts by project', () => {
const asc = sortDashboardData(rows, 'row-filter-project', 'asc');
expect(asc.map((r) => r.project)).toEqual(['project-a', 'project-b', 'project-c']);
});

it('sorts by created date', () => {
const asc = sortDashboardData(rows, 'row-filter-created', 'asc');
expect(asc.map((r) => r.id)).toEqual(['b-dashboard', 'c-dashboard', 'a-dashboard']);
});

it('sorts by modified date', () => {
const asc = sortDashboardData(rows, 'row-filter-modified', 'asc');
expect(asc.map((r) => r.id)).toEqual(['a-dashboard', 'c-dashboard', 'b-dashboard']);
});

it('sorts by number of tags', () => {
const asc = sortDashboardData(rows, 'row-filter-tags', 'asc');
expect(asc.map((r) => r.id)).toEqual(['a-dashboard', 'c-dashboard', 'b-dashboard']);

const desc = sortDashboardData(rows, 'row-filter-tags', 'desc');
expect(desc.map((r) => r.id)).toEqual(['b-dashboard', 'c-dashboard', 'a-dashboard']);
});

it('does not mutate the original array', () => {
const original = [...rows];
sortDashboardData(rows, 'row-filter-name', 'desc');
expect(rows).toEqual(original);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { DashboardResource } from '@perses-dev/client';
import type { ReactNode } from 'react';

import { rowFilter } from '@/shared/components/table/hooks/useTableFilters';
import { directedSort, localeCompareSort } from '@/shared/components/table/sort-utils';

export interface DashboardRowNameLink {
link: ReactNode;
label: string;
}

export interface DashboardRow {
name: DashboardRowNameLink;
id: string;
tags: ReactNode;
project: string;
created: ReactNode;
modified: ReactNode;
// Raw values for sorting
createdAt?: string;
updatedAt?: string;
// Reference to original dashboard data
dashboard: DashboardResource;
}

export const sortDashboardData = (
data: DashboardRow[],
sortBy: string | undefined,
direction: 'asc' | 'desc' | undefined,
): DashboardRow[] => {
if (!sortBy || !direction) {
return data;
}
if (sortBy === rowFilter('name')) {
return [...data].sort((a, b) => localeCompareSort(a.name.label, b.name.label, direction));
}
if (sortBy === rowFilter('id')) {
return [...data].sort((a, b) => localeCompareSort(a.id, b.id, direction));
}
if (sortBy === rowFilter('project')) {
return [...data].sort((a, b) => localeCompareSort(a.project, b.project, direction));
}
if (sortBy === rowFilter('created')) {
return [...data].sort((a, b) => localeCompareSort(a.createdAt, b.createdAt, direction));
}
if (sortBy === rowFilter('modified')) {
return [...data].sort((a, b) => localeCompareSort(a.updatedAt, b.updatedAt, direction));
}
if (sortBy === rowFilter('tags')) {
return [...data].sort((a, b) =>
directedSort(
(a.dashboard.metadata?.tags?.length || 0) - (b.dashboard.metadata?.tags?.length || 0),
direction,
),
);
}

return data;
};