Skip to content
Open
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
@@ -0,0 +1,48 @@
import type { Meta, StoryObj } from '@storybook/nextjs';
import { DetailsRSEProtocolsTable } from './DetailsRSEProtocolsTable';
import { fixtureRSEDetailsProtocol } from '@/test/fixtures/table-fixtures';
import { RSEDetailsProtocol } from '@/lib/core/entity/rucio';

const meta = {
title: 'Components/Pages/RSE/Details/ProtocolsTable',
component: DetailsRSEProtocolsTable,
parameters: {
docs: { disable: true },
},
// Mirrors the fixed-height container the table sits in on the RSE details page
decorators: [
Story => (
<div className="rounded-lg bg-neutral-0 dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 shadow-sm overflow-hidden h-80">
<Story />
</div>
),
],
} satisfies Meta<typeof DetailsRSEProtocolsTable>;

export default meta;
type Story = StoryObj<typeof meta>;

const withPrefix = (prefix: string): RSEDetailsProtocol => ({ ...fixtureRSEDetailsProtocol(), prefix });

export const Default: Story = {
args: {
rowData: Array.from({ length: 12 }, fixtureRSEDetailsProtocol),
},
};

/**
* The prefixes seen on deployed RSEs are deep storage paths. They should be readable
* without resizing the column or hovering over the cell.
*/
export const LongPrefixes: Story = {
args: {
rowData: [
withPrefix('/pnfs/fnal.gov/usr/cms/WAX/11/store/'),
withPrefix('/dpm/in2p3.fr/home/cms/phedex/store/mc/RunIISummer20UL18/'),
withPrefix('/eos/atlas/atlasdatadisk/rucio/'),
withPrefix('/pnfs/rucio/disk-only/scratchdisk/'),
withPrefix('/rucio/tmpdisk/rucio_tests/'),
withPrefix('/tmp/rucio_rse/'),
],
},
};
267 changes: 137 additions & 130 deletions src/component-library/pages/RSE/details/DetailsRSEProtocolsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,150 +2,157 @@ import React, { useRef, useState } from 'react';
import { AgGridReact } from 'ag-grid-react';
import { RegularTable } from '@/component-library/features/table/RegularTable/RegularTable';
import { RSEDetailsProtocol } from '@/lib/core/entity/rucio';
import { ValueGetterParams } from 'ag-grid-community';
import { ColDef, ColGroupDef, ValueGetterParams } from 'ag-grid-community';
import { DefaultTextFilterParams } from '@/component-library/features/utils/filter-parameters';

type DetailsRSEProtocolsTableProps = {
rowData: RSEDetailsProtocol[];
};

export const DetailsRSEProtocolsTable = (props: DetailsRSEProtocolsTableProps) => {
const tableRef = useRef<AgGridReact<RSEDetailsProtocol>>(null);

const [columnDefs] = useState([
{
headerName: 'Protocol',
children: [
{
headerName: 'Scheme',
field: 'scheme',
width: 120,
flex: 0,
pinned: 'left',
filter: true,
filterParams: DefaultTextFilterParams,
},
{
headerName: 'Hostname',
field: 'hostname',
width: 250,
flex: 0,
filter: true,
filterParams: DefaultTextFilterParams,
},
{
headerName: 'Port',
field: 'port',
width: 100,
flex: 0,
filter: 'agNumberColumnFilter',
},
{
headerName: 'Prefix',
field: 'prefix',
width: 200,
flex: 0,
filter: true,
filterParams: DefaultTextFilterParams,
/** Exported so the column sizing can be asserted without rendering the grid. */
export const getProtocolColumnDefs = (): (ColDef<RSEDetailsProtocol> | ColGroupDef<RSEDetailsProtocol>)[] => [
{
headerName: 'Protocol',
children: [
{
headerName: 'Scheme',
field: 'scheme',
width: 120,
flex: 0,
pinned: 'left',
filter: true,
filterParams: DefaultTextFilterParams,
},
{
headerName: 'Hostname',
field: 'hostname',
width: 250,
flex: 0,
filter: true,
filterParams: DefaultTextFilterParams,
},
{
headerName: 'Port',
field: 'port',
width: 100,
flex: 0,
filter: 'agNumberColumnFilter',
},
{
headerName: 'Prefix',
field: 'prefix',
minWidth: 300,
flex: 1,
// Deployed prefixes are deep storage paths: wrap rather than widen the table, so the
// full value stays visible without forcing horizontal scrolling on narrow viewports.
wrapText: true,
autoHeight: true,
filter: true,
filterParams: DefaultTextFilterParams,
},
],
},
{
headerName: 'LAN Priorities',
children: [
{
headerName: 'Read',
valueGetter: (params: ValueGetterParams<RSEDetailsProtocol>) => {
return params.data?.domains.lan.read;
},
],
},
{
headerName: 'LAN Priorities',
children: [
{
headerName: 'Read',
valueGetter: (params: ValueGetterParams<RSEDetailsProtocol>) => {
return params.data?.domains.lan.read;
},
width: 90,
flex: 0,
sortable: true,
filter: 'agNumberColumnFilter',
width: 90,
flex: 0,
sortable: true,
filter: 'agNumberColumnFilter',
},
{
headerName: 'Write',
valueGetter: (params: ValueGetterParams<RSEDetailsProtocol>) => {
return params.data?.domains.lan.write;
},
{
headerName: 'Write',
valueGetter: (params: ValueGetterParams<RSEDetailsProtocol>) => {
return params.data?.domains.lan.write;
},
width: 90,
flex: 0,
sortable: true,
filter: 'agNumberColumnFilter',
width: 90,
flex: 0,
sortable: true,
filter: 'agNumberColumnFilter',
},
{
headerName: 'Delete',
valueGetter: (params: ValueGetterParams<RSEDetailsProtocol>) => {
return params.data?.domains.lan.delete;
},
{
headerName: 'Delete',
valueGetter: (params: ValueGetterParams<RSEDetailsProtocol>) => {
return params.data?.domains.lan.delete;
},
width: 90,
flex: 0,
sortable: true,
filter: 'agNumberColumnFilter',
width: 90,
flex: 0,
sortable: true,
filter: 'agNumberColumnFilter',
},
],
},
{
headerName: 'WAN Priorities',
children: [
{
headerName: 'Read',
valueGetter: (params: ValueGetterParams<RSEDetailsProtocol>) => {
return params.data?.domains.wan.read;
},
],
},
{
headerName: 'WAN Priorities',
children: [
{
headerName: 'Read',
valueGetter: (params: ValueGetterParams<RSEDetailsProtocol>) => {
return params.data?.domains.wan.read;
},
width: 90,
flex: 0,
sortable: true,
filter: 'agNumberColumnFilter',
width: 90,
flex: 0,
sortable: true,
filter: 'agNumberColumnFilter',
},
{
headerName: 'Write',
valueGetter: (params: ValueGetterParams<RSEDetailsProtocol>) => {
return params.data?.domains.wan.write;
},
{
headerName: 'Write',
valueGetter: (params: ValueGetterParams<RSEDetailsProtocol>) => {
return params.data?.domains.wan.write;
},
width: 90,
flex: 0,
sortable: true,
filter: 'agNumberColumnFilter',
width: 90,
flex: 0,
sortable: true,
filter: 'agNumberColumnFilter',
},
{
headerName: 'Delete',
valueGetter: (params: ValueGetterParams<RSEDetailsProtocol>) => {
return params.data?.domains.wan.delete;
},
{
headerName: 'Delete',
valueGetter: (params: ValueGetterParams<RSEDetailsProtocol>) => {
return params.data?.domains.wan.delete;
},
width: 90,
flex: 0,
sortable: true,
filter: 'agNumberColumnFilter',
width: 90,
flex: 0,
sortable: true,
filter: 'agNumberColumnFilter',
},
],
},
{
headerName: 'Third Party Copy',
children: [
{
headerName: 'Read',
valueGetter: (params: ValueGetterParams<RSEDetailsProtocol>) => {
return params.data?.domains.wan.third_party_copy_read;
},
],
},
{
headerName: 'Third Party Copy',
children: [
{
headerName: 'Read',
valueGetter: (params: ValueGetterParams<RSEDetailsProtocol>) => {
return params.data?.domains.wan.third_party_copy_read;
},
width: 90,
flex: 0,
sortable: true,
filter: 'agNumberColumnFilter',
width: 90,
flex: 0,
sortable: true,
filter: 'agNumberColumnFilter',
},
{
headerName: 'Write',
valueGetter: (params: ValueGetterParams<RSEDetailsProtocol>) => {
return params.data?.domains.wan.third_party_copy_write;
},
{
headerName: 'Write',
valueGetter: (params: ValueGetterParams<RSEDetailsProtocol>) => {
return params.data?.domains.wan.third_party_copy_write;
},
width: 90,
flex: 0,
sortable: true,
filter: 'agNumberColumnFilter',
},
],
},
]);
width: 90,
flex: 0,
sortable: true,
filter: 'agNumberColumnFilter',
},
],
},
];

export const DetailsRSEProtocolsTable = (props: DetailsRSEProtocolsTableProps) => {
const tableRef = useRef<AgGridReact<RSEDetailsProtocol>>(null);

const [columnDefs] = useState(getProtocolColumnDefs);

return <RegularTable columnDefs={columnDefs} tableRef={tableRef} {...props} />;
};
33 changes: 33 additions & 0 deletions test/component/DetailsRSEProtocolsTable.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { getProtocolColumnDefs } from '@/component-library/pages/RSE/details/DetailsRSEProtocolsTable';
import { RSEDetailsProtocol } from '@/lib/core/entity/rucio';
import { ColDef, ColGroupDef } from 'ag-grid-community';

type ProtocolColDef = ColDef<RSEDetailsProtocol>;
type ProtocolColumn = ProtocolColDef | ColGroupDef<RSEDetailsProtocol>;

const isGroup = (def: ProtocolColumn): def is ColGroupDef<RSEDetailsProtocol> => 'children' in def;

const leafColumns = (defs: ProtocolColumn[]): ProtocolColDef[] => defs.flatMap(def => (isGroup(def) ? leafColumns(def.children) : [def]));

const prefixColumn = (): ProtocolColDef => {
const column = leafColumns(getProtocolColumnDefs()).find(def => def.field === 'prefix');
if (!column) throw new Error('prefix column is missing from the protocols table');
return column;
};

describe('DetailsRSEProtocolsTable column sizing', () => {
it('grows the prefix column instead of truncating deep storage paths', () => {
const prefix = prefixColumn();
// A fixed width pins the column regardless of viewport, which is what truncated prefixes.
expect(prefix.width).toBeUndefined();
expect(prefix.flex).toBeGreaterThan(0);
expect(prefix.minWidth).toBeGreaterThanOrEqual(300);
});

it('wraps the prefix so long paths stay fully visible without widening the table', () => {
const prefix = prefixColumn();
// Widening far enough for the longest paths would force horizontal scrolling instead.
expect(prefix.wrapText).toBe(true);
expect(prefix.autoHeight).toBe(true);
});
});
Loading