@igstack/app-catalog-frontend-core 0.3.1-alpha-20260405015231 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/__tests__/integration/harness/MockBackendVerifier.d.ts +3 -3
- package/dist/esm/__tests__/integration/mock-backend/MockBackendConfigurer.d.ts +4 -4
- package/dist/esm/__tests__/integration/mock-backend/MockDb.d.ts +11 -7
- package/dist/esm/api/infra/trpc.d.ts +3 -3
- package/dist/esm/modules/appCatalog/context/AppCatalogContext.d.ts +2 -3
- package/dist/esm/modules/appCatalog/context/AppCatalogContext.js +3 -4
- package/dist/esm/modules/appCatalog/context/AppCatalogContext.js.map +1 -1
- package/dist/esm/modules/appCatalog/hooks/useAppCounts.d.ts +2 -2
- package/dist/esm/modules/appCatalog/hooks/useAppCounts.js +3 -3
- package/dist/esm/modules/appCatalog/hooks/useAppCounts.js.map +1 -1
- package/dist/esm/modules/appCatalog/hooks/useUpdateApp.d.ts +9 -0
- package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.d.ts +2 -2
- package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/AppDetailModal.d.ts +2 -2
- package/dist/esm/modules/appCatalog/ui/components/PersonBadge.js +1 -0
- package/dist/esm/modules/appCatalog/ui/components/PersonBadge.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/ScreenshotGallery.d.ts +2 -2
- package/dist/esm/modules/appCatalog/ui/components/ScreenshotGallery.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.d.ts +2 -2
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js +13 -14
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.d.ts +2 -2
- package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.js +1 -0
- package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/filters/FilterBar.d.ts +2 -2
- package/dist/esm/modules/appCatalog/ui/filters/FilterBar.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.d.ts +3 -3
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js +19 -19
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogTable.d.ts +2 -2
- package/dist/esm/modules/appCatalog/ui/grid/appCatalogUtils.d.ts +2 -2
- package/dist/esm/modules/appCatalog/ui/hooks/useKeyboardNavigation.d.ts +3 -3
- package/dist/esm/modules/appCatalog/ui/hooks/useKeyboardNavigation.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +20 -12
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
- package/dist/esm/modules/appCatalog/utils/resolveHelpers.d.ts +5 -2
- package/dist/esm/modules/appCatalog/utils/resolveHelpers.js +4 -4
- package/dist/esm/modules/appCatalog/utils/resolveHelpers.js.map +1 -1
- package/dist/esm/modules/appCatalog/utils/searchApps.d.ts +11 -6
- package/dist/esm/modules/appCatalog/utils/searchApps.js +15 -14
- package/dist/esm/modules/appCatalog/utils/searchApps.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/integration/appCatalog.integration.test.ts +3 -3
- package/src/__tests__/integration/harness/MockBackendVerifier.ts +5 -5
- package/src/__tests__/integration/mock-backend/MockBackendConfigurer.ts +16 -12
- package/src/__tests__/integration/mock-backend/MockDb.ts +30 -22
- package/src/__tests__/modules/appCatalog/ScreenshotPreview.test.tsx +4 -5
- package/src/__tests__/modules/appCatalog/utils/searchApps.test.ts +28 -30
- package/src/__tests__/modules/gallery/EscapeDismissal.integration.test.tsx +3 -4
- package/src/modules/appCatalog/context/AppCatalogContext.tsx +4 -8
- package/src/modules/appCatalog/hooks/useAppCounts.ts +5 -5
- package/src/modules/appCatalog/ui/components/AccessRequestSection.tsx +2 -2
- package/src/modules/appCatalog/ui/components/AppDetailModal.tsx +10 -10
- package/src/modules/appCatalog/ui/components/ScreenshotGallery.tsx +2 -2
- package/src/modules/appCatalog/ui/components/SearchAndFilterHeader.tsx +6 -2
- package/src/modules/appCatalog/ui/components/SubResourcesSection.tsx +17 -17
- package/src/modules/appCatalog/ui/components/TierVariantsSection.tsx +2 -2
- package/src/modules/appCatalog/ui/filters/FilterBar.tsx +2 -2
- package/src/modules/appCatalog/ui/grid/AppCatalogGrid.tsx +34 -37
- package/src/modules/appCatalog/ui/grid/AppCatalogTable.tsx +3 -3
- package/src/modules/appCatalog/ui/grid/appCatalogUtils.ts +2 -2
- package/src/modules/appCatalog/ui/hooks/useKeyboardNavigation.ts +3 -3
- package/src/modules/appCatalog/ui/pages/AppCatalogPage.tsx +24 -14
- package/src/modules/appCatalog/utils/resolveHelpers.ts +13 -10
- package/src/modules/appCatalog/utils/searchApps.ts +36 -31
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Resource } from '@igstack/app-catalog-backend-core';
|
|
2
2
|
import { MockDb } from '../mock-backend/MockDb.js';
|
|
3
3
|
export declare class MockBackendVerifier {
|
|
4
4
|
readonly db: MockDb;
|
|
5
5
|
constructor(db: MockDb);
|
|
6
|
-
apps():
|
|
7
|
-
getApp(slug: string):
|
|
6
|
+
apps(): Resource[];
|
|
7
|
+
getApp(slug: string): Resource;
|
|
8
8
|
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { AppApprovalMethod,
|
|
1
|
+
import { AppApprovalMethod, GroupingTagDefinition, Resource } from '@igstack/app-catalog-backend-core';
|
|
2
2
|
import { MockDb } from './MockDb.js';
|
|
3
3
|
import { MockUserContext, UserConfig } from './MockUserContext.js';
|
|
4
4
|
export declare class MockBackendConfigurer {
|
|
5
5
|
readonly db: MockDb;
|
|
6
6
|
readonly userContext: MockUserContext;
|
|
7
7
|
constructor(db: MockDb, userContext: MockUserContext);
|
|
8
|
-
withApp(overrides?: Partial<
|
|
8
|
+
withApp(overrides?: Partial<Resource>): Resource;
|
|
9
9
|
withTag(overrides?: Partial<GroupingTagDefinition>): GroupingTagDefinition;
|
|
10
10
|
withApprovalMethod(overrides?: Partial<AppApprovalMethod>): AppApprovalMethod;
|
|
11
|
-
withSubResource(overrides: Partial<
|
|
11
|
+
withSubResource(overrides: Partial<Resource> & {
|
|
12
12
|
appSlug: string;
|
|
13
|
-
}):
|
|
13
|
+
}): Resource;
|
|
14
14
|
withUser(overrides: Partial<UserConfig>): void;
|
|
15
15
|
}
|
|
16
16
|
/** Reset the counter between tests */
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import { AppApprovalMethod, AppCatalogData,
|
|
1
|
+
import { AppApprovalMethod, AppCatalogData, GroupingTagDefinition, Resource } from '@igstack/app-catalog-backend-core';
|
|
2
2
|
export declare class MockDb {
|
|
3
|
-
|
|
3
|
+
resources: Resource[];
|
|
4
4
|
tagsDefinitions: GroupingTagDefinition[];
|
|
5
5
|
approvalMethods: AppApprovalMethod[];
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
upsertResource(resource: Resource): void;
|
|
7
|
+
/** @deprecated Use upsertResource */
|
|
8
|
+
upsertApp(app: Resource): void;
|
|
9
|
+
getResources(): Resource[];
|
|
10
|
+
/** @deprecated Use getResources */
|
|
11
|
+
getApps(): Resource[];
|
|
12
|
+
getResource(slug: string): Resource;
|
|
13
|
+
/** @deprecated Use getResource */
|
|
14
|
+
getApp(slug: string): Resource;
|
|
10
15
|
setTagDefinitions(defs: GroupingTagDefinition[]): void;
|
|
11
16
|
setApprovalMethods(methods: AppApprovalMethod[]): void;
|
|
12
|
-
addSubResource(sr: SubResource): void;
|
|
13
17
|
getAppCatalogData(): AppCatalogData;
|
|
14
18
|
}
|
|
@@ -31,7 +31,7 @@ declare const TRPCProvider: import('react').FunctionComponent<{
|
|
|
31
31
|
aiPrompt?: string | null | undefined;
|
|
32
32
|
};
|
|
33
33
|
};
|
|
34
|
-
output: import('@igstack/app-catalog-backend-core').
|
|
34
|
+
output: import('@igstack/app-catalog-backend-core').Resource;
|
|
35
35
|
meta: object;
|
|
36
36
|
}>;
|
|
37
37
|
}>>;
|
|
@@ -100,7 +100,7 @@ declare const TRPCProvider: import('react').FunctionComponent<{
|
|
|
100
100
|
aiPrompt?: string | null | undefined;
|
|
101
101
|
};
|
|
102
102
|
};
|
|
103
|
-
output: import('@igstack/app-catalog-backend-core').
|
|
103
|
+
output: import('@igstack/app-catalog-backend-core').Resource;
|
|
104
104
|
meta: object;
|
|
105
105
|
}>;
|
|
106
106
|
}>>;
|
|
@@ -168,7 +168,7 @@ declare const TRPCProvider: import('react').FunctionComponent<{
|
|
|
168
168
|
aiPrompt?: string | null | undefined;
|
|
169
169
|
};
|
|
170
170
|
};
|
|
171
|
-
output: import('@igstack/app-catalog-backend-core').
|
|
171
|
+
output: import('@igstack/app-catalog-backend-core').Resource;
|
|
172
172
|
meta: object;
|
|
173
173
|
}>;
|
|
174
174
|
}>>;
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import { AppApprovalMethod,
|
|
1
|
+
import { AppApprovalMethod, AppVersionInfo, Group, GroupingTagDefinition, Person, Resource } from '@igstack/app-catalog-backend-core';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
3
|
export interface AppCatalogContextIface {
|
|
4
|
-
|
|
4
|
+
resources: Resource[];
|
|
5
5
|
isLoadingApps: boolean;
|
|
6
6
|
tagsDefinitions: GroupingTagDefinition[];
|
|
7
7
|
approvalMethods: AppApprovalMethod[];
|
|
8
8
|
persons: Person[];
|
|
9
9
|
groups: Group[];
|
|
10
|
-
subResources?: SubResource[];
|
|
11
10
|
versions?: AppVersionInfo;
|
|
12
11
|
}
|
|
13
12
|
export declare const AppCatalogContext: import('react').Context<AppCatalogContextIface | undefined>;
|
|
@@ -3,6 +3,7 @@ import { useQuery } from "@tanstack/react-query";
|
|
|
3
3
|
import { createContext, useMemo, useEffect, use } from "react";
|
|
4
4
|
import { ApiQueryMagazineAppCatalog } from "../api/ApiQueryMagazineAppCatalog.js";
|
|
5
5
|
import { useUiSettings } from "../../../context/UiSettingsContext.js";
|
|
6
|
+
import "../ui/context/AppCatalogFiltersContext.js";
|
|
6
7
|
import "@tanstack/react-query-devtools";
|
|
7
8
|
import "next-themes";
|
|
8
9
|
import "@radix-ui/react-dialog";
|
|
@@ -23,13 +24,12 @@ function AppCatalogProvider({ children }) {
|
|
|
23
24
|
const uiSettings = useUiSettings();
|
|
24
25
|
const contextValue = useMemo(
|
|
25
26
|
() => ({
|
|
26
|
-
|
|
27
|
+
resources: (data == null ? void 0 : data.resources) ?? [],
|
|
27
28
|
isLoadingApps,
|
|
28
29
|
tagsDefinitions: (data == null ? void 0 : data.tagsDefinitions) ?? [],
|
|
29
30
|
approvalMethods: (data == null ? void 0 : data.approvalMethods) ?? [],
|
|
30
31
|
persons: (data == null ? void 0 : data.persons) ?? [],
|
|
31
32
|
groups: (data == null ? void 0 : data.groups) ?? [],
|
|
32
|
-
subResources: (data == null ? void 0 : data.subResources) ?? [],
|
|
33
33
|
versions: {
|
|
34
34
|
...data == null ? void 0 : data.versions,
|
|
35
35
|
...uiSettings.frontendBuildId && {
|
|
@@ -41,11 +41,10 @@ function AppCatalogProvider({ children }) {
|
|
|
41
41
|
}),
|
|
42
42
|
[
|
|
43
43
|
data == null ? void 0 : data.approvalMethods,
|
|
44
|
-
data == null ? void 0 : data.
|
|
44
|
+
data == null ? void 0 : data.resources,
|
|
45
45
|
data == null ? void 0 : data.tagsDefinitions,
|
|
46
46
|
data == null ? void 0 : data.persons,
|
|
47
47
|
data == null ? void 0 : data.groups,
|
|
48
|
-
data == null ? void 0 : data.subResources,
|
|
49
48
|
data == null ? void 0 : data.versions,
|
|
50
49
|
uiSettings.frontendBuildId,
|
|
51
50
|
isLoadingApps
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppCatalogContext.js","sources":["../../../../../src/modules/appCatalog/context/AppCatalogContext.tsx"],"sourcesContent":["import type {\n AppApprovalMethod,\n
|
|
1
|
+
{"version":3,"file":"AppCatalogContext.js","sources":["../../../../../src/modules/appCatalog/context/AppCatalogContext.tsx"],"sourcesContent":["import type {\n AppApprovalMethod,\n AppVersionInfo,\n Group,\n GroupingTagDefinition,\n Person,\n Resource,\n} from '@igstack/app-catalog-backend-core'\nimport { useQuery } from '@tanstack/react-query'\nimport type { ReactNode } from 'react'\nimport { createContext, use, useEffect, useMemo } from 'react'\nimport { ApiQueryMagazineAppCatalog } from '~/modules/appCatalog'\nimport { useUiSettings } from '~/context/UiSettingsContext'\n\nexport interface AppCatalogContextIface {\n resources: Resource[]\n isLoadingApps: boolean\n tagsDefinitions: GroupingTagDefinition[]\n approvalMethods: AppApprovalMethod[]\n persons: Person[]\n groups: Group[]\n versions?: AppVersionInfo\n}\n\nexport const AppCatalogContext = createContext<\n AppCatalogContextIface | undefined\n>(undefined)\n\ninterface AppCatalogProviderProps {\n children: ReactNode\n}\n\nexport function AppCatalogProvider({ children }: AppCatalogProviderProps) {\n const { data, isLoading: isLoadingApps } = useQuery(\n ApiQueryMagazineAppCatalog.getAppCatalog(),\n )\n const uiSettings = useUiSettings()\n\n const contextValue = useMemo<AppCatalogContextIface>(\n () => ({\n resources: data?.resources ?? [],\n isLoadingApps,\n tagsDefinitions: data?.tagsDefinitions ?? [],\n approvalMethods: data?.approvalMethods ?? [],\n persons: data?.persons ?? [],\n groups: data?.groups ?? [],\n versions: {\n ...data?.versions,\n ...(uiSettings.frontendBuildId && {\n frontend: {\n displayName:\n uiSettings.frontendBuildId === 'local'\n ? 'local'\n : `#${uiSettings.frontendBuildId}`,\n },\n }),\n },\n }),\n [\n data?.approvalMethods,\n data?.resources,\n data?.tagsDefinitions,\n data?.persons,\n data?.groups,\n data?.versions,\n uiSettings.frontendBuildId,\n isLoadingApps,\n ],\n )\n\n // Update document title based on backend version\n useEffect(() => {\n if (data?.versions?.backend?.displayName === 'local') {\n document.title = 'Local'\n } else {\n document.title = 'App Catalog'\n }\n }, [data?.versions?.backend?.displayName])\n\n return <AppCatalogContext value={contextValue}>{children}</AppCatalogContext>\n}\n\nexport function useAppCatalogContext(): AppCatalogContextIface {\n const context = use(AppCatalogContext)\n if (!context) {\n throw new Error(\n 'useAppCatalogContext must be used within AppCatalogProvider',\n )\n }\n return context\n}\n"],"names":["_b","_a"],"mappings":";;;;;;;;;;;;;;;;;AAwBO,MAAM,oBAAoB,cAE/B,MAAS;AAMJ,SAAS,mBAAmB,EAAE,YAAqC;;AACxE,QAAM,EAAE,MAAM,WAAW,cAAA,IAAkB;AAAA,IACzC,2BAA2B,cAAA;AAAA,EAAc;AAE3C,QAAM,aAAa,cAAA;AAEnB,QAAM,eAAe;AAAA,IACnB,OAAO;AAAA,MACL,YAAW,6BAAM,cAAa,CAAA;AAAA,MAC9B;AAAA,MACA,kBAAiB,6BAAM,oBAAmB,CAAA;AAAA,MAC1C,kBAAiB,6BAAM,oBAAmB,CAAA;AAAA,MAC1C,UAAS,6BAAM,YAAW,CAAA;AAAA,MAC1B,SAAQ,6BAAM,WAAU,CAAA;AAAA,MACxB,UAAU;AAAA,QACR,GAAG,6BAAM;AAAA,QACT,GAAI,WAAW,mBAAmB;AAAA,UAChC,UAAU;AAAA,YACR,aACE,WAAW,oBAAoB,UAC3B,UACA,IAAI,WAAW,eAAe;AAAA,UAAA;AAAA,QACtC;AAAA,MACF;AAAA,IACF;AAAA,IAEF;AAAA,MACE,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,WAAW;AAAA,MACX;AAAA,IAAA;AAAA,EACF;AAIF,YAAU,MAAM;;AACd,UAAIA,OAAAC,MAAA,6BAAM,aAAN,gBAAAA,IAAgB,YAAhB,gBAAAD,IAAyB,iBAAgB,SAAS;AACpD,eAAS,QAAQ;AAAA,IACnB,OAAO;AACL,eAAS,QAAQ;AAAA,IACnB;AAAA,EACF,GAAG,EAAC,wCAAM,aAAN,mBAAgB,YAAhB,mBAAyB,WAAW,CAAC;AAEzC,SAAO,oBAAC,mBAAA,EAAkB,OAAO,cAAe,SAAA,CAAS;AAC3D;AAEO,SAAS,uBAA+C;AAC7D,QAAM,UAAU,IAAI,iBAAiB;AACrC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAEJ;AACA,SAAO;AACT;"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { searchResources } from "../utils/searchApps.js";
|
|
3
3
|
import { useAppCatalogFilters } from "../ui/context/AppCatalogFiltersContext.js";
|
|
4
4
|
function useAppCounts({
|
|
5
5
|
apps,
|
|
@@ -15,7 +15,7 @@ function useAppCounts({
|
|
|
15
15
|
if (!filterState.showDeprecated) {
|
|
16
16
|
recentApps = recentApps.filter((app) => !app.deprecated);
|
|
17
17
|
}
|
|
18
|
-
return
|
|
18
|
+
return searchResources(recentApps, searchValue).length;
|
|
19
19
|
}, [apps, topAppSlugs, searchValue, filterState.showDeprecated]);
|
|
20
20
|
const allCount = useMemo(() => {
|
|
21
21
|
let result = apps;
|
|
@@ -35,7 +35,7 @@ function useAppCounts({
|
|
|
35
35
|
);
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
|
-
return
|
|
38
|
+
return searchResources(result, searchValue).length;
|
|
39
39
|
}, [apps, filterState.tagFilters, filterState.showDeprecated, searchValue]);
|
|
40
40
|
return { recentCount, allCount, deprecatedCount };
|
|
41
41
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAppCounts.js","sources":["../../../../../src/modules/appCatalog/hooks/useAppCounts.ts"],"sourcesContent":["import { useMemo } from 'react'\nimport type {
|
|
1
|
+
{"version":3,"file":"useAppCounts.js","sources":["../../../../../src/modules/appCatalog/hooks/useAppCounts.ts"],"sourcesContent":["import { useMemo } from 'react'\nimport type { Resource } from '@igstack/app-catalog-backend-core'\nimport { searchResources } from '../utils/searchApps'\nimport { useAppCatalogFilters } from '../ui/context/AppCatalogFiltersContext'\n\ninterface UseAppCountsOptions {\n apps: Resource[]\n topAppSlugs: string[]\n searchValue: string\n}\n\n/**\n * Calculates filtered app counts for display in FilterBar.\n * Counts include search filter applied.\n */\nexport function useAppCounts({\n apps,\n topAppSlugs,\n searchValue,\n}: UseAppCountsOptions) {\n const { state: filterState } = useAppCatalogFilters()\n\n // Count of deprecated apps (total, no filters applied)\n const deprecatedCount = useMemo(() => {\n return apps.filter((app) => app.deprecated).length\n }, [apps])\n\n // Count for \"My Recent\" (with search applied, respects showDeprecated)\n const recentCount = useMemo(() => {\n let recentApps = apps.filter((app) => topAppSlugs.includes(app.slug))\n if (!filterState.showDeprecated) {\n recentApps = recentApps.filter((app) => !app.deprecated)\n }\n return searchResources(recentApps, searchValue).length\n }, [apps, topAppSlugs, searchValue, filterState.showDeprecated])\n\n // Count for \"Show All\" (respects showDeprecated, tag filters, and search)\n const allCount = useMemo(() => {\n let result = apps\n\n // Apply deprecated filter\n if (!filterState.showDeprecated) {\n result = result.filter((app) => !app.deprecated)\n }\n\n // Apply tag filters if any\n if (Object.keys(filterState.tagFilters).length > 0) {\n result = result.filter((app) => {\n return Object.entries(filterState.tagFilters).every(\n ([prefix, value]) => {\n const fullTag = `${prefix}:${value}`\n return app.tags?.some(\n (tag) => tag.toLowerCase() === fullTag.toLowerCase(),\n )\n },\n )\n })\n }\n\n return searchResources(result, searchValue).length\n }, [apps, filterState.tagFilters, filterState.showDeprecated, searchValue])\n\n return { recentCount, allCount, deprecatedCount }\n}\n"],"names":[],"mappings":";;;AAeO,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AACF,GAAwB;AACtB,QAAM,EAAE,OAAO,YAAA,IAAgB,qBAAA;AAG/B,QAAM,kBAAkB,QAAQ,MAAM;AACpC,WAAO,KAAK,OAAO,CAAC,QAAQ,IAAI,UAAU,EAAE;AAAA,EAC9C,GAAG,CAAC,IAAI,CAAC;AAGT,QAAM,cAAc,QAAQ,MAAM;AAChC,QAAI,aAAa,KAAK,OAAO,CAAC,QAAQ,YAAY,SAAS,IAAI,IAAI,CAAC;AACpE,QAAI,CAAC,YAAY,gBAAgB;AAC/B,mBAAa,WAAW,OAAO,CAAC,QAAQ,CAAC,IAAI,UAAU;AAAA,IACzD;AACA,WAAO,gBAAgB,YAAY,WAAW,EAAE;AAAA,EAClD,GAAG,CAAC,MAAM,aAAa,aAAa,YAAY,cAAc,CAAC;AAG/D,QAAM,WAAW,QAAQ,MAAM;AAC7B,QAAI,SAAS;AAGb,QAAI,CAAC,YAAY,gBAAgB;AAC/B,eAAS,OAAO,OAAO,CAAC,QAAQ,CAAC,IAAI,UAAU;AAAA,IACjD;AAGA,QAAI,OAAO,KAAK,YAAY,UAAU,EAAE,SAAS,GAAG;AAClD,eAAS,OAAO,OAAO,CAAC,QAAQ;AAC9B,eAAO,OAAO,QAAQ,YAAY,UAAU,EAAE;AAAA,UAC5C,CAAC,CAAC,QAAQ,KAAK,MAAM;;AACnB,kBAAM,UAAU,GAAG,MAAM,IAAI,KAAK;AAClC,oBAAO,SAAI,SAAJ,mBAAU;AAAA,cACf,CAAC,QAAQ,IAAI,YAAA,MAAkB,QAAQ,YAAA;AAAA;AAAA,UAE3C;AAAA,QAAA;AAAA,MAEJ,CAAC;AAAA,IACH;AAEA,WAAO,gBAAgB,QAAQ,WAAW,EAAE;AAAA,EAC9C,GAAG,CAAC,MAAM,YAAY,YAAY,YAAY,gBAAgB,WAAW,CAAC;AAE1E,SAAO,EAAE,aAAa,UAAU,gBAAA;AAClC;"}
|
|
@@ -18,6 +18,7 @@ export declare function useUpdateApp(): import('@tanstack/react-query').UseMutat
|
|
|
18
18
|
type?: "deprecated" | "discouraged" | undefined;
|
|
19
19
|
replacementSlug?: string | undefined;
|
|
20
20
|
} | undefined;
|
|
21
|
+
type?: string | undefined;
|
|
21
22
|
abbreviation?: string | undefined;
|
|
22
23
|
nicknames?: string[] | undefined;
|
|
23
24
|
description?: string | undefined;
|
|
@@ -71,6 +72,14 @@ export declare function useUpdateApp(): import('@tanstack/react-query').UseMutat
|
|
|
71
72
|
} | undefined;
|
|
72
73
|
appUrl?: string | undefined;
|
|
73
74
|
}[] | undefined;
|
|
75
|
+
parentSlug?: string | undefined;
|
|
76
|
+
tier?: string | undefined;
|
|
77
|
+
familySlug?: string | undefined;
|
|
78
|
+
aliases?: string[] | undefined;
|
|
79
|
+
ownerPersonSlug?: string | undefined;
|
|
80
|
+
accessMaintainerGroupSlugs?: string[] | undefined;
|
|
81
|
+
accessComments?: string | undefined;
|
|
82
|
+
extra?: Record<string, unknown> | undefined;
|
|
74
83
|
sources?: string[] | {
|
|
75
84
|
sourceSlug: string;
|
|
76
85
|
url: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { AppApprovalMethod,
|
|
1
|
+
import { AppApprovalMethod, Resource } from '@igstack/app-catalog-backend-core';
|
|
2
2
|
interface AccessRequestSectionProps {
|
|
3
|
-
app:
|
|
3
|
+
app: Resource;
|
|
4
4
|
approvalMethods: AppApprovalMethod[];
|
|
5
5
|
}
|
|
6
6
|
export declare function AccessRequestSection({ app, approvalMethods, }: AccessRequestSectionProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AccessRequestSection.js","sources":["../../../../../../src/modules/appCatalog/ui/components/AccessRequestSection.tsx"],"sourcesContent":["import type {\n AppApprovalMethod,\n AppForCatalog,\n} from '@igstack/app-catalog-backend-core'\nimport { Bot, Check, Copy, ExternalLink, Settings, Users } from 'lucide-react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport ReactMarkdown from 'react-markdown'\nimport { Button } from '~/ui/button'\nimport {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from '~/ui/accordion'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { PersonBadge } from './PersonBadge'\n\n// Constants\nconst COPY_FEEDBACK_DURATION = 2000\n\ninterface AccessRequestSectionProps {\n app: AppForCatalog\n approvalMethods: AppApprovalMethod[]\n}\n\n// Component for rendering markdown links with security attributes\nconst MarkdownLink = ({\n href,\n children,\n}: {\n href?: string\n children?: React.ReactNode\n}) => (\n <a\n href={href}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-primary hover:underline\"\n >\n {children}\n </a>\n)\n\n// Helper function for approval method icons\nfunction getApprovalMethodIcon(\n type: 'service' | 'personTeam' | 'custom' | 'noAccessRequired' | 'unknown',\n) {\n switch (type) {\n case 'service':\n return <Bot className=\"size-5 text-primary\" />\n case 'personTeam':\n return <Users className=\"size-5 text-primary\" />\n case 'custom':\n case 'noAccessRequired':\n case 'unknown':\n return <Settings className=\"size-5 text-primary\" />\n }\n}\n\n/**\n * Custom hook for handling copy-to-clipboard functionality with feedback\n * Includes proper cleanup to prevent memory leaks\n */\nfunction useCopyToClipboard() {\n const [copiedId, setCopiedId] = useState<string | null>(null)\n const [error, setError] = useState<string | null>(null)\n const timeoutRef = useRef<NodeJS.Timeout | null>(null)\n\n // Cleanup timeout on unmount\n useEffect(() => {\n return () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current)\n }\n }\n }, [])\n\n const copyToClipboard = useCallback(async (text: string, id: string) => {\n // Clear existing timeout\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current)\n }\n\n try {\n await navigator.clipboard.writeText(text)\n setCopiedId(id)\n setError(null)\n\n // Set new timeout with cleanup\n timeoutRef.current = setTimeout(() => {\n setCopiedId(null)\n }, COPY_FEEDBACK_DURATION)\n } catch (err) {\n console.error('Failed to copy to clipboard:', err)\n setError('Failed to copy')\n\n // Clear error after duration\n timeoutRef.current = setTimeout(() => {\n setError(null)\n }, COPY_FEEDBACK_DURATION)\n }\n }, [])\n\n return { copiedId, error, copyToClipboard }\n}\n\n/**\n * Reusable copy button component with accessibility\n */\ninterface CopyButtonProps {\n onCopy: () => void\n isCopied: boolean\n ariaLabel: string\n className?: string\n}\n\nfunction CopyButton({\n onCopy,\n isCopied,\n ariaLabel,\n className,\n}: CopyButtonProps) {\n return (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={onCopy}\n className={className}\n aria-label={ariaLabel}\n title={isCopied ? 'Copied!' : 'Copy to clipboard'}\n >\n {isCopied ? (\n <>\n <Check className=\"h-3.5 w-3.5 text-green-600\" />\n <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n Copied to clipboard\n </span>\n </>\n ) : (\n <Copy className=\"h-3.5 w-3.5\" />\n )}\n </Button>\n )\n}\n\nexport function AccessRequestSection({\n app,\n approvalMethods,\n}: AccessRequestSectionProps) {\n const { copiedId, copyToClipboard } = useCopyToClipboard()\n const accessRequest = app.accessRequest\n const approvalMethod = approvalMethods.find(\n (m) => m.slug === accessRequest?.approvalMethodSlug,\n )\n\n const handleCopyPrompt = useCallback(() => {\n if (accessRequest?.requestPrompt) {\n copyToClipboard(accessRequest.requestPrompt, 'prompt')\n }\n }, [accessRequest?.requestPrompt, copyToClipboard])\n\n // Early return if no access request\n if (!accessRequest) return null\n\n return (\n <div className=\"mt-6 space-y-4\">\n <h3 className=\"text-sm font-medium\">Access Request</h3>\n\n {/* Approval Method */}\n {approvalMethod && approvalMethod.type !== 'custom' && (\n <div className=\"flex items-center gap-2\">\n {approvalMethod.type === 'service' && approvalMethod.config.url ? (\n <>\n <a\n href={approvalMethod.config.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-2 hover:text-primary transition-colors\"\n title={approvalMethod.config.url}\n >\n <div>{getApprovalMethodIcon(approvalMethod.type)}</div>\n <div className=\"font-medium\">{approvalMethod.displayName}</div>\n </a>\n <a\n href={approvalMethod.config.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-muted-foreground hover:text-primary\"\n title={approvalMethod.config.url}\n >\n <ExternalLink className=\"size-4\" />\n </a>\n </>\n ) : (\n <>\n <div>{getApprovalMethodIcon(approvalMethod.type)}</div>\n <div className=\"font-medium\">{approvalMethod.displayName}</div>\n </>\n )}\n </div>\n )}\n\n {/* Request Prompt - Inline */}\n {accessRequest.requestPrompt && (\n <div className=\"text-sm inline-flex items-center gap-2\">\n <span className=\"whitespace-nowrap shrink-0\">Request Prompt:</span>\n <span className=\"prose prose-sm inline [&>*]:inline [&>*]:m-0\">\n <ReactMarkdown components={{ a: MarkdownLink }}>\n {accessRequest.requestPrompt}\n </ReactMarkdown>\n </span>\n <CopyButton\n onCopy={handleCopyPrompt}\n isCopied={copiedId === 'prompt'}\n ariaLabel=\"Copy request prompt\"\n className=\"h-6 w-6 p-0 shrink-0\"\n />\n </div>\n )}\n\n {/* Comments */}\n {accessRequest.comments && (\n <div className=\"text-sm text-muted-foreground prose prose-sm max-w-none\">\n <ReactMarkdown components={{ a: MarkdownLink }}>\n {accessRequest.comments}\n </ReactMarkdown>\n </div>\n )}\n\n {/* Roles Table */}\n {accessRequest.roles && accessRequest.roles.length > 0 && (\n <div>\n <h4 className=\"mb-2 text-sm font-medium\">Available Roles</h4>\n <div className=\"rounded-lg border\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead className=\"whitespace-nowrap\">Role</TableHead>\n <TableHead>Description</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {accessRequest.roles.map((role, idx) => (\n <TableRow key={`${role.displayName}-${idx}`}>\n <TableCell className=\"font-medium whitespace-nowrap\">\n {role.displayName}\n </TableCell>\n <TableCell className=\"text-sm text-muted-foreground\">\n {role.description || '—'}\n {role.adminNotes && (\n <div className=\"mt-1 text-xs italic text-muted-foreground/80\">\n Note: {role.adminNotes}\n </div>\n )}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n </div>\n )}\n\n {/* Approvers */}\n {accessRequest.approverPersonSlugs &&\n accessRequest.approverPersonSlugs.length > 0 && (\n <div>\n <h4 className=\"mb-2 text-sm font-medium\">Approvers</h4>\n <div className=\"flex flex-wrap gap-2\">\n {accessRequest.approverPersonSlugs.map((slug) => (\n <PersonBadge key={slug} slug={slug} />\n ))}\n </div>\n </div>\n )}\n\n {/* Documentation URLs */}\n {accessRequest.urls && accessRequest.urls.length > 0 && (\n <div>\n <h4 className=\"mb-2 text-sm font-medium\">Documentation</h4>\n <div className=\"flex flex-col gap-1.5\">\n {accessRequest.urls.map((urlObj, idx) => (\n <a\n key={`${urlObj.url}-${idx}`}\n href={urlObj.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-sm text-muted-foreground hover:text-primary inline-flex items-center gap-1.5\"\n >\n {urlObj.label || urlObj.url}\n <ExternalLink className=\"size-3\" />\n </a>\n ))}\n </div>\n </div>\n )}\n\n {/* Post-Approval Instructions - Collapsible (secondary info) */}\n {accessRequest.postApprovalInstructions && (\n <Accordion type=\"single\" collapsible>\n <AccordionItem\n value=\"post-approval\"\n className=\"border rounded-lg px-4\"\n >\n <AccordionTrigger className=\"text-sm hover:no-underline py-3\">\n Post-Approval Instructions\n </AccordionTrigger>\n <AccordionContent className=\"pb-3\">\n <div className=\"text-sm text-muted-foreground prose prose-sm max-w-none\">\n <ReactMarkdown components={{ a: MarkdownLink }}>\n {accessRequest.postApprovalInstructions}\n </ReactMarkdown>\n </div>\n </AccordionContent>\n </AccordionItem>\n </Accordion>\n )}\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AccessRequestSection.js","sources":["../../../../../../src/modules/appCatalog/ui/components/AccessRequestSection.tsx"],"sourcesContent":["import type {\n AppApprovalMethod,\n Resource,\n} from '@igstack/app-catalog-backend-core'\nimport { Bot, Check, Copy, ExternalLink, Settings, Users } from 'lucide-react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport ReactMarkdown from 'react-markdown'\nimport { Button } from '~/ui/button'\nimport {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from '~/ui/accordion'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { PersonBadge } from './PersonBadge'\n\n// Constants\nconst COPY_FEEDBACK_DURATION = 2000\n\ninterface AccessRequestSectionProps {\n app: Resource\n approvalMethods: AppApprovalMethod[]\n}\n\n// Component for rendering markdown links with security attributes\nconst MarkdownLink = ({\n href,\n children,\n}: {\n href?: string\n children?: React.ReactNode\n}) => (\n <a\n href={href}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-primary hover:underline\"\n >\n {children}\n </a>\n)\n\n// Helper function for approval method icons\nfunction getApprovalMethodIcon(\n type: 'service' | 'personTeam' | 'custom' | 'noAccessRequired' | 'unknown',\n) {\n switch (type) {\n case 'service':\n return <Bot className=\"size-5 text-primary\" />\n case 'personTeam':\n return <Users className=\"size-5 text-primary\" />\n case 'custom':\n case 'noAccessRequired':\n case 'unknown':\n return <Settings className=\"size-5 text-primary\" />\n }\n}\n\n/**\n * Custom hook for handling copy-to-clipboard functionality with feedback\n * Includes proper cleanup to prevent memory leaks\n */\nfunction useCopyToClipboard() {\n const [copiedId, setCopiedId] = useState<string | null>(null)\n const [error, setError] = useState<string | null>(null)\n const timeoutRef = useRef<NodeJS.Timeout | null>(null)\n\n // Cleanup timeout on unmount\n useEffect(() => {\n return () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current)\n }\n }\n }, [])\n\n const copyToClipboard = useCallback(async (text: string, id: string) => {\n // Clear existing timeout\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current)\n }\n\n try {\n await navigator.clipboard.writeText(text)\n setCopiedId(id)\n setError(null)\n\n // Set new timeout with cleanup\n timeoutRef.current = setTimeout(() => {\n setCopiedId(null)\n }, COPY_FEEDBACK_DURATION)\n } catch (err) {\n console.error('Failed to copy to clipboard:', err)\n setError('Failed to copy')\n\n // Clear error after duration\n timeoutRef.current = setTimeout(() => {\n setError(null)\n }, COPY_FEEDBACK_DURATION)\n }\n }, [])\n\n return { copiedId, error, copyToClipboard }\n}\n\n/**\n * Reusable copy button component with accessibility\n */\ninterface CopyButtonProps {\n onCopy: () => void\n isCopied: boolean\n ariaLabel: string\n className?: string\n}\n\nfunction CopyButton({\n onCopy,\n isCopied,\n ariaLabel,\n className,\n}: CopyButtonProps) {\n return (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={onCopy}\n className={className}\n aria-label={ariaLabel}\n title={isCopied ? 'Copied!' : 'Copy to clipboard'}\n >\n {isCopied ? (\n <>\n <Check className=\"h-3.5 w-3.5 text-green-600\" />\n <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n Copied to clipboard\n </span>\n </>\n ) : (\n <Copy className=\"h-3.5 w-3.5\" />\n )}\n </Button>\n )\n}\n\nexport function AccessRequestSection({\n app,\n approvalMethods,\n}: AccessRequestSectionProps) {\n const { copiedId, copyToClipboard } = useCopyToClipboard()\n const accessRequest = app.accessRequest\n const approvalMethod = approvalMethods.find(\n (m) => m.slug === accessRequest?.approvalMethodSlug,\n )\n\n const handleCopyPrompt = useCallback(() => {\n if (accessRequest?.requestPrompt) {\n copyToClipboard(accessRequest.requestPrompt, 'prompt')\n }\n }, [accessRequest?.requestPrompt, copyToClipboard])\n\n // Early return if no access request\n if (!accessRequest) return null\n\n return (\n <div className=\"mt-6 space-y-4\">\n <h3 className=\"text-sm font-medium\">Access Request</h3>\n\n {/* Approval Method */}\n {approvalMethod && approvalMethod.type !== 'custom' && (\n <div className=\"flex items-center gap-2\">\n {approvalMethod.type === 'service' && approvalMethod.config.url ? (\n <>\n <a\n href={approvalMethod.config.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-2 hover:text-primary transition-colors\"\n title={approvalMethod.config.url}\n >\n <div>{getApprovalMethodIcon(approvalMethod.type)}</div>\n <div className=\"font-medium\">{approvalMethod.displayName}</div>\n </a>\n <a\n href={approvalMethod.config.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-muted-foreground hover:text-primary\"\n title={approvalMethod.config.url}\n >\n <ExternalLink className=\"size-4\" />\n </a>\n </>\n ) : (\n <>\n <div>{getApprovalMethodIcon(approvalMethod.type)}</div>\n <div className=\"font-medium\">{approvalMethod.displayName}</div>\n </>\n )}\n </div>\n )}\n\n {/* Request Prompt - Inline */}\n {accessRequest.requestPrompt && (\n <div className=\"text-sm inline-flex items-center gap-2\">\n <span className=\"whitespace-nowrap shrink-0\">Request Prompt:</span>\n <span className=\"prose prose-sm inline [&>*]:inline [&>*]:m-0\">\n <ReactMarkdown components={{ a: MarkdownLink }}>\n {accessRequest.requestPrompt}\n </ReactMarkdown>\n </span>\n <CopyButton\n onCopy={handleCopyPrompt}\n isCopied={copiedId === 'prompt'}\n ariaLabel=\"Copy request prompt\"\n className=\"h-6 w-6 p-0 shrink-0\"\n />\n </div>\n )}\n\n {/* Comments */}\n {accessRequest.comments && (\n <div className=\"text-sm text-muted-foreground prose prose-sm max-w-none\">\n <ReactMarkdown components={{ a: MarkdownLink }}>\n {accessRequest.comments}\n </ReactMarkdown>\n </div>\n )}\n\n {/* Roles Table */}\n {accessRequest.roles && accessRequest.roles.length > 0 && (\n <div>\n <h4 className=\"mb-2 text-sm font-medium\">Available Roles</h4>\n <div className=\"rounded-lg border\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead className=\"whitespace-nowrap\">Role</TableHead>\n <TableHead>Description</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {accessRequest.roles.map((role, idx) => (\n <TableRow key={`${role.displayName}-${idx}`}>\n <TableCell className=\"font-medium whitespace-nowrap\">\n {role.displayName}\n </TableCell>\n <TableCell className=\"text-sm text-muted-foreground\">\n {role.description || '—'}\n {role.adminNotes && (\n <div className=\"mt-1 text-xs italic text-muted-foreground/80\">\n Note: {role.adminNotes}\n </div>\n )}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n </div>\n )}\n\n {/* Approvers */}\n {accessRequest.approverPersonSlugs &&\n accessRequest.approverPersonSlugs.length > 0 && (\n <div>\n <h4 className=\"mb-2 text-sm font-medium\">Approvers</h4>\n <div className=\"flex flex-wrap gap-2\">\n {accessRequest.approverPersonSlugs.map((slug) => (\n <PersonBadge key={slug} slug={slug} />\n ))}\n </div>\n </div>\n )}\n\n {/* Documentation URLs */}\n {accessRequest.urls && accessRequest.urls.length > 0 && (\n <div>\n <h4 className=\"mb-2 text-sm font-medium\">Documentation</h4>\n <div className=\"flex flex-col gap-1.5\">\n {accessRequest.urls.map((urlObj, idx) => (\n <a\n key={`${urlObj.url}-${idx}`}\n href={urlObj.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-sm text-muted-foreground hover:text-primary inline-flex items-center gap-1.5\"\n >\n {urlObj.label || urlObj.url}\n <ExternalLink className=\"size-3\" />\n </a>\n ))}\n </div>\n </div>\n )}\n\n {/* Post-Approval Instructions - Collapsible (secondary info) */}\n {accessRequest.postApprovalInstructions && (\n <Accordion type=\"single\" collapsible>\n <AccordionItem\n value=\"post-approval\"\n className=\"border rounded-lg px-4\"\n >\n <AccordionTrigger className=\"text-sm hover:no-underline py-3\">\n Post-Approval Instructions\n </AccordionTrigger>\n <AccordionContent className=\"pb-3\">\n <div className=\"text-sm text-muted-foreground prose prose-sm max-w-none\">\n <ReactMarkdown components={{ a: MarkdownLink }}>\n {accessRequest.postApprovalInstructions}\n </ReactMarkdown>\n </div>\n </AccordionContent>\n </AccordionItem>\n </Accordion>\n )}\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -3,6 +3,7 @@ import { User, Check, Copy } from "lucide-react";
|
|
|
3
3
|
import { useState, useRef, useEffect, useCallback } from "react";
|
|
4
4
|
import { Badge } from "../../../../ui/badge.js";
|
|
5
5
|
import { useAppCatalogContext } from "../../context/AppCatalogContext.js";
|
|
6
|
+
import "../context/AppCatalogFiltersContext.js";
|
|
6
7
|
import "@tanstack/react-query-devtools";
|
|
7
8
|
import "next-themes";
|
|
8
9
|
import "@radix-ui/react-dialog";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PersonBadge.js","sources":["../../../../../../src/modules/appCatalog/ui/components/PersonBadge.tsx"],"sourcesContent":["import { Check, Copy, User } from 'lucide-react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { Badge } from '~/ui/badge'\nimport { useAppCatalogContext } from '~/modules/appCatalog'\nimport { getPersonBySlug } from '~/modules/appCatalog/utils/resolveHelpers'\n\ninterface PersonBadgeProps {\n slug: string\n}\n\nexport function PersonBadge({ slug }: PersonBadgeProps) {\n const { persons } = useAppCatalogContext()\n const person = getPersonBySlug(persons, slug)\n\n const displayName = person\n ? `${person.firstName} ${person.lastName}`.trim() || slug\n : slug\n\n const email = person?.email\n\n return (\n <Badge\n variant=\"outline\"\n className=\"font-normal inline-flex items-center gap-1\"\n title={email ? `${displayName} (${email})` : displayName}\n >\n <User className=\"size-3\" />\n {displayName}\n {email && <CopyEmailButton email={email} />}\n </Badge>\n )\n}\n\nfunction CopyEmailButton({ email }: { email: string }) {\n const [copied, setCopied] = useState(false)\n const timeoutRef = useRef<NodeJS.Timeout | null>(null)\n\n useEffect(() => {\n return () => {\n if (timeoutRef.current) clearTimeout(timeoutRef.current)\n }\n }, [])\n\n const handleCopy = useCallback(\n (e: React.MouseEvent) => {\n e.stopPropagation()\n navigator.clipboard.writeText(email)\n setCopied(true)\n if (timeoutRef.current) clearTimeout(timeoutRef.current)\n timeoutRef.current = setTimeout(() => setCopied(false), 2000)\n },\n [email],\n )\n\n return (\n <button\n onClick={handleCopy}\n className=\"ml-0.5 hover:text-primary transition-colors\"\n title={copied ? 'Copied!' : `Copy ${email}`}\n type=\"button\"\n >\n {copied ? (\n <Check className=\"size-3 text-green-600\" />\n ) : (\n <Copy className=\"size-3\" />\n )}\n </button>\n )\n}\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"PersonBadge.js","sources":["../../../../../../src/modules/appCatalog/ui/components/PersonBadge.tsx"],"sourcesContent":["import { Check, Copy, User } from 'lucide-react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { Badge } from '~/ui/badge'\nimport { useAppCatalogContext } from '~/modules/appCatalog'\nimport { getPersonBySlug } from '~/modules/appCatalog/utils/resolveHelpers'\n\ninterface PersonBadgeProps {\n slug: string\n}\n\nexport function PersonBadge({ slug }: PersonBadgeProps) {\n const { persons } = useAppCatalogContext()\n const person = getPersonBySlug(persons, slug)\n\n const displayName = person\n ? `${person.firstName} ${person.lastName}`.trim() || slug\n : slug\n\n const email = person?.email\n\n return (\n <Badge\n variant=\"outline\"\n className=\"font-normal inline-flex items-center gap-1\"\n title={email ? `${displayName} (${email})` : displayName}\n >\n <User className=\"size-3\" />\n {displayName}\n {email && <CopyEmailButton email={email} />}\n </Badge>\n )\n}\n\nfunction CopyEmailButton({ email }: { email: string }) {\n const [copied, setCopied] = useState(false)\n const timeoutRef = useRef<NodeJS.Timeout | null>(null)\n\n useEffect(() => {\n return () => {\n if (timeoutRef.current) clearTimeout(timeoutRef.current)\n }\n }, [])\n\n const handleCopy = useCallback(\n (e: React.MouseEvent) => {\n e.stopPropagation()\n navigator.clipboard.writeText(email)\n setCopied(true)\n if (timeoutRef.current) clearTimeout(timeoutRef.current)\n timeoutRef.current = setTimeout(() => setCopied(false), 2000)\n },\n [email],\n )\n\n return (\n <button\n onClick={handleCopy}\n className=\"ml-0.5 hover:text-primary transition-colors\"\n title={copied ? 'Copied!' : `Copy ${email}`}\n type=\"button\"\n >\n {copied ? (\n <Check className=\"size-3 text-green-600\" />\n ) : (\n <Copy className=\"size-3\" />\n )}\n </button>\n )\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAUO,SAAS,YAAY,EAAE,QAA0B;AACtD,QAAM,EAAE,QAAA,IAAY,qBAAA;AACpB,QAAM,SAAS,gBAAgB,SAAS,IAAI;AAE5C,QAAM,cAAc,SAChB,GAAG,OAAO,SAAS,IAAI,OAAO,QAAQ,GAAG,KAAA,KAAU,OACnD;AAEJ,QAAM,QAAQ,iCAAQ;AAEtB,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,SAAQ;AAAA,MACR,WAAU;AAAA,MACV,OAAO,QAAQ,GAAG,WAAW,KAAK,KAAK,MAAM;AAAA,MAE7C,UAAA;AAAA,QAAA,oBAAC,MAAA,EAAK,WAAU,SAAA,CAAS;AAAA,QACxB;AAAA,QACA,SAAS,oBAAC,iBAAA,EAAgB,MAAA,CAAc;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAG/C;AAEA,SAAS,gBAAgB,EAAE,SAA4B;AACrD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,aAAa,OAA8B,IAAI;AAErD,YAAU,MAAM;AACd,WAAO,MAAM;AACX,UAAI,WAAW,QAAS,cAAa,WAAW,OAAO;AAAA,IACzD;AAAA,EACF,GAAG,CAAA,CAAE;AAEL,QAAM,aAAa;AAAA,IACjB,CAAC,MAAwB;AACvB,QAAE,gBAAA;AACF,gBAAU,UAAU,UAAU,KAAK;AACnC,gBAAU,IAAI;AACd,UAAI,WAAW,QAAS,cAAa,WAAW,OAAO;AACvD,iBAAW,UAAU,WAAW,MAAM,UAAU,KAAK,GAAG,GAAI;AAAA,IAC9D;AAAA,IACA,CAAC,KAAK;AAAA,EAAA;AAGR,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,SAAS;AAAA,MACT,WAAU;AAAA,MACV,OAAO,SAAS,YAAY,QAAQ,KAAK;AAAA,MACzC,MAAK;AAAA,MAEJ,UAAA,6BACE,OAAA,EAAM,WAAU,yBAAwB,IAEzC,oBAAC,MAAA,EAAK,WAAU,SAAA,CAAS;AAAA,IAAA;AAAA,EAAA;AAIjC;"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Resource } from '@igstack/app-catalog-backend-core';
|
|
2
2
|
export interface ScreenshotGalleryProps {
|
|
3
|
-
app:
|
|
3
|
+
app: Resource;
|
|
4
4
|
screenshotIds: string[];
|
|
5
5
|
initialIndex?: number;
|
|
6
6
|
open: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ScreenshotGallery.js","sources":["../../../../../../src/modules/appCatalog/ui/components/ScreenshotGallery.tsx"],"sourcesContent":["import { useMemo, useRef } from 'react'\n\nimport type {
|
|
1
|
+
{"version":3,"file":"ScreenshotGallery.js","sources":["../../../../../../src/modules/appCatalog/ui/components/ScreenshotGallery.tsx"],"sourcesContent":["import { useMemo, useRef } from 'react'\n\nimport type { Resource } from '@igstack/app-catalog-backend-core'\n\nimport { Gallery } from '~/modules/gallery/Gallery'\nimport type { GalleryImage } from '~/modules/gallery/Gallery'\nimport { Dialog, DialogContent, DialogTitle } from '~/ui/dialog'\nimport { VisuallyHidden } from '~/ui/visually-hidden'\n\nexport interface ScreenshotGalleryProps {\n app: Resource\n screenshotIds: string[]\n initialIndex?: number\n open: boolean\n onOpenChange: (open: boolean) => void\n title?: string\n}\n\nexport function ScreenshotGallery({\n app,\n screenshotIds,\n initialIndex = 0,\n open,\n onOpenChange,\n title,\n}: ScreenshotGalleryProps) {\n // Track whether Gallery is in fullscreen — if so, block Radix from closing on Escape\n const isFullscreenRef = useRef(false)\n\n // Transform screenshot IDs to full URLs\n const images: GalleryImage[] = useMemo(\n () =>\n screenshotIds.map((id) => ({\n url: `/api/screenshots/${id}`,\n alt: `${app.abbreviation || app.displayName} screenshot`,\n })),\n [screenshotIds, app.abbreviation, app.displayName],\n )\n\n // Don't render if no screenshots\n if (screenshotIds.length === 0) {\n return null\n }\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n aria-describedby={undefined}\n className=\"h-[85vh] w-full max-w-[calc(100vw-2rem)] sm:max-w-[calc(100vw-3rem)] md:max-w-[calc(100vw-4rem)] p-0 overflow-hidden\"\n showCloseButton={true}\n onEscapeKeyDown={(e) => {\n // If Gallery is in fullscreen, its capture listener already handled Escape.\n // Prevent Radix from also closing the dialog on the same event.\n if (isFullscreenRef.current) {\n e.preventDefault()\n }\n }}\n >\n <VisuallyHidden>\n <DialogTitle>\n {title || `${app.abbreviation || app.displayName} screenshots`}\n </DialogTitle>\n </VisuallyHidden>\n <Gallery\n images={images}\n initialIndex={initialIndex}\n title={title}\n onFullscreenChange={(fs) => {\n isFullscreenRef.current = fs\n }}\n />\n </DialogContent>\n </Dialog>\n )\n}\n"],"names":[],"mappings":";;;;;AAkBO,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AAAA,EACA,eAAe;AAAA,EACf;AAAA,EACA;AAAA,EACA;AACF,GAA2B;AAEzB,QAAM,kBAAkB,OAAO,KAAK;AAGpC,QAAM,SAAyB;AAAA,IAC7B,MACE,cAAc,IAAI,CAAC,QAAQ;AAAA,MACzB,KAAK,oBAAoB,EAAE;AAAA,MAC3B,KAAK,GAAG,IAAI,gBAAgB,IAAI,WAAW;AAAA,IAAA,EAC3C;AAAA,IACJ,CAAC,eAAe,IAAI,cAAc,IAAI,WAAW;AAAA,EAAA;AAInD,MAAI,cAAc,WAAW,GAAG;AAC9B,WAAO;AAAA,EACT;AAEA,SACE,oBAAC,QAAA,EAAO,MAAY,cAClB,UAAA;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,oBAAkB;AAAA,MAClB,WAAU;AAAA,MACV,iBAAiB;AAAA,MACjB,iBAAiB,CAAC,MAAM;AAGtB,YAAI,gBAAgB,SAAS;AAC3B,YAAE,eAAA;AAAA,QACJ;AAAA,MACF;AAAA,MAEA,UAAA;AAAA,QAAA,oBAAC,gBAAA,EACC,UAAA,oBAAC,aAAA,EACE,UAAA,SAAS,GAAG,IAAI,gBAAgB,IAAI,WAAW,eAAA,CAClD,GACF;AAAA,QACA;AAAA,UAAC;AAAA,UAAA;AAAA,YACC;AAAA,YACA;AAAA,YACA;AAAA,YACA,oBAAoB,CAAC,OAAO;AAC1B,8BAAgB,UAAU;AAAA,YAC5B;AAAA,UAAA;AAAA,QAAA;AAAA,MACF;AAAA,IAAA;AAAA,EAAA,GAEJ;AAEJ;"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Resource } from '@igstack/app-catalog-backend-core';
|
|
2
2
|
interface SubResourcesSectionProps {
|
|
3
|
-
subResources:
|
|
3
|
+
subResources: Resource[];
|
|
4
4
|
}
|
|
5
5
|
export declare function SubResourcesSection({ subResources, }: SubResourcesSectionProps): import("react/jsx-runtime").JSX.Element | null;
|
|
6
6
|
export {};
|
|
@@ -7,6 +7,7 @@ import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from ".
|
|
|
7
7
|
import { PersonBadge } from "./PersonBadge.js";
|
|
8
8
|
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "../../../../ui/select.js";
|
|
9
9
|
import { useAppCatalogContext } from "../../context/AppCatalogContext.js";
|
|
10
|
+
import "../context/AppCatalogFiltersContext.js";
|
|
10
11
|
import "@tanstack/react-query-devtools";
|
|
11
12
|
import "next-themes";
|
|
12
13
|
import "@radix-ui/react-dialog";
|
|
@@ -49,21 +50,21 @@ function SubResourcesSection({
|
|
|
49
50
|
const uniqueTiers = useMemo(() => {
|
|
50
51
|
const tiers = /* @__PURE__ */ new Set();
|
|
51
52
|
for (const sr of subResources) {
|
|
52
|
-
if (sr.
|
|
53
|
+
if (sr.tier) tiers.add(sr.tier);
|
|
53
54
|
}
|
|
54
55
|
return [...tiers].sort();
|
|
55
56
|
}, [subResources]);
|
|
56
57
|
const filtered = useMemo(() => {
|
|
57
58
|
let result = subResources;
|
|
58
59
|
if (tierFilter !== "all") {
|
|
59
|
-
result = result.filter((sr) => sr.
|
|
60
|
+
result = result.filter((sr) => sr.tier === tierFilter);
|
|
60
61
|
}
|
|
61
62
|
if (search.trim()) {
|
|
62
63
|
const q = search.trim().toLowerCase();
|
|
63
64
|
result = result.filter(
|
|
64
65
|
(sr) => {
|
|
65
66
|
var _a;
|
|
66
|
-
return sr.displayName.toLowerCase().includes(q) || sr.aliases.some((a) => a.toLowerCase().includes(q)) || (((_a = sr.description) == null ? void 0 : _a.toLowerCase().includes(q)) ?? false);
|
|
67
|
+
return sr.displayName.toLowerCase().includes(q) || (sr.aliases ?? []).some((a) => a.toLowerCase().includes(q)) || (((_a = sr.description) == null ? void 0 : _a.toLowerCase().includes(q)) ?? false);
|
|
67
68
|
}
|
|
68
69
|
);
|
|
69
70
|
}
|
|
@@ -114,25 +115,23 @@ function SubResourcesSection({
|
|
|
114
115
|
children: "No resources match your filters"
|
|
115
116
|
}
|
|
116
117
|
) }) : filtered.map((sr) => {
|
|
117
|
-
const maintainerMembers = sr.accessMaintainerGroupSlugs.flatMap(
|
|
118
|
-
(groupSlug)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
);
|
|
118
|
+
const maintainerMembers = (sr.accessMaintainerGroupSlugs ?? []).flatMap((groupSlug) => {
|
|
119
|
+
const group = getGroupBySlug(groups, groupSlug);
|
|
120
|
+
return (group == null ? void 0 : group.memberSlugs) ?? [];
|
|
121
|
+
});
|
|
123
122
|
const uniqueMaintainers = [...new Set(maintainerMembers)];
|
|
124
123
|
return /* @__PURE__ */ jsxs(TableRow, { children: [
|
|
125
124
|
/* @__PURE__ */ jsxs(TableCell, { children: [
|
|
126
125
|
/* @__PURE__ */ jsx("div", { className: "font-medium text-sm", children: sr.displayName }),
|
|
127
|
-
sr.aliases.length > 0 && /* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground mt-0.5", children: sr.aliases.join(", ") }),
|
|
126
|
+
(sr.aliases ?? []).length > 0 && /* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground mt-0.5", children: (sr.aliases ?? []).join(", ") }),
|
|
128
127
|
sr.description && /* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground mt-0.5", children: sr.description })
|
|
129
128
|
] }),
|
|
130
|
-
/* @__PURE__ */ jsx(TableCell, { children: sr.
|
|
129
|
+
/* @__PURE__ */ jsx(TableCell, { children: sr.tier && /* @__PURE__ */ jsx(
|
|
131
130
|
Badge,
|
|
132
131
|
{
|
|
133
|
-
variant: getTierBadgeVariant(sr.
|
|
134
|
-
className: `text-xs ${getTierBadgeClassName(sr.
|
|
135
|
-
children: getTierDisplayLabel(sr.
|
|
132
|
+
variant: getTierBadgeVariant(sr.tier),
|
|
133
|
+
className: `text-xs ${getTierBadgeClassName(sr.tier)}`,
|
|
134
|
+
children: getTierDisplayLabel(sr.tier)
|
|
136
135
|
}
|
|
137
136
|
) }),
|
|
138
137
|
/* @__PURE__ */ jsx(TableCell, { children: sr.ownerPersonSlug && /* @__PURE__ */ jsx(PersonBadge, { slug: sr.ownerPersonSlug }) }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SubResourcesSection.js","sources":["../../../../../../src/modules/appCatalog/ui/components/SubResourcesSection.tsx"],"sourcesContent":["import type { SubResource } from '@igstack/app-catalog-backend-core'\nimport { Search } from 'lucide-react'\nimport { useMemo, useState } from 'react'\nimport { Badge } from '~/ui/badge'\nimport { Input } from '~/ui/input'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { PersonBadge } from './PersonBadge'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '~/ui/select'\nimport { useAppCatalogContext } from '~/modules/appCatalog'\nimport { getGroupBySlug } from '~/modules/appCatalog/utils/resolveHelpers'\n\ninterface SubResourcesSectionProps {\n subResources: SubResource[]\n}\n\nfunction getTierBadgeVariant(\n tierSlug: string,\n): 'default' | 'secondary' | 'destructive' | 'outline' {\n if (tierSlug === 'prod' || tierSlug === 'production') return 'destructive'\n if (tierSlug === 'dev' || tierSlug === 'staging') return 'secondary'\n if (tierSlug === 'preprod') return 'outline'\n if (tierSlug === 'sandbox') return 'outline'\n return 'outline'\n}\n\nfunction getTierBadgeClassName(tierSlug: string): string {\n if (tierSlug === 'preprod')\n return 'border-amber-400 bg-amber-100 text-amber-800 hover:bg-amber-200'\n if (tierSlug === 'sandbox')\n return 'border-gray-400 bg-gray-100 text-gray-700 hover:bg-gray-200'\n return ''\n}\n\nfunction getTierDisplayLabel(tierSlug: string): string {\n if (tierSlug === 'preprod') return 'Pre-Prod'\n if (tierSlug === 'sandbox') return 'Sandbox'\n if (tierSlug === 'prod' || tierSlug === 'production') return 'Prod'\n if (tierSlug === 'dev') return 'Dev'\n if (tierSlug === 'staging') return 'Staging'\n return tierSlug\n}\n\nexport function SubResourcesSection({\n subResources,\n}: SubResourcesSectionProps) {\n const { groups } = useAppCatalogContext()\n const [search, setSearch] = useState('')\n const [tierFilter, setTierFilter] = useState<string>('all')\n\n const uniqueTiers = useMemo(() => {\n const tiers = new Set<string>()\n for (const sr of subResources) {\n if (sr.tierSlug) tiers.add(sr.tierSlug)\n }\n return [...tiers].sort()\n }, [subResources])\n\n const filtered = useMemo(() => {\n let result = subResources\n\n if (tierFilter !== 'all') {\n result = result.filter((sr) => sr.tierSlug === tierFilter)\n }\n\n if (search.trim()) {\n const q = search.trim().toLowerCase()\n result = result.filter(\n (sr) =>\n sr.displayName.toLowerCase().includes(q) ||\n sr.aliases.some((a) => a.toLowerCase().includes(q)) ||\n (sr.description?.toLowerCase().includes(q) ?? false),\n )\n }\n\n return result\n }, [subResources, search, tierFilter])\n\n if (subResources.length === 0) return null\n\n return (\n <div className=\"space-y-3\">\n <div className=\"flex items-center justify-between\">\n <div className=\"text-sm font-medium\">\n Sub-Resources ({filtered.length} of {subResources.length})\n </div>\n </div>\n\n {/* Filters */}\n <div className=\"flex gap-2\">\n <div className=\"relative flex-1\">\n <Search className=\"absolute left-2.5 top-2.5 size-4 text-muted-foreground\" />\n <Input\n placeholder=\"Search resources by name or alias...\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n className=\"pl-9 h-9\"\n />\n </div>\n {uniqueTiers.length > 1 && (\n <Select value={tierFilter} onValueChange={setTierFilter}>\n <SelectTrigger className=\"w-[130px] h-9\">\n <SelectValue placeholder=\"All tiers\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">All tiers</SelectItem>\n {uniqueTiers.map((tier) => (\n <SelectItem key={tier} value={tier}>\n {tier}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )}\n </div>\n\n {/* Table */}\n <div className=\"rounded-lg border max-h-[400px] overflow-auto\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>Name</TableHead>\n <TableHead className=\"w-[80px]\">Tier</TableHead>\n <TableHead>Owner</TableHead>\n <TableHead>Access Contacts</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {filtered.length === 0 ? (\n <TableRow>\n <TableCell\n colSpan={4}\n className=\"text-center text-muted-foreground py-8\"\n >\n No resources match your filters\n </TableCell>\n </TableRow>\n ) : (\n filtered.map((sr) => {\n // Resolve maintainer group members\n const maintainerMembers = sr.accessMaintainerGroupSlugs.flatMap(\n (groupSlug) => {\n const group = getGroupBySlug(groups, groupSlug)\n return group?.memberSlugs ?? []\n },\n )\n // Deduplicate\n const uniqueMaintainers = [...new Set(maintainerMembers)]\n\n return (\n <TableRow key={sr.slug}>\n <TableCell>\n <div className=\"font-medium text-sm\">\n {sr.displayName}\n </div>\n {sr.aliases.length > 0 && (\n <div className=\"text-xs text-muted-foreground mt-0.5\">\n {sr.aliases.join(', ')}\n </div>\n )}\n {sr.description && (\n <div className=\"text-xs text-muted-foreground mt-0.5\">\n {sr.description}\n </div>\n )}\n </TableCell>\n <TableCell>\n {sr.tierSlug && (\n <Badge\n variant={getTierBadgeVariant(sr.tierSlug)}\n className={`text-xs ${getTierBadgeClassName(sr.tierSlug)}`}\n >\n {getTierDisplayLabel(sr.tierSlug)}\n </Badge>\n )}\n </TableCell>\n <TableCell>\n {sr.ownerPersonSlug && (\n <PersonBadge slug={sr.ownerPersonSlug} />\n )}\n </TableCell>\n <TableCell>\n {uniqueMaintainers.length > 0 ? (\n <div className=\"flex flex-wrap gap-1\">\n {uniqueMaintainers.map((personSlug) => (\n <PersonBadge key={personSlug} slug={personSlug} />\n ))}\n </div>\n ) : (\n <span className=\"text-muted-foreground\">-</span>\n )}\n </TableCell>\n </TableRow>\n )\n })\n )}\n </TableBody>\n </Table>\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SubResourcesSection.js","sources":["../../../../../../src/modules/appCatalog/ui/components/SubResourcesSection.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { Search } from 'lucide-react'\nimport { useMemo, useState } from 'react'\nimport { Badge } from '~/ui/badge'\nimport { Input } from '~/ui/input'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { PersonBadge } from './PersonBadge'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '~/ui/select'\nimport { useAppCatalogContext } from '~/modules/appCatalog'\nimport { getGroupBySlug } from '~/modules/appCatalog/utils/resolveHelpers'\n\ninterface SubResourcesSectionProps {\n subResources: Resource[]\n}\n\nfunction getTierBadgeVariant(\n tierSlug: string,\n): 'default' | 'secondary' | 'destructive' | 'outline' {\n if (tierSlug === 'prod' || tierSlug === 'production') return 'destructive'\n if (tierSlug === 'dev' || tierSlug === 'staging') return 'secondary'\n if (tierSlug === 'preprod') return 'outline'\n if (tierSlug === 'sandbox') return 'outline'\n return 'outline'\n}\n\nfunction getTierBadgeClassName(tierSlug: string): string {\n if (tierSlug === 'preprod')\n return 'border-amber-400 bg-amber-100 text-amber-800 hover:bg-amber-200'\n if (tierSlug === 'sandbox')\n return 'border-gray-400 bg-gray-100 text-gray-700 hover:bg-gray-200'\n return ''\n}\n\nfunction getTierDisplayLabel(tierSlug: string): string {\n if (tierSlug === 'preprod') return 'Pre-Prod'\n if (tierSlug === 'sandbox') return 'Sandbox'\n if (tierSlug === 'prod' || tierSlug === 'production') return 'Prod'\n if (tierSlug === 'dev') return 'Dev'\n if (tierSlug === 'staging') return 'Staging'\n return tierSlug\n}\n\nexport function SubResourcesSection({\n subResources,\n}: SubResourcesSectionProps) {\n const { groups } = useAppCatalogContext()\n const [search, setSearch] = useState('')\n const [tierFilter, setTierFilter] = useState<string>('all')\n\n const uniqueTiers = useMemo(() => {\n const tiers = new Set<string>()\n for (const sr of subResources) {\n if (sr.tier) tiers.add(sr.tier)\n }\n return [...tiers].sort()\n }, [subResources])\n\n const filtered = useMemo(() => {\n let result = subResources\n\n if (tierFilter !== 'all') {\n result = result.filter((sr) => sr.tier === tierFilter)\n }\n\n if (search.trim()) {\n const q = search.trim().toLowerCase()\n result = result.filter(\n (sr) =>\n sr.displayName.toLowerCase().includes(q) ||\n (sr.aliases ?? []).some((a) => a.toLowerCase().includes(q)) ||\n (sr.description?.toLowerCase().includes(q) ?? false),\n )\n }\n\n return result\n }, [subResources, search, tierFilter])\n\n if (subResources.length === 0) return null\n\n return (\n <div className=\"space-y-3\">\n <div className=\"flex items-center justify-between\">\n <div className=\"text-sm font-medium\">\n Sub-Resources ({filtered.length} of {subResources.length})\n </div>\n </div>\n\n {/* Filters */}\n <div className=\"flex gap-2\">\n <div className=\"relative flex-1\">\n <Search className=\"absolute left-2.5 top-2.5 size-4 text-muted-foreground\" />\n <Input\n placeholder=\"Search resources by name or alias...\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n className=\"pl-9 h-9\"\n />\n </div>\n {uniqueTiers.length > 1 && (\n <Select value={tierFilter} onValueChange={setTierFilter}>\n <SelectTrigger className=\"w-[130px] h-9\">\n <SelectValue placeholder=\"All tiers\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">All tiers</SelectItem>\n {uniqueTiers.map((tier) => (\n <SelectItem key={tier} value={tier}>\n {tier}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )}\n </div>\n\n {/* Table */}\n <div className=\"rounded-lg border max-h-[400px] overflow-auto\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>Name</TableHead>\n <TableHead className=\"w-[80px]\">Tier</TableHead>\n <TableHead>Owner</TableHead>\n <TableHead>Access Contacts</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {filtered.length === 0 ? (\n <TableRow>\n <TableCell\n colSpan={4}\n className=\"text-center text-muted-foreground py-8\"\n >\n No resources match your filters\n </TableCell>\n </TableRow>\n ) : (\n filtered.map((sr) => {\n // Resolve maintainer group members\n const maintainerMembers = (\n sr.accessMaintainerGroupSlugs ?? []\n ).flatMap((groupSlug) => {\n const group = getGroupBySlug(groups, groupSlug)\n return group?.memberSlugs ?? []\n })\n // Deduplicate\n const uniqueMaintainers = [...new Set(maintainerMembers)]\n\n return (\n <TableRow key={sr.slug}>\n <TableCell>\n <div className=\"font-medium text-sm\">\n {sr.displayName}\n </div>\n {(sr.aliases ?? []).length > 0 && (\n <div className=\"text-xs text-muted-foreground mt-0.5\">\n {(sr.aliases ?? []).join(', ')}\n </div>\n )}\n {sr.description && (\n <div className=\"text-xs text-muted-foreground mt-0.5\">\n {sr.description}\n </div>\n )}\n </TableCell>\n <TableCell>\n {sr.tier && (\n <Badge\n variant={getTierBadgeVariant(sr.tier)}\n className={`text-xs ${getTierBadgeClassName(sr.tier)}`}\n >\n {getTierDisplayLabel(sr.tier)}\n </Badge>\n )}\n </TableCell>\n <TableCell>\n {sr.ownerPersonSlug && (\n <PersonBadge slug={sr.ownerPersonSlug} />\n )}\n </TableCell>\n <TableCell>\n {uniqueMaintainers.length > 0 ? (\n <div className=\"flex flex-wrap gap-1\">\n {uniqueMaintainers.map((personSlug) => (\n <PersonBadge key={personSlug} slug={personSlug} />\n ))}\n </div>\n ) : (\n <span className=\"text-muted-foreground\">-</span>\n )}\n </TableCell>\n </TableRow>\n )\n })\n )}\n </TableBody>\n </Table>\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TierVariant } from '@igstack/app-catalog-backend-core';
|
|
2
2
|
interface TierVariantsSectionProps {
|
|
3
|
-
tiers:
|
|
3
|
+
tiers: TierVariant[];
|
|
4
4
|
}
|
|
5
5
|
export declare function TierVariantsSection({ tiers }: TierVariantsSectionProps): import("react/jsx-runtime").JSX.Element | null;
|
|
6
6
|
export {};
|
|
@@ -5,6 +5,7 @@ import ReactMarkdown from "react-markdown";
|
|
|
5
5
|
import { Badge } from "../../../../ui/badge.js";
|
|
6
6
|
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "../../../../ui/table.js";
|
|
7
7
|
import { useAppCatalogContext } from "../../context/AppCatalogContext.js";
|
|
8
|
+
import "../context/AppCatalogFiltersContext.js";
|
|
8
9
|
import "@tanstack/react-query-devtools";
|
|
9
10
|
import "next-themes";
|
|
10
11
|
import "@radix-ui/react-dialog";
|