Upload all selected dataset files with progress
This commit is contained in:
@@ -32,6 +32,7 @@ import "./styles.css";
|
||||
const DEFAULT_API_BASE =
|
||||
typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8010` : "http://127.0.0.1:8010";
|
||||
const API_BASE = (import.meta.env.VITE_API_BASE ?? DEFAULT_API_BASE).replace(/\/$/, "");
|
||||
const DATASET_UPLOAD_KINDS: UploadKind[] = ["images", "masks"];
|
||||
|
||||
type JobProgress = {
|
||||
percent: number | null;
|
||||
@@ -225,6 +226,24 @@ type DataLoadingState = {
|
||||
updatedAt?: number | null;
|
||||
};
|
||||
|
||||
type UploadKind = "images" | "masks";
|
||||
type UploadStepStatus = "idle" | "queued" | "uploading" | "processing" | "done" | "failed";
|
||||
|
||||
type UploadKindProgress = {
|
||||
status: UploadStepStatus;
|
||||
percent: number;
|
||||
files: number;
|
||||
saved?: number;
|
||||
};
|
||||
|
||||
type UploadProgressState = {
|
||||
active: boolean;
|
||||
phase: string;
|
||||
overall: number;
|
||||
currentKind?: UploadKind;
|
||||
kinds: Record<UploadKind, UploadKindProgress>;
|
||||
};
|
||||
|
||||
type GpuPayload = {
|
||||
available: boolean;
|
||||
gpus: Array<{
|
||||
@@ -405,6 +424,39 @@ async function api<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
return res.json();
|
||||
}
|
||||
|
||||
function uploadDatasetKind(
|
||||
datasetName: string,
|
||||
kind: UploadKind,
|
||||
files: File[],
|
||||
onProgress: (loaded: number, total: number, status: "uploading" | "processing") => void
|
||||
): Promise<DatasetUploadResponse> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const body = new FormData();
|
||||
files.forEach((file) => body.append("files", file));
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", `${API_BASE}/api/datasets/${encodeURIComponent(datasetName)}/upload/${kind}`);
|
||||
xhr.upload.onprogress = (event) => {
|
||||
if (event.lengthComputable) {
|
||||
onProgress(event.loaded, event.total, "uploading");
|
||||
}
|
||||
};
|
||||
xhr.upload.onload = () => onProgress(1, 1, "processing");
|
||||
xhr.onerror = () => reject(new Error(`上传失败,无法连接后端 ${API_BASE}`));
|
||||
xhr.onload = () => {
|
||||
if (xhr.status < 200 || xhr.status >= 300) {
|
||||
reject(new Error(xhr.responseText || `上传失败:HTTP ${xhr.status}`));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
resolve(JSON.parse(xhr.responseText) as DatasetUploadResponse);
|
||||
} catch (err) {
|
||||
reject(new Error(`上传结果解析失败:${err instanceof Error ? err.message : String(err)}`));
|
||||
}
|
||||
};
|
||||
xhr.send(body);
|
||||
});
|
||||
}
|
||||
|
||||
function artifactUrl(path: string) {
|
||||
return `${API_BASE}/api/artifacts/${path.split("/").map(encodeURIComponent).join("/")}`;
|
||||
}
|
||||
@@ -544,6 +596,18 @@ function writeDatasetCache(payload: DatasetCachePayload) {
|
||||
}
|
||||
}
|
||||
|
||||
function emptyUploadProgress(): UploadProgressState {
|
||||
return {
|
||||
active: false,
|
||||
phase: "",
|
||||
overall: 0,
|
||||
kinds: {
|
||||
images: { status: "idle", percent: 0, files: 0 },
|
||||
masks: { status: "idle", percent: 0, files: 0 }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function useData() {
|
||||
const cachedDatasetState = useMemo(() => readDatasetCache(), []);
|
||||
const [catalog, setCatalog] = useState<Catalog | null>(null);
|
||||
@@ -573,6 +637,16 @@ function useData() {
|
||||
updatedAt: cachedDatasetState?.savedAt ?? null
|
||||
});
|
||||
const refreshSeqRef = useRef(0);
|
||||
const datasetsRef = useRef<UploadedDataset[]>(cachedDatasetState?.datasets ?? []);
|
||||
const datasetValidationsRef = useRef<Record<string, DatasetValidation>>(cachedDatasetState?.datasetValidations ?? {});
|
||||
|
||||
useEffect(() => {
|
||||
datasetsRef.current = datasets;
|
||||
}, [datasets]);
|
||||
|
||||
useEffect(() => {
|
||||
datasetValidationsRef.current = datasetValidations;
|
||||
}, [datasetValidations]);
|
||||
|
||||
async function refresh() {
|
||||
const seq = refreshSeqRef.current + 1;
|
||||
@@ -582,11 +656,11 @@ function useData() {
|
||||
setDataLoading((current) => ({ ...current, ...patch }));
|
||||
};
|
||||
try {
|
||||
updateLoading({ refreshing: true, phase: "读取数据集", progress: 8, cached: datasets.length > 0 });
|
||||
updateLoading({ refreshing: true, phase: "读取数据集", progress: 8, cached: datasetsRef.current.length > 0 });
|
||||
const datasetsNext = await api<UploadedDataset[]>("/api/datasets");
|
||||
if (refreshSeqRef.current !== seq) return;
|
||||
setDatasets(datasetsNext);
|
||||
writeDatasetCache({ datasets: datasetsNext, datasetValidations, savedAt: Date.now() });
|
||||
writeDatasetCache({ datasets: datasetsNext, datasetValidations: datasetValidationsRef.current, savedAt: Date.now() });
|
||||
|
||||
updateLoading({ phase: "校验图片和 masks", progress: 32 });
|
||||
const validationResults = await Promise.allSettled(
|
||||
@@ -646,9 +720,9 @@ function useData() {
|
||||
setError(String(err));
|
||||
updateLoading({
|
||||
refreshing: false,
|
||||
phase: datasets.length ? "同步失败,继续显示缓存" : "同步失败",
|
||||
progress: datasets.length ? 100 : 0,
|
||||
cached: datasets.length > 0
|
||||
phase: datasetsRef.current.length ? "同步失败,继续显示缓存" : "同步失败",
|
||||
progress: datasetsRef.current.length ? 100 : 0,
|
||||
cached: datasetsRef.current.length > 0
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -700,6 +774,44 @@ function DataSyncStrip({ loading }: { loading: DataLoadingState }) {
|
||||
);
|
||||
}
|
||||
|
||||
function UploadProgressPanel({ progress }: { progress: UploadProgressState }) {
|
||||
if (!progress.phase) return null;
|
||||
const visibleKinds = DATASET_UPLOAD_KINDS.filter((kind) => progress.kinds[kind].files > 0 || progress.kinds[kind].status !== "idle");
|
||||
const labels: Record<UploadStepStatus, string> = {
|
||||
idle: "未选择",
|
||||
queued: "等待",
|
||||
uploading: "上传中",
|
||||
processing: "保存中",
|
||||
done: "完成",
|
||||
failed: "失败"
|
||||
};
|
||||
return (
|
||||
<div className={`uploadProgressPanel ${progress.active ? "active" : ""}`}>
|
||||
<div className="uploadProgressHead">
|
||||
<strong>{progress.phase}</strong>
|
||||
<span>{Math.max(0, Math.min(100, progress.overall))}%</span>
|
||||
</div>
|
||||
<div className="uploadProgressBar" aria-label="上传总进度">
|
||||
<span style={{ width: `${Math.max(0, Math.min(100, progress.overall))}%` }} />
|
||||
</div>
|
||||
<div className="uploadProgressRows">
|
||||
{visibleKinds.map((kind) => {
|
||||
const item = progress.kinds[kind];
|
||||
return (
|
||||
<div key={kind} className={`uploadProgressRow ${item.status}`}>
|
||||
<div>
|
||||
<strong>{kind}</strong>
|
||||
<small>{item.files} 个文件{typeof item.saved === "number" ? ` · 已保存 ${item.saved}` : ""}</small>
|
||||
</div>
|
||||
<span>{labels[item.status]} · {item.percent}%</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
const { catalog, gpus, condaEnvs, runtimeReadiness, capabilities, agentEvaluation, userAgent, jobs, results, curves, weightManifest, datasets, datasetValidations, coverage, acceptance, realAcceptance, realTrainAcceptance, deepAcceptance, error, dataLoading, refresh } = useData();
|
||||
const [page, setPage] = useState<PageId>(pageFromHash);
|
||||
@@ -727,8 +839,9 @@ function App() {
|
||||
const [resultQuery, setResultQuery] = useState("");
|
||||
const [selectedGpuIds, setSelectedGpuIds] = useState<number[]>([]);
|
||||
const [selectedCondaEnv, setSelectedCondaEnv] = useState("auto");
|
||||
const [uploadKind, setUploadKind] = useState<"images" | "masks">("images");
|
||||
const [uploadFilesByKind, setUploadFilesByKind] = useState<Record<"images" | "masks", FileList | null>>({ images: null, masks: null });
|
||||
const [uploadKind, setUploadKind] = useState<UploadKind>("images");
|
||||
const [uploadFilesByKind, setUploadFilesByKind] = useState<Record<UploadKind, FileList | null>>({ images: null, masks: null });
|
||||
const [uploadProgress, setUploadProgress] = useState<UploadProgressState>(() => emptyUploadProgress());
|
||||
const [agentValidation, setAgentValidation] = useState<ValidationAgentPayload | null>(null);
|
||||
const [weightVerification, setWeightVerification] = useState<WeightVerifyPayload | null>(null);
|
||||
const [agentBusy, setAgentBusy] = useState(false);
|
||||
@@ -853,6 +966,11 @@ function App() {
|
||||
? "image/*,video/*,.zip,.tar,.tar.gz,.tgz"
|
||||
: "image/*,.txt,.json,.yaml,.yml,.zip,.tar,.tar.gz,.tgz";
|
||||
const activeUploadFiles = uploadFilesByKind[uploadKind];
|
||||
const uploadKindsWithFiles = DATASET_UPLOAD_KINDS.filter((kind) => (uploadFilesByKind[kind]?.length ?? 0) > 0);
|
||||
const totalUploadFiles = uploadKindsWithFiles.reduce((total, kind) => total + (uploadFilesByKind[kind]?.length ?? 0), 0);
|
||||
const uploadSelectionLabel = uploadKindsWithFiles.length
|
||||
? uploadKindsWithFiles.map((kind) => `${kind} ${uploadFilesByKind[kind]?.length ?? 0}`).join(" / ")
|
||||
: `当前 ${uploadKind}`;
|
||||
const inferenceOutputs = useMemo(() => {
|
||||
const predictions = results.filter((item) => item.role === "segmentation" && item.previewable).slice(0, 8);
|
||||
const heatmaps = results.filter((item) => item.role === "heatmap" && item.previewable).slice(0, 8);
|
||||
@@ -1072,31 +1190,95 @@ function App() {
|
||||
}
|
||||
|
||||
async function uploadDatasetFiles() {
|
||||
const files = uploadFilesByKind[uploadKind];
|
||||
if (!files || files.length === 0) return;
|
||||
const pendingUploads = DATASET_UPLOAD_KINDS
|
||||
.map((kind) => ({ kind, files: Array.from(uploadFilesByKind[kind] ?? []) }))
|
||||
.filter((item) => item.files.length > 0);
|
||||
if (!pendingUploads.length) return;
|
||||
setBusy(true);
|
||||
let runningKind: UploadKind | undefined;
|
||||
try {
|
||||
setActionError("");
|
||||
const body = new FormData();
|
||||
Array.from(files).forEach((file) => body.append("files", file));
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(`${API_BASE}/api/datasets/${encodeURIComponent(datasetName)}/upload/${uploadKind}`, {
|
||||
method: "POST",
|
||||
body
|
||||
setUploadProgress({
|
||||
active: true,
|
||||
phase: "准备上传",
|
||||
overall: 0,
|
||||
kinds: {
|
||||
images: {
|
||||
status: uploadFilesByKind.images?.length ? "queued" : "idle",
|
||||
percent: 0,
|
||||
files: uploadFilesByKind.images?.length ?? 0
|
||||
},
|
||||
masks: {
|
||||
status: uploadFilesByKind.masks?.length ? "queued" : "idle",
|
||||
percent: 0,
|
||||
files: uploadFilesByKind.masks?.length ?? 0
|
||||
}
|
||||
}
|
||||
});
|
||||
let targetDatasetName = datasetName;
|
||||
let latestDatasetName = datasetName;
|
||||
for (let index = 0; index < pendingUploads.length; index += 1) {
|
||||
const { kind, files } = pendingUploads[index];
|
||||
runningKind = kind;
|
||||
setUploadProgress((current) => ({
|
||||
...current,
|
||||
currentKind: kind,
|
||||
phase: `上传 ${kind}`,
|
||||
kinds: {
|
||||
...current.kinds,
|
||||
[kind]: { ...current.kinds[kind], status: "uploading", percent: 0 }
|
||||
}
|
||||
}));
|
||||
const uploadResult = await uploadDatasetKind(targetDatasetName, kind, files, (loaded, total, status) => {
|
||||
const rawPercent = total > 0 ? Math.round((loaded / total) * 100) : 0;
|
||||
const percent = status === "processing" ? Math.max(rawPercent, 96) : rawPercent;
|
||||
const overall = Math.round(((index + percent / 100) / pendingUploads.length) * 100);
|
||||
setUploadProgress((current) => ({
|
||||
...current,
|
||||
currentKind: kind,
|
||||
phase: status === "processing" ? `保存 ${kind}` : `上传 ${kind}`,
|
||||
overall,
|
||||
kinds: {
|
||||
...current.kinds,
|
||||
[kind]: { ...current.kinds[kind], status, percent }
|
||||
}
|
||||
}));
|
||||
});
|
||||
} catch (err) {
|
||||
throw new Error(`上传失败,无法连接后端 ${API_BASE}:${err instanceof Error ? err.message : String(err)}`);
|
||||
latestDatasetName = uploadResult.dataset.name;
|
||||
targetDatasetName = uploadResult.dataset.name;
|
||||
setUploadProgress((current) => ({
|
||||
...current,
|
||||
overall: Math.round(((index + 1) / pendingUploads.length) * 100),
|
||||
kinds: {
|
||||
...current.kinds,
|
||||
[kind]: { ...current.kinds[kind], status: "done", percent: 100, saved: uploadResult.saved.length }
|
||||
}
|
||||
}));
|
||||
}
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
const uploadResult = await res.json() as DatasetUploadResponse;
|
||||
setDatasetName(uploadResult.dataset.name);
|
||||
setSelectedDatasetName(uploadResult.dataset.name);
|
||||
setUploadModalOpen(false);
|
||||
setUploadFilesByKind((current) => ({ ...current, [uploadKind]: null }));
|
||||
setDatasetName(latestDatasetName);
|
||||
setSelectedDatasetName(latestDatasetName);
|
||||
setUploadProgress((current) => ({ ...current, active: true, phase: "刷新图片墙", overall: 100 }));
|
||||
setUploadFilesByKind((current) => {
|
||||
const next = { ...current };
|
||||
for (const { kind } of pendingUploads) next[kind] = null;
|
||||
return next;
|
||||
});
|
||||
await refresh();
|
||||
setUploadModalOpen(false);
|
||||
window.setTimeout(() => setUploadProgress(emptyUploadProgress()), 500);
|
||||
} catch (err) {
|
||||
setActionError(String(err));
|
||||
setUploadProgress((current) => ({
|
||||
...current,
|
||||
active: false,
|
||||
phase: "上传失败",
|
||||
kinds: runningKind
|
||||
? {
|
||||
...current.kinds,
|
||||
[runningKind]: { ...current.kinds[runningKind], status: "failed" }
|
||||
}
|
||||
: current.kinds
|
||||
}));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
@@ -1521,7 +1703,15 @@ function App() {
|
||||
<p className="eyebrow">Dataset Bench</p>
|
||||
<h2>数据集管理</h2>
|
||||
</div>
|
||||
<button className="iconButton addDatasetButton" type="button" onClick={() => setUploadModalOpen(true)} title="新建或上传数据集">
|
||||
<button
|
||||
className="iconButton addDatasetButton"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setUploadProgress(emptyUploadProgress());
|
||||
setUploadModalOpen(true);
|
||||
}}
|
||||
title="新建或上传数据集"
|
||||
>
|
||||
<Plus size={19} />
|
||||
</button>
|
||||
</div>
|
||||
@@ -1623,23 +1813,33 @@ function App() {
|
||||
<p className="eyebrow">Upload</p>
|
||||
<h2>新建或追加数据集</h2>
|
||||
</div>
|
||||
<button className="iconButton" type="button" onClick={() => setUploadModalOpen(false)} title="关闭">
|
||||
<button
|
||||
className="iconButton"
|
||||
type="button"
|
||||
disabled={uploadProgress.active}
|
||||
onClick={() => {
|
||||
setUploadModalOpen(false);
|
||||
setUploadProgress(emptyUploadProgress());
|
||||
}}
|
||||
title="关闭"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="datasetForm">
|
||||
<label className="field compact">
|
||||
<span>数据集名称</span>
|
||||
<input value={datasetName} onChange={(event) => setDatasetName(event.target.value)} />
|
||||
<input disabled={uploadProgress.active} value={datasetName} onChange={(event) => setDatasetName(event.target.value)} />
|
||||
</label>
|
||||
<label className="field compact">
|
||||
<span>说明</span>
|
||||
<input value={datasetDescription} onChange={(event) => setDatasetDescription(event.target.value)} />
|
||||
<input disabled={uploadProgress.active} value={datasetDescription} onChange={(event) => setDatasetDescription(event.target.value)} />
|
||||
</label>
|
||||
{datasetNameExists && <small className="formWarn">将追加到现有数据集的 {uploadKind},不会新建同名数据集。</small>}
|
||||
{datasetNameExists && <small className="formWarn">将追加所有已选择文件到现有数据集:{uploadSelectionLabel}。</small>}
|
||||
{!datasetNameExists && totalUploadFiles > 0 && <small className="formHint">将创建数据集并上传:{uploadSelectionLabel}。</small>}
|
||||
<div className="segmented">
|
||||
{(["images", "masks"] as const).map((kind) => (
|
||||
<button key={kind} type="button" className={uploadKind === kind ? "active" : ""} onClick={() => setUploadKind(kind)}>
|
||||
{DATASET_UPLOAD_KINDS.map((kind) => (
|
||||
<button key={kind} type="button" disabled={uploadProgress.active} className={uploadKind === kind ? "active" : ""} onClick={() => setUploadKind(kind)}>
|
||||
{kind}
|
||||
</button>
|
||||
))}
|
||||
@@ -1656,15 +1856,17 @@ function App() {
|
||||
key={uploadKind}
|
||||
multiple
|
||||
type="file"
|
||||
disabled={uploadProgress.active}
|
||||
accept={uploadAccept}
|
||||
onChange={(event) => setUploadFilesByKind((current) => ({ ...current, [uploadKind]: event.target.files }))}
|
||||
/>
|
||||
</label>
|
||||
<UploadProgressPanel progress={uploadProgress} />
|
||||
<div className="modalActions">
|
||||
<button className="primary" disabled={busy || !activeUploadFiles?.length} onClick={uploadDatasetFiles}>
|
||||
<UploadCloud size={17} />{datasetNameExists ? "追加文件" : "创建并上传"}
|
||||
<button className="primary" disabled={busy || totalUploadFiles === 0} onClick={uploadDatasetFiles}>
|
||||
<UploadCloud size={17} />{uploadProgress.active ? "上传中..." : datasetNameExists ? "追加已选择文件" : "创建并上传全部"}
|
||||
</button>
|
||||
<button className="primary ghost" disabled={busy || datasetNameExists} onClick={createDataset}>
|
||||
<button className="primary ghost" disabled={busy || datasetNameExists || uploadProgress.active} onClick={createDataset}>
|
||||
<Boxes size={17} />仅创建空数据集
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -652,6 +652,12 @@ textarea {
|
||||
background: var(--field);
|
||||
}
|
||||
|
||||
.field input:disabled,
|
||||
.field select:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.pathStack,
|
||||
.inferenceForm,
|
||||
.modelList,
|
||||
@@ -831,12 +837,20 @@ textarea {
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.formWarn {
|
||||
.formWarn,
|
||||
.formHint {
|
||||
display: block;
|
||||
color: var(--amber);
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.formWarn {
|
||||
color: var(--amber);
|
||||
}
|
||||
|
||||
.formHint {
|
||||
color: var(--cyan);
|
||||
}
|
||||
|
||||
.modalOverlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
@@ -913,6 +927,11 @@ textarea {
|
||||
font-weight: 760;
|
||||
}
|
||||
|
||||
.segmented button:disabled {
|
||||
opacity: 0.62;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.uploadKindCounts {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
@@ -966,6 +985,119 @@ textarea {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.drop input:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.uploadProgressPanel {
|
||||
display: grid;
|
||||
gap: 9px;
|
||||
padding: 11px;
|
||||
border: 1px solid rgba(115, 210, 222, 0.32);
|
||||
border-radius: 8px;
|
||||
background: rgba(8, 12, 12, 0.74);
|
||||
}
|
||||
|
||||
.uploadProgressPanel.active {
|
||||
border-color: rgba(115, 210, 222, 0.58);
|
||||
}
|
||||
|
||||
.uploadProgressHead {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.uploadProgressHead strong,
|
||||
.uploadProgressHead span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.uploadProgressHead span {
|
||||
color: var(--cyan);
|
||||
font-size: 12px;
|
||||
font-weight: 760;
|
||||
}
|
||||
|
||||
.uploadProgressBar {
|
||||
height: 8px;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
background: #080a08;
|
||||
border: 1px solid rgba(238, 242, 232, 0.12);
|
||||
}
|
||||
|
||||
.uploadProgressBar span {
|
||||
display: block;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(90deg, var(--green), var(--cyan));
|
||||
transition: width 180ms ease;
|
||||
}
|
||||
|
||||
.uploadProgressRows {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.uploadProgressRow {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 7px 8px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
background: #101310;
|
||||
}
|
||||
|
||||
.uploadProgressRow > div {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.uploadProgressRow strong,
|
||||
.uploadProgressRow small,
|
||||
.uploadProgressRow > span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.uploadProgressRow small,
|
||||
.uploadProgressRow > span {
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.uploadProgressRow.uploading,
|
||||
.uploadProgressRow.processing {
|
||||
border-color: rgba(115, 210, 222, 0.45);
|
||||
}
|
||||
|
||||
.uploadProgressRow.done {
|
||||
border-color: rgba(157, 226, 111, 0.42);
|
||||
}
|
||||
|
||||
.uploadProgressRow.failed {
|
||||
border-color: rgba(240, 113, 103, 0.6);
|
||||
}
|
||||
|
||||
.uploadProgressRow.done > span {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.uploadProgressRow.failed > span {
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
.buttonRow {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
|
||||
Reference in New Issue
Block a user