Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/app-frontend/src/shell/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Page } from '@patternfly/react-core';

import ErrorBoundary from '@osac/ui-components/components/ErrorBoundary/ErrorBoundary';
import IdentityProviderRoutes from '@osac/ui-components/components/IdentityProvider/IdentityProviderRoutes';
import RoleBindingRoutes from '@osac/ui-components/components/RoleBinding/RoleBindingRoutes';
import { VmDetailsPage } from '@osac/ui-components/components/vm/VmDetailsPage';
import { useSession } from '@osac/ui-components/hooks/use-session';
import { SecurityGroupDetailPage } from '@osac/ui-components/pages/networking/SecurityGroupDetailPage';
Expand Down Expand Up @@ -73,6 +74,14 @@ export const AppShell = ({ logout }: { logout: () => Promise<void> }) => {
</ShellRoute>
}
/>
<Route
path="/tenant/role-binding/*"
element={
<ShellRoute>
<RoleBindingRoutes />
</ShellRoute>
}
/>
<Route
path="/vms"
element={
Expand Down
11 changes: 7 additions & 4 deletions apps/app-frontend/src/shell/shellNav.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,13 @@ describe('navRowsForRole', () => {
}
});

it('IDP administration shows up only for idp manager', () => {
expect(findSection('tenant-idp-manager', 'nav-tenant-administration')).toBeDefined();
for (const role of ['tenant-user', 'tenant-admin', 'admin'] as UserRole[]) {
expect(findSection(role, 'nav-tenant-administration')).toBeUndefined();
it('Tenant section shows up for admin, tenant-admin, and tenant-idp-manager', () => {
for (const role of ['admin', 'tenant-admin', 'tenant-idp-manager'] as UserRole[]) {
const section = findSection(role, 'nav-tenant-administration');
expect(section).toBeDefined();
expect(section?.children.map((c) => c.id)).toContain('idp');
expect(section?.children.map((c) => c.id)).toContain('role-bindings');
}
expect(findSection('tenant-user', 'nav-tenant-administration')).toBeUndefined();
});
});
29 changes: 21 additions & 8 deletions apps/app-frontend/src/shell/shellNav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,17 @@ type NavSection = {
children: NavLink[];
};

const getIdpManagerNav = (t: TFunction): NavSection[] => [
{
kind: 'section',
sectionId: 'nav-tenant-administration',
label: t('Tenant'),
children: [{ id: 'idp', label: t('Identity providers'), path: '/tenant/identity-provider' }],
},
];
const getTenantAdminSection = (t: TFunction): NavSection => ({
kind: 'section',
sectionId: 'nav-tenant-administration',
label: t('Tenant'),
children: [
{ id: 'idp', label: t('Identity providers'), path: '/tenant/identity-provider' },
{ id: 'role-bindings', label: t('Role Bindings'), path: '/tenant/role-binding' },
],
});

const getIdpManagerNav = (t: TFunction): NavSection[] => [getTenantAdminSection(t)];

const getAdminNav = (t: TFunction): NavSection[] => [
{
Expand All @@ -45,6 +48,12 @@ const getAdminNav = (t: TFunction): NavSection[] => [
},
],
},
getTenantAdminSection(t),
...getBaseNav(t),
];

const getTenantAdminNav = (t: TFunction): NavSection[] => [
getTenantAdminSection(t),
...getBaseNav(t),
];

Expand Down Expand Up @@ -88,5 +97,9 @@ export const navRowsForRole = (role: UserRole, t: TFunction): NavSection[] => {
return getIdpManagerNav(t);
}

if (role === 'tenant-admin') {
return getTenantAdminNav(t);
}

return getBaseNav(t);
};
19 changes: 19 additions & 0 deletions libs/i18n/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"Are you sure you want to enable Identity provider {{idpName}}": "Are you sure you want to enable Identity provider {{idpName}}",
"At least one CIDR (IPv4 or IPv6) is required": "At least one CIDR (IPv4 or IPv6) is required",
"At least one node set is required": "At least one node set is required",
"At least one user is required": "At least one user is required",
"Attach": "Attach",
"Attach external IP": "Attach external IP",
"Authorization URL": "Authorization URL",
Expand Down Expand Up @@ -133,6 +134,7 @@
"Create Identity provider": "Create Identity provider",
"Create Identity provider steps": "Create Identity provider steps",
"Create instance type": "Create instance type",
"Create role binding": "Create role binding",
"Create security group": "Create security group",
"Create storage backend": "Create storage backend",
"Create storage tier": "Create storage tier",
Expand All @@ -147,6 +149,7 @@
"Defined": "Defined",
"Delete": "Delete",
"Delete {{name}}?": "Delete {{name}}?",
"Delete role binding?": "Delete role binding?",
"Delete rule": "Delete rule",
"Delete rule?": "Delete rule?",
"Delete security group": "Delete security group",
Expand Down Expand Up @@ -198,6 +201,7 @@
"Failed to attach external IP": "Failed to attach external IP",
"Failed to connect to the console.": "Failed to connect to the console.",
"Failed to create resource": "Failed to create resource",
"Failed to create role binding": "Failed to create role binding",
"Failed to create storage tier": "Failed to create storage tier",
"Failed to create subnet": "Failed to create subnet",
"Failed to create tenant": "Failed to create tenant",
Expand All @@ -207,6 +211,7 @@
"Failed to delete compute instance": "Failed to delete compute instance",
"Failed to delete Identity provider": "Failed to delete Identity provider",
"Failed to delete instance type": "Failed to delete instance type",
"Failed to delete role binding": "Failed to delete role binding",
"Failed to delete storage backend": "Failed to delete storage backend",
"Failed to delete storage tier": "Failed to delete storage tier",
"Failed to delete tenant": "Failed to delete tenant",
Expand All @@ -218,6 +223,7 @@
"Failed to fetch host types": "Failed to fetch host types",
"Failed to fetch Identity provider": "Failed to fetch Identity provider",
"Failed to fetch instance type": "Failed to fetch instance type",
"Failed to fetch role binding": "Failed to fetch role binding",
"Failed to fetch security groups": "Failed to fetch security groups",
"Failed to fetch subnet": "Failed to fetch subnet",
"Failed to fetch virtual network": "Failed to fetch virtual network",
Expand All @@ -231,6 +237,7 @@
"Failed to retrieve break-glass credentials.": "Failed to retrieve break-glass credentials.",
"Failed to start virtual machine": "Failed to start virtual machine",
"Failed to stop virtual machine": "Failed to stop virtual machine",
"Failed to update role binding": "Failed to update role binding",
"Filter bare metal instances by name": "Filter bare metal instances by name",
"Filter catalog by keyword": "Filter catalog by keyword",
"Filter catalog by resource type": "Filter catalog by resource type",
Expand Down Expand Up @@ -276,13 +283,16 @@
"Manage firewall rules for your virtual networks.": "Manage firewall rules for your virtual networks.",
"Manage identity providers for your tenant.": "Manage identity providers for your tenant.",
"Manage provider-defined instance types for this cloud platform.": "Manage provider-defined instance types for this cloud platform.",
"Manage role bindings for users.": "Manage role bindings for users.",
"Manage storage backends and tiers for this cloud platform.": "Manage storage backends and tiers for this cloud platform.",
"Manage tenants for this cloud platform.": "Manage tenants for this cloud platform.",
"Manage virtual networks for your compute instances.": "Manage virtual networks for your compute instances.",
"Max read bandwidth (MB/s)": "Max read bandwidth (MB/s)",
"Max write bandwidth (MB/s)": "Max write bandwidth (MB/s)",
"Memory (GiB)": "Memory (GiB)",
"Message": "Message",
"Multiple users": "Multiple users",
"Multiple users popover": "Multiple users popover",
"Must be a valid domain (e.g. example.com)": "Must be a valid domain (e.g. example.com)",
"Must be a valid URL (e.g. https://example.com)": "Must be a valid URL (e.g. https://example.com)",
"Must be a whole number": "Must be a whole number",
Expand Down Expand Up @@ -313,6 +323,7 @@
"No node sets configured.": "No node sets configured.",
"No outbound rules yet. Add one to allow outgoing traffic.": "No outbound rules yet. Add one to allow outgoing traffic.",
"No published catalog items are available yet.": "No published catalog items are available yet.",
"No role bindings available.": "No role bindings available.",
"No security groups match your search.": "No security groups match your search.",
"No security groups yet. Create one to get started.": "No security groups yet. Create one to get started.",
"No storage backends yet. Create one to get started.": "No storage backends yet. Create one to get started.",
Expand Down Expand Up @@ -376,6 +387,9 @@
"Restart": "Restart",
"Retry": "Retry",
"Review": "Review",
"Role": "Role",
"Role Bindings": "Role Bindings",
"Role is required": "Role is required",
"Running": "Running",
"Save": "Save",
"Save these credentials now — they cannot be retrieved later.": "Save these credentials now — they cannot be retrieved later.",
Expand Down Expand Up @@ -420,6 +434,7 @@
"TCP": "TCP",
"Tenant": "Tenant",
"Tenant admin": "Tenant admin",
"Tenant is required": "Tenant is required",
"Tenant user": "Tenant user",
"Tenant: {{ tenantId }}": "Tenant: {{ tenantId }}",
"Tenants": "Tenants",
Expand All @@ -433,6 +448,7 @@
"This permanently deletes the compute instance. This action cannot be undone.": "This permanently deletes the compute instance. This action cannot be undone.",
"This permanently deletes the Identity provider and all its resources. This action cannot be undone.": "This permanently deletes the Identity provider and all its resources. This action cannot be undone.",
"This permanently deletes the instance type. This action cannot be undone.": "This permanently deletes the instance type. This action cannot be undone.",
"This permanently deletes the role binding. Users will lose the permissions granted by this binding. This action cannot be undone.": "This permanently deletes the role binding. Users will lose the permissions granted by this binding. This action cannot be undone.",
"This permanently deletes the storage backend. This action cannot be undone.": "This permanently deletes the storage backend. This action cannot be undone.",
"This permanently deletes the storage tier. This action cannot be undone.": "This permanently deletes the storage tier. This action cannot be undone.",
"This permanently deletes the tenant and all its resources. This action cannot be undone.": "This permanently deletes the tenant and all its resources. This action cannot be undone.",
Expand All @@ -449,6 +465,8 @@
"Unauthorized": "Unauthorized",
"Unknown": "Unknown",
"Unspecified": "Unspecified",
"Update": "Update",
"Update role binding": "Update role binding",
"Use IPv4 CIDR notation (for example 10.128.0.0/14).": "Use IPv4 CIDR notation (for example 10.128.0.0/14).",
"Use IPv4 CIDR notation (for example 172.30.0.0/16).": "Use IPv4 CIDR notation (for example 172.30.0.0/16).",
"User data": "User data",
Expand All @@ -457,6 +475,7 @@
"User data must not exceed 64 KB.": "User data must not exceed 64 KB.",
"User info URL": "User info URL",
"Username": "Username",
"Users": "Users",
"View and manage your bare metal instances.": "View and manage your bare metal instances.",
"View password": "View password",
"Virtual machine conditions": "Virtual machine conditions",
Expand Down
5 changes: 5 additions & 0 deletions libs/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,8 @@ export * from './osac/public/v1/baremetal_instance_catalog_items_service_pb.js';

export * from './osac/public/v1/identity_provider_type_pb.js';
export * from './osac/public/v1/identity_providers_service_pb.js';

export * from './osac/public/v1/role_type_pb.js';
export * from './osac/public/v1/roles_service_pb.js';
export * from './osac/public/v1/role_binding_type_pb.js';
export * from './osac/public/v1/role_bindings_service_pb.js';
4 changes: 3 additions & 1 deletion libs/ui-components/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ export type ApiRoute =
| 'v1/private/tenants'
| 'v1/private/storage_backends'
| 'v1/private/storage_tiers'
| 'v1/identity_providers';
| 'v1/identity_providers'
| 'v1/roles'
| 'v1/role_bindings';

/**
* Strict 3-part tuple that encodes an API address.
Expand Down
3 changes: 2 additions & 1 deletion libs/ui-components/src/api/v1/private/tenant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ import { type ApiQueryClient, useApiQuery, useApiQueryClient } from '../../use-a
const invalidateTenantsQueries = (qc: ApiQueryClient) =>
qc.invalidateQueries({ queryKey: apiQueryKey('v1/private/tenants') });

export const useTenants = (params: ListParams = {}) => {
export const useTenants = (params: ListParams = {}, disabled?: boolean) => {
const client = useApiFetch(Tenants);
return useApiQuery({
queryKey: apiQueryKey('v1/private/tenants', undefined, params),
queryFn: () => client.list(params),
select: (data) => data.items,
enabled: !disabled,
});
};

Expand Down
84 changes: 84 additions & 0 deletions libs/ui-components/src/api/v1/role-binding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { type MessageInitShape } from '@bufbuild/protobuf';
import { useMutation } from '@tanstack/react-query';

import { type RoleBindingSchema, RoleBindings } from '@osac/types';

import { useApiFetch } from '../api-context';
import { type ListParams, apiQueryKey } from '../types';
import { type ApiQueryClient, useApiQuery, useApiQueryClient } from '../use-api-query';
import { buildUpdateMaskPaths } from './update-mask';

const invalidateRoleBindingQueries = (qc: ApiQueryClient) =>
qc.invalidateQueries({ queryKey: apiQueryKey('v1/role_bindings') });

export const useRoleBindings = (params: ListParams = {}) => {
const client = useApiFetch(RoleBindings);
return useApiQuery({
queryKey: apiQueryKey('v1/role_bindings', undefined, params),
queryFn: () => client.list(params),
select: (data) => data.items,
});
};

export const useRoleBinding = (id?: string) => {
const client = useApiFetch(RoleBindings);
return useApiQuery({
queryKey: apiQueryKey('v1/role_bindings', [id]),
queryFn: () => client.get({ id }),
select: (data) => data.object,
enabled: !!id,
});
};

export const useCreateRoleBinding = () => {
const client = useApiFetch(RoleBindings);
const qc = useApiQueryClient();
return useMutation({
mutationFn: async (body: MessageInitShape<typeof RoleBindingSchema>) => {
const resp = await client.create({ object: body });
if (!resp.object) {
throw new Error('Create response missing role binding object');
}
return resp.object;
},
onSuccess: () => invalidateRoleBindingQueries(qc),
});
};

export const useUpdateRoleBinding = () => {
const client = useApiFetch(RoleBindings);
const qc = useApiQueryClient();
return useMutation({
mutationFn: async ({
id,
body,
}: {
id: string;
body: MessageInitShape<typeof RoleBindingSchema>;
}) => {
const resp = await client.update({
object: {
id,
...body,
},
updateMask: {
paths: buildUpdateMaskPaths(body),
},
});
if (!resp.object) {
throw new Error('Update response missing role binding object');
}
return resp.object;
},
onSuccess: () => invalidateRoleBindingQueries(qc),
});
};

export const useDeleteRoleBinding = () => {
const client = useApiFetch(RoleBindings);
const qc = useApiQueryClient();
return useMutation({
mutationFn: (id: string) => client.delete({ id }),
onSuccess: () => invalidateRoleBindingQueries(qc),
});
};
14 changes: 14 additions & 0 deletions libs/ui-components/src/api/v1/role.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Roles } from '@osac/types';

import { useApiFetch } from '../api-context';
import { type ListParams, apiQueryKey } from '../types';
import { useApiQuery } from '../use-api-query';

export const useRoles = (params: ListParams = {}) => {
const client = useApiFetch(Roles);
return useApiQuery({
queryKey: apiQueryKey('v1/roles', undefined, params),
queryFn: () => client.list(params),
select: (data) => data.items,
});
};
5 changes: 4 additions & 1 deletion libs/ui-components/src/api/v1/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import { useApiFetch } from '../api-context';
import { type ListParams, apiQueryKey } from '../types';
import { useApiQuery } from '../use-api-query';

export const useUsers = (params: ListParams = {}) => {
export const getTenantUsersFilter = (tenantId: string) => `this.metadata.tenant == "${tenantId}"`;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

export const useUsers = (params: ListParams = {}, disabled?: boolean) => {
const client = useApiFetch(Users);
return useApiQuery({
queryKey: apiQueryKey('v1/users', undefined, params),
queryFn: () => client.list(params),
select: (data) => data.items,
enabled: !disabled,
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const buildIdpCreatePayload = (
...values.spec.config,
},
},
enabled: true,
},
};
};
Loading
Loading