@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.
Files changed (148) hide show
  1. package/dist/components/chart/ForecastLegend.d.ts.map +1 -1
  2. package/dist/components/chart/ForecastLegend.js +10 -2
  3. package/dist/components/chart/ForecastLegend.js.map +1 -1
  4. package/dist/components/chart/forecast-model.d.ts.map +1 -1
  5. package/dist/components/chart/forecast-model.js.map +1 -1
  6. package/dist/components/chat-view/Chat.js +2 -2
  7. package/dist/components/file-dropzone/index.d.ts +25 -0
  8. package/dist/components/file-dropzone/index.d.ts.map +1 -0
  9. package/dist/components/file-dropzone/index.js +70 -0
  10. package/dist/components/file-dropzone/index.js.map +1 -0
  11. package/dist/components/planning/plan-detail-utils.d.ts.map +1 -1
  12. package/dist/components/planning/plan-detail-utils.js +2 -0
  13. package/dist/components/planning/plan-detail-utils.js.map +1 -1
  14. package/dist/components/toast/index.d.ts +1 -0
  15. package/dist/components/toast/index.d.ts.map +1 -1
  16. package/dist/components/toast/index.js +1 -0
  17. package/dist/components/toast/index.js.map +1 -1
  18. package/dist/globals.css +27 -0
  19. package/dist/graph/NodeDetailPanel/TypeSummaryPanel.d.ts.map +1 -1
  20. package/dist/graph/NodeDetailPanel/TypeSummaryPanel.js.map +1 -1
  21. package/dist/hooks/use-sparql-construct.d.ts +2 -0
  22. package/dist/hooks/use-sparql-construct.d.ts.map +1 -1
  23. package/dist/hooks/use-sparql-construct.js +2 -1
  24. package/dist/hooks/use-sparql-construct.js.map +1 -1
  25. package/dist/index.d.ts +1 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +1 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/lib/datastore-route-table.d.ts +3 -0
  30. package/dist/lib/datastore-route-table.d.ts.map +1 -1
  31. package/dist/lib/datastore-route-table.js +3 -0
  32. package/dist/lib/datastore-route-table.js.map +1 -1
  33. package/dist/lib/hooks/{useChatUpload.d.ts → useFileUpload.d.ts} +3 -3
  34. package/dist/lib/hooks/{useChatUpload.d.ts.map → useFileUpload.d.ts.map} +1 -1
  35. package/dist/lib/hooks/{useChatUpload.js → useFileUpload.js} +2 -2
  36. package/dist/lib/hooks/{useChatUpload.js.map → useFileUpload.js.map} +1 -1
  37. package/dist/routes/data-collection/index.d.ts +3 -0
  38. package/dist/routes/data-collection/index.d.ts.map +1 -0
  39. package/dist/routes/data-collection/index.js +43 -0
  40. package/dist/routes/data-collection/index.js.map +1 -0
  41. package/dist/routes/data-explorer/components/DataExplorerLayout.d.ts.map +1 -1
  42. package/dist/routes/data-explorer/components/DataExplorerLayout.js +5 -2
  43. package/dist/routes/data-explorer/components/DataExplorerLayout.js.map +1 -1
  44. package/dist/routes/data-explorer/components/Rail.d.ts.map +1 -1
  45. package/dist/routes/data-explorer/components/Rail.js +3 -3
  46. package/dist/routes/data-explorer/components/Rail.js.map +1 -1
  47. package/dist/routes/data-explorer/components/uploads/IngestRunsList.d.ts +8 -0
  48. package/dist/routes/data-explorer/components/uploads/IngestRunsList.d.ts.map +1 -0
  49. package/dist/routes/data-explorer/components/uploads/IngestRunsList.js +18 -0
  50. package/dist/routes/data-explorer/components/uploads/IngestRunsList.js.map +1 -0
  51. package/dist/routes/data-explorer/components/uploads/IngestStateStatus.d.ts +14 -0
  52. package/dist/routes/data-explorer/components/uploads/IngestStateStatus.d.ts.map +1 -0
  53. package/dist/routes/data-explorer/components/uploads/IngestStateStatus.js +33 -0
  54. package/dist/routes/data-explorer/components/uploads/IngestStateStatus.js.map +1 -0
  55. package/dist/routes/data-explorer/components/uploads/UploadedFilesList.d.ts +8 -0
  56. package/dist/routes/data-explorer/components/uploads/UploadedFilesList.d.ts.map +1 -0
  57. package/dist/routes/data-explorer/components/uploads/UploadedFilesList.js +34 -0
  58. package/dist/routes/data-explorer/components/uploads/UploadedFilesList.js.map +1 -0
  59. package/dist/routes/data-explorer/components/uploads/UploadsPages.d.ts +7 -0
  60. package/dist/routes/data-explorer/components/uploads/UploadsPages.d.ts.map +1 -0
  61. package/dist/routes/data-explorer/components/uploads/UploadsPages.js +35 -0
  62. package/dist/routes/data-explorer/components/uploads/UploadsPages.js.map +1 -0
  63. package/dist/routes/data-explorer/index.d.ts +1 -0
  64. package/dist/routes/data-explorer/index.d.ts.map +1 -1
  65. package/dist/routes/data-explorer/index.js +1 -0
  66. package/dist/routes/data-explorer/index.js.map +1 -1
  67. package/dist/routes/data-explorer/namespaces.d.ts +4 -0
  68. package/dist/routes/data-explorer/namespaces.d.ts.map +1 -1
  69. package/dist/routes/data-explorer/namespaces.js +6 -1
  70. package/dist/routes/data-explorer/namespaces.js.map +1 -1
  71. package/dist/routes/data-explorer/uploads.d.ts +56 -0
  72. package/dist/routes/data-explorer/uploads.d.ts.map +1 -0
  73. package/dist/routes/data-explorer/uploads.js +140 -0
  74. package/dist/routes/data-explorer/uploads.js.map +1 -0
  75. package/dist/routes/datastores/components/LinkUnlinkedDatastoreDialog.d.ts +1 -0
  76. package/dist/routes/datastores/components/LinkUnlinkedDatastoreDialog.d.ts.map +1 -1
  77. package/dist/routes/datastores/components/LinkUnlinkedDatastoreDialog.js +16 -9
  78. package/dist/routes/datastores/components/LinkUnlinkedDatastoreDialog.js.map +1 -1
  79. package/dist/routes/datastores/components/StatusBadge.d.ts +7 -8
  80. package/dist/routes/datastores/components/StatusBadge.d.ts.map +1 -1
  81. package/dist/routes/datastores/components/StatusBadge.js +17 -1
  82. package/dist/routes/datastores/components/StatusBadge.js.map +1 -1
  83. package/dist/routes/datastores/flatten.d.ts +6 -1
  84. package/dist/routes/datastores/flatten.d.ts.map +1 -1
  85. package/dist/routes/datastores/flatten.js +37 -2
  86. package/dist/routes/datastores/flatten.js.map +1 -1
  87. package/dist/routes/datastores/hooks/useDatastoreColumns.d.ts +2 -0
  88. package/dist/routes/datastores/hooks/useDatastoreColumns.d.ts.map +1 -1
  89. package/dist/routes/datastores/hooks/useDatastoreColumns.js +8 -4
  90. package/dist/routes/datastores/hooks/useDatastoreColumns.js.map +1 -1
  91. package/dist/routes/datastores/index.d.ts.map +1 -1
  92. package/dist/routes/datastores/index.js +53 -18
  93. package/dist/routes/datastores/index.js.map +1 -1
  94. package/dist/routes/integrations/components/connect-forms/GoogleMerchantCenterConnectForm.d.ts +3 -0
  95. package/dist/routes/integrations/components/connect-forms/GoogleMerchantCenterConnectForm.d.ts.map +1 -0
  96. package/dist/routes/integrations/components/connect-forms/GoogleMerchantCenterConnectForm.js +38 -0
  97. package/dist/routes/integrations/components/connect-forms/GoogleMerchantCenterConnectForm.js.map +1 -0
  98. package/dist/routes/integrations/components/connect-forms/google-merchant-center-account-tree.d.ts +4 -0
  99. package/dist/routes/integrations/components/connect-forms/google-merchant-center-account-tree.d.ts.map +1 -0
  100. package/dist/routes/integrations/components/connect-forms/google-merchant-center-account-tree.js +32 -0
  101. package/dist/routes/integrations/components/connect-forms/google-merchant-center-account-tree.js.map +1 -0
  102. package/dist/routes/integrations/components/connect-forms/setup-account-unavailable-reason.d.ts.map +1 -1
  103. package/dist/routes/integrations/components/connect-forms/setup-account-unavailable-reason.js +1 -0
  104. package/dist/routes/integrations/components/connect-forms/setup-account-unavailable-reason.js.map +1 -1
  105. package/dist/routes/integrations/connect-forms.d.ts +1 -0
  106. package/dist/routes/integrations/connect-forms.d.ts.map +1 -1
  107. package/dist/routes/integrations/connect-forms.js +1 -0
  108. package/dist/routes/integrations/connect-forms.js.map +1 -1
  109. package/dist/routes/integrations/connection-view.d.ts.map +1 -1
  110. package/dist/routes/integrations/connection-view.js +1 -0
  111. package/dist/routes/integrations/connection-view.js.map +1 -1
  112. package/dist/shared/graph-derived-queries.d.ts.map +1 -1
  113. package/dist/shared/graph-derived-queries.js +2 -1
  114. package/dist/shared/graph-derived-queries.js.map +1 -1
  115. package/package.json +21 -9
  116. package/src/app-subpaths.json +1 -0
  117. package/src/components/chart/ForecastLegend.tsx +15 -13
  118. package/src/components/chart/forecast-model.ts +6 -8
  119. package/src/components/chat-view/Chat.tsx +2 -2
  120. package/src/components/file-dropzone/index.tsx +165 -0
  121. package/src/components/planning/plan-detail-utils.tsx +2 -0
  122. package/src/components/toast/index.tsx +1 -0
  123. package/src/graph/NodeDetailPanel/TypeSummaryPanel.tsx +12 -14
  124. package/src/hooks/use-sparql-construct.ts +4 -2
  125. package/src/index.ts +1 -0
  126. package/src/lib/datastore-route-table.ts +6 -0
  127. package/src/lib/hooks/{useChatUpload.ts → useFileUpload.ts} +2 -2
  128. package/src/routes/data-collection/index.tsx +82 -0
  129. package/src/routes/data-explorer/components/DataExplorerLayout.tsx +5 -2
  130. package/src/routes/data-explorer/components/Rail.tsx +13 -2
  131. package/src/routes/data-explorer/components/uploads/IngestRunsList.tsx +63 -0
  132. package/src/routes/data-explorer/components/uploads/IngestStateStatus.tsx +45 -0
  133. package/src/routes/data-explorer/components/uploads/UploadedFilesList.tsx +93 -0
  134. package/src/routes/data-explorer/components/uploads/UploadsPages.tsx +80 -0
  135. package/src/routes/data-explorer/index.tsx +6 -0
  136. package/src/routes/data-explorer/namespaces.ts +7 -1
  137. package/src/routes/data-explorer/uploads.ts +205 -0
  138. package/src/routes/datastores/components/LinkUnlinkedDatastoreDialog.tsx +49 -10
  139. package/src/routes/datastores/components/StatusBadge.tsx +22 -7
  140. package/src/routes/datastores/flatten.ts +50 -2
  141. package/src/routes/datastores/hooks/useDatastoreColumns.tsx +21 -1
  142. package/src/routes/datastores/index.tsx +64 -13
  143. package/src/routes/integrations/components/connect-forms/GoogleMerchantCenterConnectForm.tsx +61 -0
  144. package/src/routes/integrations/components/connect-forms/google-merchant-center-account-tree.ts +38 -0
  145. package/src/routes/integrations/components/connect-forms/setup-account-unavailable-reason.ts +1 -0
  146. package/src/routes/integrations/connect-forms.ts +1 -0
  147. package/src/routes/integrations/connection-view.ts +1 -0
  148. package/src/shared/graph-derived-queries.ts +2 -0
@@ -1,3 +1,4 @@
1
+ import { enableDatastoreRemoval } from "@manta-eu/client/shared/constants";
1
2
  import { isPresent } from "@manta-eu/lib/present";
2
3
  import { LoaderCircle } from "lucide-react";
3
4
  import { Badge, type BadgeVariant } from "#components/badge/index";
@@ -7,13 +8,13 @@ import {
7
8
  type DatastoreStatusCategory,
8
9
  } from "#components/datastore-management/tree";
9
10
 
10
- export type DatastoreStatusFilterValue = "noDatastore" | "checking" | DatastoreStatusCategory;
11
+ export type DatastoreStatusFilterValue = "noDatastore" | "checking" | "notBuilt" | DatastoreStatusCategory;
11
12
 
12
- export function datastoreStatusFilterValue(
13
- node: { datastoreId?: string; removed: boolean; isUnlinked?: boolean },
14
- status?: DatastoreStatus,
15
- ): DatastoreStatusFilterValue {
13
+ type StatusNode = { datastoreId?: string; removed: boolean; isUnlinked?: boolean; unbuilt?: boolean };
14
+
15
+ export function datastoreStatusFilterValue(node: StatusNode, status?: DatastoreStatus): DatastoreStatusFilterValue {
16
16
  if (!isPresent(node.datastoreId)) return "noDatastore";
17
+ if (node.unbuilt === true && status?.exists !== true) return "notBuilt";
17
18
  return datastoreStatusCategory(node, status) ?? "checking";
18
19
  }
19
20
 
@@ -33,10 +34,23 @@ const badgeConfigByFilterValue: Record<DatastoreStatusFilterValue, BadgeConfig>
33
34
  variant: "destructive",
34
35
  title: "This id never reached the datastore registry, so nothing will build it. Provision it again.",
35
36
  },
37
+ notBuilt: {
38
+ label: "Not built",
39
+ variant: "warning",
40
+ title: "Registered in Infisical, but no datastore exists for it. Link it to build it, or delete it.",
41
+ },
36
42
  offline: { label: "Offline", variant: "destructive" },
37
43
  unavailable: { label: "Status unavailable", variant: "copy" },
38
44
  checking: { label: "Checking…", variant: "copy", spinning: true },
39
- removed: { label: "Removed", variant: "copy" },
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
+ },
40
54
  noDatastore: { label: "No datastore", variant: "copy" },
41
55
  };
42
56
 
@@ -44,6 +58,7 @@ const filterOptionOrder: DatastoreStatusFilterValue[] = [
44
58
  "ready",
45
59
  "materializing",
46
60
  "notRegistered",
61
+ "notBuilt",
47
62
  "offline",
48
63
  "unavailable",
49
64
  "removed",
@@ -55,7 +70,7 @@ export const datastoreStatusFilterOptions: { id: "ALL" | DatastoreStatusFilterVa
55
70
  ];
56
71
 
57
72
  export type DatastoreStatusBadgeProps = {
58
- node: { datastoreId?: string; removed: boolean; isUnlinked?: boolean };
73
+ node: StatusNode;
59
74
  status?: DatastoreStatus;
60
75
  };
61
76
 
@@ -25,6 +25,7 @@ export type DatastoreRow = {
25
25
  // Real in the datastore, but no Keycloak node behind it, so it can only be selected.
26
26
  isRealmDefault?: boolean;
27
27
  isUnlinked?: boolean;
28
+ unbuilt?: boolean;
28
29
  };
29
30
 
30
31
  export function realmDefaultRow(datastoreId: Realm): DatastoreRow {
@@ -105,6 +106,15 @@ export function hiddenRowCount(
105
106
  return visibleDatastoreRows(rows, { ...visibility, [key]: true }).length - shownCount;
106
107
  }
107
108
 
109
+ export function hiddenRemovedRowCount(rows: DatastoreRow[], visibility: DatastoreRowVisibility): number {
110
+ if (visibility.showArchived) return 0;
111
+
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;
116
+ }
117
+
108
118
  // Matches Keycloak's own org lookup (ensureOrganization), which is case-insensitive and trims too.
109
119
  export function organizationNameKey(name: string): string {
110
120
  return name.trim().toLowerCase();
@@ -118,6 +128,33 @@ export function organizationNames(rows: DatastoreRow[]): string[] {
118
128
  return [...new Set(rows.filter((row) => row.isOrganization).map((row) => row.organizationName))].sort();
119
129
  }
120
130
 
131
+ export function removedSubtreeRowIds(rows: DatastoreRow[]): Set<string> {
132
+ const ids = new Set<string>();
133
+ let removedDepth: number | undefined;
134
+ for (const row of rows) {
135
+ if (removedDepth === undefined || row.depth <= removedDepth) removedDepth = row.removed ? row.depth : undefined;
136
+ if (removedDepth !== undefined) ids.add(row.id);
137
+ }
138
+ return ids;
139
+ }
140
+
141
+ export function outsideRemovedSubtrees(rows: DatastoreRow[]): DatastoreRow[] {
142
+ const removed = removedSubtreeRowIds(rows);
143
+ return rows.filter((row) => !removed.has(row.id));
144
+ }
145
+
146
+ export function organizationGroupPaths(rows: DatastoreRow[]): Map<string, string[]> {
147
+ const paths = new Map<string, string[]>();
148
+ for (const row of outsideRemovedSubtrees(rows)) {
149
+ if (row.isOrganization || row.isRealmDefault === true || row.isUnlinked === true) continue;
150
+
151
+ const organizationPaths = paths.get(row.organizationName);
152
+ if (organizationPaths) organizationPaths.push(row.path);
153
+ else paths.set(row.organizationName, [row.path]);
154
+ }
155
+ return paths;
156
+ }
157
+
121
158
  export function visibilityForStatusFilter(
122
159
  visibility: DatastoreRowVisibility,
123
160
  status: DatastoreStatusFilterValue | "ALL",
@@ -125,11 +162,21 @@ export function visibilityForStatusFilter(
125
162
  return match(status)
126
163
  .with("removed", () => ({ ...visibility, showArchived: true }))
127
164
  .with("noDatastore", () => ({ ...visibility, showUnprovisioned: true }))
128
- .with("ALL", "ready", "materializing", "notRegistered", "offline", "unavailable", "checking", () => visibility)
165
+ .with(
166
+ "ALL",
167
+ "ready",
168
+ "materializing",
169
+ "notRegistered",
170
+ "notBuilt",
171
+ "offline",
172
+ "unavailable",
173
+ "checking",
174
+ () => visibility,
175
+ )
129
176
  .exhaustive();
130
177
  }
131
178
 
132
- export function unlinkedDatastoreRow(datastoreId: string): DatastoreRow {
179
+ export function unlinkedDatastoreRow(datastoreId: string, built = true): DatastoreRow {
133
180
  return {
134
181
  id: `unlinked:${datastoreId}`,
135
182
  name: datastoreId,
@@ -142,6 +189,7 @@ export function unlinkedDatastoreRow(datastoreId: string): DatastoreRow {
142
189
  removed: false,
143
190
  isOrganization: false,
144
191
  isUnlinked: true,
192
+ unbuilt: !built,
145
193
  };
146
194
  }
147
195
 
@@ -6,6 +6,7 @@ import {
6
6
  Blocks,
7
7
  Check,
8
8
  DatabaseZap,
9
+ Hammer,
9
10
  Link2,
10
11
  MoreHorizontal,
11
12
  Pencil,
@@ -113,6 +114,8 @@ type UseDatastoreColumnsArgs = {
113
114
  onDelete: (row: DatastoreRow) => void;
114
115
  onRename: (row: DatastoreRow) => void;
115
116
  onRegister: (row: DatastoreRow) => void;
117
+ registeringDatastoreIds: ReadonlySet<string>;
118
+ removedSubtreeRowIds: ReadonlySet<string>;
116
119
  onManageUsers: (row: DatastoreRow) => void;
117
120
  onManageCustomApps: (row: DatastoreRow) => void;
118
121
  onBringOnline: (row: DatastoreRow) => void;
@@ -141,6 +144,8 @@ export function useDatastoreColumns(args: UseDatastoreColumnsArgs): ColumnDef<Da
141
144
  onDelete,
142
145
  onRename,
143
146
  onRegister,
147
+ registeringDatastoreIds,
148
+ removedSubtreeRowIds,
144
149
  onManageUsers,
145
150
  onManageCustomApps,
146
151
  onBringOnline,
@@ -311,6 +316,8 @@ export function useDatastoreColumns(args: UseDatastoreColumnsArgs): ColumnDef<Da
311
316
  const status = hasDatastore ? statusByDatastoreId[datastoreId] : undefined;
312
317
  const category = datastoreStatusCategory(datastoreRow, status);
313
318
  const notRegistered = category === "notRegistered";
319
+ const buildable = category === "materializing";
320
+ const registering = hasDatastore && registeringDatastoreIds.has(datastoreId);
314
321
  const reason = match({ notRegistered, materializing: isMaterializing(status) && !isRealmDefault })
315
322
  .with({ notRegistered: true }, () => "Not registered - use Register to fix, then try selecting again.")
316
323
  .with({ materializing: true }, () => "Still materializing - this can take a few minutes.")
@@ -359,12 +366,13 @@ export function useDatastoreColumns(args: UseDatastoreColumnsArgs): ColumnDef<Da
359
366
  Bring online
360
367
  </Button>
361
368
  )}
362
- {!isUnlinked && hasDatastore && !removed && notRegistered && (
369
+ {!isUnlinked && hasDatastore && !removedSubtreeRowIds.has(datastoreRow.id) && notRegistered && (
363
370
  <Button
364
371
  data-testid={`datastore-register-${datastoreRow.id}`}
365
372
  size="sm"
366
373
  variant="secondary"
367
374
  icon={RefreshCw}
375
+ disabled={registering}
368
376
  onClick={() => onRegister(datastoreRow)}
369
377
  >
370
378
  Register
@@ -439,6 +447,16 @@ export function useDatastoreColumns(args: UseDatastoreColumnsArgs): ColumnDef<Da
439
447
  Provision datastore
440
448
  </DropdownItem>
441
449
  ))}
450
+ {hasDatastore && !removedSubtreeRowIds.has(datastoreRow.id) && buildable && (
451
+ <DropdownItem
452
+ icon={<Hammer className="size-4" />}
453
+ disabled={registering}
454
+ title="Starts a build, or joins the one already running"
455
+ onSelect={() => onRegister(datastoreRow)}
456
+ >
457
+ Build datastore now
458
+ </DropdownItem>
459
+ )}
442
460
  <DropdownItem icon={<Plus className="size-4" />} onSelect={() => onAddChild(datastoreRow)}>
443
461
  Add datastore
444
462
  </DropdownItem>
@@ -505,6 +523,8 @@ export function useDatastoreColumns(args: UseDatastoreColumnsArgs): ColumnDef<Da
505
523
  onRestore,
506
524
  onDelete,
507
525
  onRegister,
526
+ registeringDatastoreIds,
527
+ removedSubtreeRowIds,
508
528
  onRename,
509
529
  onManageUsers,
510
530
  onManageCustomApps,
@@ -11,6 +11,7 @@ import {
11
11
  useRenameDatastoreGroupMutation,
12
12
  useRestoreDatastoreMutation,
13
13
  useUnlinkedDatastoresQuery,
14
+ type GQLRegisterDatastoreMutationVariables,
14
15
  } from "@manta-eu/client";
15
16
  import { enableDatastoreRemoval } from "@manta-eu/client/shared/constants";
16
17
  import { deriveDatastoreId } from "@manta-eu/client/shared/datastore-id";
@@ -18,7 +19,7 @@ import { parseError } from "@manta-eu/client/shared/error-utils";
18
19
  import { namesAGroup, pathSegments } from "@manta-eu/client/shared/team";
19
20
  import { activeRealm } from "@manta-eu/ldo/realm";
20
21
  import { isPresent } from "@manta-eu/lib/present";
21
- import { useQueryClient } from "@tanstack/react-query";
22
+ import { useMutationState, useQueryClient } from "@tanstack/react-query";
22
23
  import type { SortingState } from "@tanstack/react-table";
23
24
  import { Archive, Database, EyeOff, type LucideIcon, Plus, RefreshCcw, TriangleAlert } from "lucide-react";
24
25
  import { useCallback, useEffect, useMemo, useState } from "react";
@@ -34,12 +35,16 @@ import { RenameDatastoreGroupDialog } from "./components/RenameDatastoreGroupDia
34
35
  import { UnlinkedDatastoresPanel } from "./components/UnlinkedDatastoresPanel";
35
36
  import {
36
37
  existingOrganizationNameKeys,
38
+ hiddenRemovedRowCount,
37
39
  hiddenRowCount,
38
40
  matchesProviderFilter,
39
41
  nestedDatastoreNamesByRowId,
42
+ organizationGroupPaths,
40
43
  organizationNameKey,
41
44
  organizationNames,
45
+ outsideRemovedSubtrees,
42
46
  realmDefaultRow,
47
+ removedSubtreeRowIds,
43
48
  toDatastoreRows,
44
49
  unlinkedDatastoreRow,
45
50
  visibilityForStatusFilter,
@@ -75,6 +80,8 @@ import { useMantaClient } from "#providers/index";
75
80
 
76
81
  const realmDatastoreId = activeRealm();
77
82
 
83
+ const registerDatastoreMutationKey = ["RegisterDatastore"];
84
+
78
85
  type ActiveTab = "organizations" | "unlinked";
79
86
 
80
87
  const tabLabels: Record<ActiveTab, string> = {
@@ -233,17 +240,37 @@ export function DatastoresRoute() {
233
240
  });
234
241
 
235
242
  const registerMutation = useRegisterDatastoreMutation(mantaClient, {
236
- onSuccess: (result) => {
243
+ mutationKey: registerDatastoreMutationKey,
244
+ onSuccess: async (result, variables) => {
237
245
  // "Not registered" is treated as settled, so status polling stopped - force a re-check.
238
- void queryClient.invalidateQueries({ queryKey: ["DatastoreProvisioningStatuses"] });
239
- if (result.registerDatastore.success) toast.success("Datastore registered");
240
- else toast.error("Couldn't register the datastore", { description: result.registerDatastore.error ?? undefined });
246
+ await refreshDatastores();
247
+ const { success, error, pending } = result.registerDatastore;
248
+ if (!success) {
249
+ toast.error("Couldn't build the datastore", { description: error ?? undefined });
250
+ return;
251
+ }
252
+
253
+ if (pending) {
254
+ toast.success(`Datastore "${variables.datastoreId}" is being built`, {
255
+ description: "It is taking longer than usual, and becomes available once its knowledge graph is built.",
256
+ });
257
+ } else {
258
+ toast.success(`Datastore "${variables.datastoreId}" is registered and built`);
259
+ }
241
260
  },
242
261
  onError: (error) => {
243
- const info = parseError(error, "Couldn't register the datastore");
262
+ const info = parseError(error, "Couldn't build the datastore");
244
263
  toast.error(info.message, { description: info.description });
245
264
  },
246
265
  });
266
+ const registeringIds = useMutationState({
267
+ filters: { mutationKey: registerDatastoreMutationKey, status: "pending" },
268
+ select: (mutation) => (mutation.state.variables as GQLRegisterDatastoreMutationVariables | undefined)?.datastoreId,
269
+ });
270
+ const registeringDatastoreIds = useMemo(
271
+ () => new Set(registeringIds.filter((id): id is string => id !== undefined)),
272
+ [registeringIds],
273
+ );
247
274
 
248
275
  const renameMutation = useRenameDatastoreGroupMutation(mantaClient, {
249
276
  onSuccess: (result) => {
@@ -313,7 +340,7 @@ export function DatastoresRoute() {
313
340
  [data],
314
341
  );
315
342
  const unlinkedRows = useMemo(
316
- () => (unlinkedData?.unlinkedDatastores ?? []).map((entry) => unlinkedDatastoreRow(entry.id)),
343
+ () => (unlinkedData?.unlinkedDatastores ?? []).map((entry) => unlinkedDatastoreRow(entry.id, entry.built)),
317
344
  [unlinkedData],
318
345
  );
319
346
  const allRows = useMemo(() => [...treeRows, ...unlinkedRows], [treeRows, unlinkedRows]);
@@ -323,6 +350,9 @@ export function DatastoresRoute() {
323
350
  () => hiddenRowCount(treeRows, visibility, "showUnprovisioned", rows.length),
324
351
  [treeRows, visibility, rows.length],
325
352
  );
353
+ const archivedCount = useMemo(() => hiddenRemovedRowCount(treeRows, visibility), [treeRows, visibility]);
354
+ const groupPathsByOrganization = useMemo(() => organizationGroupPaths(treeRows), [treeRows]);
355
+ const removedSubtreeIds = useMemo(() => removedSubtreeRowIds(treeRows), [treeRows]);
326
356
  const providerLinkEnabled = linkingQuery.data?.organizationLinking?.linkEnabled;
327
357
  const linkingUnreadable = linkingQuery.isError || linkingQuery.data?.organizationLinking === null;
328
358
  const tableRows = useMemo(
@@ -331,7 +361,7 @@ export function DatastoresRoute() {
331
361
  );
332
362
  const unlinkedCount = unlinkedRows.length;
333
363
  const existingOrganizationNames = useMemo(() => existingOrganizationNameKeys(allRows), [allRows]);
334
- const organizationNameOptions = useMemo(() => organizationNames(allRows), [allRows]);
364
+ const organizationNameOptions = useMemo(() => organizationNames(outsideRemovedSubtrees(allRows)), [allRows]);
335
365
  // Mirrors provisionDatastore's assertDatastoreIdAvailable (Infisical folder-per-id is global,
336
366
  // not scoped to siblings), so a slug collision is caught before the server round-trip.
337
367
  const existingDatastoreIds = useMemo(
@@ -354,7 +384,7 @@ export function DatastoresRoute() {
354
384
  );
355
385
  const suggestedMatchByDatastoreId = useMemo(() => {
356
386
  const matches: Record<string, DatastoreRow> = {};
357
- for (const row of treeRows) {
387
+ for (const row of outsideRemovedSubtrees(treeRows)) {
358
388
  if (isPresent(row.datastoreId)) continue;
359
389
  const derived = derivedIdForRow(row);
360
390
  if (unlinkedIdSet.has(derived)) matches[derived] ??= row;
@@ -383,7 +413,7 @@ export function DatastoresRoute() {
383
413
 
384
414
  const matchableRowIds = useMemo(() => {
385
415
  const ids = new Set<string>();
386
- for (const row of allRows) {
416
+ for (const row of outsideRemovedSubtrees(allRows)) {
387
417
  if (!isPresent(row.datastoreId) && unlinkedIdSet.has(derivedIdForRow(row))) ids.add(row.id);
388
418
  }
389
419
  return ids;
@@ -410,7 +440,11 @@ export function DatastoresRoute() {
410
440
  void queryClient.invalidateQueries({ queryKey: useUnlinkedDatastoresQuery.getKey() });
411
441
  void queryClient.invalidateQueries({ queryKey: ["DatastoreProvisioningStatuses"] });
412
442
  if (result.linkUnlinkedDatastore.success) {
413
- toast.success(`Linked to the existing datastore "${result.linkUnlinkedDatastore.datastoreId}"`);
443
+ toast.success(
444
+ result.linkUnlinkedDatastore.pending
445
+ ? `Linked to "${result.linkUnlinkedDatastore.datastoreId}", which is still being built`
446
+ : `Linked to the existing datastore "${result.linkUnlinkedDatastore.datastoreId}"`,
447
+ );
414
448
  toastProviderWarning(result.linkUnlinkedDatastore.providerWarning);
415
449
  } else {
416
450
  toast.error("Couldn't link the existing datastore", {
@@ -471,7 +505,9 @@ export function DatastoresRoute() {
471
505
 
472
506
  const onRegister = useCallback(
473
507
  (row: DatastoreRow) => {
474
- if (isPresent(row.datastoreId)) registerMutation.mutate({ datastoreId: row.datastoreId });
508
+ if (isPresent(row.datastoreId)) {
509
+ registerMutation.mutate({ datastoreId: row.datastoreId, launchProvisioning: true });
510
+ }
475
511
  },
476
512
  [registerMutation.mutate],
477
513
  );
@@ -524,6 +560,8 @@ export function DatastoresRoute() {
524
560
  onRestore,
525
561
  onRename: setRenameTarget,
526
562
  onRegister,
563
+ registeringDatastoreIds,
564
+ removedSubtreeRowIds: removedSubtreeIds,
527
565
  onManageUsers,
528
566
  onManageCustomApps,
529
567
  onBringOnline,
@@ -545,7 +583,19 @@ export function DatastoresRoute() {
545
583
  onToggle: () => toggleVisibility("showArchived"),
546
584
  },
547
585
  ]
548
- : []),
586
+ : showArchived || archivedCount > 0
587
+ ? [
588
+ {
589
+ key: "archived",
590
+ label: showArchived ? "Hide removed" : `Show removed (${archivedCount})`,
591
+ description:
592
+ "Out of use: a permanent delete stopped partway, or they were removed before removal was turned off.",
593
+ icon: showArchived ? EyeOff : Archive,
594
+ active: showArchived,
595
+ onToggle: () => toggleVisibility("showArchived"),
596
+ },
597
+ ]
598
+ : []),
549
599
  ...(showUnprovisioned || unprovisionedCount > 0
550
600
  ? [
551
601
  {
@@ -813,6 +863,7 @@ export function DatastoresRoute() {
813
863
  datastoreId={linkDialogTarget}
814
864
  suggestedMatch={suggestedLinkTarget(linkDialogTarget)}
815
865
  organizationNames={organizationNameOptions}
866
+ groupPathsByOrganization={groupPathsByOrganization}
816
867
  existingOrganizationNames={existingOrganizationNames}
817
868
  onClose={() => setLinkDialogTarget(null)}
818
869
  />
@@ -0,0 +1,61 @@
1
+ import {
2
+ GQLIntegration,
3
+ useGoogleMerchantCenterSetupConnectMutation,
4
+ useGoogleMerchantCenterSetupProgressQuery,
5
+ type GQLGoogleMerchantCenterAccount,
6
+ } from "@manta-eu/client";
7
+ import { useQueryClient } from "@tanstack/react-query";
8
+ import { googleMerchantCenterTreeItems } from "./google-merchant-center-account-tree";
9
+ import { OAuthSetupConnectForm, type OAuthSetup, type SetupCopy } from "./OAuthSetupConnectForm";
10
+ import type { ConnectFormProps } from "./types";
11
+ import { useMantaClient } from "#providers/index";
12
+
13
+ const copy: SetupCopy = {
14
+ providerName: "Merchant Center",
15
+ identityProviderName: "Google",
16
+ entitySingular: "account",
17
+ entityPlural: "accounts",
18
+ signInDescription:
19
+ "We'll take you to Google sign-in so you can grant Manta access to your Google Merchant Center accounts.",
20
+ pickTitle: "Pick the Merchant Center accounts to connect",
21
+ pickDescription:
22
+ "Choose the accounts whose products Manta should manage. An advanced account only groups its sub-accounts, so connect the sub-accounts. An account whose latest terms aren't accepted yet can be connected once they are accepted in Merchant Center.",
23
+ noEntitiesLabel:
24
+ "We couldn't find any Merchant Center accounts on this Google identity. Sign in with a different account or check your Merchant Center access.",
25
+ noneConnectableLabel:
26
+ "No connectable Merchant Center accounts remain on this Google identity — they are already connected or unavailable. Sign in with a different Google account to connect a new one.",
27
+ };
28
+
29
+ function useGoogleMerchantCenterSetup(): OAuthSetup<GQLGoogleMerchantCenterAccount> {
30
+ const { mantaClient } = useMantaClient();
31
+ const queryClient = useQueryClient();
32
+ const progressQuery = useGoogleMerchantCenterSetupProgressQuery(mantaClient, undefined, { staleTime: 0 });
33
+ const connectMutation = useGoogleMerchantCenterSetupConnectMutation(mantaClient);
34
+ const progress = progressQuery.data?.googleMerchantCenterSetupProgress;
35
+
36
+ return {
37
+ progress: progress && { state: progress.state, rows: progress.accounts, lastError: progress.lastError },
38
+ refetchProgress: progressQuery.refetch,
39
+ resetProgress: () =>
40
+ queryClient.setQueryData(useGoogleMerchantCenterSetupProgressQuery.getKey(), {
41
+ googleMerchantCenterSetupProgress: { state: [], accounts: null, lastError: null },
42
+ }),
43
+ connect: async (accountIds) => (await connectMutation.mutateAsync({ accountIds })).googleMerchantCenterSetupConnect,
44
+ isConnecting: connectMutation.isPending,
45
+ };
46
+ }
47
+
48
+ export function GoogleMerchantCenterConnectForm(props: ConnectFormProps) {
49
+ const setup = useGoogleMerchantCenterSetup();
50
+
51
+ return (
52
+ <OAuthSetupConnectForm
53
+ {...props}
54
+ integration={GQLIntegration.GoogleMerchantCenter}
55
+ oauthStartPath="/oauth/google-merchant-center/start"
56
+ copy={copy}
57
+ setup={setup}
58
+ treeItems={googleMerchantCenterTreeItems}
59
+ />
60
+ );
61
+ }
@@ -0,0 +1,38 @@
1
+ import type { GQLGoogleMerchantCenterAccount } from "@manta-eu/client";
2
+ import { isPresent } from "@manta-eu/lib/present";
3
+ import { unavailableReasonLabel } from "./setup-account-unavailable-reason";
4
+ import { buildTreeRows, type TreeSelectItem } from "#components/tree-multi-select/index";
5
+
6
+ function rowHint(
7
+ row: GQLGoogleMerchantCenterAccount,
8
+ alreadyConnected: boolean,
9
+ reconnectable: boolean,
10
+ ): string | undefined {
11
+ if (row.advanced === true) return "Advanced account";
12
+ if (row.isConnectable !== true) return unavailableReasonLabel(row.unavailableReason);
13
+ if (reconnectable) return "Reconnect";
14
+ if (alreadyConnected) return "Already connected";
15
+ if (row.testAccount === true) return "Test account";
16
+ return undefined;
17
+ }
18
+
19
+ export function googleMerchantCenterTreeItems(
20
+ rows: GQLGoogleMerchantCenterAccount[],
21
+ alreadyConnectedExternalIds: ReadonlySet<string>,
22
+ reconnectableExternalIds: ReadonlySet<string> = new Set(),
23
+ ): TreeSelectItem[] {
24
+ return buildTreeRows(rows).map(({ node, depth }) => {
25
+ const reconnectable = isPresent(node.externalId) && reconnectableExternalIds.has(node.externalId);
26
+ const alreadyConnected =
27
+ !reconnectable && isPresent(node.externalId) && alreadyConnectedExternalIds.has(node.externalId);
28
+ return {
29
+ id: node.id,
30
+ label: node.name ?? node.externalId ?? node.id,
31
+ caption: node.name != null ? (node.externalId ?? undefined) : undefined,
32
+ hint: rowHint(node, alreadyConnected, reconnectable),
33
+ selectable: node.isConnectable === true,
34
+ disabled: alreadyConnected,
35
+ depth,
36
+ };
37
+ });
38
+ }
@@ -3,6 +3,7 @@ import { isPresent } from "@manta-eu/lib/present";
3
3
 
4
4
  const unavailableReasonLabels: Record<GQLSetupAccountUnavailableReason, string> = {
5
5
  [GQLSetupAccountUnavailableReason.InsufficientPermissions]: "Insufficient permissions",
6
+ [GQLSetupAccountUnavailableReason.TermsNotAccepted]: "Accept the latest terms in Merchant Center",
6
7
  };
7
8
 
8
9
  export function unavailableReasonLabel(reason: GQLSetupAccountUnavailableReason | null | undefined): string {
@@ -11,6 +11,7 @@ export { FigmaConnectForm } from "./components/connect-forms/FigmaConnectForm";
11
11
  export { GmailConnectForm } from "./components/connect-forms/GmailConnectForm";
12
12
  export { GoogleAdsConnectForm } from "./components/connect-forms/GoogleAdsConnectForm";
13
13
  export { GoogleAnalyticsConnectForm } from "./components/connect-forms/GoogleAnalyticsConnectForm";
14
+ export { GoogleMerchantCenterConnectForm } from "./components/connect-forms/GoogleMerchantCenterConnectForm";
14
15
  export { KlaviyoConnectForm } from "./components/connect-forms/KlaviyoConnectForm";
15
16
  export { LinkedinConnectForm } from "./components/connect-forms/LinkedinConnectForm";
16
17
  export { MetaAdsConnectForm } from "./components/connect-forms/MetaAdsConnectForm";
@@ -42,6 +42,7 @@ const oauthAccountNoun = {
42
42
  [GQLIntegration.Slack]: "Workspace",
43
43
  [GQLIntegration.Google]: "Customer",
44
44
  [GQLIntegration.GoogleAnalytics]: "Property",
45
+ [GQLIntegration.GoogleMerchantCenter]: "Account",
45
46
  [GQLIntegration.Meta]: "Ad account",
46
47
  [GQLIntegration.Linkedin]: "Ad account",
47
48
  [GQLIntegration.MicrosoftAds]: "Account",
@@ -10,6 +10,7 @@ import {
10
10
  useCurrentFactValueQuery,
11
11
  useExplainFactQuery,
12
12
  useGetActionPlansSummaryQuery,
13
+ useManualActionProgressQuery,
13
14
  useMemoriesQuery,
14
15
  useMemorySchemasQuery,
15
16
  useNodePropertiesQuery,
@@ -36,6 +37,7 @@ export const graphDerivedQueries: QueryWithGetKey[] = [
36
37
  useCurrentFactValueQuery,
37
38
  useExplainFactQuery,
38
39
  useGetActionPlansSummaryQuery,
40
+ useManualActionProgressQuery,
39
41
  useMemoriesQuery,
40
42
  useMemorySchemasQuery,
41
43
  useNodePropertiesQuery,