@manta-eu/react 0.8.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/chart/ForecastLegend.d.ts.map +1 -1
- package/dist/components/chart/ForecastLegend.js +10 -2
- package/dist/components/chart/ForecastLegend.js.map +1 -1
- package/dist/components/chart/forecast-model.d.ts.map +1 -1
- package/dist/components/chart/forecast-model.js.map +1 -1
- package/dist/components/chat-view/Chat.js +2 -2
- package/dist/components/file-dropzone/index.d.ts +25 -0
- package/dist/components/file-dropzone/index.d.ts.map +1 -0
- package/dist/components/file-dropzone/index.js +70 -0
- package/dist/components/file-dropzone/index.js.map +1 -0
- package/dist/components/planning/plan-detail-utils.d.ts.map +1 -1
- package/dist/components/planning/plan-detail-utils.js +2 -0
- package/dist/components/planning/plan-detail-utils.js.map +1 -1
- package/dist/components/toast/index.d.ts +1 -0
- package/dist/components/toast/index.d.ts.map +1 -1
- package/dist/components/toast/index.js +1 -0
- package/dist/components/toast/index.js.map +1 -1
- package/dist/globals.css +27 -0
- package/dist/graph/NodeDetailPanel/TypeSummaryPanel.d.ts.map +1 -1
- package/dist/graph/NodeDetailPanel/TypeSummaryPanel.js.map +1 -1
- package/dist/hooks/use-sparql-construct.d.ts +2 -0
- package/dist/hooks/use-sparql-construct.d.ts.map +1 -1
- package/dist/hooks/use-sparql-construct.js +2 -1
- package/dist/hooks/use-sparql-construct.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/datastore-route-table.d.ts +3 -0
- package/dist/lib/datastore-route-table.d.ts.map +1 -1
- package/dist/lib/datastore-route-table.js +3 -0
- package/dist/lib/datastore-route-table.js.map +1 -1
- package/dist/lib/hooks/{useChatUpload.d.ts → useFileUpload.d.ts} +3 -3
- package/dist/lib/hooks/{useChatUpload.d.ts.map → useFileUpload.d.ts.map} +1 -1
- package/dist/lib/hooks/{useChatUpload.js → useFileUpload.js} +2 -2
- package/dist/lib/hooks/{useChatUpload.js.map → useFileUpload.js.map} +1 -1
- package/dist/routes/data-collection/index.d.ts +3 -0
- package/dist/routes/data-collection/index.d.ts.map +1 -0
- package/dist/routes/data-collection/index.js +43 -0
- package/dist/routes/data-collection/index.js.map +1 -0
- package/dist/routes/data-explorer/components/DataExplorerLayout.d.ts.map +1 -1
- package/dist/routes/data-explorer/components/DataExplorerLayout.js +5 -2
- package/dist/routes/data-explorer/components/DataExplorerLayout.js.map +1 -1
- package/dist/routes/data-explorer/components/Rail.d.ts.map +1 -1
- package/dist/routes/data-explorer/components/Rail.js +3 -3
- package/dist/routes/data-explorer/components/Rail.js.map +1 -1
- package/dist/routes/data-explorer/components/uploads/IngestRunsList.d.ts +8 -0
- package/dist/routes/data-explorer/components/uploads/IngestRunsList.d.ts.map +1 -0
- package/dist/routes/data-explorer/components/uploads/IngestRunsList.js +18 -0
- package/dist/routes/data-explorer/components/uploads/IngestRunsList.js.map +1 -0
- package/dist/routes/data-explorer/components/uploads/IngestStateStatus.d.ts +14 -0
- package/dist/routes/data-explorer/components/uploads/IngestStateStatus.d.ts.map +1 -0
- package/dist/routes/data-explorer/components/uploads/IngestStateStatus.js +33 -0
- package/dist/routes/data-explorer/components/uploads/IngestStateStatus.js.map +1 -0
- package/dist/routes/data-explorer/components/uploads/UploadedFilesList.d.ts +8 -0
- package/dist/routes/data-explorer/components/uploads/UploadedFilesList.d.ts.map +1 -0
- package/dist/routes/data-explorer/components/uploads/UploadedFilesList.js +34 -0
- package/dist/routes/data-explorer/components/uploads/UploadedFilesList.js.map +1 -0
- package/dist/routes/data-explorer/components/uploads/UploadsPages.d.ts +7 -0
- package/dist/routes/data-explorer/components/uploads/UploadsPages.d.ts.map +1 -0
- package/dist/routes/data-explorer/components/uploads/UploadsPages.js +35 -0
- package/dist/routes/data-explorer/components/uploads/UploadsPages.js.map +1 -0
- package/dist/routes/data-explorer/index.d.ts +1 -0
- package/dist/routes/data-explorer/index.d.ts.map +1 -1
- package/dist/routes/data-explorer/index.js +1 -0
- package/dist/routes/data-explorer/index.js.map +1 -1
- package/dist/routes/data-explorer/namespaces.d.ts +4 -0
- package/dist/routes/data-explorer/namespaces.d.ts.map +1 -1
- package/dist/routes/data-explorer/namespaces.js +6 -1
- package/dist/routes/data-explorer/namespaces.js.map +1 -1
- package/dist/routes/data-explorer/uploads.d.ts +56 -0
- package/dist/routes/data-explorer/uploads.d.ts.map +1 -0
- package/dist/routes/data-explorer/uploads.js +140 -0
- package/dist/routes/data-explorer/uploads.js.map +1 -0
- package/dist/routes/datastores/components/LinkUnlinkedDatastoreDialog.d.ts +1 -0
- package/dist/routes/datastores/components/LinkUnlinkedDatastoreDialog.d.ts.map +1 -1
- package/dist/routes/datastores/components/LinkUnlinkedDatastoreDialog.js +16 -9
- package/dist/routes/datastores/components/LinkUnlinkedDatastoreDialog.js.map +1 -1
- package/dist/routes/datastores/components/StatusBadge.d.ts +7 -8
- package/dist/routes/datastores/components/StatusBadge.d.ts.map +1 -1
- package/dist/routes/datastores/components/StatusBadge.js +17 -1
- package/dist/routes/datastores/components/StatusBadge.js.map +1 -1
- package/dist/routes/datastores/flatten.d.ts +6 -1
- package/dist/routes/datastores/flatten.d.ts.map +1 -1
- package/dist/routes/datastores/flatten.js +37 -2
- package/dist/routes/datastores/flatten.js.map +1 -1
- package/dist/routes/datastores/hooks/useDatastoreColumns.d.ts +2 -0
- package/dist/routes/datastores/hooks/useDatastoreColumns.d.ts.map +1 -1
- package/dist/routes/datastores/hooks/useDatastoreColumns.js +8 -4
- package/dist/routes/datastores/hooks/useDatastoreColumns.js.map +1 -1
- package/dist/routes/datastores/index.d.ts.map +1 -1
- package/dist/routes/datastores/index.js +53 -18
- package/dist/routes/datastores/index.js.map +1 -1
- package/dist/routes/integrations/components/connect-forms/GoogleMerchantCenterConnectForm.d.ts +3 -0
- package/dist/routes/integrations/components/connect-forms/GoogleMerchantCenterConnectForm.d.ts.map +1 -0
- package/dist/routes/integrations/components/connect-forms/GoogleMerchantCenterConnectForm.js +38 -0
- package/dist/routes/integrations/components/connect-forms/GoogleMerchantCenterConnectForm.js.map +1 -0
- package/dist/routes/integrations/components/connect-forms/google-merchant-center-account-tree.d.ts +4 -0
- package/dist/routes/integrations/components/connect-forms/google-merchant-center-account-tree.d.ts.map +1 -0
- package/dist/routes/integrations/components/connect-forms/google-merchant-center-account-tree.js +32 -0
- package/dist/routes/integrations/components/connect-forms/google-merchant-center-account-tree.js.map +1 -0
- package/dist/routes/integrations/components/connect-forms/setup-account-unavailable-reason.d.ts.map +1 -1
- package/dist/routes/integrations/components/connect-forms/setup-account-unavailable-reason.js +1 -0
- package/dist/routes/integrations/components/connect-forms/setup-account-unavailable-reason.js.map +1 -1
- package/dist/routes/integrations/connect-forms.d.ts +1 -0
- package/dist/routes/integrations/connect-forms.d.ts.map +1 -1
- package/dist/routes/integrations/connect-forms.js +1 -0
- package/dist/routes/integrations/connect-forms.js.map +1 -1
- package/dist/routes/integrations/connection-view.d.ts.map +1 -1
- package/dist/routes/integrations/connection-view.js +1 -0
- package/dist/routes/integrations/connection-view.js.map +1 -1
- package/dist/shared/graph-derived-queries.d.ts.map +1 -1
- package/dist/shared/graph-derived-queries.js +2 -1
- package/dist/shared/graph-derived-queries.js.map +1 -1
- package/package.json +21 -9
- package/src/app-subpaths.json +1 -0
- package/src/components/chart/ForecastLegend.tsx +15 -13
- package/src/components/chart/forecast-model.ts +6 -8
- package/src/components/chat-view/Chat.tsx +2 -2
- package/src/components/file-dropzone/index.tsx +165 -0
- package/src/components/planning/plan-detail-utils.tsx +2 -0
- package/src/components/toast/index.tsx +1 -0
- package/src/graph/NodeDetailPanel/TypeSummaryPanel.tsx +12 -14
- package/src/hooks/use-sparql-construct.ts +4 -2
- package/src/index.ts +1 -0
- package/src/lib/datastore-route-table.ts +6 -0
- package/src/lib/hooks/{useChatUpload.ts → useFileUpload.ts} +2 -2
- package/src/routes/data-collection/index.tsx +82 -0
- package/src/routes/data-explorer/components/DataExplorerLayout.tsx +5 -2
- package/src/routes/data-explorer/components/Rail.tsx +13 -2
- package/src/routes/data-explorer/components/uploads/IngestRunsList.tsx +63 -0
- package/src/routes/data-explorer/components/uploads/IngestStateStatus.tsx +45 -0
- package/src/routes/data-explorer/components/uploads/UploadedFilesList.tsx +93 -0
- package/src/routes/data-explorer/components/uploads/UploadsPages.tsx +80 -0
- package/src/routes/data-explorer/index.tsx +6 -0
- package/src/routes/data-explorer/namespaces.ts +7 -1
- package/src/routes/data-explorer/uploads.ts +205 -0
- package/src/routes/datastores/components/LinkUnlinkedDatastoreDialog.tsx +49 -10
- package/src/routes/datastores/components/StatusBadge.tsx +22 -7
- package/src/routes/datastores/flatten.ts +50 -2
- package/src/routes/datastores/hooks/useDatastoreColumns.tsx +21 -1
- package/src/routes/datastores/index.tsx +64 -13
- package/src/routes/integrations/components/connect-forms/GoogleMerchantCenterConnectForm.tsx +61 -0
- package/src/routes/integrations/components/connect-forms/google-merchant-center-account-tree.ts +38 -0
- package/src/routes/integrations/components/connect-forms/setup-account-unavailable-reason.ts +1 -0
- package/src/routes/integrations/connect-forms.ts +1 -0
- package/src/routes/integrations/connection-view.ts +1 -0
- package/src/shared/graph-derived-queries.ts +2 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Link } from "react-router";
|
|
2
|
+
import { entityPath } from "../../namespaces";
|
|
3
|
+
import type { IngestRun, UploadedFile } from "../../uploads";
|
|
4
|
+
import { ArrowLinkIcon, arrowLinkClass } from "../ArrowLink";
|
|
5
|
+
import { ModifiedAgo } from "../ModifiedAgo";
|
|
6
|
+
import { EntityName, LinkList } from "../RawTriples";
|
|
7
|
+
import { IngestStateStatus } from "./IngestStateStatus";
|
|
8
|
+
import { TruncateTooltip } from "#components/tooltip/index";
|
|
9
|
+
import { cn } from "#lib/utils";
|
|
10
|
+
|
|
11
|
+
type IngestRunRowProps = { run: IngestRun; file: UploadedFile | undefined };
|
|
12
|
+
|
|
13
|
+
function IngestRunRow(props: IngestRunRowProps) {
|
|
14
|
+
const { run, file } = props;
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="flex flex-col gap-2 px-4 py-3 sm:flex-row sm:items-center sm:gap-3">
|
|
18
|
+
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
19
|
+
<Link to={entityPath(run.file)} className="min-w-0 hover:underline">
|
|
20
|
+
{file ? (
|
|
21
|
+
<TruncateTooltip
|
|
22
|
+
text={file.fileName}
|
|
23
|
+
className="typography-small-medium block truncate text-default-foreground"
|
|
24
|
+
/>
|
|
25
|
+
) : (
|
|
26
|
+
<EntityName iri={run.file} />
|
|
27
|
+
)}
|
|
28
|
+
</Link>
|
|
29
|
+
|
|
30
|
+
<div className="typography-tiny flex flex-wrap items-center gap-x-2 text-muted-foreground">
|
|
31
|
+
{run.endedAt && <ModifiedAgo modified={run.endedAt} />}
|
|
32
|
+
<Link to={entityPath(run.iri)} className="hover:underline">
|
|
33
|
+
Run details
|
|
34
|
+
</Link>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div className="flex items-center gap-3">
|
|
39
|
+
<IngestStateStatus state={{ kind: "run", run }} />
|
|
40
|
+
{run.dataset && (
|
|
41
|
+
<Link to={entityPath(run.dataset)} className={cn("typography-small", arrowLinkClass)}>
|
|
42
|
+
Dataset
|
|
43
|
+
<ArrowLinkIcon />
|
|
44
|
+
</Link>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type IngestRunsListProps = { runs: readonly IngestRun[]; fileByIri: ReadonlyMap<string, UploadedFile> };
|
|
52
|
+
|
|
53
|
+
export function IngestRunsList(props: IngestRunsListProps) {
|
|
54
|
+
const { runs, fileByIri } = props;
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<LinkList>
|
|
58
|
+
{runs.map((run) => (
|
|
59
|
+
<IngestRunRow key={run.iri} run={run} file={fileByIri.get(run.file)} />
|
|
60
|
+
))}
|
|
61
|
+
</LinkList>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { match } from "ts-pattern";
|
|
2
|
+
import type { FileIngestState, IngestRun } from "../../uploads";
|
|
3
|
+
import { Status, type StatusVariant } from "#components/status/index";
|
|
4
|
+
|
|
5
|
+
type StatusDisplay = { label: string; variant: StatusVariant };
|
|
6
|
+
|
|
7
|
+
const countFormat = new Intl.NumberFormat("en-US");
|
|
8
|
+
|
|
9
|
+
function recordCount(count: number): string {
|
|
10
|
+
return `${countFormat.format(count)} ${count === 1 ? "record" : "records"}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function ingestedDisplay(run: IngestRun): StatusDisplay {
|
|
14
|
+
const read = run.recordsRead === undefined ? "Ingested" : `Ingested ${recordCount(run.recordsRead)}`;
|
|
15
|
+
const dropped = run.recordsDropped ? `, ${countFormat.format(run.recordsDropped)} dropped` : "";
|
|
16
|
+
|
|
17
|
+
return { label: `${read}${dropped}`, variant: run.recordsDropped ? "warning" : "success" };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function runDisplay(run: IngestRun): StatusDisplay {
|
|
21
|
+
return match(run.status)
|
|
22
|
+
.with("ingested", () => ingestedDisplay(run))
|
|
23
|
+
.with("unparsable", (): StatusDisplay => ({ label: "Could not be read", variant: "error" }))
|
|
24
|
+
.with("too_large", (): StatusDisplay => ({ label: "Too large to ingest", variant: "warning" }))
|
|
25
|
+
.with("gone", (): StatusDisplay => ({ label: "Removed before ingest", variant: "neutral" }))
|
|
26
|
+
.exhaustive();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function ingestStateDisplay(state: FileIngestState): StatusDisplay {
|
|
30
|
+
return match(state)
|
|
31
|
+
.with({ kind: "waiting" }, (): StatusDisplay => ({ label: "Waiting for ingest", variant: "neutral" }))
|
|
32
|
+
.with({ kind: "stored" }, (): StatusDisplay => ({ label: "Stored", variant: "neutral" }))
|
|
33
|
+
.with({ kind: "run" }, ({ run }) => runDisplay(run))
|
|
34
|
+
.exhaustive();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type IngestStateStatusProps = { state: FileIngestState; className?: string };
|
|
38
|
+
|
|
39
|
+
export function IngestStateStatus(props: IngestStateStatusProps) {
|
|
40
|
+
const { state, className } = props;
|
|
41
|
+
|
|
42
|
+
const { label, variant } = ingestStateDisplay(state);
|
|
43
|
+
|
|
44
|
+
return <Status label={label} variant={variant} className={className} />;
|
|
45
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { userIriPrefix } from "@manta-eu/client/shared/stakeholder-iri";
|
|
2
|
+
import { redirectPath } from "@manta-eu/ldo/redirect-queries";
|
|
3
|
+
import { humanBytes } from "@manta-eu/lib/humanize";
|
|
4
|
+
import { isPresent } from "@manta-eu/lib/present";
|
|
5
|
+
import { Download, FileText } from "lucide-react";
|
|
6
|
+
import { Link } from "react-router";
|
|
7
|
+
import { entityPath } from "../../namespaces";
|
|
8
|
+
import type { UploadedFileWithState } from "../../uploads";
|
|
9
|
+
import { ModifiedAgo } from "../ModifiedAgo";
|
|
10
|
+
import { EntityName, LinkList } from "../RawTriples";
|
|
11
|
+
import { IngestStateStatus } from "./IngestStateStatus";
|
|
12
|
+
import { Button } from "#components/button/index";
|
|
13
|
+
import { TruncateTooltip } from "#components/tooltip/index";
|
|
14
|
+
import { useRedirectEndpoint } from "#ldo-ui";
|
|
15
|
+
|
|
16
|
+
type UploaderProps = { iri: string | undefined; currentUserIri: string | undefined };
|
|
17
|
+
|
|
18
|
+
function Uploader(props: UploaderProps) {
|
|
19
|
+
const { iri, currentUserIri } = props;
|
|
20
|
+
|
|
21
|
+
if (!iri) return null;
|
|
22
|
+
|
|
23
|
+
if (iri === currentUserIri) return <span>by you</span>;
|
|
24
|
+
|
|
25
|
+
if (iri.startsWith(userIriPrefix)) return <span>by a teammate</span>;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<span className="flex min-w-0 items-center gap-1">
|
|
29
|
+
by <EntityName iri={iri} />
|
|
30
|
+
</span>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type UploadedFileRowProps = { file: UploadedFileWithState; currentUserIri: string | undefined };
|
|
35
|
+
|
|
36
|
+
function UploadedFileRow(props: UploadedFileRowProps) {
|
|
37
|
+
const { file, currentUserIri } = props;
|
|
38
|
+
|
|
39
|
+
const redirectEndpoint = useRedirectEndpoint();
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="flex flex-col gap-2 px-4 py-3 sm:flex-row sm:items-center sm:gap-3">
|
|
43
|
+
<div className="flex min-w-0 flex-1 items-center gap-3">
|
|
44
|
+
<FileText className="size-4 shrink-0 text-muted-foreground" />
|
|
45
|
+
|
|
46
|
+
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
47
|
+
<Link to={entityPath(file.iri)} className="min-w-0 hover:underline">
|
|
48
|
+
<TruncateTooltip
|
|
49
|
+
text={file.fileName}
|
|
50
|
+
className="typography-small-medium block truncate text-default-foreground"
|
|
51
|
+
/>
|
|
52
|
+
</Link>
|
|
53
|
+
|
|
54
|
+
<div className="typography-tiny flex min-w-0 flex-wrap items-center gap-x-2 text-muted-foreground">
|
|
55
|
+
{file.byteSize !== undefined && <span>{humanBytes(file.byteSize)}</span>}
|
|
56
|
+
{isPresent(file.extension) && <span title={file.mediaType}>{file.extension.toUpperCase()}</span>}
|
|
57
|
+
{file.uploadedAt && <ModifiedAgo modified={file.uploadedAt} />}
|
|
58
|
+
<Uploader iri={file.uploader} currentUserIri={currentUserIri} />
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div className="flex items-center justify-between gap-3 pl-7 sm:pl-0">
|
|
64
|
+
<IngestStateStatus state={file.state} />
|
|
65
|
+
|
|
66
|
+
<Button asChild variant="ghost" size="xs" className="size-7 shrink-0 p-0">
|
|
67
|
+
<a
|
|
68
|
+
href={redirectPath(file.iri, redirectEndpoint)}
|
|
69
|
+
target="_blank"
|
|
70
|
+
rel="noreferrer"
|
|
71
|
+
aria-label={`Download ${file.fileName}`}
|
|
72
|
+
>
|
|
73
|
+
<Download className="size-4" />
|
|
74
|
+
</a>
|
|
75
|
+
</Button>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
type UploadedFilesListProps = { files: readonly UploadedFileWithState[]; currentUserIri?: string };
|
|
82
|
+
|
|
83
|
+
export function UploadedFilesList(props: UploadedFilesListProps) {
|
|
84
|
+
const { files, currentUserIri } = props;
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<LinkList>
|
|
88
|
+
{files.map((file) => (
|
|
89
|
+
<UploadedFileRow key={file.iri} file={file} currentUserIri={currentUserIri} />
|
|
90
|
+
))}
|
|
91
|
+
</LinkList>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { getUserIri } from "@manta-eu/client/shared/stakeholder-iri";
|
|
2
|
+
import { isPresent } from "@manta-eu/lib/present";
|
|
3
|
+
import { explorerCrumb } from "../../namespaces";
|
|
4
|
+
import { uploadsLimit, useUploads } from "../../uploads";
|
|
5
|
+
import { Breadcrumbs } from "../Breadcrumbs";
|
|
6
|
+
import { DetailHeader } from "../DetailHeader";
|
|
7
|
+
import { LoadingRows, StatusText } from "../RawTriples";
|
|
8
|
+
import { IngestRunsList } from "./IngestRunsList";
|
|
9
|
+
import { UploadedFilesList } from "./UploadedFilesList";
|
|
10
|
+
import { useAuthContext } from "#providers/AuthContext";
|
|
11
|
+
|
|
12
|
+
export type UploadedFilesSectionProps = { emptyText?: string };
|
|
13
|
+
|
|
14
|
+
export function UploadedFilesSection(props: UploadedFilesSectionProps) {
|
|
15
|
+
const { emptyText = "No files uploaded yet." } = props;
|
|
16
|
+
|
|
17
|
+
const { isOperator, user } = useAuthContext();
|
|
18
|
+
const { files, isLoading, error } = useUploads({ includeAllFiles: isOperator });
|
|
19
|
+
|
|
20
|
+
if (isLoading) return <LoadingRows />;
|
|
21
|
+
|
|
22
|
+
if (error) return <StatusText role="alert">Failed to load files.</StatusText>;
|
|
23
|
+
|
|
24
|
+
if (!isPresent(files)) return <StatusText>{emptyText}</StatusText>;
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="flex flex-col gap-2">
|
|
28
|
+
<UploadedFilesList files={files} currentUserIri={getUserIri(user.id)} />
|
|
29
|
+
{files.length >= uploadsLimit && (
|
|
30
|
+
<p className="typography-caption text-muted-foreground">Showing the newest {uploadsLimit} files.</p>
|
|
31
|
+
)}
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function UploadedFilesPage() {
|
|
37
|
+
const { isOperator } = useAuthContext();
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="flex max-w-5xl flex-col gap-6">
|
|
41
|
+
<Breadcrumbs trail={[explorerCrumb, { label: "Files" }]} />
|
|
42
|
+
|
|
43
|
+
<DetailHeader title="Files" />
|
|
44
|
+
|
|
45
|
+
<p className="typography-small -mt-3 max-w-prose text-muted-foreground">
|
|
46
|
+
{isOperator
|
|
47
|
+
? "Every file in this datastore, newest first, with how its ingest into the knowledge graph ended."
|
|
48
|
+
: "Files uploaded to this datastore, newest first, with how their ingest into the knowledge graph ended."}{" "}
|
|
49
|
+
CSV, Parquet, Turtle and N-Triples files are ingested automatically; other files are stored.
|
|
50
|
+
</p>
|
|
51
|
+
|
|
52
|
+
<UploadedFilesSection />
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function IngestRunsPage() {
|
|
58
|
+
const { isOperator } = useAuthContext();
|
|
59
|
+
const { runs, fileByIri, isLoading, error } = useUploads({ includeAllFiles: isOperator });
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className="flex max-w-5xl flex-col gap-6">
|
|
63
|
+
<Breadcrumbs trail={[explorerCrumb, { label: "Ingest runs" }]} />
|
|
64
|
+
|
|
65
|
+
<DetailHeader title="Ingest runs" />
|
|
66
|
+
|
|
67
|
+
<p className="typography-small -mt-3 max-w-prose text-muted-foreground">
|
|
68
|
+
Each attempt to add an uploaded file to the knowledge graph, newest first.
|
|
69
|
+
</p>
|
|
70
|
+
|
|
71
|
+
{isLoading && <LoadingRows />}
|
|
72
|
+
|
|
73
|
+
{error && <StatusText role="alert">Failed to load ingest runs.</StatusText>}
|
|
74
|
+
|
|
75
|
+
{!isLoading && !error && !isPresent(runs) && <StatusText>No file has been ingested yet.</StatusText>}
|
|
76
|
+
|
|
77
|
+
{isPresent(runs) && <IngestRunsList runs={runs} fileByIri={fileByIri} />}
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -9,6 +9,12 @@ export { DataExplorerLayout } from "./components/DataExplorerLayout";
|
|
|
9
9
|
export { ExplorerEmptyState as DataExplorerIndexRoute } from "./components/ExplorerEmptyState";
|
|
10
10
|
export { CoverageOverview as DataExplorerCoverageRoute } from "./components/ontology-coverage";
|
|
11
11
|
export { RecentTypes as DataExplorerRecentRoute } from "./components/RecentTypes";
|
|
12
|
+
export {
|
|
13
|
+
IngestRunsPage as DataExplorerIngestRoute,
|
|
14
|
+
UploadedFilesPage as DataExplorerFilesRoute,
|
|
15
|
+
UploadedFilesSection,
|
|
16
|
+
type UploadedFilesSectionProps,
|
|
17
|
+
} from "./components/uploads/UploadsPages";
|
|
12
18
|
|
|
13
19
|
export function DataExplorerPrefixRoute() {
|
|
14
20
|
const { prefix } = useParams<{ prefix: string }>();
|
|
@@ -96,8 +96,12 @@ export function localName(iri: string, prefix: string): string {
|
|
|
96
96
|
export { entityPath, entitySegment, explorerPath };
|
|
97
97
|
export const recentSegment = "recent";
|
|
98
98
|
export const coverageSegment = "coverage";
|
|
99
|
+
export const filesSegment = "files";
|
|
100
|
+
export const ingestRunsSegment = "ingest-runs";
|
|
99
101
|
export const recentPath = `${explorerPath}/${recentSegment}`;
|
|
100
102
|
export const coveragePath = `${explorerPath}/${coverageSegment}`;
|
|
103
|
+
export const filesPath = `${explorerPath}/${filesSegment}`;
|
|
104
|
+
export const ingestRunsPath = `${explorerPath}/${ingestRunsSegment}`;
|
|
101
105
|
export const explorerCrumb = { label: "Data", to: explorerPath };
|
|
102
106
|
|
|
103
107
|
export function isExplorerPrefix(value: string | undefined): value is string {
|
|
@@ -105,7 +109,9 @@ export function isExplorerPrefix(value: string | undefined): value is string {
|
|
|
105
109
|
}
|
|
106
110
|
|
|
107
111
|
export function isNamespaceSegment(segment: string | undefined): segment is string {
|
|
108
|
-
|
|
112
|
+
const reserved = [recentSegment, coverageSegment, filesSegment, ingestRunsSegment, entitySegment];
|
|
113
|
+
|
|
114
|
+
return isPresent(segment) && !reserved.includes(segment);
|
|
109
115
|
}
|
|
110
116
|
|
|
111
117
|
export function namespacePath(key: string): string {
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { userIriPrefix } from "@manta-eu/client/shared/stakeholder-iri";
|
|
2
|
+
import { isIngestableExtension } from "@manta-eu/client/shared/upload";
|
|
3
|
+
import { ingestManual } from "@manta-eu/ldo";
|
|
4
|
+
import { ingest, nfo, nmo, prov } from "@manta-eu/ldo/namespaces";
|
|
5
|
+
import { getExtension } from "@manta-eu/lib/file";
|
|
6
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
7
|
+
import type { RDF } from "ldkit/rdf";
|
|
8
|
+
import { useCallback, useMemo } from "react";
|
|
9
|
+
import { bindingCount, bindingValue, useSparqlSelect } from "#ldo-ui";
|
|
10
|
+
|
|
11
|
+
export const uploadsLimit = 500;
|
|
12
|
+
|
|
13
|
+
export const ingestPollIntervalMs = 15_000;
|
|
14
|
+
|
|
15
|
+
const uploadsQueryKey = ["uploaded-files"] as const;
|
|
16
|
+
|
|
17
|
+
export type UploadedFile = {
|
|
18
|
+
iri: string;
|
|
19
|
+
fileName: string;
|
|
20
|
+
mediaType?: string;
|
|
21
|
+
byteSize?: number;
|
|
22
|
+
extension?: string;
|
|
23
|
+
uploadedAt?: string;
|
|
24
|
+
uploader?: string;
|
|
25
|
+
purpose?: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type IngestStatus = "ingested" | "unparsable" | "gone" | "too_large";
|
|
29
|
+
|
|
30
|
+
export type IngestRun = {
|
|
31
|
+
iri: string;
|
|
32
|
+
file: string;
|
|
33
|
+
status: IngestStatus;
|
|
34
|
+
endedAt?: string;
|
|
35
|
+
recordsRead?: number;
|
|
36
|
+
recordsDropped?: number;
|
|
37
|
+
dataset?: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type FileIngestState = { kind: "waiting" } | { kind: "stored" } | { kind: "run"; run: IngestRun };
|
|
41
|
+
|
|
42
|
+
const ingestStatuses = new Set<string>(["ingested", "unparsable", "gone", "too_large"]);
|
|
43
|
+
|
|
44
|
+
function isIngestStatus(value: string | undefined): value is IngestStatus {
|
|
45
|
+
return value !== undefined && ingestStatuses.has(value);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Operators see every file node; members see the files a user uploaded.
|
|
49
|
+
export function uploadedFilesQuery(includeAllFiles: boolean): string {
|
|
50
|
+
const uploaderPattern = includeAllFiles
|
|
51
|
+
? `OPTIONAL { ?file <${prov.wasAttributedTo}> ?uploader }`
|
|
52
|
+
: `?file <${prov.wasAttributedTo}> ?uploader .
|
|
53
|
+
FILTER(STRSTARTS(STR(?uploader), "${userIriPrefix}"))`;
|
|
54
|
+
|
|
55
|
+
return `SELECT ?file ?fileName ?mediaType ?byteSize ?extension ?uploadedAt ?uploader ?purpose WHERE {
|
|
56
|
+
?file a <${nfo.FileDataObject}> .
|
|
57
|
+
FILTER(isIRI(?file))
|
|
58
|
+
${uploaderPattern}
|
|
59
|
+
OPTIONAL { ?file <${nfo.fileName}> ?fileName }
|
|
60
|
+
OPTIONAL { ?file <${nmo.contentMimeType}> ?mediaType }
|
|
61
|
+
OPTIONAL { ?file <${nfo.fileSize}> ?byteSize }
|
|
62
|
+
OPTIONAL { ?file <${nfo.fileExtension}> ?extension }
|
|
63
|
+
OPTIONAL { ?file <${prov.generatedAtTime}> ?uploadedAt }
|
|
64
|
+
OPTIONAL { ?file <${ingestManual.uploadPurpose}> ?purpose }
|
|
65
|
+
}
|
|
66
|
+
ORDER BY DESC(STR(?uploadedAt)) STR(?file)
|
|
67
|
+
LIMIT ${uploadsLimit}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const ingestRunsQuery = `SELECT ?run ?file ?status ?endedAt ?recordsRead ?recordsDropped ?dataset WHERE {
|
|
71
|
+
?run a <${ingest.IngestRun}> ;
|
|
72
|
+
<${prov.used}> ?file ;
|
|
73
|
+
<${ingest.status}> ?status .
|
|
74
|
+
FILTER(isIRI(?run))
|
|
75
|
+
OPTIONAL { ?run <${prov.endedAtTime}> ?endedAt }
|
|
76
|
+
OPTIONAL { ?run <${ingest.recordsRead}> ?recordsRead }
|
|
77
|
+
OPTIONAL { ?run <${ingest.recordsDropped}> ?recordsDropped }
|
|
78
|
+
OPTIONAL { ?run <${prov.generated}> ?dataset }
|
|
79
|
+
}
|
|
80
|
+
ORDER BY DESC(STR(?endedAt)) STR(?run)
|
|
81
|
+
LIMIT ${uploadsLimit}`;
|
|
82
|
+
|
|
83
|
+
export function uploadedFilesFromBindings(bindings: RDF.Bindings[]): UploadedFile[] {
|
|
84
|
+
const files = new Map<string, UploadedFile>();
|
|
85
|
+
|
|
86
|
+
for (const binding of bindings) {
|
|
87
|
+
const iri = bindingValue(binding, "file");
|
|
88
|
+
|
|
89
|
+
if (!iri || files.has(iri)) continue;
|
|
90
|
+
|
|
91
|
+
const fileName = bindingValue(binding, "fileName") ?? iri;
|
|
92
|
+
|
|
93
|
+
files.set(iri, {
|
|
94
|
+
iri,
|
|
95
|
+
fileName,
|
|
96
|
+
mediaType: bindingValue(binding, "mediaType"),
|
|
97
|
+
byteSize: bindingCount(binding, "byteSize"),
|
|
98
|
+
extension: bindingValue(binding, "extension") ?? getExtension(fileName),
|
|
99
|
+
uploadedAt: bindingValue(binding, "uploadedAt"),
|
|
100
|
+
uploader: bindingValue(binding, "uploader"),
|
|
101
|
+
purpose: bindingValue(binding, "purpose"),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return [...files.values()];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function ingestRunsFromBindings(bindings: RDF.Bindings[]): IngestRun[] {
|
|
109
|
+
const runs = new Map<string, IngestRun>();
|
|
110
|
+
|
|
111
|
+
for (const binding of bindings) {
|
|
112
|
+
const iri = bindingValue(binding, "run");
|
|
113
|
+
const file = bindingValue(binding, "file");
|
|
114
|
+
const status = bindingValue(binding, "status");
|
|
115
|
+
|
|
116
|
+
if (!iri || !file || !isIngestStatus(status) || runs.has(iri)) continue;
|
|
117
|
+
|
|
118
|
+
runs.set(iri, {
|
|
119
|
+
iri,
|
|
120
|
+
file,
|
|
121
|
+
status,
|
|
122
|
+
endedAt: bindingValue(binding, "endedAt"),
|
|
123
|
+
recordsRead: bindingCount(binding, "recordsRead"),
|
|
124
|
+
recordsDropped: bindingCount(binding, "recordsDropped"),
|
|
125
|
+
dataset: bindingValue(binding, "dataset"),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return [...runs.values()];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function latestRunByFile(runs: readonly IngestRun[]): Map<string, IngestRun> {
|
|
133
|
+
const latest = new Map<string, IngestRun>();
|
|
134
|
+
|
|
135
|
+
for (const run of runs) {
|
|
136
|
+
const current = latest.get(run.file);
|
|
137
|
+
|
|
138
|
+
if (!current || (run.endedAt ?? "") > (current.endedAt ?? "")) latest.set(run.file, run);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return latest;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Only a file recorded with the "file" purpose is picked up by ingest; a bundle or an older upload is only stored.
|
|
145
|
+
export function fileIngestState(file: UploadedFile, run: IngestRun | undefined): FileIngestState {
|
|
146
|
+
if (run) return { kind: "run", run };
|
|
147
|
+
|
|
148
|
+
return file.purpose === "file" && isIngestableExtension(file.extension) ? { kind: "waiting" } : { kind: "stored" };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function filesWithIngestState(
|
|
152
|
+
files: readonly UploadedFile[],
|
|
153
|
+
runs: readonly IngestRun[],
|
|
154
|
+
): UploadedFileWithState[] {
|
|
155
|
+
const latest = latestRunByFile(runs);
|
|
156
|
+
|
|
157
|
+
return files.map((file) => ({ ...file, state: fileIngestState(file, latest.get(file.iri)) }));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function hasWaitingFile(files: readonly UploadedFileWithState[]): boolean {
|
|
161
|
+
return files.some((file) => file.state.kind === "waiting");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export type UploadedFileWithState = UploadedFile & { state: FileIngestState };
|
|
165
|
+
|
|
166
|
+
type UseUploadsOptions = { includeAllFiles: boolean };
|
|
167
|
+
|
|
168
|
+
export function useUploads(options: UseUploadsOptions) {
|
|
169
|
+
const { includeAllFiles } = options;
|
|
170
|
+
|
|
171
|
+
const filesQuery = useSparqlSelect({
|
|
172
|
+
query: uploadedFilesQuery(includeAllFiles),
|
|
173
|
+
queryKey: [...uploadsQueryKey, "files", includeAllFiles],
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const files = useMemo(() => uploadedFilesFromBindings(filesQuery.data ?? []), [filesQuery.data]);
|
|
177
|
+
|
|
178
|
+
const runsQuery = useSparqlSelect({
|
|
179
|
+
query: ingestRunsQuery,
|
|
180
|
+
queryKey: [...uploadsQueryKey, "runs"],
|
|
181
|
+
refetchInterval: (query) =>
|
|
182
|
+
hasWaitingFile(filesWithIngestState(files, ingestRunsFromBindings(query.state.data ?? [])))
|
|
183
|
+
? ingestPollIntervalMs
|
|
184
|
+
: false,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const runs = useMemo(() => ingestRunsFromBindings(runsQuery.data ?? []), [runsQuery.data]);
|
|
188
|
+
const filesWithState = useMemo(() => filesWithIngestState(files, runs), [files, runs]);
|
|
189
|
+
const fileByIri = useMemo(() => new Map(files.map((file) => [file.iri, file])), [files]);
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
files: filesWithState,
|
|
193
|
+
runs,
|
|
194
|
+
fileByIri,
|
|
195
|
+
hasWaiting: hasWaitingFile(filesWithState),
|
|
196
|
+
isLoading: filesQuery.isLoading || runsQuery.isLoading,
|
|
197
|
+
error: filesQuery.error ?? runsQuery.error,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function useInvalidateUploads() {
|
|
202
|
+
const queryClient = useQueryClient();
|
|
203
|
+
|
|
204
|
+
return useCallback(() => queryClient.invalidateQueries({ queryKey: uploadsQueryKey }), [queryClient]);
|
|
205
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useLinkUnlinkedDatastoreMutation, useDatastoreTreeQuery, useUnlinkedDatastoresQuery } from "@manta-eu/client";
|
|
2
2
|
import { parseError } from "@manta-eu/client/shared/error-utils";
|
|
3
|
+
import { normalizeGroupPath, pathSegments } from "@manta-eu/client/shared/team";
|
|
3
4
|
import { isPresent } from "@manta-eu/lib/present";
|
|
4
5
|
import { useQueryClient } from "@tanstack/react-query";
|
|
5
6
|
import { useState } from "react";
|
|
@@ -30,6 +31,7 @@ export type SuggestedLinkTarget = {
|
|
|
30
31
|
export type LinkUnlinkedDatastoreDialogProps = {
|
|
31
32
|
datastoreId: string;
|
|
32
33
|
organizationNames: string[];
|
|
34
|
+
groupPathsByOrganization: Map<string, string[]>;
|
|
33
35
|
existingOrganizationNames: Set<string>;
|
|
34
36
|
suggestedMatch?: SuggestedLinkTarget;
|
|
35
37
|
onClose: () => void;
|
|
@@ -37,8 +39,17 @@ export type LinkUnlinkedDatastoreDialogProps = {
|
|
|
37
39
|
|
|
38
40
|
type Mode = "new" | "existing";
|
|
39
41
|
|
|
42
|
+
const organizationRootPath = "/";
|
|
43
|
+
|
|
40
44
|
export function LinkUnlinkedDatastoreDialog(props: LinkUnlinkedDatastoreDialogProps) {
|
|
41
|
-
const {
|
|
45
|
+
const {
|
|
46
|
+
datastoreId,
|
|
47
|
+
organizationNames,
|
|
48
|
+
groupPathsByOrganization,
|
|
49
|
+
existingOrganizationNames,
|
|
50
|
+
suggestedMatch,
|
|
51
|
+
onClose,
|
|
52
|
+
} = props;
|
|
42
53
|
const { mantaClient } = useMantaClient();
|
|
43
54
|
const queryClient = useQueryClient();
|
|
44
55
|
|
|
@@ -46,6 +57,11 @@ export function LinkUnlinkedDatastoreDialog(props: LinkUnlinkedDatastoreDialogPr
|
|
|
46
57
|
const [organizationName, setOrganizationName] = useState("");
|
|
47
58
|
const [existingOrganizationName, setExistingOrganizationName] = useState(suggestedMatch?.organizationName ?? "");
|
|
48
59
|
const [name, setName] = useState(suggestedMatch?.name ?? "");
|
|
60
|
+
const [parentGroupPath, setParentGroupPath] = useState(
|
|
61
|
+
suggestedMatch ? normalizeGroupPath(suggestedMatch.parentPath.join("/")) : organizationRootPath,
|
|
62
|
+
);
|
|
63
|
+
const parentGroupPaths = groupPathsByOrganization.get(existingOrganizationName) ?? [];
|
|
64
|
+
const offeredParentGroupPath = parentGroupPaths.includes(parentGroupPath) ? parentGroupPath : organizationRootPath;
|
|
49
65
|
|
|
50
66
|
const linkMutation = useLinkUnlinkedDatastoreMutation(mantaClient, {
|
|
51
67
|
onSuccess: (result) => {
|
|
@@ -53,7 +69,11 @@ export function LinkUnlinkedDatastoreDialog(props: LinkUnlinkedDatastoreDialogPr
|
|
|
53
69
|
void queryClient.invalidateQueries({ queryKey: useDatastoreTreeQuery.getKey() });
|
|
54
70
|
void queryClient.invalidateQueries({ queryKey: useUnlinkedDatastoresQuery.getKey() });
|
|
55
71
|
void queryClient.invalidateQueries({ queryKey: ["DatastoreProvisioningStatuses"] });
|
|
56
|
-
toast.success(
|
|
72
|
+
toast.success(
|
|
73
|
+
result.linkUnlinkedDatastore.pending
|
|
74
|
+
? `Linked to '${datastoreId}', which is still being built`
|
|
75
|
+
: `Linked to the existing datastore '${datastoreId}'`,
|
|
76
|
+
);
|
|
57
77
|
toastProviderWarning(result.linkUnlinkedDatastore.providerWarning);
|
|
58
78
|
onClose();
|
|
59
79
|
} else {
|
|
@@ -72,12 +92,12 @@ export function LinkUnlinkedDatastoreDialog(props: LinkUnlinkedDatastoreDialogPr
|
|
|
72
92
|
|
|
73
93
|
const canSubmit = mode === "new" ? !!organizationName.trim() && !tenantCheck.blocked : !!existingOrganizationName;
|
|
74
94
|
const atExistingOrganizationRoot = mode === "existing" && !name.trim();
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
95
|
+
const parentPath = pathSegments(offeredParentGroupPath);
|
|
96
|
+
|
|
97
|
+
function selectOrganization(organization: string) {
|
|
98
|
+
setExistingOrganizationName(organization);
|
|
99
|
+
setParentGroupPath(organizationRootPath);
|
|
100
|
+
}
|
|
81
101
|
|
|
82
102
|
function submit() {
|
|
83
103
|
if (!canSubmit) return;
|
|
@@ -107,7 +127,7 @@ export function LinkUnlinkedDatastoreDialog(props: LinkUnlinkedDatastoreDialogPr
|
|
|
107
127
|
: {
|
|
108
128
|
datastoreId,
|
|
109
129
|
organizationName: existingOrganizationName,
|
|
110
|
-
...(isPresent(
|
|
130
|
+
...(isPresent(parentPath) ? { parentPath } : {}),
|
|
111
131
|
name: name.trim(),
|
|
112
132
|
atOrganizationRoot: false,
|
|
113
133
|
};
|
|
@@ -143,7 +163,7 @@ export function LinkUnlinkedDatastoreDialog(props: LinkUnlinkedDatastoreDialogPr
|
|
|
143
163
|
</>
|
|
144
164
|
) : (
|
|
145
165
|
<>
|
|
146
|
-
<Select value={existingOrganizationName} onValueChange={
|
|
166
|
+
<Select value={existingOrganizationName} onValueChange={selectOrganization}>
|
|
147
167
|
<SelectTrigger label="Organization" id="link-organization">
|
|
148
168
|
<SelectValue placeholder="Select an organization…" />
|
|
149
169
|
</SelectTrigger>
|
|
@@ -155,6 +175,25 @@ export function LinkUnlinkedDatastoreDialog(props: LinkUnlinkedDatastoreDialogPr
|
|
|
155
175
|
))}
|
|
156
176
|
</SelectContent>
|
|
157
177
|
</Select>
|
|
178
|
+
{isPresent(parentGroupPaths) && (
|
|
179
|
+
<Select
|
|
180
|
+
value={offeredParentGroupPath}
|
|
181
|
+
onValueChange={setParentGroupPath}
|
|
182
|
+
disabled={atExistingOrganizationRoot}
|
|
183
|
+
>
|
|
184
|
+
<SelectTrigger label="Under" id="link-parent-group">
|
|
185
|
+
<SelectValue />
|
|
186
|
+
</SelectTrigger>
|
|
187
|
+
<SelectContent>
|
|
188
|
+
<SelectItem value={organizationRootPath}>Organization root</SelectItem>
|
|
189
|
+
{parentGroupPaths.map((path) => (
|
|
190
|
+
<SelectItem key={path} value={path}>
|
|
191
|
+
{path}
|
|
192
|
+
</SelectItem>
|
|
193
|
+
))}
|
|
194
|
+
</SelectContent>
|
|
195
|
+
</Select>
|
|
196
|
+
)}
|
|
158
197
|
<Input
|
|
159
198
|
label="Datastore name"
|
|
160
199
|
placeholder="Leave empty to link to the organization root"
|