Skip to content
Draft
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
12 changes: 6 additions & 6 deletions web/ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 29 additions & 7 deletions web/ui/react-app/src/components/approvals/service-info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import type { FC } from 'react';
import { ServiceActionRelease } from '@/components/approvals';
import ServiceInfoDeployedVersion from '@/components/approvals/service-info--deployed-version';
import ServiceInfoLatestVersion from '@/components/approvals/service-info--latest-version';
import { useToolbar } from '@/components/approvals/toolbar/toolbar-context';
import { CardTimestamp } from '@/constants/toolbar';
import { relativeDate } from '@/utils';
import type { ServiceSummary } from '@/utils/api/types/config/summary';

Expand All @@ -24,6 +26,9 @@ const ServiceInfo: FC<ServiceInfoProps> = ({
updateAvailable,
updateSkipped,
}) => {
const { cardTimestamps } = useToolbar();
const status = service?.status;

return (
<div className="flex size-full min-h-22 flex-col gap-y-2">
<ul className="wrap-anywhere mb-auto flex w-full flex-col gap-1">
Expand All @@ -46,13 +51,30 @@ const ServiceInfo: FC<ServiceInfoProps> = ({
updateSkipped={updateSkipped}
/>
)}
<small className="w-full items-center font-medium text-muted-foreground text-xs leading-none">
{service?.status?.last_queried ? (
<>queried {relativeDate(new Date(service?.status.last_queried))}</>
) : service?.loading ? (
'loading'
) : (
'no successful queries'
<small className="flex w-full flex-col gap-1 font-medium text-muted-foreground text-xs leading-none">
{cardTimestamps.includes(CardTimestamp.Deployed) &&
status?.deployed_version_timestamp && (
<span data-timestamp={CardTimestamp.Deployed}>
deployed{' '}
{relativeDate(new Date(status.deployed_version_timestamp))}
</span>
)}
{cardTimestamps.includes(CardTimestamp.Found) &&
status?.latest_version_timestamp && (
<span data-timestamp={CardTimestamp.Found}>
found {relativeDate(new Date(status.latest_version_timestamp))}
</span>
)}
{cardTimestamps.includes(CardTimestamp.Queried) && (
<span data-timestamp={CardTimestamp.Queried}>
{status?.last_queried ? (
<>queried {relativeDate(new Date(status.last_queried))}</>
) : service?.loading ? (
'loading'
) : (
'no successful queries'
)}
</span>
)}
</small>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
type HideValueType,
TABLE_COLUMNS_ORDER_STORAGE_KEY,
toolbarHideOptions,
toolbarTimestampOptions,
} from '@/constants/toolbar';
import { getServiceSummaries } from '@/hooks/use-services';
import {
Expand All @@ -55,6 +56,8 @@ const FilterDropdown: FC = () => {
const queryClient = useQueryClient();
const {
values,
cardTimestamps,
toggleCardTimestamp,
setHide,
setView,
tableInstance,
Expand Down Expand Up @@ -212,19 +215,45 @@ const FilterDropdown: FC = () => {
<DropdownMenuCheckboxItem
checked={isSelected}
key={key}
onClick={() => handleHideOptionClick(key)}
onClick={(event) => {
event.preventDefault();
handleHideOptionClick(key);
}}
>
{label}
</DropdownMenuCheckboxItem>
);
})}
<DropdownMenuItem
className="cursor-pointer"
onClick={handleResetHideFilters}
onClick={(event) => {
event.preventDefault();
handleResetHideFilters();
}}
>
Reset
</DropdownMenuItem>
</DropdownMenuGroup>
{values.view === APPROVALS_TOOLBAR_VIEW.GRID.value && (
<>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel>Timestamps:</DropdownMenuLabel>
{toolbarTimestampOptions.map(({ key, label }) => (
<DropdownMenuCheckboxItem
checked={cardTimestamps.includes(key)}
key={key}
onClick={(event) => {
event.preventDefault();
toggleCardTimestamp(key);
}}
>
{label}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuGroup>
</>
)}
<DropdownMenuSeparator className="sm:hidden" />
<DropdownMenuGroup className="sm:hidden">
<DropdownMenuLabel>Layout:</DropdownMenuLabel>
Expand Down Expand Up @@ -261,7 +290,10 @@ const FilterDropdown: FC = () => {
{tableColumnOptions}
<DropdownMenuItem
className="cursor-pointer"
onClick={handleResetColumns}
onClick={(event) => {
event.preventDefault();
handleResetColumns();
}}
>
Reset
</DropdownMenuItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from 'react';
import type {
ApprovalsToolbarOptions,
CardTimestampType,
ToolbarViewOption,
} from '@/constants/toolbar';
import type { TagsTriType } from '@/types/util';
Expand All @@ -27,6 +28,11 @@ export type ToolbarContextValue = {
/* Toggles the 'editMode' value in the URL */
toggleEditMode: () => void;

/* Timestamps shown at the bottom of the service card */
cardTimestamps: CardTimestampType[];
/* Toggles a timestamp shown at the bottom of the service card */
toggleCardTimestamp: (value: CardTimestampType) => void;

/* Tanstack table instance */
tableInstance?: Table<ServiceSummary>;
/* Function to set the table instance */
Expand Down
24 changes: 24 additions & 0 deletions web/ui/react-app/src/constants/toolbar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,28 @@ export const DEFAULT_HIDE_VALUE: HideValueType[] = [
HideValue.Inactive,
] as const;

/* Timestamps that can be shown at the bottom of the service card. */
export const CardTimestamp = {
Deployed: 'deployed',
Found: 'found',
Queried: 'queried',
} as const;

export type CardTimestampType =
(typeof CardTimestamp)[keyof typeof CardTimestamp];

/* Card timestamp options for the toolbar. */
export const toolbarTimestampOptions = [
{ key: CardTimestamp.Deployed, label: 'Show deployed' },
{ key: CardTimestamp.Found, label: 'Show found' },
{ key: CardTimestamp.Queried, label: 'Show queried' },
] as const;

/* Timestamps shown until the user says otherwise. */
export const DEFAULT_CARD_TIMESTAMPS: CardTimestampType[] = [
CardTimestamp.Queried,
] as const;

/* Query params for the toolbar. */
export const URL_PARAMS = {
EDIT_MODE: 'editMode',
Expand All @@ -77,3 +99,5 @@ export const TABLE_COLUMNS_HIDDEN_STORAGE_KEY = 'tableColumnsHidden';
export const TABLE_COLUMNS_VISIBLE_STORAGE_KEY_LEGACY = 'tableColumnsVisible';
/* Storage key for the order of table columns. */
export const TABLE_COLUMNS_ORDER_STORAGE_KEY = 'tableColumnsOrder';
/* Storage key for the timestamps shown under each service on a card. */
export const CARD_TIMESTAMPS_STORAGE_KEY = 'cardTimestamps';
20 changes: 20 additions & 0 deletions web/ui/react-app/src/pages/approvals/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ToolbarProvider } from '@/components/approvals/toolbar/toolbar-context'
import {
APPROVALS_TOOLBAR_VIEW,
type ApprovalsToolbarOptions,
type CardTimestampType,
DEFAULT_HIDE_VALUE,
DEFAULT_VIEW_VALUE,
HideValue,
Expand All @@ -21,6 +22,10 @@ import { GridLayout } from '@/pages/approvals/layouts/grid';
import { TableLayout } from '@/pages/approvals/layouts/table/table';
import type { TagsTriType } from '@/types/util';
import type { ServiceSummary } from '@/utils/api/types/config/summary';
import {
loadCardTimestamps,
persistCardTimestamps,
} from '@/utils/card-timestamps';
import { visibleServices as getVisibleServices } from '@/utils/visible-services';

const toolbarDefaults: ApprovalsToolbarOptions = {
Expand Down Expand Up @@ -225,9 +230,23 @@ export const Approvals = (): ReactElement => {
const [tableColumnVisibility, setTableColumnVisibility] =
useState<VisibilityState>({});

// Timestamps shown at the bottom of the service card..
const [cardTimestamps, setCardTimestamps] =
useState<CardTimestampType[]>(loadCardTimestamps);
const toggleCardTimestamp = useCallback((value: CardTimestampType) => {
setCardTimestamps((current) => {
const next = current.includes(value)
? current.filter((timestamp) => timestamp !== value)
: [...current, value];
persistCardTimestamps(next);
return next;
});
}, []);

return (
<ToolbarProvider
value={{
cardTimestamps,
hasOrderChanged,
onSaveOrder: handleSaveOrder,
setHide,
Expand All @@ -241,6 +260,7 @@ export const Approvals = (): ReactElement => {
tableColumnVisibility,

tableInstance,
toggleCardTimestamp,
toggleEditMode,

values: toolbarOptions,
Expand Down
32 changes: 32 additions & 0 deletions web/ui/react-app/src/utils/card-timestamps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {
CARD_TIMESTAMPS_STORAGE_KEY,
type CardTimestampType,
DEFAULT_CARD_TIMESTAMPS,
toolbarTimestampOptions,
} from '@/constants/toolbar';

/**
* Reads which timestamps to show at the bottom of the service card.
* An empty string means every timestamp was switched off, which is distinct
* from the key being absent (never configured).
*
* @returns The enabled timestamps.
*/
export const loadCardTimestamps = (): CardTimestampType[] => {
const stored = localStorage.getItem(CARD_TIMESTAMPS_STORAGE_KEY);
if (stored === null) return [...DEFAULT_CARD_TIMESTAMPS];

const enabled = new Set(stored.split(',').filter(Boolean));
return toolbarTimestampOptions
.map(({ key }) => key)
.filter((key) => enabled.has(key));
};

/**
* Persists the timestamps to show at the bottom of the service card.
*
* @param timestamps - The enabled timestamps.
*/
export const persistCardTimestamps = (timestamps: CardTimestampType[]) => {
localStorage.setItem(CARD_TIMESTAMPS_STORAGE_KEY, timestamps.join(','));
};
4 changes: 4 additions & 0 deletions web/ui/react-app/src/utils/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
export { default as beautifyGoErrors } from './beautify-go-errors';
export {
loadCardTimestamps,
persistCardTimestamps,
} from './card-timestamps';
export {
containsEndsWith,
containsStartsWith,
Expand Down
10 changes: 4 additions & 6 deletions web/ui/react-app/src/utils/relative-date.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { formatRelative } from 'date-fns';
import { formatDistanceToNow } from 'date-fns';
import { enGB } from 'date-fns/locale';

/**
* Returns a relative date string.
* Returns how long ago date was, e.g. '20 days ago'.
*
* @param date - The date to format.
*/
const relativeDate = (date: Date) => {
const now = new Date();
return formatRelative(date, now, { locale: enGB });
};
const relativeDate = (date: Date) =>
formatDistanceToNow(date, { addSuffix: true, locale: enGB });
export default relativeDate;
15 changes: 15 additions & 0 deletions web/ui/react-app/tests/dashboard-table.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,19 @@ test.describe('Dashboard table view', () => {
await expect(page.getByRole('table')).toBeVisible();
await expect(iconHeader).toBeHidden();
});

test('the card timestamp options are not offered', async ({ page }) => {
// GIVEN: timestamps are a grid-only option.
const timestampLabels = ['Show deployed', 'Show found', 'Show queried'];

// WHEN: the filter dropdown is opened in the table view.
await page.getByRole('button', { name: 'Filter shown services' }).click();
await expect(page.getByRole('menu')).toBeVisible();

// THEN: none of them are listed.
for (const label of timestampLabels)
await expect(
page.getByRole('menuitemcheckbox', { exact: true, name: label }),
).toHaveCount(0);
});
});
Loading
Loading