@cytario/web 2.1.4 → 2.1.5
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/README.md +20 -20
- package/app/.server/auth/README.md +8 -8
- package/app/.server/auth/authMiddleware.ts +9 -25
- package/app/.server/auth/exchangeAuthCode.ts +2 -6
- package/app/.server/auth/getS3Client.ts +3 -13
- package/app/.server/auth/getSessionCredentials.ts +6 -20
- package/app/.server/auth/getUserInfo.ts +2 -6
- package/app/.server/auth/keycloakAdmin/client.ts +2 -9
- package/app/.server/auth/keycloakAdmin/groups.ts +9 -26
- package/app/.server/auth/keycloakAdmin/users.ts +7 -23
- package/app/.server/auth/oauthState.ts +4 -13
- package/app/.server/auth/redirectIfAuthenticated.ts +1 -3
- package/app/.server/auth/refreshAuthTokens.ts +5 -19
- package/app/.server/auth/sessionMiddleware.ts +1 -4
- package/app/.server/auth/sessionStorage.ts +1 -4
- package/app/.server/auth/verifyIdToken.ts +1 -3
- package/app/.server/auth/wellKnownEndpoints.ts +1 -4
- package/app/.server/db/redis.ts +5 -1
- package/app/.server/logging.ts +1 -4
- package/app/.server/requestDurationMiddleware.ts +1 -4
- package/app/components/.client/ImageViewer/README.md +5 -5
- package/app/components/.client/ImageViewer/components/ChannelsController/ChannelsController.tsx +7 -9
- package/app/components/.client/ImageViewer/components/ChannelsController/ChannelsControllerBrightfieldItem.tsx +1 -2
- package/app/components/.client/ImageViewer/components/ChannelsController/ChannelsControllerItem.tsx +2 -5
- package/app/components/.client/ImageViewer/components/ChannelsController/ChannelsControllerItemList.tsx +7 -15
- package/app/components/.client/ImageViewer/components/ChannelsController/ColorPicker/ColorPicker.tsx +2 -9
- package/app/components/.client/ImageViewer/components/ChannelsController/ColorPicker/ColorSwatch.tsx +1 -4
- package/app/components/.client/ImageViewer/components/ChannelsController/DomainSlider.tsx +1 -3
- package/app/components/.client/ImageViewer/components/ChannelsController/Histogram.tsx +16 -18
- package/app/components/.client/ImageViewer/components/ChannelsController/HistogramChannel.tsx +2 -8
- package/app/components/.client/ImageViewer/components/ChannelsController/MinMaxSettings.tsx +6 -15
- package/app/components/.client/ImageViewer/components/FeatureBar/FeatureBarDragHandle.tsx +1 -5
- package/app/components/.client/ImageViewer/components/FeatureBar/FeatureBarToggle.tsx +1 -5
- package/app/components/.client/ImageViewer/components/FeatureBar/FeatureItem.tsx +1 -5
- package/app/components/.client/ImageViewer/components/FeatureBar/Presets.tsx +3 -11
- package/app/components/.client/ImageViewer/components/FeatureBar/useFeatureBar.tsx +16 -25
- package/app/components/.client/ImageViewer/components/Image/Channels/useChannelsLayer.ts +7 -18
- package/app/components/.client/ImageViewer/components/Image/ImageContainer.tsx +1 -1
- package/app/components/.client/ImageViewer/components/Image/ImagePanel.tsx +6 -26
- package/app/components/.client/ImageViewer/components/Image/ImagePreview.tsx +2 -9
- package/app/components/.client/ImageViewer/components/Image/Overlays/AdditivePolygonLayer.tsx +1 -5
- package/app/components/.client/ImageViewer/components/Image/Overlays/AdditiveScatterplotLayer.tsx +1 -5
- package/app/components/.client/ImageViewer/components/Image/Overlays/OverlaysLayer.tsx +6 -24
- package/app/components/.client/ImageViewer/components/Image/Overlays/markerUniforms.ts +2 -5
- package/app/components/.client/ImageViewer/components/Image/Overlays/useOverlaysLayer.tsx +7 -21
- package/app/components/.client/ImageViewer/components/Image/useInitializeChannels.ts +1 -7
- package/app/components/.client/ImageViewer/components/Image/useResizeObserver.ts +1 -1
- package/app/components/.client/ImageViewer/components/Magnifier.tsx +5 -13
- package/app/components/.client/ImageViewer/components/Measurements/ActiveViewStatePreview.tsx +3 -8
- package/app/components/.client/ImageViewer/components/Measurements/CursorTick.tsx +2 -7
- package/app/components/.client/ImageViewer/components/Measurements/Ruler.tsx +1 -8
- package/app/components/.client/ImageViewer/components/Measurements/SlideCarrier.tsx +3 -13
- package/app/components/.client/ImageViewer/components/Measurements/Tick.tsx +1 -1
- package/app/components/.client/ImageViewer/components/Measurements/calculateViewStateToFit.ts +1 -1
- package/app/components/.client/ImageViewer/components/Measurements/useMeasurements.ts +9 -28
- package/app/components/.client/ImageViewer/components/OverlaysController/AddOverlay.tsx +4 -13
- package/app/components/.client/ImageViewer/components/OverlaysController/OverlayPicker.modal.tsx +1 -6
- package/app/components/.client/ImageViewer/components/OverlaysController/OverlaysController.Item.tsx +24 -54
- package/app/components/.client/ImageViewer/components/OverlaysController/OverlaysController.tsx +1 -3
- package/app/components/.client/ImageViewer/components/SplitViewToggle.tsx +1 -3
- package/app/components/.client/ImageViewer/components/ViewerHeader.tsx +1 -3
- package/app/components/.client/ImageViewer/state/decoders/decodeJPEG2000.d.ts +9 -11
- package/app/components/.client/ImageViewer/state/decoders/decodeJPEG2000.js +11 -11
- package/app/components/.client/ImageViewer/state/decoders/decoder.worker.js +49 -49
- package/app/components/.client/ImageViewer/state/decoders/genericDecoder.ts +76 -81
- package/app/components/.client/ImageViewer/state/decoders/jp2k-decoder.ts +9 -9
- package/app/components/.client/ImageViewer/state/decoders/lzwDecoder.ts +9 -9
- package/app/components/.client/ImageViewer/state/loaders/loadBioformatsZarrWithCredentials.ts +10 -22
- package/app/components/.client/ImageViewer/state/store/ViewerStoreContext.tsx +4 -18
- package/app/components/.client/ImageViewer/state/store/createViewerStore.ts +110 -194
- package/app/components/.client/ImageViewer/state/store/getInitialChannelsState.ts +2 -6
- package/app/components/.client/ImageViewer/state/store/selectors.ts +9 -9
- package/app/components/.client/ImageViewer/state/store/types.ts +3 -12
- package/app/components/.client/ImageViewer/state/transport/CredentialedHTTPStore.ts +1 -5
- package/app/components/.client/ImageViewer/state/transport/SigV4TiffClient.ts +2 -9
- package/app/components/.client/ImageViewer/utils/getSelectionStats.ts +1 -4
- package/app/components/.client/ImageViewer/utils/handleImageViewerHover.ts +1 -1
- package/app/components/.client/ImageViewer/utils/mapChannelConfigsToState.ts +2 -4
- package/app/components/.client/ImageViewer/utils/useTilesLoading.ts +3 -3
- package/app/components/AppHeader.tsx +1 -4
- package/app/components/Breadcrumbs/Breadcrumbs.tsx +4 -13
- package/app/components/Breadcrumbs/getCrumbs.tsx +1 -1
- package/app/components/ClientOnly.tsx +1 -1
- package/app/components/Container.tsx +3 -15
- package/app/components/DataGrid/ConvertOverlay.modal.tsx +1 -6
- package/app/components/DataGrid/DataGrid.tsx +7 -27
- package/app/components/DataGrid/WktSvg.tsx +2 -4
- package/app/components/DataGrid/getParquetSchema.ts +1 -4
- package/app/components/DescriptionList.tsx +1 -3
- package/app/components/DirectoryView/ConnectionMenu.tsx +8 -22
- package/app/components/DirectoryView/DirectoryView.tsx +10 -46
- package/app/components/DirectoryView/DirectoryViewGrid.tsx +16 -49
- package/app/components/DirectoryView/DirectoryViewTableConnection.tsx +1 -4
- package/app/components/DirectoryView/DirectoryViewTableDirectory.tsx +2 -7
- package/app/components/DirectoryView/DirectoryViewTree.tsx +5 -21
- package/app/components/DirectoryView/FilterBar.tsx +9 -48
- package/app/components/DirectoryView/buildDirectoryTree.ts +6 -25
- package/app/components/DirectoryView/filterNodes.ts +4 -11
- package/app/components/DirectoryView/modals/Cyberduck.modal.tsx +6 -15
- package/app/components/DirectoryView/modals/FileInfo.modal.tsx +1 -5
- package/app/components/DirectoryView/useLayoutStore.ts +5 -25
- package/app/components/GlobalSearch/GlobalSearch.tsx +1 -4
- package/app/components/GlobalSearch/SearchBar.tsx +1 -7
- package/app/components/GlobalSearch/Suggestions.tsx +0 -1
- package/app/components/ImageViewer/state/formatRegistry.ts +5 -18
- package/app/components/LavaLoader.tsx +4 -11
- package/app/components/Layout/Footer.tsx +1 -4
- package/app/components/Pills/ScopePill.tsx +2 -8
- package/app/components/Table/ColumnFilterInput.tsx +5 -20
- package/app/components/Table/ColumnResizeHandle.tsx +1 -5
- package/app/components/Table/ColumnSortButton.tsx +1 -5
- package/app/components/Table/SelectionFooter.tsx +3 -5
- package/app/components/Table/Table.tsx +5 -21
- package/app/components/Table/TableBodyRow.tsx +19 -31
- package/app/components/Table/TableHeaderRow.tsx +7 -28
- package/app/components/Table/TableMenu.tsx +4 -20
- package/app/components/Table/state/createTableStore.ts +3 -9
- package/app/components/Table/state/useTableStore.ts +1 -3
- package/app/components/Table/types.ts +4 -10
- package/app/components/Table/useColumnFilters.ts +1 -4
- package/app/components/Table/useColumnVisibility.ts +4 -11
- package/app/components/Table/useColumnWidths.ts +1 -3
- package/app/components/Table/useTableSorting.ts +4 -12
- package/app/components/Tooltip/Tooltip.tsx +4 -17
- package/app/components/Tooltip/TooltipSpan.tsx +5 -28
- package/app/components/Tooltip/useCopyToClipboard.ts +1 -3
- package/app/components/Tooltip/useMiddleEllipsis.ts +2 -7
- package/app/components/Tooltip/useOverflowDetection.ts +2 -5
- package/app/components/UserMenu.tsx +2 -9
- package/app/entry.server.tsx +9 -19
- package/app/hooks/useSearchParam.ts +2 -4
- package/app/lib/bootstrapPluginsCore.ts +4 -9
- package/app/root.tsx +4 -15
- package/app/routes/admin/assertAdminScope.ts +1 -3
- package/app/routes/admin/assertGroupPathsInScope.ts +3 -11
- package/app/routes/admin/assertGroupsInScope.ts +3 -11
- package/app/routes/admin/assertUsersInScope.ts +2 -8
- package/app/routes/admin/bulkInvite/bulkInvite.action.ts +2 -13
- package/app/routes/admin/bulkInvite/bulkInvite.form.tsx +18 -35
- package/app/routes/admin/createGroup/createGroup.action.ts +3 -10
- package/app/routes/admin/createGroup/createGroup.form.tsx +2 -9
- package/app/routes/admin/createGroup/createGroup.modal.tsx +1 -5
- package/app/routes/admin/inviteUser/inviteUser.action.ts +1 -4
- package/app/routes/admin/inviteUser/inviteUser.form.tsx +2 -8
- package/app/routes/admin/inviteUser/inviteUser.loader.ts +1 -4
- package/app/routes/admin/inviteUser/inviteUser.modal.tsx +3 -16
- package/app/routes/admin/updateUser/updateUser.form.tsx +4 -15
- package/app/routes/admin/updateUser/updateUser.modal.tsx +5 -23
- package/app/routes/admin/updateUser/userDetail.action.ts +2 -10
- package/app/routes/admin/users/BulkActions.tsx +15 -38
- package/app/routes/admin/users/bulkUsers.action.ts +2 -9
- package/app/routes/admin/users/bulkUsers.schema.ts +1 -6
- package/app/routes/admin/users/users.route.tsx +14 -63
- package/app/routes/api/cyberduck-profile.$name.ts +6 -2
- package/app/routes/auth/callback.route.tsx +8 -33
- package/app/routes/auth/login.route.tsx +8 -11
- package/app/routes/auth/logout.route.tsx +4 -14
- package/app/routes/config.route.tsx +1 -5
- package/app/routes/connections/connection.form.tsx +5 -14
- package/app/routes/connections/connection.schema.ts +4 -16
- package/app/routes/connections/connections.loader.ts +11 -23
- package/app/routes/connections/connections.route.tsx +1 -5
- package/app/routes/connections/connections.server.ts +1 -3
- package/app/routes/connections/createConnection.action.ts +2 -8
- package/app/routes/connections/createConnection.modal.tsx +3 -13
- package/app/routes/connections/deleteConnection.action.ts +1 -4
- package/app/routes/connections/updateConnection.action.ts +2 -8
- package/app/routes/connections/updateConnection.modal.tsx +4 -14
- package/app/routes/home/home.route.tsx +8 -33
- package/app/routes/layouts/ModalOutlet.tsx +6 -18
- package/app/routes/objects/objects.loader.ts +5 -19
- package/app/routes/objects/objects.route.tsx +11 -30
- package/app/routes/presign.route.tsx +5 -18
- package/app/routes/recent.route.tsx +1 -4
- package/app/routes/search.route.tsx +4 -17
- package/app/routes.ts +1 -4
- package/app/tailwind.css +17 -12
- package/app/types/cornerstone-codecs.d.ts +25 -29
- package/app/utils/connectionsStore/selectors.ts +2 -6
- package/app/utils/connectionsStore/useConnectionsStore.ts +2 -6
- package/app/utils/db/convertCsvToParquet.ts +1 -3
- package/app/utils/db/createDatabase.ts +1 -3
- package/app/utils/db/createSingleton.ts +1 -1
- package/app/utils/db/getBlobFromObjectNode.ts +3 -9
- package/app/utils/db/getGeomQuery.ts +1 -1
- package/app/utils/db/getMarkerInfoWasm.ts +1 -3
- package/app/utils/db/getTileBoundingBox.ts +1 -4
- package/app/utils/db/sqlQueries.ts +1 -4
- package/app/utils/fileType.ts +80 -10
- package/app/utils/filterObjects.ts +2 -5
- package/app/utils/localFilesStore/useFileStore.ts +7 -7
- package/app/utils/recentlyViewed.server.ts +1 -4
- package/app/utils/resourceId.ts +3 -11
- package/app/utils/s3Provider.ts +3 -7
- package/app/utils/signedFetch.ts +4 -13
- package/bin-src/codegen.ts +4 -1
- package/package.json +5 -1
- package/prisma/seed.ts +1 -2
- package/public/favicon/site.webmanifest +1 -1
- package/server.js +1 -4
- package/server.js.map +1 -1
- package/vite-plugins/cytario-plugins.ts +2 -8
|
@@ -33,13 +33,7 @@ const CopiedOverlay = () => (
|
|
|
33
33
|
|
|
34
34
|
// ─── Right (default): pure CSS truncation ───────────────────────────
|
|
35
35
|
|
|
36
|
-
const RightEllipsis = ({
|
|
37
|
-
children,
|
|
38
|
-
copyValue,
|
|
39
|
-
}: {
|
|
40
|
-
children: ReactNode;
|
|
41
|
-
copyValue?: string;
|
|
42
|
-
}) => {
|
|
36
|
+
const RightEllipsis = ({ children, copyValue }: { children: ReactNode; copyValue?: string }) => {
|
|
43
37
|
const ref = useRef<HTMLSpanElement>(null);
|
|
44
38
|
const isTruncated = useOverflowDetection(ref);
|
|
45
39
|
const { handleClick, isCopied } = useCopyToClipboard(copyValue);
|
|
@@ -67,13 +61,7 @@ const RightEllipsis = ({
|
|
|
67
61
|
|
|
68
62
|
// ─── Left: CSS direction trick ──────────────────────────────────────
|
|
69
63
|
|
|
70
|
-
const LeftEllipsis = ({
|
|
71
|
-
children,
|
|
72
|
-
copyValue,
|
|
73
|
-
}: {
|
|
74
|
-
children: ReactNode;
|
|
75
|
-
copyValue?: string;
|
|
76
|
-
}) => {
|
|
64
|
+
const LeftEllipsis = ({ children, copyValue }: { children: ReactNode; copyValue?: string }) => {
|
|
77
65
|
const ref = useRef<HTMLSpanElement>(null);
|
|
78
66
|
const isTruncated = useOverflowDetection(ref);
|
|
79
67
|
const { handleClick, isCopied } = useCopyToClipboard(copyValue);
|
|
@@ -104,13 +92,7 @@ const LeftEllipsis = ({
|
|
|
104
92
|
|
|
105
93
|
const middleCx = "overflow-hidden whitespace-nowrap block min-w-0 w-full";
|
|
106
94
|
|
|
107
|
-
const MiddleEllipsisString = ({
|
|
108
|
-
text,
|
|
109
|
-
copyValue,
|
|
110
|
-
}: {
|
|
111
|
-
text: string;
|
|
112
|
-
copyValue?: string;
|
|
113
|
-
}) => {
|
|
95
|
+
const MiddleEllipsisString = ({ text, copyValue }: { text: string; copyValue?: string }) => {
|
|
114
96
|
const ref = useRef<HTMLSpanElement>(null);
|
|
115
97
|
const displayed = useMiddleEllipsis(ref, text);
|
|
116
98
|
const isTruncated = displayed !== text;
|
|
@@ -139,13 +121,8 @@ const MiddleEllipsisString = ({
|
|
|
139
121
|
|
|
140
122
|
// ─── Public component ───────────────────────────────────────────────
|
|
141
123
|
|
|
142
|
-
export const TooltipSpan = ({
|
|
143
|
-
children
|
|
144
|
-
ellipsis = "right",
|
|
145
|
-
copyValue,
|
|
146
|
-
}: TooltipSpanProps) => {
|
|
147
|
-
if (ellipsis === "left")
|
|
148
|
-
return <LeftEllipsis copyValue={copyValue}>{children}</LeftEllipsis>;
|
|
124
|
+
export const TooltipSpan = ({ children, ellipsis = "right", copyValue }: TooltipSpanProps) => {
|
|
125
|
+
if (ellipsis === "left") return <LeftEllipsis copyValue={copyValue}>{children}</LeftEllipsis>;
|
|
149
126
|
if (ellipsis === "middle" && typeof children === "string")
|
|
150
127
|
return <MiddleEllipsisString text={children} copyValue={copyValue} />;
|
|
151
128
|
return <RightEllipsis copyValue={copyValue}>{children}</RightEllipsis>;
|
|
@@ -7,9 +7,7 @@ interface UseCopyToClipboardResult {
|
|
|
7
7
|
isCopied: boolean;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
export function useCopyToClipboard(
|
|
11
|
-
copyValue: string | undefined,
|
|
12
|
-
): UseCopyToClipboardResult {
|
|
10
|
+
export function useCopyToClipboard(copyValue: string | undefined): UseCopyToClipboardResult {
|
|
13
11
|
const [isCopied, setIsCopied] = useState(false);
|
|
14
12
|
const timerRef = useRef<number | null>(null);
|
|
15
13
|
|
|
@@ -19,9 +19,7 @@ function computeTruncated(el: HTMLElement, text: string): string {
|
|
|
19
19
|
const startLen = Math.ceil(mid / 2);
|
|
20
20
|
const endLen = Math.floor(mid / 2);
|
|
21
21
|
el.textContent =
|
|
22
|
-
text.slice(0, startLen) +
|
|
23
|
-
ELLIPSIS +
|
|
24
|
-
(endLen > 0 ? text.slice(text.length - endLen) : "");
|
|
22
|
+
text.slice(0, startLen) + ELLIPSIS + (endLen > 0 ? text.slice(text.length - endLen) : "");
|
|
25
23
|
|
|
26
24
|
if (el.scrollWidth <= el.clientWidth) {
|
|
27
25
|
lo = mid;
|
|
@@ -49,10 +47,7 @@ function computeTruncated(el: HTMLElement, text: string): string {
|
|
|
49
47
|
* Observes the container width and returns a middle-truncated string.
|
|
50
48
|
* Returns the original text if it fits within the container.
|
|
51
49
|
*/
|
|
52
|
-
export function useMiddleEllipsis(
|
|
53
|
-
ref: RefObject<HTMLSpanElement | null>,
|
|
54
|
-
text: string,
|
|
55
|
-
): string {
|
|
50
|
+
export function useMiddleEllipsis(ref: RefObject<HTMLSpanElement | null>, text: string): string {
|
|
56
51
|
const [displayed, setDisplayed] = useState(text);
|
|
57
52
|
|
|
58
53
|
// useLayoutEffect runs before paint, so the user never sees the full text.
|
|
@@ -5,17 +5,14 @@ import { RefObject, useLayoutEffect, useState } from "react";
|
|
|
5
5
|
* Uses ResizeObserver to recheck on any container size change
|
|
6
6
|
* (column resize, flex layout shifts, not just window resize).
|
|
7
7
|
*/
|
|
8
|
-
export function useOverflowDetection(
|
|
9
|
-
ref: RefObject<HTMLElement | null>,
|
|
10
|
-
): boolean {
|
|
8
|
+
export function useOverflowDetection(ref: RefObject<HTMLElement | null>): boolean {
|
|
11
9
|
const [isTruncated, setIsTruncated] = useState(false);
|
|
12
10
|
|
|
13
11
|
useLayoutEffect(() => {
|
|
14
12
|
const el = ref.current;
|
|
15
13
|
if (!el) return;
|
|
16
14
|
|
|
17
|
-
const check = () =>
|
|
18
|
-
setIsTruncated(el.scrollWidth > el.offsetWidth);
|
|
15
|
+
const check = () => setIsTruncated(el.scrollWidth > el.offsetWidth);
|
|
19
16
|
|
|
20
17
|
check();
|
|
21
18
|
|
|
@@ -26,9 +26,7 @@ export function UserMenu({ user, accountSettingsUrl }: UserMenuProps) {
|
|
|
26
26
|
<div className="font-semibold">
|
|
27
27
|
{user.given_name} {user.family_name}
|
|
28
28
|
</div>
|
|
29
|
-
<div className="text-[var(--color-text-secondary)]">
|
|
30
|
-
{user.email}
|
|
31
|
-
</div>
|
|
29
|
+
<div className="text-[var(--color-text-secondary)]">{user.email}</div>
|
|
32
30
|
</div>
|
|
33
31
|
</MenuHeader>
|
|
34
32
|
|
|
@@ -68,12 +66,7 @@ export function UserMenu({ user, accountSettingsUrl }: UserMenuProps) {
|
|
|
68
66
|
</>
|
|
69
67
|
)}
|
|
70
68
|
|
|
71
|
-
<MenuItem
|
|
72
|
-
id="account-settings"
|
|
73
|
-
icon={Settings}
|
|
74
|
-
href={accountSettingsUrl}
|
|
75
|
-
target="_blank"
|
|
76
|
-
>
|
|
69
|
+
<MenuItem id="account-settings" icon={Settings} href={accountSettingsUrl} target="_blank">
|
|
77
70
|
Account Settings
|
|
78
71
|
</MenuItem>
|
|
79
72
|
<MenuItem id="logout" icon={LogOut} href="/logout">
|
package/app/entry.server.tsx
CHANGED
|
@@ -30,30 +30,20 @@ export default async function handleRequest(
|
|
|
30
30
|
// This is ignored so we can keep it in the template for visibility. Feel
|
|
31
31
|
// free to delete this parameter in your app if you're not using it!
|
|
32
32
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
33
|
-
loadContext: AppLoadContext
|
|
33
|
+
loadContext: AppLoadContext,
|
|
34
34
|
) {
|
|
35
35
|
await bootstrapPromise;
|
|
36
36
|
|
|
37
37
|
return isbot(request.headers.get("user-agent") || "")
|
|
38
|
-
? handleBotRequest(
|
|
39
|
-
|
|
40
|
-
responseStatusCode,
|
|
41
|
-
responseHeaders,
|
|
42
|
-
reactRouterContext
|
|
43
|
-
)
|
|
44
|
-
: handleBrowserRequest(
|
|
45
|
-
request,
|
|
46
|
-
responseStatusCode,
|
|
47
|
-
responseHeaders,
|
|
48
|
-
reactRouterContext
|
|
49
|
-
);
|
|
38
|
+
? handleBotRequest(request, responseStatusCode, responseHeaders, reactRouterContext)
|
|
39
|
+
: handleBrowserRequest(request, responseStatusCode, responseHeaders, reactRouterContext);
|
|
50
40
|
}
|
|
51
41
|
|
|
52
42
|
function handleBotRequest(
|
|
53
43
|
request: Request,
|
|
54
44
|
responseStatusCode: number,
|
|
55
45
|
responseHeaders: Headers,
|
|
56
|
-
reactRouterContext: EntryContext
|
|
46
|
+
reactRouterContext: EntryContext,
|
|
57
47
|
) {
|
|
58
48
|
return new Promise((resolve, reject) => {
|
|
59
49
|
let shellRendered = false;
|
|
@@ -71,7 +61,7 @@ function handleBotRequest(
|
|
|
71
61
|
new Response(stream, {
|
|
72
62
|
headers: responseHeaders,
|
|
73
63
|
status: responseStatusCode,
|
|
74
|
-
})
|
|
64
|
+
}),
|
|
75
65
|
);
|
|
76
66
|
|
|
77
67
|
pipe(body);
|
|
@@ -88,7 +78,7 @@ function handleBotRequest(
|
|
|
88
78
|
console.error(error);
|
|
89
79
|
}
|
|
90
80
|
},
|
|
91
|
-
}
|
|
81
|
+
},
|
|
92
82
|
);
|
|
93
83
|
|
|
94
84
|
setTimeout(abort, ABORT_DELAY);
|
|
@@ -99,7 +89,7 @@ function handleBrowserRequest(
|
|
|
99
89
|
request: Request,
|
|
100
90
|
responseStatusCode: number,
|
|
101
91
|
responseHeaders: Headers,
|
|
102
|
-
reactRouterContext: EntryContext
|
|
92
|
+
reactRouterContext: EntryContext,
|
|
103
93
|
) {
|
|
104
94
|
return new Promise((resolve, reject) => {
|
|
105
95
|
let shellRendered = false;
|
|
@@ -117,7 +107,7 @@ function handleBrowserRequest(
|
|
|
117
107
|
new Response(stream, {
|
|
118
108
|
headers: responseHeaders,
|
|
119
109
|
status: responseStatusCode,
|
|
120
|
-
})
|
|
110
|
+
}),
|
|
121
111
|
);
|
|
122
112
|
|
|
123
113
|
pipe(body);
|
|
@@ -134,7 +124,7 @@ function handleBrowserRequest(
|
|
|
134
124
|
console.error(error);
|
|
135
125
|
}
|
|
136
126
|
},
|
|
137
|
-
}
|
|
127
|
+
},
|
|
138
128
|
);
|
|
139
129
|
|
|
140
130
|
setTimeout(abort, ABORT_DELAY);
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { useSearchParams } from "react-router";
|
|
2
2
|
|
|
3
|
-
export const useSearchParam = (
|
|
4
|
-
paramName: string
|
|
5
|
-
): [string, (value: string) => void] => {
|
|
3
|
+
export const useSearchParam = (paramName: string): [string, (value: string) => void] => {
|
|
6
4
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
7
5
|
|
|
8
6
|
const paramValue = searchParams.get(paramName) ?? "";
|
|
@@ -18,7 +16,7 @@ export const useSearchParam = (
|
|
|
18
16
|
}
|
|
19
17
|
return newParams;
|
|
20
18
|
},
|
|
21
|
-
{ replace: true }
|
|
19
|
+
{ replace: true },
|
|
22
20
|
);
|
|
23
21
|
};
|
|
24
22
|
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import type { CytarioPlugin, Logger, PluginContext } from "@cytario/plugin-api";
|
|
2
|
-
import {
|
|
3
|
-
IncompatiblePluginError,
|
|
4
|
-
assertApiCompatible,
|
|
5
|
-
hostApiVersion,
|
|
6
|
-
} from "@cytario/plugin-api";
|
|
2
|
+
import { IncompatiblePluginError, assertApiCompatible, hostApiVersion } from "@cytario/plugin-api";
|
|
7
3
|
import { formatRegistry } from "~/components/ImageViewer/state/formatRegistry";
|
|
8
4
|
|
|
9
5
|
/**
|
|
@@ -37,10 +33,9 @@ export async function bootstrapPluginsCore(
|
|
|
37
33
|
: err instanceof Error
|
|
38
34
|
? err.message
|
|
39
35
|
: String(err);
|
|
40
|
-
logger.error(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
);
|
|
36
|
+
logger.error(`Skipping incompatible plugin "${plugin?.name ?? "<unknown>"}"`, {
|
|
37
|
+
error: message,
|
|
38
|
+
});
|
|
44
39
|
continue;
|
|
45
40
|
}
|
|
46
41
|
|
package/app/root.tsx
CHANGED
|
@@ -20,10 +20,7 @@ import {
|
|
|
20
20
|
} from "react-router";
|
|
21
21
|
|
|
22
22
|
import { UserProfile } from "./.server/auth/getUserInfo";
|
|
23
|
-
import {
|
|
24
|
-
sessionContext,
|
|
25
|
-
sessionMiddleware,
|
|
26
|
-
} from "./.server/auth/sessionMiddleware";
|
|
23
|
+
import { sessionContext, sessionMiddleware } from "./.server/auth/sessionMiddleware";
|
|
27
24
|
import { sessionStorage } from "./.server/auth/sessionStorage";
|
|
28
25
|
import { AppHeader } from "./components/AppHeader";
|
|
29
26
|
import { Section } from "./components/Container";
|
|
@@ -75,9 +72,7 @@ interface RootLoaderResponse {
|
|
|
75
72
|
accountSettingsUrl?: string;
|
|
76
73
|
}
|
|
77
74
|
|
|
78
|
-
export const loader = async ({
|
|
79
|
-
context,
|
|
80
|
-
}: LoaderFunctionArgs): Promise<RootLoaderResponse> => {
|
|
75
|
+
export const loader = async ({ context }: LoaderFunctionArgs): Promise<RootLoaderResponse> => {
|
|
81
76
|
const session = context.get(sessionContext);
|
|
82
77
|
const user = session.get("user");
|
|
83
78
|
const notification = session.get("notification");
|
|
@@ -158,10 +153,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
|
|
158
153
|
<RouterProvider navigate={navigate} useHref={useHref}>
|
|
159
154
|
{data?.user && <AppHeader />}
|
|
160
155
|
|
|
161
|
-
<main
|
|
162
|
-
id="main-content"
|
|
163
|
-
className="relative flex-1 min-h-0 outline-none"
|
|
164
|
-
>
|
|
156
|
+
<main id="main-content" className="relative flex-1 min-h-0 outline-none">
|
|
165
157
|
{children}
|
|
166
158
|
</main>
|
|
167
159
|
</RouterProvider>
|
|
@@ -202,10 +194,7 @@ export function ErrorBoundary() {
|
|
|
202
194
|
<div role="alert">
|
|
203
195
|
<H1>{title}</H1>
|
|
204
196
|
<p>{message}</p>
|
|
205
|
-
<a
|
|
206
|
-
href="/"
|
|
207
|
-
className="text-cytario-purple-500 underline mt-4 inline-block"
|
|
208
|
-
>
|
|
197
|
+
<a href="/" className="text-cytario-purple-500 underline mt-4 inline-block">
|
|
209
198
|
Go home
|
|
210
199
|
</a>
|
|
211
200
|
</div>
|
|
@@ -9,9 +9,7 @@ export function assertAdminScope(
|
|
|
9
9
|
const scope = new URL(url).searchParams.get("scope");
|
|
10
10
|
if (!scope) throw new Response("Missing scope", { status: 400 });
|
|
11
11
|
|
|
12
|
-
const isAdmin = adminScopes.some(
|
|
13
|
-
(s) => scope === s || scope.startsWith(s + "/"),
|
|
14
|
-
);
|
|
12
|
+
const isAdmin = adminScopes.some((s) => scope === s || scope.startsWith(s + "/"));
|
|
15
13
|
if (!isAdmin) throw new Response("Not authorized", { status: 403 });
|
|
16
14
|
|
|
17
15
|
return {
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
flattenGroupsWithIds,
|
|
3
|
-
getGroupWithMembers,
|
|
4
|
-
} from "~/.server/auth/keycloakAdmin";
|
|
1
|
+
import { flattenGroupsWithIds, getGroupWithMembers } from "~/.server/auth/keycloakAdmin";
|
|
5
2
|
|
|
6
3
|
/**
|
|
7
4
|
* Validates that every groupPath belongs to the group tree of the given scope.
|
|
@@ -11,18 +8,13 @@ import {
|
|
|
11
8
|
*
|
|
12
9
|
* Throws 404 if the scope does not exist, 403 if any groupPath is out of scope.
|
|
13
10
|
*/
|
|
14
|
-
export async function assertGroupPathsInScope(
|
|
15
|
-
groupPaths: string[],
|
|
16
|
-
scope: string,
|
|
17
|
-
): Promise<void> {
|
|
11
|
+
export async function assertGroupPathsInScope(groupPaths: string[], scope: string): Promise<void> {
|
|
18
12
|
if (groupPaths.length === 0) return;
|
|
19
13
|
|
|
20
14
|
const group = await getGroupWithMembers(scope);
|
|
21
15
|
if (!group) throw new Response("Scope not found", { status: 404 });
|
|
22
16
|
|
|
23
|
-
const scopeGroupPaths = new Set(
|
|
24
|
-
flattenGroupsWithIds(group).map((g) => g.path),
|
|
25
|
-
);
|
|
17
|
+
const scopeGroupPaths = new Set(flattenGroupsWithIds(group).map((g) => g.path));
|
|
26
18
|
|
|
27
19
|
for (const groupPath of groupPaths) {
|
|
28
20
|
if (!scopeGroupPaths.has(groupPath)) {
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
flattenGroupsWithIds,
|
|
3
|
-
getGroupWithMembers,
|
|
4
|
-
} from "~/.server/auth/keycloakAdmin";
|
|
1
|
+
import { flattenGroupsWithIds, getGroupWithMembers } from "~/.server/auth/keycloakAdmin";
|
|
5
2
|
|
|
6
3
|
/**
|
|
7
4
|
* Validates that every groupId belongs to the group tree of the given scope.
|
|
@@ -11,18 +8,13 @@ import {
|
|
|
11
8
|
*
|
|
12
9
|
* Throws 404 if the scope does not exist, 403 if any groupId is out of scope.
|
|
13
10
|
*/
|
|
14
|
-
export async function assertGroupsInScope(
|
|
15
|
-
groupIds: string[],
|
|
16
|
-
scope: string,
|
|
17
|
-
): Promise<void> {
|
|
11
|
+
export async function assertGroupsInScope(groupIds: string[], scope: string): Promise<void> {
|
|
18
12
|
if (groupIds.length === 0) return;
|
|
19
13
|
|
|
20
14
|
const group = await getGroupWithMembers(scope);
|
|
21
15
|
if (!group) throw new Response("Scope not found", { status: 404 });
|
|
22
16
|
|
|
23
|
-
const scopeGroupIds = new Set(
|
|
24
|
-
flattenGroupsWithIds(group).map((g) => g.id),
|
|
25
|
-
);
|
|
17
|
+
const scopeGroupIds = new Set(flattenGroupsWithIds(group).map((g) => g.id));
|
|
26
18
|
|
|
27
19
|
for (const groupId of groupIds) {
|
|
28
20
|
if (!scopeGroupIds.has(groupId)) {
|
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
collectAllUsers,
|
|
3
|
-
getGroupWithMembers,
|
|
4
|
-
} from "~/.server/auth/keycloakAdmin";
|
|
1
|
+
import { collectAllUsers, getGroupWithMembers } from "~/.server/auth/keycloakAdmin";
|
|
5
2
|
|
|
6
|
-
export async function assertUsersInScope(
|
|
7
|
-
userIds: string[],
|
|
8
|
-
scope: string,
|
|
9
|
-
): Promise<void> {
|
|
3
|
+
export async function assertUsersInScope(userIds: string[], scope: string): Promise<void> {
|
|
10
4
|
const group = await getGroupWithMembers(scope);
|
|
11
5
|
if (!group) throw new Response("Scope not found", { status: 404 });
|
|
12
6
|
|
|
@@ -8,10 +8,7 @@ import { getSession } from "~/.server/auth/getSession";
|
|
|
8
8
|
import { inviteUser } from "~/.server/auth/keycloakAdmin/users";
|
|
9
9
|
import { sessionStorage } from "~/.server/auth/sessionStorage";
|
|
10
10
|
|
|
11
|
-
export const bulkInviteAction: ActionFunction = async ({
|
|
12
|
-
request,
|
|
13
|
-
context,
|
|
14
|
-
}) => {
|
|
11
|
+
export const bulkInviteAction: ActionFunction = async ({ request, context }) => {
|
|
15
12
|
const { user } = context.get(authContext);
|
|
16
13
|
const { adminUrl, scope } = assertAdminScope(request.url, user.adminScopes);
|
|
17
14
|
|
|
@@ -27,15 +24,7 @@ export const bulkInviteAction: ActionFunction = async ({
|
|
|
27
24
|
await assertGroupPathsInScope([groupPath], scope);
|
|
28
25
|
|
|
29
26
|
const results = await Promise.allSettled(
|
|
30
|
-
rows.map((row) =>
|
|
31
|
-
inviteUser(
|
|
32
|
-
row.email,
|
|
33
|
-
row.firstName,
|
|
34
|
-
row.lastName,
|
|
35
|
-
groupPath,
|
|
36
|
-
enabled,
|
|
37
|
-
),
|
|
38
|
-
),
|
|
27
|
+
rows.map((row) => inviteUser(row.email, row.firstName, row.lastName, groupPath, enabled)),
|
|
39
28
|
);
|
|
40
29
|
|
|
41
30
|
const succeeded = results.filter((r) => r.status === "fulfilled").length;
|
|
@@ -3,11 +3,7 @@ import { Plus, X } from "lucide-react";
|
|
|
3
3
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
4
|
import { useSubmit } from "react-router";
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
type BulkInviteRow,
|
|
8
|
-
bulkInviteRowSchema,
|
|
9
|
-
bulkInviteSchema,
|
|
10
|
-
} from "./bulkInvite.schema";
|
|
6
|
+
import { type BulkInviteRow, bulkInviteRowSchema, bulkInviteSchema } from "./bulkInvite.schema";
|
|
11
7
|
import { ScopePill } from "~/components/Pills/ScopePill";
|
|
12
8
|
|
|
13
9
|
interface BulkInviteFormProps {
|
|
@@ -44,37 +40,28 @@ export function BulkInviteForm({
|
|
|
44
40
|
const [groupPath, setGroupPath] = useState(scope);
|
|
45
41
|
const [enabled, setEnabled] = useState(true);
|
|
46
42
|
const [formError, setFormError] = useState<string | null>(null);
|
|
47
|
-
const [rows, setRows] = useState<RowState[]>([
|
|
48
|
-
emptyRow(),
|
|
49
|
-
emptyRow(),
|
|
50
|
-
emptyRow(),
|
|
51
|
-
]);
|
|
43
|
+
const [rows, setRows] = useState<RowState[]>([emptyRow(), emptyRow(), emptyRow()]);
|
|
52
44
|
|
|
53
45
|
const tableRef = useRef<HTMLTableElement>(null);
|
|
54
46
|
|
|
55
|
-
const updateRow = useCallback(
|
|
56
|
-
(index: number, field: keyof BulkInviteRow, value: string) => {
|
|
57
|
-
setRows((prev) =>
|
|
58
|
-
prev.map((row, i) =>
|
|
59
|
-
i === index
|
|
60
|
-
? {
|
|
61
|
-
...row,
|
|
62
|
-
[field]: value,
|
|
63
|
-
errors: { ...row.errors, [field]: undefined },
|
|
64
|
-
}
|
|
65
|
-
: row,
|
|
66
|
-
),
|
|
67
|
-
);
|
|
68
|
-
},
|
|
69
|
-
[],
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
const removeRow = useCallback((index: number) => {
|
|
47
|
+
const updateRow = useCallback((index: number, field: keyof BulkInviteRow, value: string) => {
|
|
73
48
|
setRows((prev) =>
|
|
74
|
-
prev.
|
|
49
|
+
prev.map((row, i) =>
|
|
50
|
+
i === index
|
|
51
|
+
? {
|
|
52
|
+
...row,
|
|
53
|
+
[field]: value,
|
|
54
|
+
errors: { ...row.errors, [field]: undefined },
|
|
55
|
+
}
|
|
56
|
+
: row,
|
|
57
|
+
),
|
|
75
58
|
);
|
|
76
59
|
}, []);
|
|
77
60
|
|
|
61
|
+
const removeRow = useCallback((index: number) => {
|
|
62
|
+
setRows((prev) => (prev.length <= 1 ? prev : prev.filter((_, i) => i !== index)));
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
78
65
|
const addRow = useCallback(() => {
|
|
79
66
|
setRows((prev) => [...prev, emptyRow()]);
|
|
80
67
|
// Focus the email input of the new row after render
|
|
@@ -164,9 +151,7 @@ export function BulkInviteForm({
|
|
|
164
151
|
/>
|
|
165
152
|
) : (
|
|
166
153
|
<Field label="Group Membership">
|
|
167
|
-
<p className="text-sm text-slate-400">
|
|
168
|
-
No groups available in this scope.
|
|
169
|
-
</p>
|
|
154
|
+
<p className="text-sm text-slate-400">No groups available in this scope.</p>
|
|
170
155
|
</Field>
|
|
171
156
|
)}
|
|
172
157
|
<div className="flex items-center gap-2">
|
|
@@ -179,9 +164,7 @@ export function BulkInviteForm({
|
|
|
179
164
|
</div>
|
|
180
165
|
</div>
|
|
181
166
|
|
|
182
|
-
{formError &&
|
|
183
|
-
<p className="text-sm text-rose-600 mb-4">{formError}</p>
|
|
184
|
-
)}
|
|
167
|
+
{formError && <p className="text-sm text-rose-600 mb-4">{formError}</p>}
|
|
185
168
|
|
|
186
169
|
<table ref={tableRef} className="w-full border-collapse">
|
|
187
170
|
<thead>
|
|
@@ -8,10 +8,7 @@ import { addUserToGroup, createGroup } from "~/.server/auth/keycloakAdmin";
|
|
|
8
8
|
import { KeycloakAdminError } from "~/.server/auth/keycloakAdmin/client";
|
|
9
9
|
import { sessionStorage } from "~/.server/auth/sessionStorage";
|
|
10
10
|
|
|
11
|
-
export const createGroupAction: ActionFunction = async ({
|
|
12
|
-
request,
|
|
13
|
-
context,
|
|
14
|
-
}) => {
|
|
11
|
+
export const createGroupAction: ActionFunction = async ({ request, context }) => {
|
|
15
12
|
const { user } = context.get(authContext);
|
|
16
13
|
const { adminUrl, scope } = assertAdminScope(request.url, user.adminScopes);
|
|
17
14
|
|
|
@@ -25,10 +22,7 @@ export const createGroupAction: ActionFunction = async ({
|
|
|
25
22
|
const session = await getSession(request);
|
|
26
23
|
|
|
27
24
|
try {
|
|
28
|
-
const { path, adminsGroupId } = await createGroup(
|
|
29
|
-
scope,
|
|
30
|
-
result.data.name,
|
|
31
|
-
);
|
|
25
|
+
const { path, adminsGroupId } = await createGroup(scope, result.data.name);
|
|
32
26
|
|
|
33
27
|
await addUserToGroup(user.sub, adminsGroupId);
|
|
34
28
|
|
|
@@ -45,8 +39,7 @@ export const createGroupAction: ActionFunction = async ({
|
|
|
45
39
|
} catch (e) {
|
|
46
40
|
console.error("Create group failed:", e);
|
|
47
41
|
|
|
48
|
-
const status =
|
|
49
|
-
e instanceof KeycloakAdminError ? e.status : undefined;
|
|
42
|
+
const status = e instanceof KeycloakAdminError ? e.status : undefined;
|
|
50
43
|
const message =
|
|
51
44
|
status === 409
|
|
52
45
|
? `A group named "${result.data.name}" already exists in this group.`
|
|
@@ -3,10 +3,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|
|
3
3
|
import { Controller, useForm, useWatch } from "react-hook-form";
|
|
4
4
|
import { useSubmit } from "react-router";
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
type CreateGroupFormData,
|
|
8
|
-
createGroupSchema,
|
|
9
|
-
} from "./createGroup.schema";
|
|
6
|
+
import { type CreateGroupFormData, createGroupSchema } from "./createGroup.schema";
|
|
10
7
|
import { ScopePill } from "~/components/Pills/ScopePill";
|
|
11
8
|
|
|
12
9
|
interface CreateGroupFormProps {
|
|
@@ -35,11 +32,7 @@ export function CreateGroupForm({ scope }: CreateGroupFormProps) {
|
|
|
35
32
|
};
|
|
36
33
|
|
|
37
34
|
return (
|
|
38
|
-
<form
|
|
39
|
-
id="create-group-form"
|
|
40
|
-
onSubmit={handleSubmit(onSubmit)}
|
|
41
|
-
className="space-y-4"
|
|
42
|
-
>
|
|
35
|
+
<form id="create-group-form" onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
|
43
36
|
<Fieldset>
|
|
44
37
|
<Field label="Group name" error={errors.name}>
|
|
45
38
|
<Controller
|
|
@@ -20,11 +20,7 @@ export default function CreateGroupModal() {
|
|
|
20
20
|
<Button onPress={() => navigate(-1)} variant="secondary">
|
|
21
21
|
Cancel
|
|
22
22
|
</Button>
|
|
23
|
-
<Button
|
|
24
|
-
type="submit"
|
|
25
|
-
form="create-group-form"
|
|
26
|
-
isDisabled={isSubmitting}
|
|
27
|
-
>
|
|
23
|
+
<Button type="submit" form="create-group-form" isDisabled={isSubmitting}>
|
|
28
24
|
{isSubmitting ? "Creating..." : "Create Group"}
|
|
29
25
|
</Button>
|
|
30
26
|
</footer>
|
|
@@ -8,10 +8,7 @@ import { getSession } from "~/.server/auth/getSession";
|
|
|
8
8
|
import { inviteUser } from "~/.server/auth/keycloakAdmin/users";
|
|
9
9
|
import { sessionStorage } from "~/.server/auth/sessionStorage";
|
|
10
10
|
|
|
11
|
-
export const inviteUserAction: ActionFunction = async ({
|
|
12
|
-
request,
|
|
13
|
-
context,
|
|
14
|
-
}) => {
|
|
11
|
+
export const inviteUserAction: ActionFunction = async ({ request, context }) => {
|
|
15
12
|
const { user } = context.get(authContext);
|
|
16
13
|
const { adminUrl, scope } = assertAdminScope(request.url, user.adminScopes);
|
|
17
14
|
|
|
@@ -63,11 +63,7 @@ export function InviteUserForm({
|
|
|
63
63
|
};
|
|
64
64
|
|
|
65
65
|
return (
|
|
66
|
-
<form
|
|
67
|
-
id="invite-form"
|
|
68
|
-
onSubmit={handleSubmit(onSubmit)}
|
|
69
|
-
className="space-y-4"
|
|
70
|
-
>
|
|
66
|
+
<form id="invite-form" onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
|
71
67
|
<Fieldset>
|
|
72
68
|
<Field label="Email" error={errors.email}>
|
|
73
69
|
<Controller
|
|
@@ -128,9 +124,7 @@ export function InviteUserForm({
|
|
|
128
124
|
/>
|
|
129
125
|
) : (
|
|
130
126
|
<Field label="Group Membership">
|
|
131
|
-
<p className="text-sm text-slate-400">
|
|
132
|
-
No groups available in this scope.
|
|
133
|
-
</p>
|
|
127
|
+
<p className="text-sm text-slate-400">No groups available in this scope.</p>
|
|
134
128
|
</Field>
|
|
135
129
|
)}
|
|
136
130
|
<div className="flex items-center gap-2">
|
|
@@ -2,10 +2,7 @@ import { type LoaderFunction } from "react-router";
|
|
|
2
2
|
|
|
3
3
|
import { assertAdminScope } from "../assertAdminScope";
|
|
4
4
|
import { authContext } from "~/.server/auth/authMiddleware";
|
|
5
|
-
import {
|
|
6
|
-
getGroupWithMembers,
|
|
7
|
-
GroupWithMembers,
|
|
8
|
-
} from "~/.server/auth/keycloakAdmin/groups";
|
|
5
|
+
import { getGroupWithMembers, GroupWithMembers } from "~/.server/auth/keycloakAdmin/groups";
|
|
9
6
|
|
|
10
7
|
function flattenGroupPaths(group: GroupWithMembers): string[] {
|
|
11
8
|
return [group.path, ...group.subGroups.flatMap(flattenGroupPaths)];
|