Skip to content

Commit

Permalink
fix: DEV-1690: Preview init (HumanSignal#2339)
Browse files Browse the repository at this point in the history
* Sync preview code with LSP

* Update build, introduce loading indicator
  • Loading branch information
nick-skriabin authored May 12, 2022
1 parent c8036da commit 25c1fed
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 41 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,4 @@ deploy/pgsql/certs/*
1/*
*/*/1/*
label_studio/tavern-output.json
docker-compose.override.yml
2 changes: 1 addition & 1 deletion label_studio/frontend/dist/react-app/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion label_studio/frontend/dist/react-app/index.js.map

Large diffs are not rendered by default.

23 changes: 20 additions & 3 deletions label_studio/frontend/src/pages/CreateProject/Config/Config.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ const formatXML = (xml) => {
}

let depth = 0;

try {
return xml.replace(/<(\/)?.*?(\/)?>[\s\n]*/g, (tag, close1, close2) => {
if (!close1) {
const res = " ".repeat(depth) + tag.trim() + "\n";

if (!close2) depth++;
return res;
} else {
Expand Down Expand Up @@ -78,6 +80,7 @@ const Label = ({ label, template, color }) => {
const ConfigureControl = ({ control, template }) => {
const refLabels = React.useRef();
const tagname = control.tagName;

if (tagname !== "Choices" && !tagname.endsWith("Labels")) return null;
const palette = Palette();

Expand Down Expand Up @@ -119,6 +122,7 @@ const ConfigureControl = ({ control, template }) => {

const ConfigureSettings = ({ template }) => {
const { settings } = template;

if (!settings) return null;
const keys = Object.keys(settings);

Expand All @@ -127,15 +131,18 @@ const ConfigureSettings = ({ template }) => {
const type = Array.isArray(options.type) ? Array : options.type;
const $object = options.object;
const $tag = options.control ? options.control : $object;

if (!$tag) return null;
if (options.when && !options.when($tag)) return;
let value = false;

if (options.value) value = options.value($tag);
else if (typeof options.param === "string") value = $tag.getAttribute(options.param);
if (value === "true") value = true;
if (value === "false") value = false;
let onChange;
let size;

switch (type) {
case Array:
onChange = e => {
Expand Down Expand Up @@ -198,6 +205,7 @@ const ConfigureSettings = ({ template }) => {
const ConfigureColumns = ({ columns, template }) => {
const updateValue = obj => e => {
const attrName = e.target.value.replace(/^\$/, "");

obj.setAttribute("value", "$" + attrName);
template.render();
};
Expand Down Expand Up @@ -241,6 +249,7 @@ const Configurator = ({ columns, config, project, template, setTemplate, onBrows
const [configure, setConfigure] = React.useState(isEmptyConfig(config) ? "code" : "visual");
const [visualLoaded, loadVisual] = React.useState(configure === "visual");
const [waiting, setWaiting] = React.useState(false);
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState();
const [configToCheck, setConfigToCheck] = React.useState();
const [data, setData] = React.useState();
Expand All @@ -256,6 +265,8 @@ const Configurator = ({ columns, config, project, template, setTemplate, onBrows
React.useEffect(async () => {
if (!configToCheck) return;

setLoading(true);

const validation = await api.callApi(`validateConfig`, {
params: { pk: project.id },
body: { label_config: configToCheck },
Expand All @@ -264,18 +275,20 @@ const Configurator = ({ columns, config, project, template, setTemplate, onBrows

if (validation?.error) {
setError(validation.response);
setLoading(false);
return;
}

setError(null);
onValidate?.(validation);

const sample = await api.callApi("createSampleTask", {
params: {pk: project.id },
params: { pk: project.id },
body: { label_config: configToCheck },
errorFilter: () => true,
});

setLoading(false);
if (sample && !sample.error) {
setData(sample.sample_task);
} else {
Expand All @@ -299,6 +312,7 @@ const Configurator = ({ columns, config, project, template, setTemplate, onBrows
setError(null);
setWaiting(true);
const res = await onSaveClick();

setWaiting(false);
if (res !== true) {
setError(res);
Expand Down Expand Up @@ -345,13 +359,13 @@ const Configurator = ({ columns, config, project, template, setTemplate, onBrows
</div>
{disableSaveButton !== true && onSaveClick && (
<Form.Actions size="small" extra={configure === "code" && extra} valid>
<Button look="primary" size="compact" style={{width: 120}} onClick={onSave} waiting={waiting}>
<Button look="primary" size="compact" style={{ width: 120 }} onClick={onSave} waiting={waiting}>
Save
</Button>
</Form.Actions>
)}
</div>
<Preview config={config} data={data} error={error} />
<Preview config={config} data={data} error={error} loading={loading} />
</div>
);
};
Expand All @@ -371,12 +385,14 @@ export const ConfigPage = ({ config: initialConfig = "", columns: externalColumn

const setTemplate = React.useCallback(config => {
const tpl = new Template({ config });

tpl.onConfigUpdate = setConfig;
setConfig(config);
setCurrentTemplate(tpl);
}, [setConfig, setCurrentTemplate]);

const [columns, setColumns] = React.useState();

React.useEffect(() => { if (externalColumns?.length) setColumns(externalColumns); }, [externalColumns]);

React.useEffect(async () => {
Expand All @@ -386,6 +402,7 @@ export const ConfigPage = ({ config: initialConfig = "", columns: externalColumn
// 404 is ok, and errors here don't matter
errorFilter: () => true,
});

if (res?.common_data_columns) {
setColumns(res.common_data_columns);
}
Expand Down
127 changes: 91 additions & 36 deletions label_studio/frontend/src/pages/CreateProject/Config/Preview.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useRef } from 'react';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { Spinner } from '../../../components';
import { useLibrary } from '../../../providers/LibraryProvider';
import { cn } from '../../../utils/bem';
Expand All @@ -7,56 +7,111 @@ import { EMPTY_CONFIG } from './Template';

const configClass = cn("configure");

export const Preview = ({ config, data, error }) => {
export const Preview = ({ config, data, error, loading }) => {
const LabelStudio = useLibrary('lsf');
const lsfRoot = useRef();
const lsf = useRef();
const lsf = useRef(null);
const rootRef = useRef();

useEffect(() => {
const currentTask = useMemo(() => {
return {
id: 1,
annotations: [],
predictions: [],
data,
};
}, [data]);

const currentConfig = useMemo(() => {
// empty string causes error in LSF
return config ?? EMPTY_CONFIG;
}, [config]);

const initLabelStudio = useCallback((config, task) => {
if (!LabelStudio) return;
if (!lsfRoot.current) return;
if (error) return;
if (!data) return;
if (!task.data) return;

console.info("Initializing LSF preview", { config, task });

const LSF = window.LabelStudio;
try {
lsf.current?.destroy();
lsf.current = new LSF(lsfRoot.current, {
config: config || EMPTY_CONFIG, // empty string causes error in LSF
interfaces: [
"side-column",
],
task: {
annotations: [],
predictions: [],
id: 1,
data,
},
onLabelStudioLoad: function(LS) {
return new window.LabelStudio(rootRef.current, {
config,
task,
interfaces: ["side-column"],
onLabelStudioLoad(LS) {
LS.settings.bottomSidePanel = true;
var c = LS.annotationStore.addAnnotation({

const as = LS.annotationStore;
const c = as.addAnnotation({
userGenerate: true,
});
LS.annotationStore.selectAnnotation(c.id);

as.selectAnnotation(c.id);
},
});
} catch(e) {
console.error(e);
} catch (err) {
console.error(err);
return null;
}
}, [LabelStudio]);

useEffect(() => {
const opacity = loading || error ? 0.6 : 1;
// to avoid rerenders and data loss we do it this way

document.getElementById("label-studio").style.opacity = opacity;
}, [loading, error]);

useEffect(() => {
if (!lsf.current) {
lsf.current = initLabelStudio(currentConfig, currentTask);
}

return () => {
if (lsf.current) {
console.info('Destroying LSF');
lsf.current.destroy();
lsf.current = null;
}
};
}, [initLabelStudio, currentConfig, currentTask]);

useEffect(() => {
if (lsf.current?.store) {
lsf.current.store.assignConfig(currentConfig);
console.log("LSF config updated");
}
}, [currentConfig]);

useEffect(() => {
if (lsf.current?.store) {
const store = lsf.current.store;

store.resetState();
store.assignTask(currentTask);
store.initializeStore(currentTask);

const c = store.annotationStore.addAnnotation({
userGenerate: true,
});

store.annotationStore.selectAnnotation(c.id);
console.log("LSF task updated");
}
}, [config, data, LabelStudio, lsfRoot]);
}, [currentTask]);

return (
<div className={configClass.elem("preview")}>
<h3>UI Preview</h3>
{error && <div className={configClass.elem("preview-error")}>
<h2>{error.detail} {error.id}</h2>
{error.validation_errors?.non_field_errors?.map?.(err => <p key={err}>{err}</p>)}
{error.validation_errors?.label_config?.map?.(err => <p key={err}>{err}</p>)}
{error.validation_errors?.map?.(err => <p key={err}>{err}</p>)}
</div>}
{!data && <Spinner style={{ width: "100%", height: "50vh" }} />}
<div id="label-studio" ref={lsfRoot}></div>
{/* <iframe srcDoc={page} frameBorder="0"></iframe> */}
{error && (
<div className={configClass.elem("preview-error")}>
<h2>{error.detail} {error.id}</h2>
{error.validation_errors?.non_field_errors?.map?.(err => <p key={err}>{err}</p>)}
{error.validation_errors?.label_config?.map?.(err => <p key={err}>{err}</p>)}
{error.validation_errors?.map?.(err => <p key={err}>{err}</p>)}
</div>
)}
{!data && loading && <Spinner style={{ width: "100%", height: "50vh" }} />}
<div id="label-studio" ref={rootRef}></div>
</div>
);
};

0 comments on commit 25c1fed

Please sign in to comment.