diff --git a/package.json b/package.json
index c59357a..fc05a1d 100644
--- a/package.json
+++ b/package.json
@@ -86,4 +86,4 @@
"*"
]
}
-}
\ No newline at end of file
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9087243..513a4cb 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -6,9 +6,6 @@ importers:
.:
configDependencies: {}
packageManagerDependencies:
- '@pnpm/exe':
- specifier: 12.3.4
- version: 12.3.4
pnpm:
specifier: 12.3.4
version: 12.3.4
@@ -59,11 +56,6 @@ packages:
cpu: [x64]
os: [win32]
- '@pnpm/exe@12.3.4':
- resolution: {integrity: sha512-Rq7JokWAyYF9IFfn8zofB/q2AgnZlmoMuH5fMHB2+Ta61I9aD7sNm+woZpx6iH+5vb38chdSoYRvOtSqlH72YQ==}
- engines: {node: '>=18.*'}
- hasBin: true
-
pnpm@12.3.4:
resolution: {integrity: sha512-lhqkH7B32joEpEHZ+OFevAyW2o73ELLrZ7+e58sGEOq9SPH9hfUc/+c4RnhfoPh8VqOocqHYk/hEZ0G1zORUVw==}
engines: {node: '>=18.*'}
@@ -95,17 +87,6 @@ snapshots:
'@pnpm/exe.win32-x64@12.3.4':
optional: true
- '@pnpm/exe@12.3.4':
- optionalDependencies:
- '@pnpm/exe.darwin-arm64': 12.3.4
- '@pnpm/exe.darwin-x64': 12.3.4
- '@pnpm/exe.linux-arm64': 12.3.4
- '@pnpm/exe.linux-arm64-musl': 12.3.4
- '@pnpm/exe.linux-x64': 12.3.4
- '@pnpm/exe.linux-x64-musl': 12.3.4
- '@pnpm/exe.win32-arm64': 12.3.4
- '@pnpm/exe.win32-x64': 12.3.4
-
pnpm@12.3.4:
optionalDependencies:
'@pnpm/exe.darwin-arm64': 12.3.4
diff --git a/src/components/rbac/catalog-search.tsx b/src/components/rbac/catalog-search.tsx
index a5f67a6..2ea49df 100644
--- a/src/components/rbac/catalog-search.tsx
+++ b/src/components/rbac/catalog-search.tsx
@@ -82,7 +82,7 @@ export function CatalogSearch({
className="flex-1 basis-56 sm:max-w-lg"
/>
- {query.trim() ? `${count} of ${total}` : total} {kind}
+ {query.trim() || count !== total ? `${count} of ${total}` : total} {kind}
{children}
diff --git a/src/routes/access/permissions/index.tsx b/src/routes/access/permissions/index.tsx
index 4760548..9cd0672 100644
--- a/src/routes/access/permissions/index.tsx
+++ b/src/routes/access/permissions/index.tsx
@@ -16,6 +16,34 @@ export const Route = createFileRoute("/access/permissions/")({
component: PermissionsIndex,
});
+function applicationKey(permission: PermissionSummary) {
+ return permission.key.split(":")[0];
+}
+
+function applicationLabel(key: string) {
+ if (key === "idp") return "PoliNetwork Auth";
+ if (key === "tg") return "Telegram";
+ if (key === "wa") return "WhatsApp";
+ if (key === "web") return "Website";
+ if (key === "azure") return "Microsoft Entra";
+ return key.charAt(0).toUpperCase() + key.slice(1);
+}
+
+/** Labels for each application, suffixed with the key where two would otherwise read the same. */
+function applicationLabels(keys: string[]) {
+ const counts = new Map();
+ for (const key of keys) {
+ const label = applicationLabel(key);
+ counts.set(label, (counts.get(label) ?? 0) + 1);
+ }
+ return new Map(
+ keys.map((key) => {
+ const label = applicationLabel(key);
+ return [key, (counts.get(label) ?? 0) > 1 ? `${label} (${key})` : label];
+ }),
+ );
+}
+
function PermissionRow({
permission,
implied,
@@ -40,6 +68,9 @@ function PermissionRow({
{permission.name}
+ {permission.managed && (
+
Built-in
+ )}
{implied.length > 0 && (
Also grants {implied.length}
)}
@@ -63,16 +94,30 @@ function PermissionsIndex() {
const { can } = useAccess();
const catalog = Route.useLoaderData();
const [query, setQuery] = useState("");
+ const [application, setApplication] = useState
(null);
+ const applications = [...new Set(catalog.permissions.map(applicationKey))].sort();
+ const labels = applicationLabels(applications);
const search = query.trim().toLowerCase();
- const filtered = catalog.permissions.filter((permission) =>
- [permission.name, permission.key, permission.description].some((value) =>
- value?.toLowerCase().includes(search),
- ),
+ const filtered = catalog.permissions.filter(
+ (permission) =>
+ (application === null || applicationKey(permission) === application) &&
+ [
+ permission.name,
+ permission.key,
+ permission.description,
+ labels.get(applicationKey(permission)),
+ ].some((value) => value?.toLowerCase().includes(search)),
);
const impliedBy = (permission: PermissionSummary) =>
expandPermissionKeys(catalog, permission.implies).filter((key) => key !== permission.key);
- const managed = filtered.filter((permission) => permission.managed);
- const custom = filtered.filter((permission) => !permission.managed);
+ const groups = applications
+ .map((key) => ({
+ key,
+ permissions: filtered
+ .filter((permission) => applicationKey(permission) === key)
+ .sort((a, b) => a.key.localeCompare(b.key)),
+ }))
+ .filter((group) => group.permissions.length > 0);
return (
@@ -103,85 +148,90 @@ function PermissionsIndex() {
)}
- {search && filtered.length === 0 &&
}
+ {applications.length > 0 && (
+
+ setApplication(null)}
+ >
+ All applications
+
+ {applications.map((key) => (
+ setApplication(key)}
+ >
+ {labels.get(key)}
+
+ {
+ catalog.permissions.filter((permission) => applicationKey(permission) === key)
+ .length
+ }
+
+
+ ))}
+
+ )}
- {
-
- {(managed.length > 0 || !search) && (
-
-
- Built into the identity provider
+ {filtered.length === 0 &&
+ (search ? (
+
+ ) : (
+
+
+
+
+
No permissions yet
+
+ Create a permission for something one of your applications should be able to
+ check.
+
+
+
+
+ ))}
+
+
+ {groups.map(({ key, permissions }) => (
+
+
+
+ {labels.get(key)}
+
+ {permissions.some((permission) => permission.key.includes(":"))
+ ? `${key}:*`
+ : key}
+
+
+ {permissions.length} {permissions.length === 1 ? "permission" : "permissions"}
+
+
+ {key === "idp" && (
- These permissions control this service itself. They always exist and cannot be
- deleted, because the code checks for these exact keys. Give them to a role to let
- someone administer part of PoliNetwork Auth.
+ Permissions for administering PoliNetwork Auth. Built-in permissions always exist
+ and cannot be deleted.
-
-
- {managed.length === 0 ? (
-
- No built-in permissions match your search.
-
- ) : (
-
- {managed.map((permission) => (
-
- ))}
-
- )}
-
-
- )}
-
- {(custom.length > 0 || !search) && (
-
-
- Permissions you created
-
- {custom.length === 0 ? (
-
-
-
-
-
-
-
No permissions of your own yet
-
- Name the first thing one of your applications should be able to check for,
- such as reading the member list.
-
-
- {can("idp:permissions:write") && (
-
-
-
- New permission
-
-
- )}
-
-
- ) : (
-
-
- {custom.map((permission) => (
-
- ))}
-
-
)}
-
- )}
-
- }
+
+
+
+ {permissions.map((permission) => (
+
+ ))}
+
+
+
+ ))}
+
);
}