Skip to content
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
26 changes: 10 additions & 16 deletions frontend/javascripts/admin/job/job_list_view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ import {
QuestionCircleTwoTone,
} from "@ant-design/icons";
import { PropTypes } from "@scalableminds/prop-types";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { cancelJob, getJobs, retryJob } from "admin/rest_api";
import { Input, Modal, Spin, Table, Tooltip, Typography } from "antd";
import { AsyncLink } from "components/async_clickables";
import FormattedDate from "components/formatted_date";
import { confirmAsync } from "dashboard/dataset/helper_components";
import { formatCreditsString, formatWkLibsNdBBox } from "libs/format_utils";
import Persistence from "libs/persistence";
import { useInterval } from "libs/react_helpers";
import { useWkSelector } from "libs/react_hooks";
import Toast from "libs/toast";
import * as Utils from "libs/utils";
Expand Down Expand Up @@ -132,26 +132,20 @@ export function JobState({ job }: { job: APIJob }) {
}

function JobListView() {
const [isLoading, setIsLoading] = useState(true);
const [jobs, setJobs] = useState<APIJob[]>([]);
const queryClient = useQueryClient();
const { data: jobs, isLoading } = useQuery({
queryKey: ["jobs"],
queryFn: getJobs,
refetchInterval: refreshInterval,
});
const [searchQuery, setSearchQuery] = useState("");
const isCurrentUserSuperUser = useWkSelector((state) => state.activeUser?.isSuperUser);

useEffect(() => {
fetchData();
const { searchQuery } = persistence.load();
setSearchQuery(searchQuery || "");
setIsLoading(false);
}, []);

async function fetchData() {
setJobs(await getJobs());
}

useInterval(async () => {
setJobs(await getJobs());
}, refreshInterval);

useEffect(() => {
persistence.persist({ searchQuery });
}, [searchQuery]);
Expand Down Expand Up @@ -316,7 +310,7 @@ function JobListView() {
});

if (isDeleteConfirmed) {
cancelJob(job.id).then(() => fetchData());
cancelJob(job.id).then(() => queryClient.invalidateQueries({ queryKey: ["jobs"] }));
}
}}
icon={<CloseCircleOutlined className="icon-margin-right" />}
Expand All @@ -331,7 +325,7 @@ function JobListView() {
onClick={async () => {
try {
await retryJob(job.id);
await fetchData();
await queryClient.invalidateQueries({ queryKey: ["jobs"] });
Toast.success("Job is being retried");
} catch (e) {
console.error("Could not retry job", e);
Expand Down Expand Up @@ -466,7 +460,7 @@ function JobListView() {
/>
<Spin spinning={isLoading} size="large">
<Table
dataSource={Utils.filterWithSearchQueryAND(jobs, ["datasetName"], searchQuery)}
dataSource={Utils.filterWithSearchQueryAND(jobs || [], ["datasetName"], searchQuery)}
rowKey="id"
pagination={{
defaultPageSize: 50,
Expand Down
227 changes: 114 additions & 113 deletions frontend/javascripts/admin/voxelytics/workflow_list_view.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { SyncOutlined } from "@ant-design/icons";
import { PropTypes } from "@scalableminds/prop-types";
import { useQuery } from "@tanstack/react-query";
import { getVoxelyticsWorkflows } from "admin/rest_api";
import { Button, Input, Progress, Table, Tooltip } from "antd";
import { Button, Input, Progress, Spin, Table, Tooltip } from "antd";
import { formatCountToDataAmountUnit, formatDateMedium, formatNumber } from "libs/format_utils";
import Persistence from "libs/persistence";
import { usePolling } from "libs/react_hooks";
import Toast from "libs/toast";
import * as Utils from "libs/utils";
import type React from "react";
Expand Down Expand Up @@ -59,8 +59,6 @@ type RenderRunInfo = Omit<VoxelyticsWorkflowListingRun, "userFirstName" | "userL
};

export default function WorkflowListView() {
const [isLoading, setIsLoading] = useState(false);
const [workflows, setWorkflows] = useState<Array<VoxelyticsWorkflowListing>>([]);
const [searchQuery, setSearchQuery] = useState("");

function handleSearch(event: React.ChangeEvent<HTMLInputElement>): void {
Expand All @@ -70,30 +68,30 @@ export default function WorkflowListView() {
useEffect(() => {
const { searchQuery } = persistence.load();
setSearchQuery(searchQuery || "");
loadData();
}, []);

useEffect(() => {
persistence.persist({ searchQuery });
}, [searchQuery]);

async function loadData() {
setIsLoading(true);
try {
const _workflows = (await getVoxelyticsWorkflows()).map(parseWorkflowInfo);
setWorkflows(_workflows);
} catch (err) {
Toast.error("Could not load workflow list.");
console.error(err);
} finally {
setIsLoading(false);
}
}

usePolling(async () => {
// initial data fetch is done above, thus only load data here if it is polled repeatedly
if (VX_POLLING_INTERVAL != null) loadData();
}, VX_POLLING_INTERVAL);
const {
data: workflows = [],
isLoading,
refetch,
isFetching,
} = useQuery({
queryKey: ["voxelyticsWorkflows"],
queryFn: async () => {
try {
return (await getVoxelyticsWorkflows()).map(parseWorkflowInfo);
} catch (err) {
Toast.error("Could not load workflow list.");
console.error(err);
throw err;
}
},
refetchInterval: VX_POLLING_INTERVAL ?? false,
});

const getUserDisplayName = (run: VoxelyticsWorkflowListingRun) => {
return run.userFirstName != null || run.userLastName != null
Expand Down Expand Up @@ -172,8 +170,8 @@ export default function WorkflowListView() {
return (
<div className="container voxelytics-view">
<div className="pull-right">
<Button onClick={() => loadData()} style={{ marginRight: 20 }}>
<SyncOutlined spin={isLoading} /> Refresh
<Button onClick={() => refetch()} style={{ marginRight: 20 }}>
<SyncOutlined spin={isFetching} /> Refresh
</Button>
<Search
style={{
Expand All @@ -184,96 +182,99 @@ export default function WorkflowListView() {
/>
</div>
<h3>Voxelytics Workflows</h3>
<Table
bordered
rowKey={(run: RenderRunInfo) => `${run.id}-${run.workflowHash}`}
pagination={{ pageSize: 100 }}
columns={[
{
title: "Workflow",
key: "workflow",
render: (run: RenderRunInfo) =>
run.id === "" ? (
<Link to={`/workflows/${run.workflowHash}`}>
{run.workflowName} ({run.workflowHash})
</Link>
) : (
<Link to={`/workflows/${run.workflowHash}?runId=${encodeURIComponent(run.id)}`}>
{run.name}
</Link>
<Spin spinning={isLoading} size="large">
<Table
bordered
rowKey={(run: RenderRunInfo) => `${run.id}-${run.workflowHash}`}
pagination={{ pageSize: 100 }}
columns={[
{
title: "Workflow",
key: "workflow",
render: (run: RenderRunInfo) =>
run.id === "" ? (
<Link to={`/workflows/${run.workflowHash}`}>
{run.workflowName} ({run.workflowHash})
</Link>
) : (
<Link to={`/workflows/${run.workflowHash}?runId=${encodeURIComponent(run.id)}`}>
{run.name}
</Link>
),
},
{
title: "User",
key: "userName",
dataIndex: "userDisplayName",
filters: uniqueify(renderRuns.map((run) => run.userDisplayName)).map((username) => ({
text: username || "",
value: username || "",
})),
onFilter: (value: Key | boolean, run: RenderRunInfo) =>
run.userDisplayName?.startsWith(String(value)) || false,
filterSearch: true,
},
{
title: "Host",
dataIndex: "hostName",
key: "host",
filters: uniqueify(renderRuns.map((run) => run.hostName)).map((hostname) => ({
text: hostname,
value: hostname,
})),
onFilter: (value: Key | boolean, run: RenderRunInfo) =>
run.hostName.startsWith(String(value)),
filterSearch: true,
},
{
title: "Progress",
key: "progress",
width: 200,
render: renderProgress,
},
{
title: "File Size",
key: "fileSize",
width: 200,
render: (run: RenderRunInfo) => (
<Tooltip
overlay={
<>
{formatCountToDataAmountUnit(run.taskCounts.fileSize)} •{" "}
{formatNumber(run.taskCounts.inodeCount)} inodes
<br />
Note: manual changes on disk are not reflected here
</>
}
>
{formatCountToDataAmountUnit(run.taskCounts.fileSize)}
</Tooltip>
),
},
{
title: "User",
key: "userName",
dataIndex: "userDisplayName",
filters: uniqueify(renderRuns.map((run) => run.userDisplayName)).map((username) => ({
text: username || "",
value: username || "",
})),
onFilter: (value: Key | boolean, run: RenderRunInfo) =>
run.userDisplayName?.startsWith(String(value)) || false,
filterSearch: true,
},
{
title: "Host",
dataIndex: "hostName",
key: "host",
filters: uniqueify(renderRuns.map((run) => run.hostName)).map((hostname) => ({
text: hostname,
value: hostname,
})),
onFilter: (value: Key | boolean, run: RenderRunInfo) =>
run.hostName.startsWith(String(value)),
filterSearch: true,
},
{
title: "Progress",
key: "progress",
width: 200,
render: renderProgress,
},
{
title: "File Size",
key: "fileSize",
width: 200,
render: (run: RenderRunInfo) => (
<Tooltip
overlay={
<>
{formatCountToDataAmountUnit(run.taskCounts.fileSize)} •{" "}
{formatNumber(run.taskCounts.inodeCount)} inodes
<br />
Note: manual changes on disk are not reflected here
</>
}
>
{formatCountToDataAmountUnit(run.taskCounts.fileSize)}
</Tooltip>
),
sorter: (a: RenderRunInfo, b: RenderRunInfo) =>
a.taskCounts.fileSize - b.taskCounts.fileSize,
},
{
title: "Begin",
key: "begin",
defaultSortOrder: "descend",
sorter: (a: RenderRunInfo, b: RenderRunInfo) =>
(a.beginTime?.getTime() ?? Number.POSITIVE_INFINITY) -
(b.beginTime?.getTime() ?? Number.POSITIVE_INFINITY),
render: (run: RenderRunInfo) => run.beginTime && formatDateMedium(run.beginTime),
},
{
title: "End",
key: "end",
sorter: (a: RenderRunInfo, b: RenderRunInfo) =>
(a.endTime?.getTime() ?? Number.POSITIVE_INFINITY) -
(b.endTime?.getTime() ?? Number.POSITIVE_INFINITY),
render: (run: RenderRunInfo) => run.endTime && formatDateMedium(run.endTime),
},
]}
dataSource={Utils.filterWithSearchQueryAND(renderRuns, ["workflowName"], searchQuery)}
/>
sorter: (a: RenderRunInfo, b: RenderRunInfo) =>
a.taskCounts.fileSize - b.taskCounts.fileSize,
},
{
title: "Begin",
key: "begin",
defaultSortOrder: "descend",
sorter: (a: RenderRunInfo, b: RenderRunInfo) =>
(a.beginTime?.getTime() ?? Number.POSITIVE_INFINITY) -
(b.beginTime?.getTime() ?? Number.POSITIVE_INFINITY),
render: (run: RenderRunInfo) => run.beginTime && formatDateMedium(run.beginTime),
},
{
title: "End",
key: "end",
sorter: (a: RenderRunInfo, b: RenderRunInfo) =>
(a.endTime?.getTime() ?? Number.POSITIVE_INFINITY) -
(b.endTime?.getTime() ?? Number.POSITIVE_INFINITY),
render: (run: RenderRunInfo) => run.endTime && formatDateMedium(run.endTime),
},
]}
dataSource={Utils.filterWithSearchQueryAND(renderRuns, ["workflowName"], searchQuery)}
locale={{ emptyText: null }}
/>
</Spin>
</div>
);
}
2 changes: 2 additions & 0 deletions unreleased_changes/9095.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
### Fixed
- Fixed loading state spinner for Jobs and VX Workflow list views