Skip to content

Commit 4075937

Browse files
matt-bernsteinnick-skriabinbmartelrobot-ci-heartexmcanu
authored
feat: DIA-1924: "Import sample data" ability in the Data Import page (#7206)
Co-authored-by: Nick Skriabin <[email protected]> Co-authored-by: bmartel <[email protected]> Co-authored-by: robot-ci-heartex <[email protected]> Co-authored-by: Nick Skriabin <[email protected]> Co-authored-by: Marcel Canu <[email protected]> Co-authored-by: mcanu <[email protected]> Co-authored-by: pakelley <[email protected]>
1 parent a92dff0 commit 4075937

File tree

15 files changed

+409
-205
lines changed

15 files changed

+409
-205
lines changed

web/apps/labelstudio/src/pages/CreateProject/CreateProject.jsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ export const CreateProject = ({ onClose, redirect = true }) => {
8989
const [name, setName] = React.useState("");
9090
const [error, setError] = React.useState();
9191
const [description, setDescription] = React.useState("");
92+
const [sample, setSample] = React.useState(null);
93+
9294
const setStep = React.useCallback((step) => {
9395
_setStep(step);
9496
const eventNameMap = {
@@ -103,7 +105,7 @@ export const CreateProject = ({ onClose, redirect = true }) => {
103105
setError(null);
104106
}, [name]);
105107

106-
const { columns, uploading, uploadDisabled, finishUpload, pageProps } = useImportPage(project);
108+
const { columns, uploading, uploadDisabled, finishUpload, pageProps, uploadSample } = useImportPage(project, sample);
107109

108110
const rootClass = cn("create-project");
109111
const tabClass = rootClass.elem("tab");
@@ -132,6 +134,10 @@ export const CreateProject = ({ onClose, redirect = true }) => {
132134
if (!imported) return;
133135

134136
setWaitingStatus(true);
137+
138+
if (sample) {
139+
await uploadSample(sample);
140+
}
135141
const response = await api.callApi("updateProject", {
136142
params: {
137143
pk: project.id,
@@ -211,8 +217,10 @@ export const CreateProject = ({ onClose, redirect = true }) => {
211217
<ImportPage
212218
project={project}
213219
show={step === "import"}
214-
{...pageProps}
220+
sample={sample}
221+
onSampleDatasetSelect={setSample}
215222
openLabelingConfig={() => setStep("config")}
223+
{...pageProps}
216224
/>
217225
<ConfigPage
218226
project={project}

web/apps/labelstudio/src/pages/CreateProject/Import/Import.jsx

Lines changed: 57 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,21 @@
1-
import { useCallback, useEffect, useReducer, useRef, useState } from "react";
2-
import { Modal } from "../../../components/Modal/Modal";
3-
import { cn } from "../../../utils/bem";
1+
import { ff } from "@humansignal/core";
2+
import { SampleDatasetSelect } from "@humansignal/core/blocks/SampleDatasetSelect/SampleDatasetSelect";
3+
import { IconError, IconFileUpload, IconInfo, IconTrash, IconUpload } from "@humansignal/icons";
4+
import { Badge } from "@humansignal/shad/components/ui/badge";
45
import { cn as scn } from "@humansignal/shad/utils";
56
import { CodeBlock, SimpleCard } from "@humansignal/ui";
6-
import { unique } from "../../../utils/helpers";
7-
import "./Import.scss";
8-
import { IconError, IconFileUpload, IconInfo, IconUpload } from "@humansignal/icons";
9-
import { useAPI } from "../../../providers/ApiProvider";
10-
import Input from "libs/datamanager/src/components/Common/Input/Input";
117
import { Button } from "apps/labelstudio/src/components";
128
import { useAtomValue } from "jotai";
9+
import Input from "libs/datamanager/src/components/Common/Input/Input";
10+
import { useCallback, useEffect, useReducer, useRef, useState } from "react";
11+
import { Modal } from "../../../components/Modal/Modal";
12+
import { useAPI } from "../../../providers/ApiProvider";
13+
import { cn } from "../../../utils/bem";
14+
import { unique } from "../../../utils/helpers";
1315
import { sampleDatasetAtom } from "../utils/atoms";
14-
import { ff } from "@humansignal/core";
15-
16-
const testCode = `
17-
import { SimpleCard } from "../simple-card";
18-
19-
export function CodeBlock({
20-
code,
21-
title,
22-
description,
23-
className,
24-
}: {
25-
title?: string;
26-
description?: string;
27-
code: string;
28-
className?: string;
29-
}) {
30-
return (
31-
<SimpleCard title={title} description={description} className={className}>
32-
<div className="whitespace-pre-wrap font-mono mt-2 p-3 bg-gray-100 rounded-sm">{code}</div>
33-
</SimpleCard>
34-
);
35-
}
36-
`;
16+
import "./Import.scss";
17+
import samples from "./samples.json";
18+
import { importFiles } from "./utils";
3719

3820
const importClass = cn("upload_page");
3921
const dropzoneClass = cn("dropzone");
@@ -175,9 +157,11 @@ const ErrorMessage = ({ error }) => {
175157

176158
export const ImportPage = ({
177159
project,
160+
sample,
178161
show = true,
179162
onWaiting,
180163
onFileListUpdate,
164+
onSampleDatasetSelect,
181165
highlightCsvHandling,
182166
dontCommitToProject = false,
183167
csvHandling,
@@ -211,7 +195,7 @@ export const ImportPage = ({
211195
};
212196

213197
const [files, dispatch] = useReducer(processFiles, { uploaded: [], uploading: [], ids: [] });
214-
const showList = Boolean(files.uploaded?.length || files.uploading?.length);
198+
const showList = Boolean(files.uploaded?.length || files.uploading?.length || sample);
215199

216200
const loadFilesList = useCallback(
217201
async (file_upload_ids) => {
@@ -267,27 +251,18 @@ export const ImportPage = ({
267251
[addColumns, loadFilesList, setLoading],
268252
);
269253

270-
const importFiles = useCallback(
254+
const importFilesImmediately = useCallback(
271255
async (files, body) => {
272-
dispatch({ sending: files });
273-
274-
const query = dontCommitToProject ? { commit_to_project: "false" } : {};
275-
// @todo use json for dataset uploads by URL
276-
const contentType =
277-
body instanceof FormData
278-
? "multipart/form-data" // usual multipart for usual files
279-
: "application/x-www-form-urlencoded"; // chad urlencoded for URL uploads
280-
const res = await api.callApi("importFiles", {
281-
params: { pk: project.id, ...query },
282-
headers: { "Content-Type": contentType },
256+
importFiles({
257+
files,
283258
body,
284-
errorFilter: () => true,
259+
project,
260+
onError,
261+
onFinish,
262+
onUploadStart: (files) => dispatch({ sending: files }),
263+
onUploadFinish: (files) => dispatch({ sent: files }),
264+
dontCommitToProject,
285265
});
286-
287-
if (res && !res.error) onFinish?.(res);
288-
else onError?.(res?.response);
289-
290-
dispatch({ sent: files });
291266
},
292267
[project, onFinish],
293268
);
@@ -306,9 +281,9 @@ export const ImportPage = ({
306281
}
307282
fd.append(f.name, f);
308283
}
309-
return importFiles(files, fd);
284+
return importFilesImmediately(files, fd);
310285
},
311-
[importFiles, onStart],
286+
[importFilesImmediately, onStart],
312287
);
313288

314289
const onUpload = useCallback(
@@ -333,9 +308,9 @@ export const ImportPage = ({
333308
onWaiting?.(true);
334309
const body = new URLSearchParams({ url });
335310

336-
importFiles([{ name: url }], body);
311+
importFilesImmediately([{ name: url }], body);
337312
},
338-
[importFiles],
313+
[importFilesImmediately],
339314
);
340315

341316
const openConfig = useCallback(
@@ -375,7 +350,7 @@ export const ImportPage = ({
375350
{highlightCsvHandling && <div className={importClass.elem("csv-splash")} />}
376351
<input id="file-input" type="file" name="file" multiple onChange={onUpload} style={{ display: "none" }} />
377352

378-
<header>
353+
<header className="flex gap-4">
379354
<form className={`${importClass.elem("url-form")} inline-flex`} method="POST" onSubmit={onLoadURL}>
380355
<Input placeholder="Dataset URL" name="url" ref={urlRef} style={{ height: 40 }} />
381356
<Button type="submit" look="primary">
@@ -391,6 +366,9 @@ export const ImportPage = ({
391366
<IconUpload width="16" height="16" className={importClass.elem("upload-icon")} />
392367
Upload {files.uploaded.length ? "More " : ""}Files
393368
</Button>
369+
{ff.isActive(ff.FF_SAMPLE_DATASETS) && (
370+
<SampleDatasetSelect samples={samples} sample={sample} onSampleApplied={onSampleDatasetSelect} />
371+
)}
394372
<div
395373
className={importClass.elem("csv-handling").mod({ highlighted: highlightCsvHandling, hidden: !csvHandling })}
396374
>
@@ -475,10 +453,33 @@ export const ImportPage = ({
475453
{showList && (
476454
<table>
477455
<tbody>
456+
{sample && (
457+
<tr key={sample.url}>
458+
<td>
459+
<div className="flex items-center gap-2">
460+
{sample.title}
461+
<Badge variant="info" className="h-5 text-xs rounded-sm">
462+
Sample
463+
</Badge>
464+
</div>
465+
</td>
466+
<td>{sample.description}</td>
467+
<td>
468+
<Button
469+
size="icon"
470+
look="destructive"
471+
style={{ height: 26, width: 26, padding: 0 }}
472+
onClick={() => onSampleDatasetSelect(undefined)}
473+
>
474+
<IconTrash style={{ width: 12, height: 12 }} />
475+
</Button>
476+
</td>
477+
</tr>
478+
)}
478479
{files.uploading.map((file, idx) => (
479480
<tr key={`${idx}-${file.name}`}>
480481
<td>{file.name}</td>
481-
<td>
482+
<td colSpan={2}>
482483
<span className={importClass.elem("file-status").mod({ uploading: true })} />
483484
</td>
484485
</tr>

web/apps/labelstudio/src/pages/CreateProject/Import/Import.scss

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
}
4545

4646
& + span {
47-
margin: 0 16px;
4847
color: var(--sand_600);
4948
}
5049
}

web/apps/labelstudio/src/pages/CreateProject/Import/ImportModal.jsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ export const Inner = () => {
1818
const refresh = useRefresh();
1919
const { project } = useProject();
2020
const [waiting, setWaitingStatus] = useState(false);
21+
const [sample, setSample] = useState(null);
2122
const api = useAPI();
2223

23-
const { uploading, uploadDisabled, finishUpload, fileIds, pageProps } = useImportPage(project);
24+
const { uploading, uploadDisabled, finishUpload, fileIds, pageProps, uploadSample } = useImportPage(project);
2425

2526
const backToDM = useCallback(() => {
2627
const path = location.pathname.replace(ImportModal.path, "");
@@ -46,11 +47,17 @@ export const Inner = () => {
4647
}, [modal, project, fileIds, backToDM]);
4748

4849
const onFinish = useCallback(async () => {
50+
await uploadSample(
51+
sample,
52+
() => setWaitingStatus(true),
53+
() => setWaitingStatus(false),
54+
);
55+
4956
const imported = await finishUpload();
5057

5158
if (!imported) return;
5259
backToDM();
53-
}, [backToDM, finishUpload]);
60+
}, [backToDM, finishUpload, sample]);
5461

5562
return (
5663
<Modal
@@ -78,11 +85,13 @@ export const Inner = () => {
7885
</Modal.Header>
7986
<ImportPage
8087
project={project}
81-
{...pageProps}
88+
sample={sample}
89+
onSampleDatasetSelect={setSample}
8290
projectConfigured={Object.keys(project.parsed_label_config ?? {}).length > 0}
8391
openLabelingConfig={() => {
8492
history.push(`/projects/${project.id}/settings/labeling`);
8593
}}
94+
{...pageProps}
8695
/>
8796
</Modal>
8897
);
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[
2+
{
3+
"title": "COCO 2017 Images",
4+
"description": "A 100-image subset of the COCO image segmentation benchmark dataset.",
5+
"url": "https://hs-sandbox-pub.s3.us-east-1.amazonaws.com/sample_data/coco_2017_val_100tasks.json"
6+
},
7+
{
8+
"title": "CoNLL 2012 NER",
9+
"description": "The CoNLL dataset of 13k sentences for Named Entity Recognition.",
10+
"url": "https://hs-sandbox-pub.s3.us-east-1.amazonaws.com/sample_data/conll_2012.json"
11+
}
12+
]

web/apps/labelstudio/src/pages/CreateProject/Import/useImportPage.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import React from "react";
1+
import React, { useCallback } from "react";
22
import { useAPI } from "../../../providers/ApiProvider";
33
import { unique } from "../../../utils/helpers";
4+
import { importFiles } from "./utils";
45

56
const DEFAULT_COLUMN = "$undefined$";
67

7-
export const useImportPage = (project) => {
8+
export const useImportPage = (project, sample) => {
89
const [uploading, setUploadingStatus] = React.useState(false);
910
const [fileIds, setFileIds] = React.useState([]);
1011
const [_columns, _setColumns] = React.useState([]);
@@ -35,6 +36,21 @@ export const useImportPage = (project) => {
3536
return imported;
3637
};
3738

39+
const uploadSample = useCallback(
40+
async (sample, onStart, onFinish) => {
41+
onStart?.();
42+
const url = sample.url;
43+
const body = new URLSearchParams({ url });
44+
await importFiles({
45+
files: [{ name: url }],
46+
body,
47+
project,
48+
});
49+
onFinish?.();
50+
},
51+
[project],
52+
);
53+
3854
const pageProps = {
3955
onWaiting: setUploadingStatus,
4056
// onDisableSubmit: onDisableSubmit,
@@ -46,5 +62,5 @@ export const useImportPage = (project) => {
4662
dontCommitToProject: true,
4763
};
4864

49-
return { columns, uploading, uploadDisabled, finishUpload, fileIds, pageProps };
65+
return { columns, uploading, uploadDisabled, finishUpload, fileIds, pageProps, uploadSample };
5066
};
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { API } from "apps/labelstudio/src/providers/ApiProvider";
2+
3+
export const importFiles = async ({
4+
files,
5+
body,
6+
project,
7+
onUploadStart,
8+
onUploadFinish,
9+
onFinish,
10+
onError,
11+
dontCommitToProject,
12+
}: {
13+
files: { name: string }[];
14+
body: Record<string, any> | FormData;
15+
project: APIProject;
16+
onUploadStart?: (files: { name: string }[]) => void;
17+
onUploadFinish?: (files: { name: string }[]) => void;
18+
onFinish?: (response: any) => void;
19+
onError?: (response: any) => void;
20+
dontCommitToProject?: boolean;
21+
}) => {
22+
onUploadStart?.(files);
23+
24+
const query = dontCommitToProject ? { commit_to_project: "false" } : {};
25+
26+
const contentType =
27+
body instanceof FormData
28+
? "multipart/form-data" // usual multipart for usual files
29+
: "application/x-www-form-urlencoded"; // chad urlencoded for URL uploads
30+
const res = await API.invoke(
31+
"importFiles",
32+
{ pk: project.id, ...query },
33+
{ headers: { "Content-Type": contentType }, body },
34+
);
35+
36+
if (res && !res.error) onFinish?.(res);
37+
else onError?.(res?.response);
38+
39+
onUploadFinish?.(files);
40+
};
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
// TODO: migrate all usages to core feature flags instead of local ones
22
// local ones used in LSO, Editor and DM
3-
export * from "@humansignal/core/lib/utils/feature-flags";
3+
export * from "@humansignal/core/lib/utils/feature-flags/ff";

0 commit comments

Comments
 (0)