Skip to content

Commit

Permalink
Sortable header for component list in web UI (#5642)
Browse files Browse the repository at this point in the history
Signed-off-by: hainenber <[email protected]>
Co-authored-by: William Dumont <[email protected]>
Co-authored-by: Paulin Todev <[email protected]>
Co-authored-by: Paschalis Tsilias <[email protected]>
  • Loading branch information
4 people authored Nov 2, 2023
1 parent 0a23b6d commit d27f011
Show file tree
Hide file tree
Showing 10 changed files with 129 additions and 16 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,16 @@ Main (unreleased)

- Added an `exclude_event_message` option to `loki.source.windowsevent` in flow mode,
which excludes the human-friendly event message from Windows event logs. (@ptodev)

- Improve detection of rolled log files in `loki.source.kubernetes` and
`loki.source.podlogs` (@slim-bean).

- Support clustering in `loki.source.kubernetes` (@slim-bean).

- Support clustering in `loki.source.podlogs` (@rfratto).

- Make component list sortable in web UI. (@hainenber)

- Adds new metrics (`mssql_server_total_memory_bytes`, `mssql_server_target_memory_bytes`,
and `mssql_available_commit_memory_bytes`) for `mssql` integration.

Expand Down
12 changes: 9 additions & 3 deletions web/ui/src/features/component/ComponentList.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { NavLink } from 'react-router-dom';

import { HealthLabel } from '../component/HealthLabel';
import { ComponentInfo } from '../component/types';
import { ComponentInfo, SortOrder } from '../component/types';

import Table from './Table';

Expand All @@ -10,11 +10,12 @@ import styles from './ComponentList.module.css';
interface ComponentListProps {
components: ComponentInfo[];
moduleID?: string;
handleSorting?: (sortField: string, sortOrder: SortOrder) => void;
}

const TABLEHEADERS = ['Health', 'ID'];

const ComponentList = ({ components, moduleID }: ComponentListProps) => {
const ComponentList = ({ components, moduleID, handleSorting }: ComponentListProps) => {
const tableStyles = { width: '130px' };
const pathPrefix = moduleID ? moduleID + '/' : '';

Expand All @@ -39,7 +40,12 @@ const ComponentList = ({ components, moduleID }: ComponentListProps) => {

return (
<div className={styles.list}>
<Table tableHeaders={TABLEHEADERS} renderTableData={renderTableData} style={tableStyles} />
<Table
tableHeaders={TABLEHEADERS}
renderTableData={renderTableData}
handleSorting={handleSorting}
style={tableStyles}
/>
</div>
);
};
Expand Down
18 changes: 18 additions & 0 deletions web/ui/src/features/component/Table.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,21 @@
.table td:first-child {
width: 2%;
}

.table tr th[data-sort-order="desc"] {
cursor: pointer;
box-shadow: none !important;
&:before {
content: "▼";
float: right;
}
}

.table tr th[data-sort-order="asc"] {
cursor: pointer;
box-shadow: none !important;
&:before {
content: "▲";
float: right;
}
}
14 changes: 7 additions & 7 deletions web/ui/src/features/component/Table.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import React from 'react';

import TableHead from './TableHead';
import { SortOrder } from './types';

import styles from './Table.module.css';

interface Props {
tableHeaders: string[];
style?: React.CSSProperties;
handleSorting?: (sortField: string, sortOrder: SortOrder) => void;
renderTableData: () => JSX.Element[];
}

/**
* Simple table component that accept a custom header, and custom render
* Simple table component that accept a custom header, custom sorting function and custom render
* function for the table data
*/
const Table = ({ tableHeaders, style = {}, renderTableData }: Props) => {
const Table = ({ tableHeaders, style = {}, handleSorting, renderTableData }: Props) => {
return (
<table className={styles.table}>
<colgroup span={1} style={style} />
<tbody>
<tr>
{tableHeaders.map((header) => (
<th key={header}>{header}</th>
))}
</tr>
<TableHead headers={tableHeaders} handleSorting={handleSorting} />
{renderTableData()}
</tbody>
</table>
Expand Down
47 changes: 47 additions & 0 deletions web/ui/src/features/component/TableHead.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useState } from 'react';

import { SortOrder } from './types';

interface Props {
headers: string[];
handleSorting?: (header: string, sortOrder: SortOrder) => void;
}

const TableHead = ({ headers, handleSorting }: Props) => {
const [sortField, setSortField] = useState('ID');
const [order, setOrder] = useState(SortOrder.ASC);

const handleSortingChange = (header: string) => {
// User clicks on the new header, use default ASC sort order
let sortOrder = SortOrder.ASC;

// User clicks again on the header, we toggle the previous sort order
if (header === sortField) {
sortOrder = order === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;
}

if (handleSorting !== undefined) {
setSortField(header);
setOrder(sortOrder);
handleSorting(header, sortOrder);
}
};

return (
<tr>
{headers.map((header) => {
return (
<th
key={header}
onClick={() => handleSortingChange(header)}
data-sort-order={handleSorting && sortField === header ? order : undefined}
>
{header}
</th>
);
})}
</tr>
);
};

export default TableHead;
8 changes: 8 additions & 0 deletions web/ui/src/features/component/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,11 @@ export interface PartitionedBody {
attrs: AttrStmt[];
inner: PartitionedBody[];
}

/**
* Sort order for component list
*/
export enum SortOrder {
ASC = 'asc',
DESC = 'desc',
}
6 changes: 4 additions & 2 deletions web/ui/src/hooks/componentInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import { ComponentInfo } from '../features/component/types';
* @param fromComponent The component requesting component info. Required for
* determining the proper list of components from the context of a module.
*/
export const useComponentInfo = (moduleID: string): ComponentInfo[] => {
export const useComponentInfo = (
moduleID: string
): [ComponentInfo[], React.Dispatch<React.SetStateAction<ComponentInfo[]>>] => {
const [components, setComponents] = useState<ComponentInfo[]>([]);

useEffect(
Expand All @@ -29,5 +31,5 @@ export const useComponentInfo = (moduleID: string): ComponentInfo[] => {
[moduleID]
);

return components;
return [components, setComponents];
};
2 changes: 1 addition & 1 deletion web/ui/src/pages/ComponentDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const ComponentDetailPage: FC = () => {
const { '*': id } = useParams();

const { moduleID } = parseID(id || '');
const components = useComponentInfo(moduleID);
const [components] = useComponentInfo(moduleID);
const infoByID = componentInfoByID(components);

const [component, setComponent] = useState<ComponentDetail | undefined>(undefined);
Expand Down
2 changes: 1 addition & 1 deletion web/ui/src/pages/Graph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Page from '../features/layout/Page';
import { useComponentInfo } from '../hooks/componentInfo';

function Graph() {
const components = useComponentInfo('');
const [components] = useComponentInfo('');

return (
<Page name="Graph" desc="Relationships between defined components" icon={faDiagramProject}>
Expand Down
33 changes: 31 additions & 2 deletions web/ui/src/pages/PageComponentList.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,44 @@
import { faCubes } from '@fortawesome/free-solid-svg-icons';

import ComponentList from '../features/component/ComponentList';
import { ComponentInfo, SortOrder } from '../features/component/types';
import Page from '../features/layout/Page';
import { useComponentInfo } from '../hooks/componentInfo';

const fieldMappings: { [key: string]: (comp: ComponentInfo) => string | undefined } = {
Health: (comp) => comp.health?.state?.toString(),
ID: (comp) => comp.localID,
// Add new fields if needed here.
};

function getSortValue(component: ComponentInfo, field: string): string | undefined {
const valueGetter = fieldMappings[field];
return valueGetter ? valueGetter(component) : undefined;
}

function PageComponentList() {
const components = useComponentInfo('');
const [components, setComponents] = useComponentInfo('');

// TODO: make this sorting logic reusable
const handleSorting = (sortField: string, sortOrder: SortOrder): void => {
if (!sortField || !sortOrder) return;
const sorted = [...components].sort((a, b) => {
const sortValueA = getSortValue(a, sortField);
const sortValueB = getSortValue(b, sortField);
if (!sortValueA) return 1;
if (!sortValueB) return -1;
return (
sortValueA.localeCompare(sortValueB, 'en', {
numeric: true,
}) * (sortOrder === SortOrder.ASC ? 1 : -1)
);
});
setComponents(sorted);
};

return (
<Page name="Components" desc="List of defined components" icon={faCubes}>
<ComponentList components={components} />
<ComponentList components={components} handleSorting={handleSorting} />
</Page>
);
}
Expand Down

0 comments on commit d27f011

Please sign in to comment.