Skip to content

Commit 29b09c1

Browse files
authored
Fixes for AI model and Job list views (#9140)
Added several improvements for the AI model and job list views: - Use `<LinkButton>` components to prevent text wrap between the icon and action texts - Before: <img width="171" height="426" alt="Screenshot 2025-12-10 at 15 39 18" src="https://github.com/user-attachments/assets/42ae4fb4-3aaf-4fba-9de3-cd0dcebeb428" /> - Use modals obtain by the new antd hook API for proper light/dark mode styling - <img width="599" height="257" alt="Screenshot 2025-12-10 at 16 08 10" src="https://github.com/user-attachments/assets/cda5f4c4-770c-4fa8-b736-875182aa8ac6" /> ### URL of deployed dev instance (used for testing): - https://___.webknossos.xyz ### Steps to test: 1. Start a few tiff downloads or AI jobs (can be unsuccessful) 2. Navigate to Analysis > Jobs. Check the actions buttons text wrap 3. Navigate to Analysis > AI Models. Click on show training data. Modal show be correctly themed. Testing with multiple training annotations for the AI modal training might be blocked by #9141 ### Issues: - fixes #https://scm.slack.com/archives/C5AKLAV0B/p1765370965274249 ------ (Please delete unneeded items, merge only when none are left open) - [ ] Added changelog entry (create a `$PR_NUMBER.md` file in `unreleased_changes` or use `./tools/create-changelog-entry.py`) - [ ] Added migration guide entry if applicable (edit the same file as for the changelog) - [ ] Updated [documentation](../blob/master/docs) if applicable - [ ] Adapted [wk-libs python client](https://github.com/scalableminds/webknossos-libs/tree/master/webknossos/webknossos/client) if relevant API parts change - [ ] Removed dev-only changes like prints and application.conf edits - [x] Considered [common edge cases](../blob/master/.github/common_edge_cases.md) - [ ] Needs datastore update after deployment
1 parent 37f0da2 commit 29b09c1

File tree

2 files changed

+48
-45
lines changed

2 files changed

+48
-45
lines changed

frontend/javascripts/admin/job/job_list_view.tsx

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ import {
1313
import { PropTypes } from "@scalableminds/prop-types";
1414
import { useQuery, useQueryClient } from "@tanstack/react-query";
1515
import { cancelJob, getJobs, retryJob } from "admin/rest_api";
16-
import { Flex, Input, Modal, Spin, Table, Tooltip, Typography } from "antd";
16+
import { App, Flex, Input, Spin, Table, Tooltip, Typography } from "antd";
1717
import { AsyncLink } from "components/async_clickables";
1818
import FormattedDate from "components/formatted_date";
1919
import FormattedId from "components/formatted_id";
20+
import LinkButton from "components/link_button";
2021
import { confirmAsync } from "dashboard/dataset/helper_components";
2122
import { formatCreditsString, formatWkLibsNdBBox } from "libs/format_utils";
2223
import Persistence from "libs/persistence";
@@ -65,18 +66,23 @@ const { Column } = Table;
6566
const { Search } = Input;
6667

6768
export const getShowTrainingDataLink = (
69+
modal: ReturnType<typeof App.useApp>["modal"],
6870
trainingAnnotations: {
6971
annotationId: string;
7072
}[],
7173
) => {
7274
return trainingAnnotations == null ? null : trainingAnnotations.length > 1 ? (
73-
<a
75+
<LinkButton
76+
icon={<EyeOutlined />}
7477
onClick={() => {
75-
Modal.info({
78+
modal.info({
79+
title: "Training Data",
80+
closable: true,
81+
maskClosable: true,
7682
content: (
7783
<div>
7884
The following annotations were used during training:
79-
<ul>
85+
<ul style={{ padding: 15 }}>
8086
{trainingAnnotations.map((annotation: { annotationId: string }, index: number) => (
8187
<li key={`annotation_${index}`}>
8288
<a
@@ -95,15 +101,16 @@ export const getShowTrainingDataLink = (
95101
}}
96102
>
97103
Show Training Data
98-
</a>
104+
</LinkButton>
99105
) : (
100-
<a
106+
<LinkButton
107+
icon={<EyeOutlined />}
101108
href={`/annotations/${trainingAnnotations[0].annotationId}`}
102109
target="_blank"
103110
rel="noreferrer noopener"
104111
>
105112
Show Training Data
106-
</a>
113+
</LinkButton>
107114
);
108115
};
109116

@@ -134,6 +141,7 @@ export function JobState({ job }: { job: APIJob }) {
134141

135142
function JobListView() {
136143
const queryClient = useQueryClient();
144+
const { modal } = App.useApp();
137145
const { data: jobs, isLoading } = useQuery({
138146
queryKey: ["jobs"],
139147
queryFn: getJobs,
@@ -170,6 +178,7 @@ function JobListView() {
170178
function renderDescription(__: any, job: APIJob) {
171179
const linkToDataset = getLinkToDataset(job);
172180
const layerName = job.args.annotationLayerName || job.args.layerName;
181+
173182
if (job.command === APIJobCommand.CONVERT_TO_WKW && job.args.datasetName) {
174183
return <span>{`Conversion to WKW of ${job.args.datasetName}`}</span>;
175184
} else if (job.command === APIJobCommand.EXPORT_TIFF && linkToDataset != null) {
@@ -308,7 +317,7 @@ function JobListView() {
308317
return (
309318
<span>
310319
{`Train ${modelName} on ${numberOfTrainingAnnotations} ${Utils.pluralize("annotation", numberOfTrainingAnnotations)}. `}
311-
{getShowTrainingDataLink(job.args.trainingAnnotations)}
320+
{getShowTrainingDataLink(modal, job.args.trainingAnnotations)}
312321
</span>
313322
);
314323
} else {
@@ -371,8 +380,7 @@ function JobListView() {
371380
<span>
372381
{job.resultLink && (
373382
<Link to={job.resultLink} title="View Dataset">
374-
<EyeOutlined className="icon-margin-right" />
375-
View
383+
<LinkButton icon={<EyeOutlined />}>View</LinkButton>
376384
</Link>
377385
)}
378386
</span>
@@ -381,21 +389,19 @@ function JobListView() {
381389
return (
382390
<span>
383391
{job.resultLink && (
384-
<a href={job.resultLink} title="Download">
385-
<DownloadOutlined className="icon-margin-right" />
392+
<LinkButton href={job.resultLink} icon={<DownloadOutlined />}>
386393
Download
387-
</a>
394+
</LinkButton>
388395
)}
389396
</span>
390397
);
391398
} else if (job.command === APIJobCommand.RENDER_ANIMATION) {
392399
return (
393400
<span>
394401
{job.resultLink && (
395-
<a href={job.resultLink} title="Download">
396-
<DownloadOutlined className="icon-margin-right" />
402+
<LinkButton href={job.resultLink} icon={<DownloadOutlined />}>
397403
Download
398-
</a>
404+
</LinkButton>
399405
)}
400406
</span>
401407
);
@@ -435,9 +441,9 @@ function JobListView() {
435441
return (
436442
<span>
437443
{job.resultLink && (
438-
<a href={job.resultLink} title="Result">
444+
<LinkButton href={job.resultLink} icon={<DownloadOutlined />}>
439445
Result
440-
</a>
446+
</LinkButton>
441447
)}
442448
{job.returnValue && <p>{job.returnValue}</p>}
443449
</span>

frontend/javascripts/admin/voxelytics/ai_model_list_view.tsx

Lines changed: 24 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {
2-
EyeOutlined,
32
FileTextOutlined,
43
InfoCircleOutlined,
54
SyncOutlined,
@@ -8,22 +7,25 @@ import {
87
import { useQuery } from "@tanstack/react-query";
98
import { JobState, getShowTrainingDataLink } from "admin/job/job_list_view";
109
import { getAiModels, getUsersOrganizations, updateAiModel } from "admin/rest_api";
11-
import { Button, Col, Flex, Modal, Row, Select, Table, Tooltip, Typography } from "antd";
10+
import { App, Button, Col, Flex, Modal, Row, Select, Table, Tooltip, Typography } from "antd";
1211
import FormattedDate from "components/formatted_date";
12+
import LinkButton from "components/link_button";
1313
import { useFetch } from "libs/react_helpers";
1414
import { useWkSelector } from "libs/react_hooks";
1515
import Toast from "libs/toast";
1616
import uniq from "lodash/uniq";
1717
import { useState } from "react";
1818
import type { Key } from "react";
19+
import { formatUserName } from "viewer/model/accessors/user_accessor";
20+
1921
import { Link } from "react-router-dom";
2022
import type { AiModel } from "types/api_types";
21-
import { formatUserName } from "viewer/model/accessors/user_accessor";
2223
import { enforceActiveUser } from "viewer/model/accessors/user_accessor";
2324

2425
export default function AiModelListView() {
2526
const activeUser = useWkSelector((state) => enforceActiveUser(state.activeUser));
2627
const [currentlyEditedModel, setCurrentlyEditedModel] = useState<AiModel | null>(null);
28+
const { modal } = App.useApp();
2729

2830
const {
2931
data: aiModels = [],
@@ -123,7 +125,7 @@ export default function AiModelListView() {
123125
{
124126
title: "Actions",
125127
render: (aiModel: AiModel) =>
126-
renderActionsForModel(aiModel, () => setCurrentlyEditedModel(aiModel)),
128+
renderActionsForModel(modal, aiModel, () => setCurrentlyEditedModel(aiModel)),
127129
key: "actions",
128130
},
129131
]}
@@ -133,12 +135,15 @@ export default function AiModelListView() {
133135
);
134136
}
135137

136-
const renderActionsForModel = (model: AiModel, onChangeSharedOrganizations: () => void) => {
138+
const renderActionsForModel = (
139+
modal: ReturnType<typeof App.useApp>["modal"],
140+
model: AiModel,
141+
onChangeSharedOrganizations: () => void,
142+
) => {
137143
const organizationSharingButton = model.isOwnedByUsersOrganization ? (
138-
<a onClick={onChangeSharedOrganizations}>
139-
<TeamOutlined className="icon-margin-right" />
144+
<LinkButton onClick={onChangeSharedOrganizations} icon={<TeamOutlined />}>
140145
Manage Access
141-
</a>
146+
</LinkButton>
142147
) : null;
143148
if (model.trainingJob == null) {
144149
return organizationSharingButton;
@@ -150,22 +155,14 @@ const renderActionsForModel = (model: AiModel, onChangeSharedOrganizations: () =
150155
<Col>
151156
{trainingJobState === "SUCCESS" ? <Row>{organizationSharingButton}</Row> : null}
152157
{voxelyticsWorkflowHash != null ? (
153-
/* margin left is needed as organizationSharingButton is a button with a 16 margin */
154158
<Row>
155159
<Link to={`/workflows/${voxelyticsWorkflowHash}`}>
156-
<FileTextOutlined className="icon-margin-right" />
157-
Voxelytics Report
160+
<LinkButton icon={<FileTextOutlined />}>Voxelytics Report</LinkButton>
158161
</Link>
159162
</Row>
160163
) : null}
161164
{trainingAnnotations != null ? (
162-
<Row>
163-
<EyeOutlined
164-
className="icon-margin-right"
165-
style={{ color: "var(--ant-color-primary)" }}
166-
/>
167-
{getShowTrainingDataLink(trainingAnnotations)}
168-
</Row>
165+
<Row>{getShowTrainingDataLink(modal, trainingAnnotations)}</Row>
169166
) : null}
170167
</Col>
171168
);
@@ -211,7 +208,7 @@ function EditModelSharedOrganizationsModal({
211208

212209
return (
213210
<Modal
214-
title={`Edit Organizations with Access to AI Model ${model.name}`}
211+
title={"Edit Organizations with Access to this AI Model"}
215212
open
216213
onOk={submitNewSharedOrganizations}
217214
onCancel={onClose}
@@ -220,27 +217,27 @@ function EditModelSharedOrganizationsModal({
220217
width={800}
221218
>
222219
<p>
223-
Select all organization that should have access to the AI model{" "}
220+
Select all organizations that should have access to the AI model{" "}
224221
<Typography.Text italic>{model.name}</Typography.Text>.
225222
</p>
226223
<Typography.Paragraph type="secondary">
227-
You can only select or deselect organizations that you are a member of. However, other users
228-
in your organization may have granted access to additional organizations that you are not
229-
part of. Only members of your organization who have access to those organizations can modify
230-
their access.
224+
You can only manage access for organizations you belong to. Other members of your
225+
organization may have access to additional organizations not listed here. Only they can
226+
modify access for those organizations.
231227
</Typography.Paragraph>
232-
<Col span={14} offset={4}>
228+
<Flex justify="center">
233229
<Select
234230
mode="multiple"
235231
allowClear
236232
autoFocus
237-
style={{ width: "100%" }}
233+
style={{ minWidth: 400 }}
234+
dropdownMatchSelectWidth={false}
238235
placeholder="Please select"
239236
onChange={handleChange}
240237
options={options}
241238
value={selectedOrganizationIds}
242239
/>
243-
</Col>
240+
</Flex>
244241
</Modal>
245242
);
246243
}

0 commit comments

Comments
 (0)