@burdenoff/microfe-devportal 2026.1006.1 → 2026.1013.2
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/devportal/DevportalRoutes.js.map +1 -1
- package/dist/devportal/components/FeatureFlaggedRoute.js.map +1 -1
- package/dist/devportal/components/FormField.d.ts.map +1 -1
- package/dist/devportal/components/FormField.js +1 -1
- package/dist/devportal/components/FormField.js.map +1 -1
- package/dist/devportal/pages/AnalyticsDashboardPage.js +2 -2
- package/dist/devportal/pages/AnalyticsDashboardPage.js.map +1 -1
- package/dist/devportal/pages/ApiKeysPage.js +1 -1
- package/dist/devportal/pages/ApiKeysPage.js.map +1 -1
- package/dist/devportal/pages/AppLogsPage.js +1 -1
- package/dist/devportal/pages/AppLogsPage.js.map +1 -1
- package/dist/devportal/pages/ApplicationDetailPage.js +1 -1
- package/dist/devportal/pages/ApplicationDetailPage.js.map +1 -1
- package/dist/devportal/pages/ApplicationsPage.js +2 -2
- package/dist/devportal/pages/ApplicationsPage.js.map +1 -1
- package/dist/devportal/pages/CreateWebhookPage.js +1 -1
- package/dist/devportal/pages/CreateWebhookPage.js.map +1 -1
- package/dist/devportal/pages/OAuthClientsPage.js +1 -1
- package/dist/devportal/pages/OAuthClientsPage.js.map +1 -1
- package/dist/devportal/pages/OrganizationMembersPage.js +1 -1
- package/dist/devportal/pages/OrganizationMembersPage.js.map +1 -1
- package/dist/devportal/pages/RevenueDashboardPage.js +1 -1
- package/dist/devportal/pages/RevenueDashboardPage.js.map +1 -1
- package/dist/devportal/pages/SellerItemsPage.js +1 -1
- package/dist/devportal/pages/SellerItemsPage.js.map +1 -1
- package/dist/devportal/pages/SellerOrdersPage.js +2 -2
- package/dist/devportal/pages/SellerOrdersPage.js.map +1 -1
- package/dist/devportal/pages/WebhooksPage.js +1 -1
- package/dist/devportal/pages/WebhooksPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -189,7 +189,7 @@ var S = () => {
|
|
|
189
189
|
/* @__PURE__ */ y("select", {
|
|
190
190
|
value: j,
|
|
191
191
|
onChange: (e) => de(e.target.value),
|
|
192
|
-
className: "rounded-md border bg-bg-surface px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary",
|
|
192
|
+
className: "rounded-md border bg-bg-surface px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary",
|
|
193
193
|
children: [/* @__PURE__ */ v("option", {
|
|
194
194
|
value: "",
|
|
195
195
|
children: T("devportal.allStatus", "All Status")
|
|
@@ -201,7 +201,7 @@ var S = () => {
|
|
|
201
201
|
/* @__PURE__ */ y("select", {
|
|
202
202
|
value: M,
|
|
203
203
|
onChange: (e) => fe(e.target.value),
|
|
204
|
-
className: "rounded-md border bg-bg-surface px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary",
|
|
204
|
+
className: "rounded-md border bg-bg-surface px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary",
|
|
205
205
|
children: [/* @__PURE__ */ v("option", {
|
|
206
206
|
value: "",
|
|
207
207
|
children: T("devportal.allTypes", "All Types")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ApplicationsPage.js","names":[],"sources":["../../../src/devportal/pages/ApplicationsPage.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { useMemo, useState } from \"react\";\nimport {\n Plus,\n Search,\n Grid,\n List,\n AppWindow,\n Loader2,\n AlertCircle,\n Archive,\n} from \"lucide-react\";\nimport { EmphasisPanel, GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../components/PageLayout\";\nimport { AppCard } from \"../components/AppCard\";\nimport { EmptyState } from \"../components/EmptyState\";\nimport { ConfirmDialog } from \"../components/ConfirmDialog\";\nimport { SelectionCheckbox } from \"../components/SelectionCheckbox\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\nimport { PermissionGuard } from \"../components\";\nimport { DEVPORTAL_PERMISSIONS } from \"../constants/permissions\";\nimport { useTr } from \"../utils/i18n\";\nimport type { Application, AppStatus, AppType } from \"../types\";\nimport { APP_TYPE_LABELS, APP_STATUS_COLORS } from \"../utils/constants\";\nimport {\n useGetApplicationsQuery,\n useDeactivateApplicationMutation,\n getErrorMessage,\n useDevportalPermission,\n type ApplicationFieldsFragment,\n} from \"../hooks\";\n\ntype BulkAction = \"deactivate\" | null;\n\nfunction formatBulkResult(\n action: string,\n succeeded: number,\n failed: number,\n): string {\n return `${action} ${succeeded} application${succeeded === 1 ? \"\" : \"s\"}${\n failed > 0 ? ` • ${failed} failed` : \"\"\n }`;\n}\n\nfunction StatusBadge({\n status,\n tr,\n}: {\n status: AppStatus;\n tr: (key: string, fallback: string) => string;\n}) {\n const colors: Record<string, string> = {\n ACTIVE: \"bg-status-success-bg-subtle text-status-success-text\",\n PUBLISHED: \"bg-status-success-bg-subtle text-status-success-text\",\n DRAFT: \"bg-bg-sunken text-text-secondary\",\n SUBMITTED: \"bg-status-warning-bg-subtle text-status-warning-text\",\n IN_REVIEW: \"bg-status-warning-bg-subtle text-status-warning-text\",\n APPROVED: \"bg-status-success-bg-subtle text-status-success-text\",\n UNLISTED: \"bg-status-warning-bg-subtle text-status-warning-text\",\n REJECTED: \"bg-status-error-bg-subtle text-status-error-text\",\n SUSPENDED: \"bg-status-error-bg-subtle text-status-error-text\",\n INACTIVE: \"bg-bg-sunken text-text-muted\",\n REVOKED: \"bg-status-error-bg-subtle text-status-error-text\",\n DEPRECATED: \"bg-bg-sunken text-text-muted\",\n };\n return (\n <span\n className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${colors[status] ?? \"bg-bg-sunken text-text-secondary\"}`}\n >\n {status === \"INACTIVE\" || status === \"REVOKED\"\n ? tr(\"devportal.status.deactivated\", \"Deactivated\")\n : status.replace(/_/g, \" \")}\n </span>\n );\n}\n\n/**\n * Map GraphQL application to local Application type\n */\nfunction mapApplication(app: ApplicationFieldsFragment): Application {\n // Convert screenshots array to Record if needed\n const screenshots = Array.isArray(app.screenshots)\n ? app.screenshots.length > 0\n ? (app.screenshots as unknown as Record<string, unknown>)\n : undefined\n : (app.screenshots as Record<string, unknown> | undefined);\n\n // Convert pricingDetails\n const pricingDetails =\n app.pricingDetails && typeof app.pricingDetails === \"object\"\n ? (app.pricingDetails as Record<string, unknown>)\n : undefined;\n\n return {\n id: app.id,\n name: app.name,\n slug: app.slug,\n description: app.description ?? undefined,\n shortDescription: app.shortDescription ?? undefined,\n appType: app.appType as AppType,\n status: app.status as AppStatus,\n visibility: app.visibility as \"PUBLIC\" | \"PRIVATE\" | \"UNLISTED\",\n category: app.category ?? undefined,\n tags: app.tags ?? [],\n keywords: app.keywords ?? [],\n iconUrl: app.iconUrl ?? undefined,\n bannerUrl: app.bannerUrl ?? undefined,\n screenshots,\n videoUrl: app.videoUrl ?? undefined,\n websiteUrl: app.websiteUrl ?? undefined,\n documentationUrl: app.documentationUrl ?? undefined,\n supportUrl: app.supportUrl ?? undefined,\n privacyPolicyUrl: app.privacyPolicyUrl ?? undefined,\n termsOfServiceUrl: app.termsOfServiceUrl ?? undefined,\n githubUrl: app.githubUrl ?? undefined,\n pricingType: app.pricingType ?? undefined,\n pricingDetails,\n isVerified: app.isVerified,\n isFeatured: app.isFeatured,\n averageRating: app.averageRating ?? undefined,\n reviewCount: app.reviewCount ?? 0,\n installCount: app.installCount ?? 0,\n activeInstallCount: app.activeInstallCount ?? 0,\n requiresApproval: app.requiresApproval,\n reviewStatus: app.reviewStatus ?? undefined,\n reviewNotes: app.reviewNotes ?? undefined,\n reviewedBy: app.reviewedBy ?? undefined,\n lastReviewedAt: app.lastReviewedAt ?? undefined,\n submittedAt: app.submittedAt ?? undefined,\n publishedAt: app.publishedAt ?? undefined,\n developerId: app.developerId,\n developerOrganizationId: app.developerOrganizationId ?? undefined,\n tenantId: app.tenantId,\n createdAt: app.createdAt,\n updatedAt: app.updatedAt,\n };\n}\n\n/**\n * Applications listing page\n *\n * Displays all applications for the current developer.\n * Uses GraphQL to fetch real data from the DevPortal backend.\n */\nexport const ApplicationsPage: FC = () => {\n const { navigate, basePath, activeOrgId } = useDevportal();\n const tr = useTr();\n const createPerm = useDevportalPermission(DEVPORTAL_PERMISSIONS.APP_CREATE);\n const updatePerm = useDevportalPermission(DEVPORTAL_PERMISSIONS.APP_UPDATE);\n const [viewMode, setViewMode] = useState<\"grid\" | \"list\">(\"list\");\n const [searchQuery, setSearchQuery] = useState(\"\");\n const [statusFilter, setStatusFilter] = useState<AppStatus | \"\">(\"\");\n const [typeFilter, setTypeFilter] = useState<AppType | \"\">(\"\");\n const [selectedAppIds, setSelectedAppIds] = useState<Set<string>>(new Set());\n const [bulkAction, setBulkAction] = useState<BulkAction>(null);\n const [feedback, setFeedback] = useState<{\n type: \"success\" | \"error\";\n message: string;\n } | null>(null);\n\n // Fetch applications from GraphQL API — always scope to active org so\n // totalCount from the server matches what the user actually owns.\n const { data, loading, error, refetch } = useGetApplicationsQuery({\n variables: {\n limit: 50,\n offset: 0,\n status: statusFilter || undefined,\n organizationId: activeOrgId ?? undefined,\n },\n fetchPolicy: \"cache-and-network\",\n });\n\n const applications: Application[] = useMemo(\n () =>\n data?.applications?.edges?.map(\n (edge: { node: ApplicationFieldsFragment }) =>\n mapApplication(edge.node),\n ) ?? [],\n [data?.applications?.edges],\n );\n\n const [deactivateApp] = useDeactivateApplicationMutation();\n\n const handleNavigate = (path: string) => {\n navigate?.(`${basePath}${path}`);\n };\n\n // Client-side filtering: org scope + search + type (status filtered server-side)\n const filteredApps = applications.filter((app: Application) => {\n const matchesOrg =\n !activeOrgId || app.developerOrganizationId === activeOrgId;\n const matchesSearch =\n !searchQuery ||\n app.name.toLowerCase().includes(searchQuery.toLowerCase()) ||\n app.shortDescription?.toLowerCase().includes(searchQuery.toLowerCase());\n const matchesType = !typeFilter || app.appType === typeFilter;\n return matchesOrg && matchesSearch && matchesType;\n });\n\n const appStats = useMemo(() => {\n // Use filteredApps (org-scoped) for all counts so stats match what's shown.\n const total = data?.applications?.totalCount ?? filteredApps.length;\n const active = filteredApps.filter(\n (app: Application) => app.status === \"ACTIVE\",\n ).length;\n const draft = filteredApps.filter(\n (app: Application) => app.status === \"DRAFT\",\n ).length;\n const published = filteredApps.filter(\n (app: Application) => app.status === \"PUBLISHED\",\n ).length;\n return { total, active, draft, published };\n }, [filteredApps, data?.applications?.totalCount]);\n\n const selectedApps = useMemo(\n () => applications.filter((app: Application) => selectedAppIds.has(app.id)),\n [applications, selectedAppIds],\n );\n\n const deactivatableApps = useMemo(\n () =>\n selectedApps.filter(\n (app: Application) =>\n app.status !== \"INACTIVE\" && app.status !== \"REVOKED\",\n ),\n [selectedApps],\n );\n\n const allVisibleSelected =\n filteredApps.length > 0 &&\n filteredApps.every((app: Application) => selectedAppIds.has(app.id));\n const partiallySelected =\n filteredApps.some((app: Application) => selectedAppIds.has(app.id)) &&\n !allVisibleSelected;\n\n const toggleAppSelection = (appId: string, checked: boolean) => {\n setSelectedAppIds((prev) => {\n const next = new Set(prev);\n if (checked) {\n next.add(appId);\n } else {\n next.delete(appId);\n }\n return next;\n });\n };\n\n const toggleAllVisible = (checked: boolean) => {\n setSelectedAppIds((prev) => {\n const next = new Set(prev);\n filteredApps.forEach((app: Application) => {\n if (checked) {\n next.add(app.id);\n } else {\n next.delete(app.id);\n }\n });\n return next;\n });\n };\n\n const clearSelection = () => setSelectedAppIds(new Set());\n\n const runBulkDeactivate = async () => {\n if (deactivatableApps.length === 0) return;\n\n const results = await Promise.allSettled(\n deactivatableApps.map((app: Application) =>\n deactivateApp({ variables: { id: app.id } }),\n ),\n );\n\n const succeeded = results.filter(\n (result: PromiseSettledResult<unknown>) => result.status === \"fulfilled\",\n ).length;\n const failed = results.length - succeeded;\n\n setFeedback({\n type: failed === 0 ? \"success\" : \"error\",\n message: formatBulkResult(\n tr(\"devportal.deactivated\", \"Deactivated\"),\n succeeded,\n failed,\n ),\n });\n clearSelection();\n setBulkAction(null);\n await refetch();\n };\n\n // Loading state\n if (loading && !data) {\n return (\n <PageLayout\n title={tr(\"devportal.myApplications\", \"My Applications\")}\n description={tr(\n \"devportal.manageYourApps\",\n \"Manage your developer applications\",\n )}\n >\n <div className=\"flex items-center justify-center py-12\">\n <Loader2 className=\"h-8 w-8 animate-spin text-text-secondary\" />\n <span className=\"ml-2 text-text-secondary\">\n {tr(\"devportal.loadingApplications\", \"Loading applications...\")}\n </span>\n </div>\n </PageLayout>\n );\n }\n\n // Error state\n if (error) {\n return (\n <PageLayout\n title={tr(\"devportal.myApplications\", \"My Applications\")}\n description={tr(\n \"devportal.manageYourApps\",\n \"Manage your developer applications\",\n )}\n >\n <div className=\"rounded-lg border border-status-error-border/50 bg-status-error-bg/10 p-6 text-center\">\n <AlertCircle className=\"mx-auto h-8 w-8 text-status-error-text\" />\n <h3 className=\"mt-2 font-medium text-status-error-text\">\n {tr(\n \"devportal.failedToLoadApplications\",\n \"Failed to load applications\",\n )}\n </h3>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {getErrorMessage(error)}\n </p>\n <button\n onClick={() => refetch()}\n className=\"mt-4 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n {tr(\"devportal.tryAgain\", \"Try Again\")}\n </button>\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PermissionGuard authOnly>\n <PageLayout\n title={tr(\"devportal.myApplications\", \"My Applications\")}\n description={tr(\n \"devportal.manageYourApps\",\n \"Manage your developer applications\",\n )}\n purpose={tr(\n \"devportal.applicationsPurpose\",\n \"Applications are the integrations you build on the platform — OAuth apps, store listings, and API consumers. Create one here to get client credentials, request scopes, and ship to the marketplace.\",\n )}\n actions={\n <button\n onClick={() => handleNavigate(\"/apps/new\")}\n disabled={!createPerm.allowed}\n aria-disabled={!createPerm.allowed}\n title={\n !createPerm.allowed\n ? tr(\n \"devportal.common.noPermission\",\n \"You do not have permission to perform this action\",\n )\n : undefined\n }\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n >\n <Plus size={16} aria-hidden=\"true\" />\n {tr(\"devportal.createApp\", \"Create App\")}\n </button>\n }\n >\n {/* Filters */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex flex-1 items-center gap-4\">\n {/* Search */}\n <div className=\"relative flex-1 max-w-md\">\n <Search\n size={16}\n className=\"absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary\"\n />\n <input\n type=\"text\"\n placeholder={tr(\n \"devportal.searchApplications\",\n \"Search applications...\",\n )}\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n className=\"w-full rounded-md border bg-bg-surface pl-9 pr-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary\"\n />\n </div>\n\n {/* Status Filter */}\n <select\n value={statusFilter}\n onChange={(e) =>\n setStatusFilter(e.target.value as AppStatus | \"\")\n }\n className=\"rounded-md border bg-bg-surface px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary\"\n >\n <option value=\"\">\n {tr(\"devportal.allStatus\", \"All Status\")}\n </option>\n {Object.keys(APP_STATUS_COLORS).map((status) => (\n <option key={status} value={status}>\n {tr(\n `devportal.appStatus.${status}`,\n status.replace(/_/g, \" \"),\n )}\n </option>\n ))}\n </select>\n\n {/* Type Filter */}\n <select\n value={typeFilter}\n onChange={(e) => setTypeFilter(e.target.value as AppType | \"\")}\n className=\"rounded-md border bg-bg-surface px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary\"\n >\n <option value=\"\">{tr(\"devportal.allTypes\", \"All Types\")}</option>\n {Object.entries(APP_TYPE_LABELS).map(([value, label]) => (\n <option key={value} value={value}>\n {label}\n </option>\n ))}\n </select>\n </div>\n\n {/* View Toggle */}\n <div className=\"flex items-center gap-1 rounded-md border p-1\">\n <button\n onClick={() => setViewMode(\"grid\")}\n className={`rounded p-1.5 ${viewMode === \"grid\" ? \"bg-bg-sunken\" : \"hover:bg-bg-sunken/50\"}`}\n title={tr(\"devportal.gridView\", \"Grid view\")}\n >\n <Grid size={16} />\n </button>\n <button\n onClick={() => setViewMode(\"list\")}\n className={`rounded p-1.5 ${viewMode === \"list\" ? \"bg-bg-sunken\" : \"hover:bg-bg-sunken/50\"}`}\n title={tr(\"devportal.tableView\", \"Table view\")}\n >\n <List size={16} />\n </button>\n </div>\n </div>\n\n {/* Summary Stats — primary emphasis zone */}\n <EmphasisPanel className=\"mt-4\">\n <div className=\"grid grid-cols-2 gap-4 sm:grid-cols-4\">\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <p className=\"text-sm text-text-secondary\">\n {tr(\"devportal.totalApps\", \"Total Apps\")}\n </p>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums\">\n {appStats.total}\n </p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <p className=\"text-sm text-status-success-text\">\n {tr(\"devportal.active\", \"Active\")}\n </p>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums\">\n {appStats.active}\n </p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <p className=\"text-sm text-text-secondary\">\n {tr(\"devportal.published\", \"Published\")}\n </p>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums\">\n {appStats.published}\n </p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <p className=\"text-sm text-text-secondary\">\n {tr(\"devportal.draft\", \"Draft\")}\n </p>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums\">\n {appStats.draft}\n </p>\n </GlassCard>\n </div>\n </EmphasisPanel>\n\n {feedback && (\n <div\n className={`rounded-lg border px-4 py-3 text-sm ${\n feedback.type === \"success\"\n ? \"border-border-subtle bg-status-success-bg-subtle text-status-success-text\"\n : \"border-status-error-border/40 bg-status-error-bg/10 text-status-error-text\"\n }`}\n >\n <div className=\"flex items-center justify-between gap-3\">\n <span>{feedback.message}</span>\n <button\n onClick={() => setFeedback(null)}\n className=\"text-xs font-medium underline underline-offset-2\"\n >\n {tr(\"devportal.dismiss\", \"Dismiss\")}\n </button>\n </div>\n </div>\n )}\n\n {filteredApps.length > 0 && (\n <div className=\"rounded-card border border-border-seam bg-bg-surface p-4 shadow-[var(--shadow-pop)]\">\n <div className=\"flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between\">\n <div className=\"flex flex-wrap items-center gap-3\">\n <div className=\"flex items-center gap-2\">\n <SelectionCheckbox\n checked={allVisibleSelected}\n indeterminate={partiallySelected}\n onChange={toggleAllVisible}\n ariaLabel={tr(\n \"devportal.selectAllVisibleApplications\",\n \"Select all visible applications\",\n )}\n />\n <span className=\"text-sm font-medium\">\n {tr(\"devportal.selectAllVisible\", \"Select all visible\")}\n </span>\n </div>\n {selectedAppIds.size > 0 && (\n <button\n onClick={clearSelection}\n className=\"text-sm text-text-secondary hover:text-text-primary\"\n >\n {tr(\"devportal.clearSelection\", \"Clear selection\")}\n </button>\n )}\n </div>\n\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"rounded-full bg-bg-sunken px-3 py-1 text-sm font-medium text-text-secondary\">\n {selectedAppIds.size}{\" \"}\n {tr(\"devportal.selectedLower\", \"selected\")}\n </span>\n <button\n onClick={() => setBulkAction(\"deactivate\")}\n disabled={\n deactivatableApps.length === 0 || !updatePerm.allowed\n }\n aria-disabled={\n deactivatableApps.length === 0 || !updatePerm.allowed\n }\n title={\n !updatePerm.allowed\n ? tr(\n \"devportal.common.noPermission\",\n \"You do not have permission to perform this action\",\n )\n : undefined\n }\n className=\"inline-flex items-center gap-2 rounded-md border border-status-warning-bg px-3 py-2 text-sm font-medium text-status-warning-text transition-colors hover:bg-status-warning-bg-subtle disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n >\n <Archive size={16} aria-hidden=\"true\" />\n {tr(\"devportal.deactivateSelected\", \"Deactivate Selected\")}\n {deactivatableApps.length > 0\n ? ` (${deactivatableApps.length})`\n : \"\"}\n </button>\n </div>\n </div>\n </div>\n )}\n\n {/* Results count */}\n <p className=\"text-sm text-text-secondary\">\n {filteredApps.length}{\" \"}\n {filteredApps.length === 1\n ? tr(\"devportal.applicationFound\", \"application found\")\n : tr(\"devportal.applicationsFound\", \"applications found\")}\n </p>\n\n {/* Applications */}\n {filteredApps.length > 0 ? (\n viewMode === \"grid\" ? (\n <div className=\"grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3\">\n {filteredApps.map((app: Application) => (\n <AppCard\n key={app.id}\n app={app}\n selected={selectedAppIds.has(app.id)}\n onToggleSelect={(checked) =>\n toggleAppSelection(app.id, checked)\n }\n />\n ))}\n </div>\n ) : (\n <div className=\"overflow-x-auto rounded-lg border border-border-seam\">\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"border-b border-border-seam bg-bg-sunken text-left\">\n <th className=\"w-10 px-4 py-3\">\n <SelectionCheckbox\n checked={allVisibleSelected}\n indeterminate={partiallySelected}\n onChange={toggleAllVisible}\n ariaLabel={tr(\"devportal.selectAll\", \"Select all\")}\n />\n </th>\n <th className=\"px-4 py-3 font-medium text-text-secondary\">\n {tr(\"devportal.name\", \"Name\")}\n </th>\n <th className=\"px-4 py-3 font-medium text-text-secondary\">\n {tr(\"devportal.status\", \"Status\")}\n </th>\n <th className=\"px-4 py-3 font-medium text-text-secondary\">\n {tr(\"devportal.type\", \"Type\")}\n </th>\n <th className=\"px-4 py-3 font-medium text-text-secondary\">\n {tr(\"devportal.created\", \"Created\")}\n </th>\n <th className=\"px-4 py-3 font-medium text-text-secondary\"></th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border-seam\">\n {filteredApps.map((app: Application) => (\n <tr\n key={app.id}\n className={`hover:bg-bg-surface-hover transition-colors ${selectedAppIds.has(app.id) ? \"bg-action-primary-bg/5\" : \"\"}`}\n >\n <td className=\"w-10 px-4 py-3\">\n <SelectionCheckbox\n checked={selectedAppIds.has(app.id)}\n onChange={(checked) =>\n toggleAppSelection(app.id, checked)\n }\n ariaLabel={tr(\n \"devportal.selectNamed\",\n \"Select {{name}}\",\n { name: app.name },\n )}\n />\n </td>\n <td className=\"px-4 py-3\">\n <button\n onClick={() => handleNavigate(`/apps/${app.id}`)}\n className=\"group flex items-center gap-3 text-left\"\n >\n {app.iconUrl ? (\n <img\n src={app.iconUrl}\n alt=\"\"\n className=\"h-8 w-8 rounded object-cover\"\n />\n ) : (\n <div className=\"flex h-8 w-8 items-center justify-center rounded bg-bg-sunken text-text-secondary\">\n <AppWindow size={14} />\n </div>\n )}\n <div>\n <p className=\"font-medium text-text-primary group-hover:text-action-primary-text transition-colors\">\n {app.name}\n </p>\n {app.shortDescription && (\n <p className=\"text-xs text-text-secondary line-clamp-1\">\n {app.shortDescription}\n </p>\n )}\n </div>\n </button>\n </td>\n <td className=\"px-4 py-3\">\n <StatusBadge status={app.status} tr={tr} />\n </td>\n <td className=\"px-4 py-3 text-text-secondary\">\n {APP_TYPE_LABELS[app.appType] || app.appType}\n </td>\n <td className=\"px-4 py-3 text-text-secondary tabular-nums\">\n {new Date(app.createdAt).toLocaleDateString()}\n </td>\n <td className=\"px-4 py-3 text-right\">\n <button\n onClick={() => handleNavigate(`/apps/${app.id}`)}\n className=\"rounded-md px-3 py-1.5 text-xs font-medium text-text-secondary hover:bg-bg-sunken hover:text-text-primary transition-colors\"\n >\n {tr(\"devportal.view\", \"View\")}\n </button>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )\n ) : (\n <EmptyState\n illustration={\n searchQuery || statusFilter || typeFilter\n ? \"empty-search\"\n : \"empty-data\"\n }\n icon={<AppWindow size={32} />}\n title={tr(\"devportal.noApplicationsFound\", \"No applications found\")}\n description={\n searchQuery || statusFilter || typeFilter\n ? tr(\n \"devportal.tryAdjustingFilters\",\n \"Try adjusting your filters\",\n )\n : tr(\n \"devportal.createFirstApp\",\n \"Create your first application to get started\",\n )\n }\n action={\n !searchQuery && !statusFilter && !typeFilter ? (\n <button\n onClick={() => handleNavigate(\"/apps/new\")}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus size={16} />\n {tr(\"devportal.createApp\", \"Create App\")}\n </button>\n ) : undefined\n }\n />\n )}\n\n <ConfirmDialog\n open={bulkAction === \"deactivate\"}\n onOpenChange={(open) => {\n if (!open) setBulkAction(null);\n }}\n onConfirm={runBulkDeactivate}\n title={tr(\n \"devportal.deactivateSelectedApplications\",\n \"Deactivate selected applications\",\n )}\n description={tr(\n \"devportal.deactivateSelectedApplicationsDescription\",\n \"Deactivate {{count}} selected application(s)? Users and integrations may lose access until these apps are re-enabled.\",\n { count: deactivatableApps.length },\n )}\n confirmLabel={tr(\"devportal.deactivate\", \"Deactivate\")}\n cancelLabel={tr(\"devportal.cancel\", \"Cancel\")}\n variant=\"warning\"\n />\n </PageLayout>\n </PermissionGuard>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ApplicationsPage.js","names":[],"sources":["../../../src/devportal/pages/ApplicationsPage.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { useMemo, useState } from \"react\";\nimport {\n Plus,\n Search,\n Grid,\n List,\n AppWindow,\n Loader2,\n AlertCircle,\n Archive,\n} from \"lucide-react\";\nimport { EmphasisPanel, GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../components/PageLayout\";\nimport { AppCard } from \"../components/AppCard\";\nimport { EmptyState } from \"../components/EmptyState\";\nimport { ConfirmDialog } from \"../components/ConfirmDialog\";\nimport { SelectionCheckbox } from \"../components/SelectionCheckbox\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\nimport { PermissionGuard } from \"../components\";\nimport { DEVPORTAL_PERMISSIONS } from \"../constants/permissions\";\nimport { useTr } from \"../utils/i18n\";\nimport type { Application, AppStatus, AppType } from \"../types\";\nimport { APP_TYPE_LABELS, APP_STATUS_COLORS } from \"../utils/constants\";\nimport {\n useGetApplicationsQuery,\n useDeactivateApplicationMutation,\n getErrorMessage,\n useDevportalPermission,\n type ApplicationFieldsFragment,\n} from \"../hooks\";\n\ntype BulkAction = \"deactivate\" | null;\n\nfunction formatBulkResult(\n action: string,\n succeeded: number,\n failed: number,\n): string {\n return `${action} ${succeeded} application${succeeded === 1 ? \"\" : \"s\"}${\n failed > 0 ? ` • ${failed} failed` : \"\"\n }`;\n}\n\nfunction StatusBadge({\n status,\n tr,\n}: {\n status: AppStatus;\n tr: (key: string, fallback: string) => string;\n}) {\n const colors: Record<string, string> = {\n ACTIVE: \"bg-status-success-bg-subtle text-status-success-text\",\n PUBLISHED: \"bg-status-success-bg-subtle text-status-success-text\",\n DRAFT: \"bg-bg-sunken text-text-secondary\",\n SUBMITTED: \"bg-status-warning-bg-subtle text-status-warning-text\",\n IN_REVIEW: \"bg-status-warning-bg-subtle text-status-warning-text\",\n APPROVED: \"bg-status-success-bg-subtle text-status-success-text\",\n UNLISTED: \"bg-status-warning-bg-subtle text-status-warning-text\",\n REJECTED: \"bg-status-error-bg-subtle text-status-error-text\",\n SUSPENDED: \"bg-status-error-bg-subtle text-status-error-text\",\n INACTIVE: \"bg-bg-sunken text-text-muted\",\n REVOKED: \"bg-status-error-bg-subtle text-status-error-text\",\n DEPRECATED: \"bg-bg-sunken text-text-muted\",\n };\n return (\n <span\n className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${colors[status] ?? \"bg-bg-sunken text-text-secondary\"}`}\n >\n {status === \"INACTIVE\" || status === \"REVOKED\"\n ? tr(\"devportal.status.deactivated\", \"Deactivated\")\n : status.replace(/_/g, \" \")}\n </span>\n );\n}\n\n/**\n * Map GraphQL application to local Application type\n */\nfunction mapApplication(app: ApplicationFieldsFragment): Application {\n // Convert screenshots array to Record if needed\n const screenshots = Array.isArray(app.screenshots)\n ? app.screenshots.length > 0\n ? (app.screenshots as unknown as Record<string, unknown>)\n : undefined\n : (app.screenshots as Record<string, unknown> | undefined);\n\n // Convert pricingDetails\n const pricingDetails =\n app.pricingDetails && typeof app.pricingDetails === \"object\"\n ? (app.pricingDetails as Record<string, unknown>)\n : undefined;\n\n return {\n id: app.id,\n name: app.name,\n slug: app.slug,\n description: app.description ?? undefined,\n shortDescription: app.shortDescription ?? undefined,\n appType: app.appType as AppType,\n status: app.status as AppStatus,\n visibility: app.visibility as \"PUBLIC\" | \"PRIVATE\" | \"UNLISTED\",\n category: app.category ?? undefined,\n tags: app.tags ?? [],\n keywords: app.keywords ?? [],\n iconUrl: app.iconUrl ?? undefined,\n bannerUrl: app.bannerUrl ?? undefined,\n screenshots,\n videoUrl: app.videoUrl ?? undefined,\n websiteUrl: app.websiteUrl ?? undefined,\n documentationUrl: app.documentationUrl ?? undefined,\n supportUrl: app.supportUrl ?? undefined,\n privacyPolicyUrl: app.privacyPolicyUrl ?? undefined,\n termsOfServiceUrl: app.termsOfServiceUrl ?? undefined,\n githubUrl: app.githubUrl ?? undefined,\n pricingType: app.pricingType ?? undefined,\n pricingDetails,\n isVerified: app.isVerified,\n isFeatured: app.isFeatured,\n averageRating: app.averageRating ?? undefined,\n reviewCount: app.reviewCount ?? 0,\n installCount: app.installCount ?? 0,\n activeInstallCount: app.activeInstallCount ?? 0,\n requiresApproval: app.requiresApproval,\n reviewStatus: app.reviewStatus ?? undefined,\n reviewNotes: app.reviewNotes ?? undefined,\n reviewedBy: app.reviewedBy ?? undefined,\n lastReviewedAt: app.lastReviewedAt ?? undefined,\n submittedAt: app.submittedAt ?? undefined,\n publishedAt: app.publishedAt ?? undefined,\n developerId: app.developerId,\n developerOrganizationId: app.developerOrganizationId ?? undefined,\n tenantId: app.tenantId,\n createdAt: app.createdAt,\n updatedAt: app.updatedAt,\n };\n}\n\n/**\n * Applications listing page\n *\n * Displays all applications for the current developer.\n * Uses GraphQL to fetch real data from the DevPortal backend.\n */\nexport const ApplicationsPage: FC = () => {\n const { navigate, basePath, activeOrgId } = useDevportal();\n const tr = useTr();\n const createPerm = useDevportalPermission(DEVPORTAL_PERMISSIONS.APP_CREATE);\n const updatePerm = useDevportalPermission(DEVPORTAL_PERMISSIONS.APP_UPDATE);\n const [viewMode, setViewMode] = useState<\"grid\" | \"list\">(\"list\");\n const [searchQuery, setSearchQuery] = useState(\"\");\n const [statusFilter, setStatusFilter] = useState<AppStatus | \"\">(\"\");\n const [typeFilter, setTypeFilter] = useState<AppType | \"\">(\"\");\n const [selectedAppIds, setSelectedAppIds] = useState<Set<string>>(new Set());\n const [bulkAction, setBulkAction] = useState<BulkAction>(null);\n const [feedback, setFeedback] = useState<{\n type: \"success\" | \"error\";\n message: string;\n } | null>(null);\n\n // Fetch applications from GraphQL API — always scope to active org so\n // totalCount from the server matches what the user actually owns.\n const { data, loading, error, refetch } = useGetApplicationsQuery({\n variables: {\n limit: 50,\n offset: 0,\n status: statusFilter || undefined,\n organizationId: activeOrgId ?? undefined,\n },\n fetchPolicy: \"cache-and-network\",\n });\n\n const applications: Application[] = useMemo(\n () =>\n data?.applications?.edges?.map(\n (edge: { node: ApplicationFieldsFragment }) =>\n mapApplication(edge.node),\n ) ?? [],\n [data?.applications?.edges],\n );\n\n const [deactivateApp] = useDeactivateApplicationMutation();\n\n const handleNavigate = (path: string) => {\n navigate?.(`${basePath}${path}`);\n };\n\n // Client-side filtering: org scope + search + type (status filtered server-side)\n const filteredApps = applications.filter((app: Application) => {\n const matchesOrg =\n !activeOrgId || app.developerOrganizationId === activeOrgId;\n const matchesSearch =\n !searchQuery ||\n app.name.toLowerCase().includes(searchQuery.toLowerCase()) ||\n app.shortDescription?.toLowerCase().includes(searchQuery.toLowerCase());\n const matchesType = !typeFilter || app.appType === typeFilter;\n return matchesOrg && matchesSearch && matchesType;\n });\n\n const appStats = useMemo(() => {\n // Use filteredApps (org-scoped) for all counts so stats match what's shown.\n const total = data?.applications?.totalCount ?? filteredApps.length;\n const active = filteredApps.filter(\n (app: Application) => app.status === \"ACTIVE\",\n ).length;\n const draft = filteredApps.filter(\n (app: Application) => app.status === \"DRAFT\",\n ).length;\n const published = filteredApps.filter(\n (app: Application) => app.status === \"PUBLISHED\",\n ).length;\n return { total, active, draft, published };\n }, [filteredApps, data?.applications?.totalCount]);\n\n const selectedApps = useMemo(\n () => applications.filter((app: Application) => selectedAppIds.has(app.id)),\n [applications, selectedAppIds],\n );\n\n const deactivatableApps = useMemo(\n () =>\n selectedApps.filter(\n (app: Application) =>\n app.status !== \"INACTIVE\" && app.status !== \"REVOKED\",\n ),\n [selectedApps],\n );\n\n const allVisibleSelected =\n filteredApps.length > 0 &&\n filteredApps.every((app: Application) => selectedAppIds.has(app.id));\n const partiallySelected =\n filteredApps.some((app: Application) => selectedAppIds.has(app.id)) &&\n !allVisibleSelected;\n\n const toggleAppSelection = (appId: string, checked: boolean) => {\n setSelectedAppIds((prev) => {\n const next = new Set(prev);\n if (checked) {\n next.add(appId);\n } else {\n next.delete(appId);\n }\n return next;\n });\n };\n\n const toggleAllVisible = (checked: boolean) => {\n setSelectedAppIds((prev) => {\n const next = new Set(prev);\n filteredApps.forEach((app: Application) => {\n if (checked) {\n next.add(app.id);\n } else {\n next.delete(app.id);\n }\n });\n return next;\n });\n };\n\n const clearSelection = () => setSelectedAppIds(new Set());\n\n const runBulkDeactivate = async () => {\n if (deactivatableApps.length === 0) return;\n\n const results = await Promise.allSettled(\n deactivatableApps.map((app: Application) =>\n deactivateApp({ variables: { id: app.id } }),\n ),\n );\n\n const succeeded = results.filter(\n (result: PromiseSettledResult<unknown>) => result.status === \"fulfilled\",\n ).length;\n const failed = results.length - succeeded;\n\n setFeedback({\n type: failed === 0 ? \"success\" : \"error\",\n message: formatBulkResult(\n tr(\"devportal.deactivated\", \"Deactivated\"),\n succeeded,\n failed,\n ),\n });\n clearSelection();\n setBulkAction(null);\n await refetch();\n };\n\n // Loading state\n if (loading && !data) {\n return (\n <PageLayout\n title={tr(\"devportal.myApplications\", \"My Applications\")}\n description={tr(\n \"devportal.manageYourApps\",\n \"Manage your developer applications\",\n )}\n >\n <div className=\"flex items-center justify-center py-12\">\n <Loader2 className=\"h-8 w-8 animate-spin text-text-secondary\" />\n <span className=\"ml-2 text-text-secondary\">\n {tr(\"devportal.loadingApplications\", \"Loading applications...\")}\n </span>\n </div>\n </PageLayout>\n );\n }\n\n // Error state\n if (error) {\n return (\n <PageLayout\n title={tr(\"devportal.myApplications\", \"My Applications\")}\n description={tr(\n \"devportal.manageYourApps\",\n \"Manage your developer applications\",\n )}\n >\n <div className=\"rounded-lg border border-status-error-border/50 bg-status-error-bg/10 p-6 text-center\">\n <AlertCircle className=\"mx-auto h-8 w-8 text-status-error-text\" />\n <h3 className=\"mt-2 font-medium text-status-error-text\">\n {tr(\n \"devportal.failedToLoadApplications\",\n \"Failed to load applications\",\n )}\n </h3>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {getErrorMessage(error)}\n </p>\n <button\n onClick={() => refetch()}\n className=\"mt-4 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n {tr(\"devportal.tryAgain\", \"Try Again\")}\n </button>\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PermissionGuard authOnly>\n <PageLayout\n title={tr(\"devportal.myApplications\", \"My Applications\")}\n description={tr(\n \"devportal.manageYourApps\",\n \"Manage your developer applications\",\n )}\n purpose={tr(\n \"devportal.applicationsPurpose\",\n \"Applications are the integrations you build on the platform — OAuth apps, store listings, and API consumers. Create one here to get client credentials, request scopes, and ship to the marketplace.\",\n )}\n actions={\n <button\n onClick={() => handleNavigate(\"/apps/new\")}\n disabled={!createPerm.allowed}\n aria-disabled={!createPerm.allowed}\n title={\n !createPerm.allowed\n ? tr(\n \"devportal.common.noPermission\",\n \"You do not have permission to perform this action\",\n )\n : undefined\n }\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n >\n <Plus size={16} aria-hidden=\"true\" />\n {tr(\"devportal.createApp\", \"Create App\")}\n </button>\n }\n >\n {/* Filters */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex flex-1 items-center gap-4\">\n {/* Search */}\n <div className=\"relative flex-1 max-w-md\">\n <Search\n size={16}\n className=\"absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary\"\n />\n <input\n type=\"text\"\n placeholder={tr(\n \"devportal.searchApplications\",\n \"Search applications...\",\n )}\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n className=\"w-full rounded-md border bg-bg-surface pl-9 pr-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary\"\n />\n </div>\n\n {/* Status Filter */}\n <select\n value={statusFilter}\n onChange={(e) =>\n setStatusFilter(e.target.value as AppStatus | \"\")\n }\n className=\"rounded-md border bg-bg-surface px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary\"\n >\n <option value=\"\">\n {tr(\"devportal.allStatus\", \"All Status\")}\n </option>\n {Object.keys(APP_STATUS_COLORS).map((status) => (\n <option key={status} value={status}>\n {tr(\n `devportal.appStatus.${status}`,\n status.replace(/_/g, \" \"),\n )}\n </option>\n ))}\n </select>\n\n {/* Type Filter */}\n <select\n value={typeFilter}\n onChange={(e) => setTypeFilter(e.target.value as AppType | \"\")}\n className=\"rounded-md border bg-bg-surface px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary\"\n >\n <option value=\"\">{tr(\"devportal.allTypes\", \"All Types\")}</option>\n {Object.entries(APP_TYPE_LABELS).map(([value, label]) => (\n <option key={value} value={value}>\n {label}\n </option>\n ))}\n </select>\n </div>\n\n {/* View Toggle */}\n <div className=\"flex items-center gap-1 rounded-md border p-1\">\n <button\n onClick={() => setViewMode(\"grid\")}\n className={`rounded p-1.5 ${viewMode === \"grid\" ? \"bg-bg-sunken\" : \"hover:bg-bg-sunken/50\"}`}\n title={tr(\"devportal.gridView\", \"Grid view\")}\n >\n <Grid size={16} />\n </button>\n <button\n onClick={() => setViewMode(\"list\")}\n className={`rounded p-1.5 ${viewMode === \"list\" ? \"bg-bg-sunken\" : \"hover:bg-bg-sunken/50\"}`}\n title={tr(\"devportal.tableView\", \"Table view\")}\n >\n <List size={16} />\n </button>\n </div>\n </div>\n\n {/* Summary Stats — primary emphasis zone */}\n <EmphasisPanel className=\"mt-4\">\n <div className=\"grid grid-cols-2 gap-4 sm:grid-cols-4\">\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <p className=\"text-sm text-text-secondary\">\n {tr(\"devportal.totalApps\", \"Total Apps\")}\n </p>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums\">\n {appStats.total}\n </p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <p className=\"text-sm text-status-success-text\">\n {tr(\"devportal.active\", \"Active\")}\n </p>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums\">\n {appStats.active}\n </p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <p className=\"text-sm text-text-secondary\">\n {tr(\"devportal.published\", \"Published\")}\n </p>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums\">\n {appStats.published}\n </p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <p className=\"text-sm text-text-secondary\">\n {tr(\"devportal.draft\", \"Draft\")}\n </p>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums\">\n {appStats.draft}\n </p>\n </GlassCard>\n </div>\n </EmphasisPanel>\n\n {feedback && (\n <div\n className={`rounded-lg border px-4 py-3 text-sm ${\n feedback.type === \"success\"\n ? \"border-border-subtle bg-status-success-bg-subtle text-status-success-text\"\n : \"border-status-error-border/40 bg-status-error-bg/10 text-status-error-text\"\n }`}\n >\n <div className=\"flex items-center justify-between gap-3\">\n <span>{feedback.message}</span>\n <button\n onClick={() => setFeedback(null)}\n className=\"text-xs font-medium underline underline-offset-2\"\n >\n {tr(\"devportal.dismiss\", \"Dismiss\")}\n </button>\n </div>\n </div>\n )}\n\n {filteredApps.length > 0 && (\n <div className=\"rounded-card border border-border-seam bg-bg-surface p-4 shadow-[var(--shadow-pop)]\">\n <div className=\"flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between\">\n <div className=\"flex flex-wrap items-center gap-3\">\n <div className=\"flex items-center gap-2\">\n <SelectionCheckbox\n checked={allVisibleSelected}\n indeterminate={partiallySelected}\n onChange={toggleAllVisible}\n ariaLabel={tr(\n \"devportal.selectAllVisibleApplications\",\n \"Select all visible applications\",\n )}\n />\n <span className=\"text-sm font-medium\">\n {tr(\"devportal.selectAllVisible\", \"Select all visible\")}\n </span>\n </div>\n {selectedAppIds.size > 0 && (\n <button\n onClick={clearSelection}\n className=\"text-sm text-text-secondary hover:text-text-primary\"\n >\n {tr(\"devportal.clearSelection\", \"Clear selection\")}\n </button>\n )}\n </div>\n\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"rounded-full bg-bg-sunken px-3 py-1 text-sm font-medium text-text-secondary\">\n {selectedAppIds.size}{\" \"}\n {tr(\"devportal.selectedLower\", \"selected\")}\n </span>\n <button\n onClick={() => setBulkAction(\"deactivate\")}\n disabled={\n deactivatableApps.length === 0 || !updatePerm.allowed\n }\n aria-disabled={\n deactivatableApps.length === 0 || !updatePerm.allowed\n }\n title={\n !updatePerm.allowed\n ? tr(\n \"devportal.common.noPermission\",\n \"You do not have permission to perform this action\",\n )\n : undefined\n }\n className=\"inline-flex items-center gap-2 rounded-md border border-status-warning-bg px-3 py-2 text-sm font-medium text-status-warning-text transition-colors hover:bg-status-warning-bg-subtle disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n >\n <Archive size={16} aria-hidden=\"true\" />\n {tr(\"devportal.deactivateSelected\", \"Deactivate Selected\")}\n {deactivatableApps.length > 0\n ? ` (${deactivatableApps.length})`\n : \"\"}\n </button>\n </div>\n </div>\n </div>\n )}\n\n {/* Results count */}\n <p className=\"text-sm text-text-secondary\">\n {filteredApps.length}{\" \"}\n {filteredApps.length === 1\n ? tr(\"devportal.applicationFound\", \"application found\")\n : tr(\"devportal.applicationsFound\", \"applications found\")}\n </p>\n\n {/* Applications */}\n {filteredApps.length > 0 ? (\n viewMode === \"grid\" ? (\n <div className=\"grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3\">\n {filteredApps.map((app: Application) => (\n <AppCard\n key={app.id}\n app={app}\n selected={selectedAppIds.has(app.id)}\n onToggleSelect={(checked) =>\n toggleAppSelection(app.id, checked)\n }\n />\n ))}\n </div>\n ) : (\n <div className=\"overflow-x-auto rounded-lg border border-border-seam\">\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"border-b border-border-seam bg-bg-sunken text-left\">\n <th className=\"w-10 px-4 py-3\">\n <SelectionCheckbox\n checked={allVisibleSelected}\n indeterminate={partiallySelected}\n onChange={toggleAllVisible}\n ariaLabel={tr(\"devportal.selectAll\", \"Select all\")}\n />\n </th>\n <th className=\"px-4 py-3 font-medium text-text-secondary\">\n {tr(\"devportal.name\", \"Name\")}\n </th>\n <th className=\"px-4 py-3 font-medium text-text-secondary\">\n {tr(\"devportal.status\", \"Status\")}\n </th>\n <th className=\"px-4 py-3 font-medium text-text-secondary\">\n {tr(\"devportal.type\", \"Type\")}\n </th>\n <th className=\"px-4 py-3 font-medium text-text-secondary\">\n {tr(\"devportal.created\", \"Created\")}\n </th>\n <th className=\"px-4 py-3 font-medium text-text-secondary\"></th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border-seam\">\n {filteredApps.map((app: Application) => (\n <tr\n key={app.id}\n className={`hover:bg-bg-surface-hover transition-colors ${selectedAppIds.has(app.id) ? \"bg-action-primary-bg/5\" : \"\"}`}\n >\n <td className=\"w-10 px-4 py-3\">\n <SelectionCheckbox\n checked={selectedAppIds.has(app.id)}\n onChange={(checked) =>\n toggleAppSelection(app.id, checked)\n }\n ariaLabel={tr(\n \"devportal.selectNamed\",\n \"Select {{name}}\",\n { name: app.name },\n )}\n />\n </td>\n <td className=\"px-4 py-3\">\n <button\n onClick={() => handleNavigate(`/apps/${app.id}`)}\n className=\"group flex items-center gap-3 text-left\"\n >\n {app.iconUrl ? (\n <img\n src={app.iconUrl}\n alt=\"\"\n className=\"h-8 w-8 rounded object-cover\"\n />\n ) : (\n <div className=\"flex h-8 w-8 items-center justify-center rounded bg-bg-sunken text-text-secondary\">\n <AppWindow size={14} />\n </div>\n )}\n <div>\n <p className=\"font-medium text-text-primary group-hover:text-action-primary-text transition-colors\">\n {app.name}\n </p>\n {app.shortDescription && (\n <p className=\"text-xs text-text-secondary line-clamp-1\">\n {app.shortDescription}\n </p>\n )}\n </div>\n </button>\n </td>\n <td className=\"px-4 py-3\">\n <StatusBadge status={app.status} tr={tr} />\n </td>\n <td className=\"px-4 py-3 text-text-secondary\">\n {APP_TYPE_LABELS[app.appType] || app.appType}\n </td>\n <td className=\"px-4 py-3 text-text-secondary tabular-nums\">\n {new Date(app.createdAt).toLocaleDateString()}\n </td>\n <td className=\"px-4 py-3 text-right\">\n <button\n onClick={() => handleNavigate(`/apps/${app.id}`)}\n className=\"rounded-md px-3 py-1.5 text-xs font-medium text-text-secondary hover:bg-bg-sunken hover:text-text-primary transition-colors\"\n >\n {tr(\"devportal.view\", \"View\")}\n </button>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )\n ) : (\n <EmptyState\n illustration={\n searchQuery || statusFilter || typeFilter\n ? \"empty-search\"\n : \"empty-data\"\n }\n icon={<AppWindow size={32} />}\n title={tr(\"devportal.noApplicationsFound\", \"No applications found\")}\n description={\n searchQuery || statusFilter || typeFilter\n ? tr(\n \"devportal.tryAdjustingFilters\",\n \"Try adjusting your filters\",\n )\n : tr(\n \"devportal.createFirstApp\",\n \"Create your first application to get started\",\n )\n }\n action={\n !searchQuery && !statusFilter && !typeFilter ? (\n <button\n onClick={() => handleNavigate(\"/apps/new\")}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus size={16} />\n {tr(\"devportal.createApp\", \"Create App\")}\n </button>\n ) : undefined\n }\n />\n )}\n\n <ConfirmDialog\n open={bulkAction === \"deactivate\"}\n onOpenChange={(open) => {\n if (!open) setBulkAction(null);\n }}\n onConfirm={runBulkDeactivate}\n title={tr(\n \"devportal.deactivateSelectedApplications\",\n \"Deactivate selected applications\",\n )}\n description={tr(\n \"devportal.deactivateSelectedApplicationsDescription\",\n \"Deactivate {{count}} selected application(s)? Users and integrations may lose access until these apps are re-enabled.\",\n { count: deactivatableApps.length },\n )}\n confirmLabel={tr(\"devportal.deactivate\", \"Deactivate\")}\n cancelLabel={tr(\"devportal.cancel\", \"Cancel\")}\n variant=\"warning\"\n />\n </PageLayout>\n </PermissionGuard>\n );\n};\n"],"mappings":"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"}
|
|
@@ -140,7 +140,7 @@ var E = {
|
|
|
140
140
|
onChange: (e) => K("applicationId", e.target.value),
|
|
141
141
|
disabled: L,
|
|
142
142
|
"aria-label": H("devportal.createWebhook.application", "Application"),
|
|
143
|
-
className: "rounded-md border bg-bg-surface px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary",
|
|
143
|
+
className: "rounded-md border bg-bg-surface px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary",
|
|
144
144
|
children: [/* @__PURE__ */ C("option", {
|
|
145
145
|
value: "",
|
|
146
146
|
children: H("devportal.createWebhook.errors.appRequired", "Select an application")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CreateWebhookPage.js","names":[],"sources":["../../../src/devportal/pages/CreateWebhookPage.tsx"],"sourcesContent":["import type { FC, FormEvent } from \"react\";\nimport { useEffect, useMemo, useState } from \"react\";\nimport { useParams } from \"react-router\";\nimport { Loader2, Webhook } from \"lucide-react\";\nimport { ShowMore } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout, PageSection } from \"../components/PageLayout\";\nimport {\n InputField,\n TextareaField,\n CheckboxField,\n} from \"../components/FormField\";\nimport { Alert } from \"../components/ConfirmDialog\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\nimport { PermissionGuard } from \"../components\";\nimport { DEVPORTAL_PERMISSIONS } from \"../constants/permissions\";\nimport { useDevportalEventBus } from \"../events\";\nimport { WEBHOOK_EVENTS } from \"../types\";\nimport {\n type GetApplicationsQuery,\n getErrorMessage,\n useCreateWebhookMutation,\n useGetApplicationQuery,\n useGetApplicationsQuery,\n useDevportalPermission,\n} from \"../hooks\";\nimport { useTr } from \"../utils/i18n\";\n\nconst eventCategories = {\n Application: WEBHOOK_EVENTS.filter((event) =>\n event.startsWith(\"application.\"),\n ),\n Installation: WEBHOOK_EVENTS.filter((event) =>\n event.startsWith(\"installation.\"),\n ),\n User: WEBHOOK_EVENTS.filter((event) => event.startsWith(\"user.\")),\n Workspace: WEBHOOK_EVENTS.filter((event) => event.startsWith(\"workspace.\")),\n Project: WEBHOOK_EVENTS.filter((event) => event.startsWith(\"project.\")),\n Task: WEBHOOK_EVENTS.filter((event) => event.startsWith(\"task.\")),\n Comment: WEBHOOK_EVENTS.filter((event) => event.startsWith(\"comment.\")),\n File: WEBHOOK_EVENTS.filter((event) => event.startsWith(\"file.\")),\n Member: WEBHOOK_EVENTS.filter((event) => event.startsWith(\"member.\")),\n};\n\ninterface CreateWebhookFormState {\n applicationId: string;\n name: string;\n description: string;\n url: string;\n events: string[];\n retryEnabled: boolean;\n maxRetries: number;\n}\n\ntype ApplicationOption =\n GetApplicationsQuery[\"applications\"][\"edges\"][number][\"node\"];\n\nexport const CreateWebhookPage: FC = () => {\n const { appId } = useParams<{ appId: string }>();\n const { navigate, basePath } = useDevportal();\n const [formData, setFormData] = useState<CreateWebhookFormState>({\n applicationId: appId || \"\",\n name: \"\",\n description: \"\",\n url: \"\",\n events: [],\n retryEnabled: true,\n maxRetries: 3,\n });\n const [errors, setErrors] = useState<Record<string, string>>({});\n const [submitError, setSubmitError] = useState<string | null>(null);\n\n const { data: singleAppData } = useGetApplicationQuery({\n variables: { id: appId ?? \"\" },\n skip: !appId,\n fetchPolicy: \"cache-first\",\n });\n\n useEffect(() => {\n const name = singleAppData?.application?.name;\n if (!appId || !name) return;\n const reg = (\n window as unknown as {\n __breadcrumbRegistry?: {\n register: (id: string, name: string) => void;\n unregister: (id: string) => void;\n };\n }\n ).__breadcrumbRegistry;\n reg?.register(appId, name);\n return () => reg?.unregister(appId);\n }, [appId, singleAppData?.application?.name]);\n\n const {\n data: appsData,\n loading: appsLoading,\n error: appsError,\n } = useGetApplicationsQuery({\n variables: { limit: 100 },\n skip: !!appId,\n fetchPolicy: \"cache-and-network\",\n });\n const [createWebhook, { loading: isSubmitting }] = useCreateWebhookMutation();\n const bus = useDevportalEventBus();\n const tr = useTr();\n const createPerm = useDevportalPermission(\n DEVPORTAL_PERMISSIONS.WEBHOOK_CREATE,\n );\n\n const applications = useMemo(\n () =>\n appsData?.applications?.edges\n ?.map(\n (edge: GetApplicationsQuery[\"applications\"][\"edges\"][number]) =>\n edge.node,\n )\n .map((application: ApplicationOption) => ({\n id: application.id,\n name: application.name,\n slug: application.slug,\n })) ?? [],\n [appsData?.applications?.edges],\n );\n\n const selectedApplication = applications.find(\n (application: { id: string; name: string; slug: string }) =>\n application.id === formData.applicationId,\n );\n\n const handleChange = <K extends keyof CreateWebhookFormState>(\n key: K,\n value: CreateWebhookFormState[K],\n ) => {\n setFormData((previous) => ({\n ...previous,\n [key]: value,\n }));\n setErrors((previous) => {\n if (!previous[key as string]) return previous;\n const nextErrors = { ...previous };\n delete nextErrors[key as string];\n return nextErrors;\n });\n };\n\n const toggleEvent = (event: string) => {\n setFormData((previous) => ({\n ...previous,\n events: previous.events.includes(event)\n ? previous.events.filter((value) => value !== event)\n : [...previous.events, event],\n }));\n };\n\n const toggleCategory = (events: readonly string[]) => {\n const allSelected = events.every((event) =>\n formData.events.includes(event),\n );\n\n setFormData((previous) => ({\n ...previous,\n events: allSelected\n ? previous.events.filter((event) => !events.includes(event))\n : [...new Set([...previous.events, ...events])],\n }));\n };\n\n const validate = (): boolean => {\n const nextErrors: Record<string, string> = {};\n\n if (!formData.applicationId) {\n nextErrors.applicationId = tr(\n \"devportal.createWebhook.errors.appRequired\",\n \"Select an application\",\n );\n }\n if (!formData.name.trim()) {\n nextErrors.name = tr(\n \"devportal.createWebhook.errors.nameRequired\",\n \"Name is required\",\n );\n }\n if (!formData.url.trim()) {\n nextErrors.url = tr(\n \"devportal.createWebhook.errors.urlRequired\",\n \"Endpoint URL is required\",\n );\n } else if (!import.meta.env.DEV && !/^https:\\/\\//i.test(formData.url)) {\n nextErrors.url = tr(\n \"devportal.createWebhook.errors.urlHttps\",\n \"Endpoint URL must use HTTPS\",\n );\n }\n if (formData.events.length === 0) {\n nextErrors.events = tr(\n \"devportal.createWebhook.errors.eventsRequired\",\n \"Select at least one event\",\n );\n }\n if (\n formData.retryEnabled &&\n (formData.maxRetries < 1 || formData.maxRetries > 10)\n ) {\n nextErrors.maxRetries = tr(\n \"devportal.createWebhook.errors.retriesRange\",\n \"Retry count must be between 1 and 10\",\n );\n }\n\n setErrors(nextErrors);\n return Object.keys(nextErrors).length === 0;\n };\n\n const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n setSubmitError(null);\n\n if (!validate()) return;\n\n try {\n const result = await createWebhook({\n variables: {\n applicationId: formData.applicationId,\n input: {\n name: formData.name.trim(),\n description: formData.description.trim() || undefined,\n url: formData.url.trim(),\n events: formData.events,\n retryEnabled: formData.retryEnabled,\n maxRetries: formData.retryEnabled ? formData.maxRetries : 0,\n },\n },\n });\n\n const createResult = result.data?.createWebhook;\n const createdWebhook = createResult?.webhook;\n if (!createdWebhook?.id) {\n throw new Error(\n tr(\n \"devportal.createWebhook.errors.noId\",\n \"Webhook creation did not return an identifier\",\n ),\n );\n }\n\n bus.emit(\"devportal.webhook.created\", {\n webhookId: createdWebhook.id,\n applicationId: formData.applicationId,\n secret: createResult?.secret,\n });\n\n navigate?.(\n `${basePath}/apps/${formData.applicationId}/webhooks/${createdWebhook.id}`,\n );\n } catch (error) {\n setSubmitError(\n error instanceof Error\n ? error.message\n : tr(\n \"devportal.createWebhook.errors.createFailed\",\n \"Failed to create webhook\",\n ),\n );\n }\n };\n\n return (\n <PermissionGuard authOnly>\n <PageLayout\n title={tr(\"devportal.createWebhook.title\", \"Create Webhook\")}\n description={tr(\n \"devportal.createWebhook.description\",\n \"Set up a real webhook endpoint to receive platform events through the public gateway\",\n )}\n purpose={tr(\n \"devportal.createWebhook.purpose\",\n \"Point a webhook at a URL you host so the platform pushes events (installs, payments, app updates) to your backend in real time. Choose which events to subscribe to; you will get a signing secret to verify that deliveries genuinely came from us.\",\n )}\n backLink={\n formData.applicationId\n ? `/apps/${formData.applicationId}/webhooks`\n : \"/webhooks\"\n }\n backLabel={tr(\"devportal.createWebhook.back\", \"Back to Webhooks\")}\n >\n <form onSubmit={handleSubmit} className=\"space-y-6\">\n <Alert variant=\"info\">\n {tr(\n \"devportal.createWebhook.info\",\n \"Webhook deliveries are signed and sent through the public global gateway. Use an HTTPS endpoint that accepts POST requests and returns a 2xx response for successful deliveries.\",\n )}\n </Alert>\n\n {submitError && <Alert variant=\"error\">{submitError}</Alert>}\n {appsError && !appId && (\n <Alert variant=\"error\">{getErrorMessage(appsError)}</Alert>\n )}\n\n <PageSection\n title={tr(\"devportal.createWebhook.basicInfo\", \"Basic Information\")}\n >\n <div className=\"space-y-4 rounded-card border border-border-seam bg-bg-surface p-4\">\n {!appId && (\n <label className=\"grid gap-2 text-sm font-medium\">\n <span>\n {tr(\"devportal.createWebhook.application\", \"Application\")}\n </span>\n <select\n value={formData.applicationId}\n onChange={(changeEvent) =>\n handleChange(\"applicationId\", changeEvent.target.value)\n }\n disabled={appsLoading}\n aria-label={tr(\n \"devportal.createWebhook.application\",\n \"Application\",\n )}\n className=\"rounded-md border bg-bg-surface px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary\"\n >\n <option value=\"\">\n {tr(\n \"devportal.createWebhook.errors.appRequired\",\n \"Select an application\",\n )}\n </option>\n {applications.map(\n (application: { id: string; name: string }) => (\n <option key={application.id} value={application.id}>\n {application.name}\n </option>\n ),\n )}\n </select>\n {errors.applicationId && (\n <span className=\"text-sm text-status-error-text\">\n {errors.applicationId}\n </span>\n )}\n </label>\n )}\n\n {appsLoading && !appId && (\n <div\n className=\"flex items-center gap-2 text-sm text-text-secondary\"\n role=\"status\"\n aria-live=\"polite\"\n >\n <Loader2\n className=\"h-4 w-4 animate-spin\"\n aria-hidden=\"true\"\n />\n {tr(\n \"devportal.createWebhook.loadingApps\",\n \"Loading applications...\",\n )}\n </div>\n )}\n\n {selectedApplication && (\n <div className=\"rounded-md border bg-bg-sunken/40 px-3 py-2 text-sm text-text-secondary\">\n {tr(\n \"devportal.createWebhook.creatingFor\",\n \"Creating webhook for\",\n )}{\" \"}\n <span className=\"font-medium text-text-primary\">\n {selectedApplication.name}\n </span>\n </div>\n )}\n\n <InputField\n label={tr(\n \"devportal.createWebhook.webhookName\",\n \"Webhook Name\",\n )}\n value={formData.name}\n onChange={(changeEvent) =>\n handleChange(\"name\", changeEvent.target.value)\n }\n placeholder={tr(\n \"devportal.createWebhook.webhookNamePlaceholder\",\n \"Production webhook\",\n )}\n error={errors.name}\n required\n />\n\n <TextareaField\n label={tr(\n \"devportal.createWebhook.descriptionLabel\",\n \"Description\",\n )}\n value={formData.description}\n onChange={(changeEvent) =>\n handleChange(\"description\", changeEvent.target.value)\n }\n placeholder={tr(\n \"devportal.createWebhook.descriptionPlaceholder\",\n \"What this webhook is used for\",\n )}\n rows={2}\n />\n\n <InputField\n label={tr(\n \"devportal.createWebhook.endpointUrl\",\n \"Endpoint URL\",\n )}\n type=\"url\"\n value={formData.url}\n onChange={(changeEvent) =>\n handleChange(\"url\", changeEvent.target.value)\n }\n placeholder={tr(\n \"devportal.createWebhook.endpointPlaceholder\",\n \"https://your-app.example.com/webhooks/burdenoff\",\n )}\n hint={tr(\"devportal.createWebhook.httpsOnly\", \"HTTPS only\")}\n error={errors.url}\n required\n />\n </div>\n </PageSection>\n\n <PageSection\n title={tr(\n \"devportal.createWebhook.subscribedEvents\",\n \"Subscribed Events\",\n )}\n >\n <div className=\"rounded-card border border-border-seam bg-bg-surface p-4\">\n {errors.events && (\n <p className=\"mb-3 text-sm text-status-error-text\" role=\"alert\">\n {errors.events}\n </p>\n )}\n <ShowMore\n collapsedHeight={320}\n label={tr(\n \"devportal.createWebhook.showAllEvents\",\n \"Show all event categories\",\n )}\n >\n <div className=\"space-y-4\">\n {Object.entries(eventCategories).map(([category, events]) => (\n <section\n key={category}\n className=\"space-y-3 border-b border-border-seam pb-4 last:border-b-0 last:pb-0\"\n >\n <div className=\"flex items-center justify-between gap-3\">\n <h3 className=\"font-medium\">\n {tr(\n `devportal.createWebhook.category.${category.toLowerCase()}`,\n category,\n )}\n </h3>\n <button\n type=\"button\"\n onClick={() => toggleCategory(events)}\n className=\"text-xs font-medium text-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n >\n {events.every((item) =>\n formData.events.includes(item),\n )\n ? tr(\n \"devportal.createWebhook.deselectAll\",\n \"Deselect all\",\n )\n : tr(\n \"devportal.createWebhook.selectAll\",\n \"Select all\",\n )}\n </button>\n </div>\n <div className=\"grid gap-2 sm:grid-cols-2\">\n {events.map((eventName) => (\n <label\n key={eventName}\n className=\"flex items-center gap-2 rounded-md border border-border-seam bg-bg-surface px-3 py-2 text-sm\"\n >\n <input\n type=\"checkbox\"\n checked={formData.events.includes(eventName)}\n onChange={() => toggleEvent(eventName)}\n className=\"rounded border-border-default text-primary focus:ring-primary\"\n aria-label={eventName}\n />\n <code className=\"text-xs text-text-primary\">\n {eventName}\n </code>\n </label>\n ))}\n </div>\n </section>\n ))}\n </div>\n </ShowMore>\n </div>\n </PageSection>\n\n <PageSection\n title={tr(\n \"devportal.createWebhook.retrySettings\",\n \"Retry Settings\",\n )}\n >\n <div className=\"space-y-4 rounded-card border border-border-seam bg-bg-surface p-4\">\n <CheckboxField\n label={tr(\n \"devportal.createWebhook.enableRetries\",\n \"Enable retries\",\n )}\n description={tr(\n \"devportal.createWebhook.enableRetriesDesc\",\n \"Retry failed deliveries automatically\",\n )}\n checked={formData.retryEnabled}\n onChange={(changeEvent) =>\n handleChange(\"retryEnabled\", changeEvent.target.checked)\n }\n />\n\n {formData.retryEnabled && (\n <InputField\n label={tr(\n \"devportal.createWebhook.maxRetries\",\n \"Maximum retries\",\n )}\n type=\"number\"\n value={formData.maxRetries}\n onChange={(changeEvent) =>\n handleChange(\"maxRetries\", Number(changeEvent.target.value))\n }\n min={1}\n max={10}\n hint={tr(\n \"devportal.createWebhook.attemptsHint\",\n \"1 to 10 attempts\",\n )}\n error={errors.maxRetries}\n />\n )}\n </div>\n </PageSection>\n\n <div className=\"flex flex-col gap-3 sm:flex-row\">\n <button\n type=\"button\"\n onClick={() =>\n navigate?.(\n `${basePath}${formData.applicationId ? `/apps/${formData.applicationId}/webhooks` : \"/webhooks\"}`,\n )\n }\n className=\"rounded-md border px-4 py-2 text-sm font-medium hover:bg-accent focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n >\n {tr(\"devportal.common.cancel\", \"Cancel\")}\n </button>\n <button\n type=\"submit\"\n disabled={isSubmitting || appsLoading || !createPerm.allowed}\n aria-disabled={isSubmitting || appsLoading || !createPerm.allowed}\n title={\n !createPerm.allowed\n ? tr(\n \"devportal.common.noPermission\",\n \"You do not have permission to perform this action\",\n )\n : undefined\n }\n className=\"inline-flex items-center justify-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n >\n {isSubmitting ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" aria-hidden=\"true\" />\n ) : (\n <Webhook size={16} aria-hidden=\"true\" />\n )}\n {isSubmitting\n ? tr(\"devportal.createWebhook.creating\", \"Creating...\")\n : tr(\"devportal.createWebhook.title\", \"Create Webhook\")}\n </button>\n </div>\n </form>\n </PageLayout>\n </PermissionGuard>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CreateWebhookPage.js","names":[],"sources":["../../../src/devportal/pages/CreateWebhookPage.tsx"],"sourcesContent":["import type { FC, FormEvent } from \"react\";\nimport { useEffect, useMemo, useState } from \"react\";\nimport { useParams } from \"react-router\";\nimport { Loader2, Webhook } from \"lucide-react\";\nimport { ShowMore } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout, PageSection } from \"../components/PageLayout\";\nimport {\n InputField,\n TextareaField,\n CheckboxField,\n} from \"../components/FormField\";\nimport { Alert } from \"../components/ConfirmDialog\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\nimport { PermissionGuard } from \"../components\";\nimport { DEVPORTAL_PERMISSIONS } from \"../constants/permissions\";\nimport { useDevportalEventBus } from \"../events\";\nimport { WEBHOOK_EVENTS } from \"../types\";\nimport {\n type GetApplicationsQuery,\n getErrorMessage,\n useCreateWebhookMutation,\n useGetApplicationQuery,\n useGetApplicationsQuery,\n useDevportalPermission,\n} from \"../hooks\";\nimport { useTr } from \"../utils/i18n\";\n\nconst eventCategories = {\n Application: WEBHOOK_EVENTS.filter((event) =>\n event.startsWith(\"application.\"),\n ),\n Installation: WEBHOOK_EVENTS.filter((event) =>\n event.startsWith(\"installation.\"),\n ),\n User: WEBHOOK_EVENTS.filter((event) => event.startsWith(\"user.\")),\n Workspace: WEBHOOK_EVENTS.filter((event) => event.startsWith(\"workspace.\")),\n Project: WEBHOOK_EVENTS.filter((event) => event.startsWith(\"project.\")),\n Task: WEBHOOK_EVENTS.filter((event) => event.startsWith(\"task.\")),\n Comment: WEBHOOK_EVENTS.filter((event) => event.startsWith(\"comment.\")),\n File: WEBHOOK_EVENTS.filter((event) => event.startsWith(\"file.\")),\n Member: WEBHOOK_EVENTS.filter((event) => event.startsWith(\"member.\")),\n};\n\ninterface CreateWebhookFormState {\n applicationId: string;\n name: string;\n description: string;\n url: string;\n events: string[];\n retryEnabled: boolean;\n maxRetries: number;\n}\n\ntype ApplicationOption =\n GetApplicationsQuery[\"applications\"][\"edges\"][number][\"node\"];\n\nexport const CreateWebhookPage: FC = () => {\n const { appId } = useParams<{ appId: string }>();\n const { navigate, basePath } = useDevportal();\n const [formData, setFormData] = useState<CreateWebhookFormState>({\n applicationId: appId || \"\",\n name: \"\",\n description: \"\",\n url: \"\",\n events: [],\n retryEnabled: true,\n maxRetries: 3,\n });\n const [errors, setErrors] = useState<Record<string, string>>({});\n const [submitError, setSubmitError] = useState<string | null>(null);\n\n const { data: singleAppData } = useGetApplicationQuery({\n variables: { id: appId ?? \"\" },\n skip: !appId,\n fetchPolicy: \"cache-first\",\n });\n\n useEffect(() => {\n const name = singleAppData?.application?.name;\n if (!appId || !name) return;\n const reg = (\n window as unknown as {\n __breadcrumbRegistry?: {\n register: (id: string, name: string) => void;\n unregister: (id: string) => void;\n };\n }\n ).__breadcrumbRegistry;\n reg?.register(appId, name);\n return () => reg?.unregister(appId);\n }, [appId, singleAppData?.application?.name]);\n\n const {\n data: appsData,\n loading: appsLoading,\n error: appsError,\n } = useGetApplicationsQuery({\n variables: { limit: 100 },\n skip: !!appId,\n fetchPolicy: \"cache-and-network\",\n });\n const [createWebhook, { loading: isSubmitting }] = useCreateWebhookMutation();\n const bus = useDevportalEventBus();\n const tr = useTr();\n const createPerm = useDevportalPermission(\n DEVPORTAL_PERMISSIONS.WEBHOOK_CREATE,\n );\n\n const applications = useMemo(\n () =>\n appsData?.applications?.edges\n ?.map(\n (edge: GetApplicationsQuery[\"applications\"][\"edges\"][number]) =>\n edge.node,\n )\n .map((application: ApplicationOption) => ({\n id: application.id,\n name: application.name,\n slug: application.slug,\n })) ?? [],\n [appsData?.applications?.edges],\n );\n\n const selectedApplication = applications.find(\n (application: { id: string; name: string; slug: string }) =>\n application.id === formData.applicationId,\n );\n\n const handleChange = <K extends keyof CreateWebhookFormState>(\n key: K,\n value: CreateWebhookFormState[K],\n ) => {\n setFormData((previous) => ({\n ...previous,\n [key]: value,\n }));\n setErrors((previous) => {\n if (!previous[key as string]) return previous;\n const nextErrors = { ...previous };\n delete nextErrors[key as string];\n return nextErrors;\n });\n };\n\n const toggleEvent = (event: string) => {\n setFormData((previous) => ({\n ...previous,\n events: previous.events.includes(event)\n ? previous.events.filter((value) => value !== event)\n : [...previous.events, event],\n }));\n };\n\n const toggleCategory = (events: readonly string[]) => {\n const allSelected = events.every((event) =>\n formData.events.includes(event),\n );\n\n setFormData((previous) => ({\n ...previous,\n events: allSelected\n ? previous.events.filter((event) => !events.includes(event))\n : [...new Set([...previous.events, ...events])],\n }));\n };\n\n const validate = (): boolean => {\n const nextErrors: Record<string, string> = {};\n\n if (!formData.applicationId) {\n nextErrors.applicationId = tr(\n \"devportal.createWebhook.errors.appRequired\",\n \"Select an application\",\n );\n }\n if (!formData.name.trim()) {\n nextErrors.name = tr(\n \"devportal.createWebhook.errors.nameRequired\",\n \"Name is required\",\n );\n }\n if (!formData.url.trim()) {\n nextErrors.url = tr(\n \"devportal.createWebhook.errors.urlRequired\",\n \"Endpoint URL is required\",\n );\n } else if (!import.meta.env.DEV && !/^https:\\/\\//i.test(formData.url)) {\n nextErrors.url = tr(\n \"devportal.createWebhook.errors.urlHttps\",\n \"Endpoint URL must use HTTPS\",\n );\n }\n if (formData.events.length === 0) {\n nextErrors.events = tr(\n \"devportal.createWebhook.errors.eventsRequired\",\n \"Select at least one event\",\n );\n }\n if (\n formData.retryEnabled &&\n (formData.maxRetries < 1 || formData.maxRetries > 10)\n ) {\n nextErrors.maxRetries = tr(\n \"devportal.createWebhook.errors.retriesRange\",\n \"Retry count must be between 1 and 10\",\n );\n }\n\n setErrors(nextErrors);\n return Object.keys(nextErrors).length === 0;\n };\n\n const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n setSubmitError(null);\n\n if (!validate()) return;\n\n try {\n const result = await createWebhook({\n variables: {\n applicationId: formData.applicationId,\n input: {\n name: formData.name.trim(),\n description: formData.description.trim() || undefined,\n url: formData.url.trim(),\n events: formData.events,\n retryEnabled: formData.retryEnabled,\n maxRetries: formData.retryEnabled ? formData.maxRetries : 0,\n },\n },\n });\n\n const createResult = result.data?.createWebhook;\n const createdWebhook = createResult?.webhook;\n if (!createdWebhook?.id) {\n throw new Error(\n tr(\n \"devportal.createWebhook.errors.noId\",\n \"Webhook creation did not return an identifier\",\n ),\n );\n }\n\n bus.emit(\"devportal.webhook.created\", {\n webhookId: createdWebhook.id,\n applicationId: formData.applicationId,\n secret: createResult?.secret,\n });\n\n navigate?.(\n `${basePath}/apps/${formData.applicationId}/webhooks/${createdWebhook.id}`,\n );\n } catch (error) {\n setSubmitError(\n error instanceof Error\n ? error.message\n : tr(\n \"devportal.createWebhook.errors.createFailed\",\n \"Failed to create webhook\",\n ),\n );\n }\n };\n\n return (\n <PermissionGuard authOnly>\n <PageLayout\n title={tr(\"devportal.createWebhook.title\", \"Create Webhook\")}\n description={tr(\n \"devportal.createWebhook.description\",\n \"Set up a real webhook endpoint to receive platform events through the public gateway\",\n )}\n purpose={tr(\n \"devportal.createWebhook.purpose\",\n \"Point a webhook at a URL you host so the platform pushes events (installs, payments, app updates) to your backend in real time. Choose which events to subscribe to; you will get a signing secret to verify that deliveries genuinely came from us.\",\n )}\n backLink={\n formData.applicationId\n ? `/apps/${formData.applicationId}/webhooks`\n : \"/webhooks\"\n }\n backLabel={tr(\"devportal.createWebhook.back\", \"Back to Webhooks\")}\n >\n <form onSubmit={handleSubmit} className=\"space-y-6\">\n <Alert variant=\"info\">\n {tr(\n \"devportal.createWebhook.info\",\n \"Webhook deliveries are signed and sent through the public global gateway. Use an HTTPS endpoint that accepts POST requests and returns a 2xx response for successful deliveries.\",\n )}\n </Alert>\n\n {submitError && <Alert variant=\"error\">{submitError}</Alert>}\n {appsError && !appId && (\n <Alert variant=\"error\">{getErrorMessage(appsError)}</Alert>\n )}\n\n <PageSection\n title={tr(\"devportal.createWebhook.basicInfo\", \"Basic Information\")}\n >\n <div className=\"space-y-4 rounded-card border border-border-seam bg-bg-surface p-4\">\n {!appId && (\n <label className=\"grid gap-2 text-sm font-medium\">\n <span>\n {tr(\"devportal.createWebhook.application\", \"Application\")}\n </span>\n <select\n value={formData.applicationId}\n onChange={(changeEvent) =>\n handleChange(\"applicationId\", changeEvent.target.value)\n }\n disabled={appsLoading}\n aria-label={tr(\n \"devportal.createWebhook.application\",\n \"Application\",\n )}\n className=\"rounded-md border bg-bg-surface px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary\"\n >\n <option value=\"\">\n {tr(\n \"devportal.createWebhook.errors.appRequired\",\n \"Select an application\",\n )}\n </option>\n {applications.map(\n (application: { id: string; name: string }) => (\n <option key={application.id} value={application.id}>\n {application.name}\n </option>\n ),\n )}\n </select>\n {errors.applicationId && (\n <span className=\"text-sm text-status-error-text\">\n {errors.applicationId}\n </span>\n )}\n </label>\n )}\n\n {appsLoading && !appId && (\n <div\n className=\"flex items-center gap-2 text-sm text-text-secondary\"\n role=\"status\"\n aria-live=\"polite\"\n >\n <Loader2\n className=\"h-4 w-4 animate-spin\"\n aria-hidden=\"true\"\n />\n {tr(\n \"devportal.createWebhook.loadingApps\",\n \"Loading applications...\",\n )}\n </div>\n )}\n\n {selectedApplication && (\n <div className=\"rounded-md border bg-bg-sunken/40 px-3 py-2 text-sm text-text-secondary\">\n {tr(\n \"devportal.createWebhook.creatingFor\",\n \"Creating webhook for\",\n )}{\" \"}\n <span className=\"font-medium text-text-primary\">\n {selectedApplication.name}\n </span>\n </div>\n )}\n\n <InputField\n label={tr(\n \"devportal.createWebhook.webhookName\",\n \"Webhook Name\",\n )}\n value={formData.name}\n onChange={(changeEvent) =>\n handleChange(\"name\", changeEvent.target.value)\n }\n placeholder={tr(\n \"devportal.createWebhook.webhookNamePlaceholder\",\n \"Production webhook\",\n )}\n error={errors.name}\n required\n />\n\n <TextareaField\n label={tr(\n \"devportal.createWebhook.descriptionLabel\",\n \"Description\",\n )}\n value={formData.description}\n onChange={(changeEvent) =>\n handleChange(\"description\", changeEvent.target.value)\n }\n placeholder={tr(\n \"devportal.createWebhook.descriptionPlaceholder\",\n \"What this webhook is used for\",\n )}\n rows={2}\n />\n\n <InputField\n label={tr(\n \"devportal.createWebhook.endpointUrl\",\n \"Endpoint URL\",\n )}\n type=\"url\"\n value={formData.url}\n onChange={(changeEvent) =>\n handleChange(\"url\", changeEvent.target.value)\n }\n placeholder={tr(\n \"devportal.createWebhook.endpointPlaceholder\",\n \"https://your-app.example.com/webhooks/burdenoff\",\n )}\n hint={tr(\"devportal.createWebhook.httpsOnly\", \"HTTPS only\")}\n error={errors.url}\n required\n />\n </div>\n </PageSection>\n\n <PageSection\n title={tr(\n \"devportal.createWebhook.subscribedEvents\",\n \"Subscribed Events\",\n )}\n >\n <div className=\"rounded-card border border-border-seam bg-bg-surface p-4\">\n {errors.events && (\n <p className=\"mb-3 text-sm text-status-error-text\" role=\"alert\">\n {errors.events}\n </p>\n )}\n <ShowMore\n collapsedHeight={320}\n label={tr(\n \"devportal.createWebhook.showAllEvents\",\n \"Show all event categories\",\n )}\n >\n <div className=\"space-y-4\">\n {Object.entries(eventCategories).map(([category, events]) => (\n <section\n key={category}\n className=\"space-y-3 border-b border-border-seam pb-4 last:border-b-0 last:pb-0\"\n >\n <div className=\"flex items-center justify-between gap-3\">\n <h3 className=\"font-medium\">\n {tr(\n `devportal.createWebhook.category.${category.toLowerCase()}`,\n category,\n )}\n </h3>\n <button\n type=\"button\"\n onClick={() => toggleCategory(events)}\n className=\"text-xs font-medium text-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n >\n {events.every((item) =>\n formData.events.includes(item),\n )\n ? tr(\n \"devportal.createWebhook.deselectAll\",\n \"Deselect all\",\n )\n : tr(\n \"devportal.createWebhook.selectAll\",\n \"Select all\",\n )}\n </button>\n </div>\n <div className=\"grid gap-2 sm:grid-cols-2\">\n {events.map((eventName) => (\n <label\n key={eventName}\n className=\"flex items-center gap-2 rounded-md border border-border-seam bg-bg-surface px-3 py-2 text-sm\"\n >\n <input\n type=\"checkbox\"\n checked={formData.events.includes(eventName)}\n onChange={() => toggleEvent(eventName)}\n className=\"rounded border-border-default text-primary focus:ring-primary\"\n aria-label={eventName}\n />\n <code className=\"text-xs text-text-primary\">\n {eventName}\n </code>\n </label>\n ))}\n </div>\n </section>\n ))}\n </div>\n </ShowMore>\n </div>\n </PageSection>\n\n <PageSection\n title={tr(\n \"devportal.createWebhook.retrySettings\",\n \"Retry Settings\",\n )}\n >\n <div className=\"space-y-4 rounded-card border border-border-seam bg-bg-surface p-4\">\n <CheckboxField\n label={tr(\n \"devportal.createWebhook.enableRetries\",\n \"Enable retries\",\n )}\n description={tr(\n \"devportal.createWebhook.enableRetriesDesc\",\n \"Retry failed deliveries automatically\",\n )}\n checked={formData.retryEnabled}\n onChange={(changeEvent) =>\n handleChange(\"retryEnabled\", changeEvent.target.checked)\n }\n />\n\n {formData.retryEnabled && (\n <InputField\n label={tr(\n \"devportal.createWebhook.maxRetries\",\n \"Maximum retries\",\n )}\n type=\"number\"\n value={formData.maxRetries}\n onChange={(changeEvent) =>\n handleChange(\"maxRetries\", Number(changeEvent.target.value))\n }\n min={1}\n max={10}\n hint={tr(\n \"devportal.createWebhook.attemptsHint\",\n \"1 to 10 attempts\",\n )}\n error={errors.maxRetries}\n />\n )}\n </div>\n </PageSection>\n\n <div className=\"flex flex-col gap-3 sm:flex-row\">\n <button\n type=\"button\"\n onClick={() =>\n navigate?.(\n `${basePath}${formData.applicationId ? `/apps/${formData.applicationId}/webhooks` : \"/webhooks\"}`,\n )\n }\n className=\"rounded-md border px-4 py-2 text-sm font-medium hover:bg-accent focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n >\n {tr(\"devportal.common.cancel\", \"Cancel\")}\n </button>\n <button\n type=\"submit\"\n disabled={isSubmitting || appsLoading || !createPerm.allowed}\n aria-disabled={isSubmitting || appsLoading || !createPerm.allowed}\n title={\n !createPerm.allowed\n ? tr(\n \"devportal.common.noPermission\",\n \"You do not have permission to perform this action\",\n )\n : undefined\n }\n className=\"inline-flex items-center justify-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n >\n {isSubmitting ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" aria-hidden=\"true\" />\n ) : (\n <Webhook size={16} aria-hidden=\"true\" />\n )}\n {isSubmitting\n ? tr(\"devportal.createWebhook.creating\", \"Creating...\")\n : tr(\"devportal.createWebhook.title\", \"Create Webhook\")}\n </button>\n </div>\n </form>\n </PageLayout>\n </PermissionGuard>\n );\n};\n"],"mappings":"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"}
|
|
@@ -170,7 +170,7 @@ var b = () => {
|
|
|
170
170
|
}), /* @__PURE__ */ g("select", {
|
|
171
171
|
value: E,
|
|
172
172
|
onChange: (e) => D(e.target.value),
|
|
173
|
-
className: "rounded-md border bg-bg-surface px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary",
|
|
173
|
+
className: "rounded-md border bg-bg-surface px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary",
|
|
174
174
|
children: [
|
|
175
175
|
/* @__PURE__ */ h("option", {
|
|
176
176
|
value: "",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OAuthClientsPage.js","names":[],"sources":["../../../src/devportal/pages/OAuthClientsPage.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { useState } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport { Plus, Search, Shield, Loader2, AlertCircle } from \"lucide-react\";\nimport { EmphasisPanel, GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../components/PageLayout\";\nimport { DataTable } from \"../components/DataTable\";\nimport { ActiveIndicator } from \"../components/StatusBadge\";\nimport { EmptyState } from \"../components/EmptyState\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\nimport { PermissionGuard } from \"../components\";\nimport type { AuthApp } from \"../types\";\nimport { cn } from \"../utils/cn\";\nimport { useCurrentUserAppsQuery, getErrorMessage } from \"../hooks\";\n\n/**\n * OAuth Apps listing page\n *\n * Displays the current user's OAuth apps from auth-svc.\n * Previously showed devportal-svc OAuthClients per application;\n * now shows all user-owned apps centrally.\n */\nexport const OAuthClientsPage: FC = () => {\n const { navigate, basePath } = useDevportal();\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n const [searchQuery, setSearchQuery] = useState(\"\");\n const [typeFilter, setTypeFilter] = useState<string>(\"\");\n\n // Fetch apps from auth-svc (user-scoped, no applicationId needed)\n const { data, loading, error, refetch } = useCurrentUserAppsQuery({\n fetchPolicy: \"cache-and-network\",\n });\n\n const apps: AuthApp[] =\n data?.currentUserApps?.apps.map(\n (app: (typeof data.currentUserApps.apps)[number]) => ({\n id: app.id,\n name: app.name,\n description: app.description ?? undefined,\n clientId: app.clientId,\n type: app.type as AuthApp[\"type\"],\n status: app.status as AuthApp[\"status\"],\n scopes: app.scopes ?? [],\n redirectUris: app.redirectUris ?? [],\n ownerUserId: app.ownerUserId,\n tenantId: app.tenantId ?? \"\",\n createdAt: app.createdAt,\n updatedAt: app.updatedAt,\n }),\n ) ?? [];\n\n const handleNavigate = (path: string) => {\n navigate?.(`${basePath}${path}`);\n };\n\n // Filter apps\n const filteredApps = apps.filter((app) => {\n const matchesSearch =\n !searchQuery ||\n app.name.toLowerCase().includes(searchQuery.toLowerCase()) ||\n app.clientId.toLowerCase().includes(searchQuery.toLowerCase());\n const matchesType = !typeFilter || app.type === typeFilter;\n return matchesSearch && matchesType;\n });\n\n // Loading state\n if (loading && !data) {\n return (\n <PageLayout\n title={tr(\"devportal.oauthApps\", \"OAuth Apps\")}\n description={tr(\n \"devportal.manageOAuthApps\",\n \"Manage your OAuth applications and credentials\",\n )}\n >\n <div className=\"flex items-center justify-center py-12\">\n <Loader2 className=\"h-8 w-8 animate-spin text-text-secondary\" />\n <span className=\"ml-2 text-text-secondary\">\n {tr(\"devportal.loadingApps\", \"Loading apps...\")}\n </span>\n </div>\n </PageLayout>\n );\n }\n\n // Error state\n if (error) {\n return (\n <PageLayout\n title={tr(\"devportal.oauthApps\", \"OAuth Apps\")}\n description={tr(\n \"devportal.manageOAuthApps\",\n \"Manage your OAuth applications and credentials\",\n )}\n >\n <div className=\"rounded-lg border border-status-error-border/50 bg-status-error-bg/10 p-6 text-center\">\n <AlertCircle className=\"mx-auto h-8 w-8 text-status-error-text\" />\n <h3 className=\"mt-2 font-medium text-status-error-text\">\n {tr(\"devportal.failedToLoadApps\", \"Failed to load apps\")}\n </h3>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {getErrorMessage(error)}\n </p>\n <button\n onClick={() => refetch()}\n className=\"mt-4 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n {tr(\"devportal.tryAgain\", \"Try Again\")}\n </button>\n </div>\n </PageLayout>\n );\n }\n\n const columns = [\n {\n key: \"name\",\n header: \"App\",\n render: (app: AuthApp) => (\n <div className=\"flex items-center gap-3\">\n <div\n className={cn(\n \"flex h-10 w-10 items-center justify-center rounded-lg\",\n app.status === \"ACTIVE\"\n ? \"bg-action-primary-bg/10 text-primary\"\n : \"bg-bg-sunken text-text-tertiary\",\n )}\n >\n <Shield size={20} />\n </div>\n <div>\n <div className=\"flex items-center gap-2\">\n <p className=\"font-medium\">{app.name}</p>\n <span\n className={cn(\n \"rounded px-1.5 py-0.5 text-xs\",\n app.type === \"SERVICE\"\n ? \"bg-accent-purple-subtle text-accent-purple\"\n : app.type === \"INTEGRATION\" ||\n app.type === \"API_INTEGRATION\"\n ? \"bg-status-info-bg-subtle text-status-info-text\"\n : \"bg-status-success-bg-subtle text-status-success-text\",\n )}\n >\n {app.type}\n </span>\n </div>\n <code className=\"text-xs text-text-secondary\">{app.clientId}</code>\n </div>\n </div>\n ),\n },\n {\n key: \"scopes\",\n header: \"Scopes\",\n render: (app: AuthApp) => (\n <div className=\"flex flex-wrap gap-1\">\n {app.scopes.slice(0, 3).map((scope) => (\n <span\n key={scope}\n className=\"inline-flex rounded bg-bg-sunken px-1.5 py-0.5 text-xs\"\n >\n {scope}\n </span>\n ))}\n {app.scopes.length > 3 && (\n <span className=\"text-xs text-text-secondary\">\n +{app.scopes.length - 3}\n </span>\n )}\n </div>\n ),\n },\n {\n key: \"redirectUris\",\n header: \"Redirect URIs\",\n render: (app: AuthApp) => (\n <span className=\"text-sm\">\n {app.redirectUris.length} URI\n {app.redirectUris.length !== 1 ? \"s\" : \"\"}\n </span>\n ),\n },\n {\n key: \"status\",\n header: \"Status\",\n render: (app: AuthApp) => (\n <div className=\"flex items-center gap-2\">\n <ActiveIndicator active={app.status === \"ACTIVE\"} />\n <span className=\"text-sm\">{app.status}</span>\n </div>\n ),\n },\n ];\n\n // Stats\n const stats = {\n total: apps.length,\n active: apps.filter((a) => a.status === \"ACTIVE\").length,\n integration: apps.filter(\n (a) => a.type === \"INTEGRATION\" || a.type === \"API_INTEGRATION\",\n ).length,\n service: apps.filter((a) => a.type === \"SERVICE\").length,\n };\n\n return (\n <PermissionGuard authOnly>\n <PageLayout\n title={tr(\"devportal.oauthApps\", \"OAuth Apps\")}\n description={tr(\n \"devportal.manageOAuthApps\",\n \"Manage your OAuth applications and credentials\",\n )}\n purpose={tr(\n \"devportal.oauthAppsPurpose\",\n \"OAuth apps let external services sign in users and call the API on their behalf. Register one here to get a client ID and secret, set redirect URIs, and choose the scopes your integration may request.\",\n )}\n actions={\n <button\n onClick={() => handleNavigate(\"/oauth/new\")}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 transition-colors\"\n >\n <Plus size={16} />\n {tr(\"devportal.createOAuthApp\", \"Create OAuth App\")}\n </button>\n }\n >\n {/* Filters */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex flex-1 items-center gap-4\">\n <div className=\"relative flex-1 max-w-md\">\n <Search\n size={16}\n className=\"absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary\"\n />\n <input\n type=\"text\"\n placeholder={tr(\n \"devportal.searchByNameOrClientId\",\n \"Search by name or client ID...\",\n )}\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n className=\"w-full rounded-md border bg-bg-surface pl-9 pr-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary\"\n />\n </div>\n <select\n value={typeFilter}\n onChange={(e) => setTypeFilter(e.target.value)}\n className=\"rounded-md border bg-bg-surface px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary\"\n >\n <option value=\"\">{tr(\"devportal.allTypes\", \"All Types\")}</option>\n <option value=\"SERVICE\">\n {tr(\"devportal.service\", \"Service\")}\n </option>\n <option value=\"INTEGRATION\">\n {tr(\"devportal.integration\", \"Integration\")}\n </option>\n <option value=\"BOT\">{tr(\"devportal.bot\", \"Bot\")}</option>\n <option value=\"WEB_APP\">\n {tr(\"devportal.webApp\", \"Web App\")}\n </option>\n <option value=\"AUTOMATION\">\n {tr(\"devportal.automation\", \"Automation\")}\n </option>\n </select>\n </div>\n </div>\n\n {/* Summary Stats — primary emphasis zone */}\n <EmphasisPanel>\n <div className=\"grid grid-cols-2 gap-4 sm:grid-cols-4\">\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <Shield size={16} />\n <span className=\"text-sm\">\n {tr(\"devportal.totalApps\", \"Total Apps\")}\n </span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums\">\n {stats.total}\n </p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-status-success-text\">\n <Shield size={16} />\n <span className=\"text-sm\">\n {tr(\"devportal.active\", \"Active\")}\n </span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums\">\n {stats.active}\n </p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-accent-blue\">\n <Shield size={16} />\n <span className=\"text-sm\">\n {tr(\"devportal.integration\", \"Integration\")}\n </span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums\">\n {stats.integration}\n </p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-accent-purple\">\n <Shield size={16} />\n <span className=\"text-sm\">\n {tr(\"devportal.service\", \"Service\")}\n </span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums\">\n {stats.service}\n </p>\n </GlassCard>\n </div>\n </EmphasisPanel>\n\n <p className=\"text-sm text-text-secondary\">\n {filteredApps.length}{\" \"}\n {filteredApps.length !== 1\n ? tr(\"devportal.appsFound\", \"apps found\")\n : tr(\"devportal.appFound\", \"app found\")}\n </p>\n\n {/* Apps Table */}\n {filteredApps.length > 0 ? (\n <DataTable\n data={filteredApps}\n columns={columns}\n keyField=\"id\"\n onRowClick={(app) => handleNavigate(`/oauth/${app.id}`)}\n />\n ) : (\n <EmptyState\n illustration=\"empty-generic\"\n title={tr(\"devportal.noOAuthAppsFound\", \"No OAuth apps found\")}\n description={\n searchQuery || typeFilter\n ? tr(\n \"devportal.tryAdjustingFilters\",\n \"Try adjusting your filters\",\n )\n : tr(\n \"devportal.createFirstOAuthApp\",\n \"Create your first OAuth app to enable authentication\",\n )\n }\n action={\n !searchQuery && !typeFilter ? (\n <button\n onClick={() => handleNavigate(\"/oauth/new\")}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus size={16} />\n {tr(\"devportal.createOAuthApp\", \"Create OAuth App\")}\n </button>\n ) : undefined\n }\n />\n )}\n </PageLayout>\n </PermissionGuard>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"OAuthClientsPage.js","names":[],"sources":["../../../src/devportal/pages/OAuthClientsPage.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { useState } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport { Plus, Search, Shield, Loader2, AlertCircle } from \"lucide-react\";\nimport { EmphasisPanel, GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../components/PageLayout\";\nimport { DataTable } from \"../components/DataTable\";\nimport { ActiveIndicator } from \"../components/StatusBadge\";\nimport { EmptyState } from \"../components/EmptyState\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\nimport { PermissionGuard } from \"../components\";\nimport type { AuthApp } from \"../types\";\nimport { cn } from \"../utils/cn\";\nimport { useCurrentUserAppsQuery, getErrorMessage } from \"../hooks\";\n\n/**\n * OAuth Apps listing page\n *\n * Displays the current user's OAuth apps from auth-svc.\n * Previously showed devportal-svc OAuthClients per application;\n * now shows all user-owned apps centrally.\n */\nexport const OAuthClientsPage: FC = () => {\n const { navigate, basePath } = useDevportal();\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n const [searchQuery, setSearchQuery] = useState(\"\");\n const [typeFilter, setTypeFilter] = useState<string>(\"\");\n\n // Fetch apps from auth-svc (user-scoped, no applicationId needed)\n const { data, loading, error, refetch } = useCurrentUserAppsQuery({\n fetchPolicy: \"cache-and-network\",\n });\n\n const apps: AuthApp[] =\n data?.currentUserApps?.apps.map(\n (app: (typeof data.currentUserApps.apps)[number]) => ({\n id: app.id,\n name: app.name,\n description: app.description ?? undefined,\n clientId: app.clientId,\n type: app.type as AuthApp[\"type\"],\n status: app.status as AuthApp[\"status\"],\n scopes: app.scopes ?? [],\n redirectUris: app.redirectUris ?? [],\n ownerUserId: app.ownerUserId,\n tenantId: app.tenantId ?? \"\",\n createdAt: app.createdAt,\n updatedAt: app.updatedAt,\n }),\n ) ?? [];\n\n const handleNavigate = (path: string) => {\n navigate?.(`${basePath}${path}`);\n };\n\n // Filter apps\n const filteredApps = apps.filter((app) => {\n const matchesSearch =\n !searchQuery ||\n app.name.toLowerCase().includes(searchQuery.toLowerCase()) ||\n app.clientId.toLowerCase().includes(searchQuery.toLowerCase());\n const matchesType = !typeFilter || app.type === typeFilter;\n return matchesSearch && matchesType;\n });\n\n // Loading state\n if (loading && !data) {\n return (\n <PageLayout\n title={tr(\"devportal.oauthApps\", \"OAuth Apps\")}\n description={tr(\n \"devportal.manageOAuthApps\",\n \"Manage your OAuth applications and credentials\",\n )}\n >\n <div className=\"flex items-center justify-center py-12\">\n <Loader2 className=\"h-8 w-8 animate-spin text-text-secondary\" />\n <span className=\"ml-2 text-text-secondary\">\n {tr(\"devportal.loadingApps\", \"Loading apps...\")}\n </span>\n </div>\n </PageLayout>\n );\n }\n\n // Error state\n if (error) {\n return (\n <PageLayout\n title={tr(\"devportal.oauthApps\", \"OAuth Apps\")}\n description={tr(\n \"devportal.manageOAuthApps\",\n \"Manage your OAuth applications and credentials\",\n )}\n >\n <div className=\"rounded-lg border border-status-error-border/50 bg-status-error-bg/10 p-6 text-center\">\n <AlertCircle className=\"mx-auto h-8 w-8 text-status-error-text\" />\n <h3 className=\"mt-2 font-medium text-status-error-text\">\n {tr(\"devportal.failedToLoadApps\", \"Failed to load apps\")}\n </h3>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {getErrorMessage(error)}\n </p>\n <button\n onClick={() => refetch()}\n className=\"mt-4 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n {tr(\"devportal.tryAgain\", \"Try Again\")}\n </button>\n </div>\n </PageLayout>\n );\n }\n\n const columns = [\n {\n key: \"name\",\n header: \"App\",\n render: (app: AuthApp) => (\n <div className=\"flex items-center gap-3\">\n <div\n className={cn(\n \"flex h-10 w-10 items-center justify-center rounded-lg\",\n app.status === \"ACTIVE\"\n ? \"bg-action-primary-bg/10 text-primary\"\n : \"bg-bg-sunken text-text-tertiary\",\n )}\n >\n <Shield size={20} />\n </div>\n <div>\n <div className=\"flex items-center gap-2\">\n <p className=\"font-medium\">{app.name}</p>\n <span\n className={cn(\n \"rounded px-1.5 py-0.5 text-xs\",\n app.type === \"SERVICE\"\n ? \"bg-accent-purple-subtle text-accent-purple\"\n : app.type === \"INTEGRATION\" ||\n app.type === \"API_INTEGRATION\"\n ? \"bg-status-info-bg-subtle text-status-info-text\"\n : \"bg-status-success-bg-subtle text-status-success-text\",\n )}\n >\n {app.type}\n </span>\n </div>\n <code className=\"text-xs text-text-secondary\">{app.clientId}</code>\n </div>\n </div>\n ),\n },\n {\n key: \"scopes\",\n header: \"Scopes\",\n render: (app: AuthApp) => (\n <div className=\"flex flex-wrap gap-1\">\n {app.scopes.slice(0, 3).map((scope) => (\n <span\n key={scope}\n className=\"inline-flex rounded bg-bg-sunken px-1.5 py-0.5 text-xs\"\n >\n {scope}\n </span>\n ))}\n {app.scopes.length > 3 && (\n <span className=\"text-xs text-text-secondary\">\n +{app.scopes.length - 3}\n </span>\n )}\n </div>\n ),\n },\n {\n key: \"redirectUris\",\n header: \"Redirect URIs\",\n render: (app: AuthApp) => (\n <span className=\"text-sm\">\n {app.redirectUris.length} URI\n {app.redirectUris.length !== 1 ? \"s\" : \"\"}\n </span>\n ),\n },\n {\n key: \"status\",\n header: \"Status\",\n render: (app: AuthApp) => (\n <div className=\"flex items-center gap-2\">\n <ActiveIndicator active={app.status === \"ACTIVE\"} />\n <span className=\"text-sm\">{app.status}</span>\n </div>\n ),\n },\n ];\n\n // Stats\n const stats = {\n total: apps.length,\n active: apps.filter((a) => a.status === \"ACTIVE\").length,\n integration: apps.filter(\n (a) => a.type === \"INTEGRATION\" || a.type === \"API_INTEGRATION\",\n ).length,\n service: apps.filter((a) => a.type === \"SERVICE\").length,\n };\n\n return (\n <PermissionGuard authOnly>\n <PageLayout\n title={tr(\"devportal.oauthApps\", \"OAuth Apps\")}\n description={tr(\n \"devportal.manageOAuthApps\",\n \"Manage your OAuth applications and credentials\",\n )}\n purpose={tr(\n \"devportal.oauthAppsPurpose\",\n \"OAuth apps let external services sign in users and call the API on their behalf. Register one here to get a client ID and secret, set redirect URIs, and choose the scopes your integration may request.\",\n )}\n actions={\n <button\n onClick={() => handleNavigate(\"/oauth/new\")}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 transition-colors\"\n >\n <Plus size={16} />\n {tr(\"devportal.createOAuthApp\", \"Create OAuth App\")}\n </button>\n }\n >\n {/* Filters */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex flex-1 items-center gap-4\">\n <div className=\"relative flex-1 max-w-md\">\n <Search\n size={16}\n className=\"absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary\"\n />\n <input\n type=\"text\"\n placeholder={tr(\n \"devportal.searchByNameOrClientId\",\n \"Search by name or client ID...\",\n )}\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n className=\"w-full rounded-md border bg-bg-surface pl-9 pr-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary\"\n />\n </div>\n <select\n value={typeFilter}\n onChange={(e) => setTypeFilter(e.target.value)}\n className=\"rounded-md border bg-bg-surface px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary\"\n >\n <option value=\"\">{tr(\"devportal.allTypes\", \"All Types\")}</option>\n <option value=\"SERVICE\">\n {tr(\"devportal.service\", \"Service\")}\n </option>\n <option value=\"INTEGRATION\">\n {tr(\"devportal.integration\", \"Integration\")}\n </option>\n <option value=\"BOT\">{tr(\"devportal.bot\", \"Bot\")}</option>\n <option value=\"WEB_APP\">\n {tr(\"devportal.webApp\", \"Web App\")}\n </option>\n <option value=\"AUTOMATION\">\n {tr(\"devportal.automation\", \"Automation\")}\n </option>\n </select>\n </div>\n </div>\n\n {/* Summary Stats — primary emphasis zone */}\n <EmphasisPanel>\n <div className=\"grid grid-cols-2 gap-4 sm:grid-cols-4\">\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <Shield size={16} />\n <span className=\"text-sm\">\n {tr(\"devportal.totalApps\", \"Total Apps\")}\n </span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums\">\n {stats.total}\n </p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-status-success-text\">\n <Shield size={16} />\n <span className=\"text-sm\">\n {tr(\"devportal.active\", \"Active\")}\n </span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums\">\n {stats.active}\n </p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-accent-blue\">\n <Shield size={16} />\n <span className=\"text-sm\">\n {tr(\"devportal.integration\", \"Integration\")}\n </span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums\">\n {stats.integration}\n </p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-accent-purple\">\n <Shield size={16} />\n <span className=\"text-sm\">\n {tr(\"devportal.service\", \"Service\")}\n </span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums\">\n {stats.service}\n </p>\n </GlassCard>\n </div>\n </EmphasisPanel>\n\n <p className=\"text-sm text-text-secondary\">\n {filteredApps.length}{\" \"}\n {filteredApps.length !== 1\n ? tr(\"devportal.appsFound\", \"apps found\")\n : tr(\"devportal.appFound\", \"app found\")}\n </p>\n\n {/* Apps Table */}\n {filteredApps.length > 0 ? (\n <DataTable\n data={filteredApps}\n columns={columns}\n keyField=\"id\"\n onRowClick={(app) => handleNavigate(`/oauth/${app.id}`)}\n />\n ) : (\n <EmptyState\n illustration=\"empty-generic\"\n title={tr(\"devportal.noOAuthAppsFound\", \"No OAuth apps found\")}\n description={\n searchQuery || typeFilter\n ? tr(\n \"devportal.tryAdjustingFilters\",\n \"Try adjusting your filters\",\n )\n : tr(\n \"devportal.createFirstOAuthApp\",\n \"Create your first OAuth app to enable authentication\",\n )\n }\n action={\n !searchQuery && !typeFilter ? (\n <button\n onClick={() => handleNavigate(\"/oauth/new\")}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus size={16} />\n {tr(\"devportal.createOAuthApp\", \"Create OAuth App\")}\n </button>\n ) : undefined\n }\n />\n )}\n </PageLayout>\n </PermissionGuard>\n );\n};\n"],"mappings":"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"}
|
|
@@ -354,7 +354,7 @@ var _ = {
|
|
|
354
354
|
}), e.role === "OWNER" || !H ? /* @__PURE__ */ h("span", { children: a[e.role] ?? e.role }) : /* @__PURE__ */ h("select", {
|
|
355
355
|
value: e.role,
|
|
356
356
|
onChange: (t) => void ut(e.id, t.target.value),
|
|
357
|
-
className: "rounded-md border border-border-subtle bg-bg-surface px-2 py-1 text-xs text-text-primary",
|
|
357
|
+
className: "rounded-md border border-border-subtle bg-bg-surface px-2 py-1 text-xs text-text-primary scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary",
|
|
358
358
|
children: Object.entries(a).filter(([e]) => e !== "OWNER").map(([e, t]) => /* @__PURE__ */ h("option", {
|
|
359
359
|
value: e,
|
|
360
360
|
children: t
|