Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sortable header for component list in web UI #5642

Merged
merged 16 commits into from
Nov 2, 2023
Merged
Show file tree
Hide file tree
Changes from 14 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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,18 @@ 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)

- Make component list sortable in web UI. (@hainenber)
wildum marked this conversation as resolved.
Show resolved Hide resolved

- 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)

hainenber marked this conversation as resolved.
Show resolved Hide resolved
v0.37.3 (2023-10-26)
-----------------

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;
}
}
wildum marked this conversation as resolved.
Show resolved Hide resolved
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) {
wildum marked this conversation as resolved.
Show resolved Hide resolved
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);
};
wildum marked this conversation as resolved.
Show resolved Hide resolved

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