@manta-eu/react 0.9.0 → 0.11.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.
Files changed (46) hide show
  1. package/dist/components/datastore-management/use-datastore-status.d.ts +15 -0
  2. package/dist/components/datastore-management/use-datastore-status.d.ts.map +1 -1
  3. package/dist/components/datastore-management/use-datastore-status.js +73 -2
  4. package/dist/components/datastore-management/use-datastore-status.js.map +1 -1
  5. package/dist/components/explain-fact/ExplainFact.d.ts +2 -0
  6. package/dist/components/explain-fact/ExplainFact.d.ts.map +1 -1
  7. package/dist/components/explain-fact/ExplainFact.js +6 -10
  8. package/dist/components/explain-fact/ExplainFact.js.map +1 -1
  9. package/dist/components/fact/Fact.d.ts +16 -0
  10. package/dist/components/fact/Fact.d.ts.map +1 -0
  11. package/dist/components/fact/Fact.js +27 -0
  12. package/dist/components/fact/Fact.js.map +1 -0
  13. package/dist/globals.css +15 -0
  14. package/dist/routes/datastores/components/LinkUnlinkedDatastoreDialog.d.ts.map +1 -1
  15. package/dist/routes/datastores/components/LinkUnlinkedDatastoreDialog.js +2 -0
  16. package/dist/routes/datastores/components/LinkUnlinkedDatastoreDialog.js.map +1 -1
  17. package/dist/routes/datastores/components/StatusBadge.d.ts +8 -6
  18. package/dist/routes/datastores/components/StatusBadge.d.ts.map +1 -1
  19. package/dist/routes/datastores/components/StatusBadge.js +59 -20
  20. package/dist/routes/datastores/components/StatusBadge.js.map +1 -1
  21. package/dist/routes/datastores/deletion-warning.d.ts +2 -0
  22. package/dist/routes/datastores/deletion-warning.d.ts.map +1 -0
  23. package/dist/routes/datastores/deletion-warning.js +8 -0
  24. package/dist/routes/datastores/deletion-warning.js.map +1 -0
  25. package/dist/routes/datastores/flatten.d.ts +8 -3
  26. package/dist/routes/datastores/flatten.d.ts.map +1 -1
  27. package/dist/routes/datastores/flatten.js +16 -11
  28. package/dist/routes/datastores/flatten.js.map +1 -1
  29. package/dist/routes/datastores/hooks/useDatastoreColumns.d.ts +12 -2
  30. package/dist/routes/datastores/hooks/useDatastoreColumns.d.ts.map +1 -1
  31. package/dist/routes/datastores/hooks/useDatastoreColumns.js +142 -95
  32. package/dist/routes/datastores/hooks/useDatastoreColumns.js.map +1 -1
  33. package/dist/routes/datastores/index.d.ts.map +1 -1
  34. package/dist/routes/datastores/index.js +48 -57
  35. package/dist/routes/datastores/index.js.map +1 -1
  36. package/package.json +12 -7
  37. package/src/app-subpaths.json +1 -0
  38. package/src/components/datastore-management/use-datastore-status.ts +137 -2
  39. package/src/components/explain-fact/ExplainFact.tsx +19 -17
  40. package/src/components/fact/Fact.tsx +103 -0
  41. package/src/routes/datastores/components/LinkUnlinkedDatastoreDialog.tsx +2 -0
  42. package/src/routes/datastores/components/StatusBadge.tsx +90 -27
  43. package/src/routes/datastores/deletion-warning.ts +7 -0
  44. package/src/routes/datastores/flatten.ts +27 -14
  45. package/src/routes/datastores/hooks/useDatastoreColumns.tsx +198 -117
  46. package/src/routes/datastores/index.tsx +67 -72
@@ -0,0 +1,103 @@
1
+ import { useId, useState, type ReactNode } from "react";
2
+ import { ExplainFact, type ExplainFactOutput } from "#components/explain-fact/ExplainFact";
3
+ import {
4
+ Dialog,
5
+ DialogBody,
6
+ DialogContent,
7
+ DialogDescription,
8
+ DialogHeader,
9
+ DialogTitle,
10
+ } from "#components/modal/index";
11
+ import { cn } from "#lib/utils";
12
+ import { MantaClientProvider, useMantaClient } from "#providers/MantaClientProvider";
13
+
14
+ export type FactProps = {
15
+ subject: string;
16
+ predicate: string;
17
+ value: string;
18
+ datatype?: string;
19
+ namedGraph?: string;
20
+ datastoreId?: string;
21
+ label?: string;
22
+ data?: ExplainFactOutput;
23
+ children: ReactNode;
24
+ className?: string;
25
+ };
26
+
27
+ export function Fact(props: FactProps) {
28
+ const { subject, predicate, value, datatype, namedGraph, datastoreId, label, data, children, className } = props;
29
+ const [open, setOpen] = useState(false);
30
+ const hintId = useId();
31
+ const title = label ?? "Why this value";
32
+
33
+ return (
34
+ <>
35
+ <button
36
+ type="button"
37
+ data-fact-subject={subject}
38
+ data-fact-predicate={predicate}
39
+ aria-describedby={hintId}
40
+ onClick={() => setOpen(true)}
41
+ className={cn(
42
+ "cursor-help text-left underline decoration-muted-foreground decoration-dotted underline-offset-4 hover:decoration-default-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary",
43
+ className,
44
+ )}
45
+ >
46
+ {children}
47
+ </button>
48
+ <span id={hintId} hidden>
49
+ Explain {label ?? "this value"}
50
+ </span>
51
+ <Dialog open={open} onOpenChange={setOpen}>
52
+ <DialogContent size="2xl" fullScreenOnMobile>
53
+ <DialogHeader>
54
+ <DialogTitle text={title} />
55
+ <DialogDescription>The rules and data this value comes from.</DialogDescription>
56
+ </DialogHeader>
57
+ <DialogBody className="min-h-0">
58
+ <FactDatastore datastoreId={datastoreId}>
59
+ <ExplainFact
60
+ mode="full"
61
+ subject={subject}
62
+ predicate={predicate}
63
+ object={value}
64
+ objectDatatype={datatype}
65
+ namedGraph={namedGraph}
66
+ data={data}
67
+ explainLink={null}
68
+ className="h-[65vh] overflow-auto rounded-md border border-muted-border"
69
+ />
70
+ </FactDatastore>
71
+ </DialogBody>
72
+ </DialogContent>
73
+ </Dialog>
74
+ </>
75
+ );
76
+ }
77
+
78
+ type FactDatastoreProps = { datastoreId?: string; children: ReactNode };
79
+
80
+ function FactDatastore(props: FactDatastoreProps) {
81
+ const { datastoreId, children } = props;
82
+ if (datastoreId === undefined) return <>{children}</>;
83
+ return <OtherDatastore datastoreId={datastoreId}>{children}</OtherDatastore>;
84
+ }
85
+
86
+ type OtherDatastoreProps = { datastoreId: string; children: ReactNode };
87
+
88
+ function OtherDatastore(props: OtherDatastoreProps) {
89
+ const { datastoreId, children } = props;
90
+ const client = useMantaClient();
91
+ if (datastoreId === client.datastoreId) return <>{children}</>;
92
+ return (
93
+ <MantaClientProvider
94
+ mantaBaseUrl={client.mantaBaseUrl.origin}
95
+ datastoreId={datastoreId}
96
+ onUnauthorized={client.onUnauthorized}
97
+ onSignOut={client.onSignOut}
98
+ getAccessToken={client.getAccessToken}
99
+ >
100
+ {children}
101
+ </MantaClientProvider>
102
+ );
103
+ }
@@ -7,6 +7,7 @@ import { useState } from "react";
7
7
  import { toast } from "sonner";
8
8
  import { organizationNameKey } from "../flatten";
9
9
  import { useTenantOrganizationCheck } from "../hooks/useTenantOrganizationCheck";
10
+ import { toastDeletionWarning } from "../deletion-warning";
10
11
  import { toastProviderWarning } from "../link-provider";
11
12
  import { Button } from "#components/button/index";
12
13
  import { Input } from "#components/input/index";
@@ -75,6 +76,7 @@ export function LinkUnlinkedDatastoreDialog(props: LinkUnlinkedDatastoreDialogPr
75
76
  : `Linked to the existing datastore '${datastoreId}'`,
76
77
  );
77
78
  toastProviderWarning(result.linkUnlinkedDatastore.providerWarning);
79
+ toastDeletionWarning(result.linkUnlinkedDatastore.deletionWarning);
78
80
  onClose();
79
81
  } else {
80
82
  toast.error("Couldn't link the datastore", {
@@ -1,19 +1,43 @@
1
- import { enableDatastoreRemoval } from "@manta-eu/client/shared/constants";
2
1
  import { isPresent } from "@manta-eu/lib/present";
3
- import { LoaderCircle } from "lucide-react";
2
+ import { Info, LoaderCircle } from "lucide-react";
4
3
  import { Badge, type BadgeVariant } from "#components/badge/index";
5
4
  import {
6
5
  datastoreStatusCategory,
7
6
  type DatastoreStatus,
8
7
  type DatastoreStatusCategory,
9
8
  } from "#components/datastore-management/tree";
9
+ import type { DatastoreDeletion } from "#components/datastore-management/use-datastore-status";
10
+ import { Tooltip, TooltipContent, TooltipTrigger } from "#components/tooltip/index";
10
11
 
11
- export type DatastoreStatusFilterValue = "noDatastore" | "checking" | "notBuilt" | DatastoreStatusCategory;
12
+ export type DatastoreStatusFilterValue =
13
+ | "noDatastore"
14
+ | "checking"
15
+ | "notBuilt"
16
+ | "deleting"
17
+ | "deletionRetrying"
18
+ | "deletionStopped"
19
+ | "deletionUnknown"
20
+ | DatastoreStatusCategory;
12
21
 
13
22
  type StatusNode = { datastoreId?: string; removed: boolean; isUnlinked?: boolean; unbuilt?: boolean };
14
23
 
15
- export function datastoreStatusFilterValue(node: StatusNode, status?: DatastoreStatus): DatastoreStatusFilterValue {
24
+ export type DatastoreStatusBadgeProps = {
25
+ node: StatusNode;
26
+ status?: DatastoreStatus;
27
+ deletion?: DatastoreDeletion;
28
+ };
29
+
30
+ export function datastoreStatusFilterValue({
31
+ node,
32
+ status,
33
+ deletion,
34
+ }: DatastoreStatusBadgeProps): DatastoreStatusFilterValue {
16
35
  if (!isPresent(node.datastoreId)) return "noDatastore";
36
+ if (deletion) {
37
+ if (deletion.unreadable === true) return "deletionUnknown";
38
+ if (isPresent(deletion.error)) return "deletionStopped";
39
+ return deletion.retrying ? "deletionRetrying" : "deleting";
40
+ }
17
41
  if (node.unbuilt === true && status?.exists !== true) return "notBuilt";
18
42
  return datastoreStatusCategory(node, status) ?? "checking";
19
43
  }
@@ -22,35 +46,54 @@ type BadgeConfig = { label: string; variant: BadgeVariant; title?: string; spinn
22
46
 
23
47
  // The only place these strings live: the badge and the filter dropdown both read from here.
24
48
  const badgeConfigByFilterValue: Record<DatastoreStatusFilterValue, BadgeConfig> = {
25
- ready: { label: "Ready", variant: "success" },
49
+ ready: { label: "Ready", variant: "success", title: "Built and ready to use." },
26
50
  materializing: {
27
51
  label: "Materializing",
28
52
  variant: "warning",
29
53
  spinning: true,
30
- title: "KGC is creating its knowledge graph. This can take a few minutes.",
54
+ title: "Building its knowledge graph. This can take a few minutes.",
31
55
  },
32
56
  notRegistered: {
33
57
  label: "Not registered",
34
58
  variant: "destructive",
35
- title: "This id never reached the datastore registry, so nothing will build it. Provision it again.",
59
+ title: "It was never registered, so it can't be built. Use Register to fix it.",
36
60
  },
37
61
  notBuilt: {
38
62
  label: "Not built",
39
63
  variant: "warning",
40
64
  title: "Registered in Infisical, but no datastore exists for it. Link it to build it, or delete it.",
41
65
  },
42
- offline: { label: "Offline", variant: "destructive" },
43
- unavailable: { label: "Status unavailable", variant: "copy" },
44
- checking: { label: "Checking…", variant: "copy", spinning: true },
45
- removed: enableDatastoreRemoval
46
- ? { label: "Removed", variant: "copy" }
47
- : {
48
- label: "Removed",
49
- variant: "warning",
50
- title:
51
- "Out of use: a permanent delete stopped partway, or it was removed before removal was turned off. " +
52
- "Delete it permanently only if it should go for good.",
53
- },
66
+ offline: {
67
+ label: "Offline",
68
+ variant: "destructive",
69
+ title: "Its data is kept, but it isn't running. Bring it online to use it again.",
70
+ },
71
+ unavailable: {
72
+ label: "Status unavailable",
73
+ variant: "copy",
74
+ title: "Its status couldn't be read just now. It's checked again automatically.",
75
+ },
76
+ checking: { label: "Checking…", variant: "copy", spinning: true, title: "Reading its status." },
77
+ removed: {
78
+ label: "Removed",
79
+ variant: "warning",
80
+ title:
81
+ "Out of use: a permanent delete stopped partway, or it was removed before removal was turned off. " +
82
+ "Delete it permanently only if it should go for good.",
83
+ },
84
+ deleting: {
85
+ label: "Deleting…",
86
+ variant: "warning",
87
+ spinning: true,
88
+ title: "Deleting in the background. This can take a few minutes.",
89
+ },
90
+ deletionRetrying: { label: "Retrying deletion", variant: "warning" },
91
+ deletionStopped: { label: "Deletion stopped", variant: "destructive" },
92
+ deletionUnknown: {
93
+ label: "Deletion unknown",
94
+ variant: "copy",
95
+ title: "Couldn't check how far its deletion got. If this lasts, delete it again to restart it.",
96
+ },
54
97
  noDatastore: { label: "No datastore", variant: "copy" },
55
98
  };
56
99
 
@@ -62,6 +105,10 @@ const filterOptionOrder: DatastoreStatusFilterValue[] = [
62
105
  "offline",
63
106
  "unavailable",
64
107
  "removed",
108
+ "deleting",
109
+ "deletionRetrying",
110
+ "deletionStopped",
111
+ "deletionUnknown",
65
112
  ];
66
113
 
67
114
  export const datastoreStatusFilterOptions: { id: "ALL" | DatastoreStatusFilterValue; label: string }[] = [
@@ -69,22 +116,38 @@ export const datastoreStatusFilterOptions: { id: "ALL" | DatastoreStatusFilterVa
69
116
  ...filterOptionOrder.map((id) => ({ id, label: badgeConfigByFilterValue[id].label })),
70
117
  ];
71
118
 
72
- export type DatastoreStatusBadgeProps = {
73
- node: StatusNode;
74
- status?: DatastoreStatus;
75
- };
119
+ function retryingExplanation(failure: string | null | undefined): string {
120
+ return `${failure ?? "Part of its deletion failed."} It's retried automatically - select Retry now to try again right away.`;
121
+ }
76
122
 
77
123
  export function DatastoreStatusBadge(props: DatastoreStatusBadgeProps) {
78
- const { node, status } = props;
124
+ const { node, deletion } = props;
79
125
 
80
126
  if (!isPresent(node.datastoreId)) return <span className="typography-tiny text-muted-foreground">—</span>;
81
127
 
82
- const config = badgeConfigByFilterValue[datastoreStatusFilterValue(node, status)];
128
+ const config = badgeConfigByFilterValue[datastoreStatusFilterValue(props)];
129
+ const stoppedReason = deletion?.error;
130
+ const retrying = !isPresent(stoppedReason) && deletion?.retrying === true;
131
+ const explanation = stoppedReason ?? (retrying ? retryingExplanation(deletion?.failure) : config.title);
83
132
 
84
- return (
85
- <Badge size="sm" variant={config.variant} className="cursor-default" title={config.title}>
133
+ const badge = (
134
+ <Badge
135
+ size="sm"
136
+ variant={config.variant}
137
+ className="cursor-default"
138
+ tabIndex={isPresent(explanation) ? 0 : undefined}
139
+ >
86
140
  {config.spinning && <LoaderCircle className="size-3 animate-spin" />}
87
141
  {config.label}
142
+ {(isPresent(stoppedReason) || retrying) && <Info className="size-3" aria-hidden />}
88
143
  </Badge>
89
144
  );
145
+ if (!isPresent(explanation)) return badge;
146
+
147
+ return (
148
+ <Tooltip>
149
+ <TooltipTrigger asChild>{badge}</TooltipTrigger>
150
+ <TooltipContent className="max-w-80">{explanation}</TooltipContent>
151
+ </Tooltip>
152
+ );
90
153
  }
@@ -0,0 +1,7 @@
1
+ import { isPresent } from "@manta-eu/lib/present";
2
+ import { toast } from "sonner";
3
+
4
+ export function toastDeletionWarning(deletionWarning: string | null | undefined) {
5
+ if (!isPresent(deletionWarning)) return;
6
+ toast.warning("It was taken back from its deletion", { description: deletionWarning });
7
+ }
@@ -63,8 +63,21 @@ export type DatastoreRowVisibility = {
63
63
  showUnprovisioned: boolean;
64
64
  };
65
65
 
66
- function matchesVisibility(row: DatastoreRow, visibility: DatastoreRowVisibility): boolean {
67
- return (visibility.showArchived || !row.removed) && (visibility.showUnprovisioned || isPresent(row.datastoreId));
66
+ export type DatastoreRowFilter = {
67
+ rows: DatastoreRow[];
68
+ visibility: DatastoreRowVisibility;
69
+ deleting: ReadonlySet<string>;
70
+ };
71
+
72
+ function matchesVisibility(
73
+ row: DatastoreRow,
74
+ { visibility, deleting }: Pick<DatastoreRowFilter, "visibility" | "deleting">,
75
+ ): boolean {
76
+ const beingDeleted = isPresent(row.datastoreId) && deleting.has(row.datastoreId);
77
+ return (
78
+ (visibility.showArchived || !row.removed || beingDeleted) &&
79
+ (visibility.showUnprovisioned || isPresent(row.datastoreId))
80
+ );
68
81
  }
69
82
 
70
83
  function hasVisibleDescendant(rows: DatastoreRow[], shown: boolean[], index: number): boolean {
@@ -75,8 +88,9 @@ function hasVisibleDescendant(rows: DatastoreRow[], shown: boolean[], index: num
75
88
  return false;
76
89
  }
77
90
 
78
- export function visibleDatastoreRows(rows: DatastoreRow[], visibility: DatastoreRowVisibility): DatastoreRow[] {
79
- const shown = rows.map((row) => matchesVisibility(row, visibility));
91
+ export function visibleDatastoreRows(filter: DatastoreRowFilter): DatastoreRow[] {
92
+ const { rows } = filter;
93
+ const shown = rows.map((row) => matchesVisibility(row, filter));
80
94
  return rows.filter((_row, index) => shown[index] || hasVisibleDescendant(rows, shown, index));
81
95
  }
82
96
 
@@ -96,23 +110,21 @@ export function nestedDatastoreNamesByRowId(rows: DatastoreRow[]): Map<string, s
96
110
  }
97
111
 
98
112
  export function hiddenRowCount(
99
- rows: DatastoreRow[],
100
- visibility: DatastoreRowVisibility,
113
+ filter: DatastoreRowFilter,
101
114
  key: keyof DatastoreRowVisibility,
102
115
  shownCount: number,
103
116
  ): number {
104
- if (visibility[key]) return 0;
117
+ if (filter.visibility[key]) return 0;
105
118
 
106
- return visibleDatastoreRows(rows, { ...visibility, [key]: true }).length - shownCount;
119
+ return visibleDatastoreRows({ ...filter, visibility: { ...filter.visibility, [key]: true } }).length - shownCount;
107
120
  }
108
121
 
109
- export function hiddenRemovedRowCount(rows: DatastoreRow[], visibility: DatastoreRowVisibility): number {
110
- if (visibility.showArchived) return 0;
122
+ export function hiddenRemovedRowCount(filter: DatastoreRowFilter): number {
123
+ if (filter.visibility.showArchived) return 0;
111
124
 
112
- const shown = new Set(visibleDatastoreRows(rows, visibility));
113
- return rows.filter(
114
- (row) => row.removed && !shown.has(row) && matchesVisibility(row, { ...visibility, showArchived: true }),
115
- ).length;
125
+ const shown = new Set(visibleDatastoreRows(filter));
126
+ const revealed = { ...filter, visibility: { ...filter.visibility, showArchived: true } };
127
+ return filter.rows.filter((row) => row.removed && !shown.has(row) && matchesVisibility(row, revealed)).length;
116
128
  }
117
129
 
118
130
  // Matches Keycloak's own org lookup (ensureOrganization), which is case-insensitive and trims too.
@@ -162,6 +174,7 @@ export function visibilityForStatusFilter(
162
174
  return match(status)
163
175
  .with("removed", () => ({ ...visibility, showArchived: true }))
164
176
  .with("noDatastore", () => ({ ...visibility, showUnprovisioned: true }))
177
+ .with("deleting", "deletionRetrying", "deletionStopped", "deletionUnknown", () => visibility)
165
178
  .with(
166
179
  "ALL",
167
180
  "ready",