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

feat: sort and filter function for workflow table on Web UI #147

Merged
merged 3 commits into from
Jun 1, 2022
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
1 change: 1 addition & 0 deletions admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"@fortawesome/fontawesome-free": "^6.1.1",
"@mui/icons-material": "^5.8.0",
"@mui/material": "^5.8.1",
"@tanstack/react-table": "^8.0.0-beta.3",
"bulma": "^0.9.4",
"fontsource-roboto": "^4.0.0",
"mermaid": "^9.1.1",
Expand Down
4 changes: 0 additions & 4 deletions admin/src/components/Mermaid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@ function Mermaid({ def, style = {} }: Props) {
if (!ref.current) {
return;
}
console.log({
def,
});
if (def.startsWith("<")) {
console.error("invalid definition!!");
return;
Expand All @@ -41,7 +38,6 @@ function Mermaid({ def, style = {} }: Props) {
"mermaid",
def,
(svgCode, bindFunc) => {
console.log({ svgCode });
if (ref.current) {
// @ts-ignore
ref.current.innerHTML = svgCode;
Expand Down
5 changes: 4 additions & 1 deletion admin/src/components/StatusChip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import { statusColorMapping } from "../consts";
import { SchedulerStatus } from "../models/Status";

type Props = {
status: SchedulerStatus;
status?: SchedulerStatus;
children: string;
};

function StatusChip({ status, children }: Props) {
const style = React.useMemo(() => {
if (!status) {
return {};
}
return statusColorMapping[status] || {};
}, [status]);
return (
Expand Down
308 changes: 257 additions & 51 deletions admin/src/components/WorkflowTable.tsx
Original file line number Diff line number Diff line change
@@ -1,70 +1,276 @@
import React from "react";
import WorkflowTableRow from "./WorkflowTableRow";
import WorkflowTableRowGroup from "./WorkflowTableRowGroup";
import { DAG } from "../models/Dag";
import { Group } from "../models/Group";
import {
createTable,
useTableInstance,
getCoreRowModel,
getSortedRowModel,
SortingState,
getFilteredRowModel,
} from "@tanstack/react-table";
import StatusChip from "./StatusChip";
import {
Box,
Chip,
Stack,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
TableSortLabel,
TextField,
} from "@mui/material";
import { Link } from "react-router-dom";
import {
getFirstTag,
getStatus,
getStatusField,
WorkflowData,
WorkflowDataType,
} from "../models/Workflow";
import StyledTableRow from "./StyledTableRow";

type Props = {
workflows: DAG[];
groups: Group[];
workflows: WorkflowData[];
group: string;
};

function WorkflowTable({ workflows = [], groups = [], group = "" }: Props) {
const sorted = React.useMemo(() => {
return workflows.sort((a, b) => {
if (a.File < b.File) {
return -1;
const table = createTable()
.setRowType<WorkflowData>()
.setFilterMetaType<WorkflowData>()
.setTableMetaType<{
group: string;
}>();

const defaultColumns = [
table.createDataColumn("Name", {
id: "Workflow",
header: "Workflow",
cell: (props) => {
const data = props.row.original!;
if (data.Type == WorkflowDataType.Group) {
const url = `/dags/?group=${encodeURI(data.Group.Name)}`;
return <Link to={url}>{props.getValue()}</Link>;
} else {
const group = props.instance.options.meta?.group || "";
const url = `/dags/${encodeURI(
data.DAG.File.replace(/\.[^/.]+$/, "")
)}?group=${encodeURI(group)}`;
return <Link to={url}>{props.getValue()}</Link>;
}
},
}),
table.createDataColumn("Type", {
id: "Type",
header: "Type",
cell: (props) => {
const data = props.row.original!;
if (data.Type == WorkflowDataType.Group) {
return <Chip color="secondary" size="small" label="Group" />;
} else {
return <Chip color="primary" size="small" label="Workflow" />;
}
},
sortingFn: (a, b) => {
const dataA = a.original;
const dataB = b.original;
return dataA!.Type - dataB!.Type;
},
}),
table.createDataColumn("Type", {
id: "Tags",
header: "Tags",
cell: (props) => {
const data = props.row.original!;
if (data.Type == WorkflowDataType.Workflow) {
const tags = data.DAG.Config.Tags;
return (
<Stack direction="row" spacing={1}>
{tags?.map((tag) => (
<Chip key={tag} size="small" label={tag} />
))}
</Stack>
);
}
if (a.File > b.File) {
return 1;
return null;
},
sortingFn: (a, b) => {
let valA = getFirstTag(a.original);
let valB = getFirstTag(b.original);
return valA.localeCompare(valB);
},
}),
table.createDataColumn("Type", {
id: "Config",
header: "Description",
enableSorting: false,
cell: (props) => {
const data = props.row.original!;
if (data.Type == WorkflowDataType.Workflow) {
return data.DAG.Config.Description;
}
return 0;
});
}, [workflows]);
return null;
},
}),
table.createDataColumn("Type", {
id: "Status",
header: "Status",
cell: (props) => {
const data = props.row.original!;
if (data.Type == WorkflowDataType.Workflow) {
return (
<StatusChip status={data.DAG.Status?.Status}>
{data.DAG.Status?.StatusText || ""}
</StatusChip>
);
}
return null;
},
sortingFn: (a, b) => {
let valA = getStatus(a.original);
let valB = getStatus(b.original);
return valA < valB ? -1 : 1;
},
}),
table.createDataColumn("Type", {
id: "Started At",
header: "Started At",
cell: (props) => {
const data = props.row.original!;
if (data.Type == WorkflowDataType.Workflow) {
return data.DAG.Status?.StartedAt;
}
return null;
},
sortingFn: (a, b) => {
const dataA = a.original!;
const dataB = b.original!;
let valA = getStatusField("StartedAt", dataA);
let valB = getStatusField("StartedAt", dataB);
return valA.localeCompare(valB);
},
}),
table.createDataColumn("Type", {
id: "Finished At",
header: "Finished At",
cell: (props) => {
const data = props.row.original!;
if (data.Type == WorkflowDataType.Workflow) {
return data.DAG.Status?.FinishedAt;
}
return null;
},
sortingFn: (a, b) => {
const dataA = a.original!;
const dataB = b.original!;
let valA = getStatusField("FinishedAt", dataA);
let valB = getStatusField("FinishedAt", dataB);
return valA.localeCompare(valB);
},
}),
];

function WorkflowTable({ workflows = [], group = "" }: Props) {
const [columns] = React.useState<typeof defaultColumns>(() => [
...defaultColumns,
]);

const [sorting, setSorting] = React.useState<SortingState>([]);
const [globalFilter, setGlobalFilter] = React.useState("");

const instance = useTableInstance(table, {
data: workflows,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onGlobalFilterChange: setGlobalFilter,
globalFilterFn: (row, _, globalFilter) => {
const data = row.original;
if (!data) {
return false;
}
if (data.Type == WorkflowDataType.Group) {
return data.Name.toLowerCase().includes(globalFilter);
}
const workflow = data.DAG;
if (workflow.Config.Name.toLowerCase().includes(globalFilter)) {
return true;
}
if (workflow.Config.Description.toLowerCase().includes(globalFilter)) {
return true;
}
const tags = workflow.Config?.Tags;
if (
tags &&
tags.some((tag) => tag.toLowerCase().includes(globalFilter))
) {
return true;
}
return false;
},
state: {
sorting,
globalFilter,
},
onSortingChange: setSorting,
meta: {
group,
},
});

return (
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Workflow</TableCell>
<TableCell>Type</TableCell>
<TableCell>Tags</TableCell>
<TableCell>Name</TableCell>
<TableCell>Description</TableCell>
<TableCell>Status</TableCell>
<TableCell>Pid</TableCell>
<TableCell>Started At</TableCell>
<TableCell>Finished At</TableCell>
</TableRow>
</TableHead>
<TableBody>
{group != "" ? (
<WorkflowTableRowGroup url={encodeURI("/dags/")} text="../"></WorkflowTableRowGroup>
) : null}
{groups.map((item) => {
const url = encodeURI("/dags/?group=" + item.Name);
return <WorkflowTableRowGroup key={item.Name} url={url} text={item.Name} />;
})}
{sorted
.filter((wf) => !wf.Error)
.map((wf) => {
return (
<WorkflowTableRow
key={wf.File}
workflow={wf}
group={group}
></WorkflowTableRow>
);
})}
</TableBody>
</Table>
<Box>
<TextField
label="Filter"
size="small"
InputProps={{
value: globalFilter || "",
onChange: (value) => {
const data = value.target.value;
setGlobalFilter(data);
},
type: "search",
}}
/>
<Table size="small">
<TableHead>
{instance.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableCell key={header.id} colSpan={header.colSpan}>
<Box
{...{
sx: {
cursor: header.column.getCanSort()
? "pointer"
: "default",
},
onClick: header.column.getToggleSortingHandler(),
}}
>
{header.isPlaceholder ? null : header.renderHeader()}
{{
asc: <TableSortLabel direction="asc" />,
desc: <TableSortLabel direction="desc" />,
}[header.column.getIsSorted() as string] ?? null}
</Box>
</TableCell>
))}
</TableRow>
))}
</TableHead>
<TableBody>
{instance.getRowModel().rows.map((row) => (
<StyledTableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>{cell.renderCell()}</TableCell>
))}
</StyledTableRow>
))}
</TableBody>
</Table>
</Box>
);
}
export default WorkflowTable;
Loading