mirror of
https://github.com/logto-io/logto.git
synced 2025-01-06 20:40:08 -05:00
feat(console): show permission tags in roles table
This commit is contained in:
parent
8754d86610
commit
d582fdf284
13 changed files with 144 additions and 48 deletions
|
@ -19,10 +19,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
@include _.tag;
|
||||
}
|
||||
|
||||
.description {
|
||||
@include _.text-ellipsis;
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import Search from '@/ds-components/Search';
|
|||
import Table from '@/ds-components/Table';
|
||||
import TablePlaceholder from '@/ds-components/Table/TablePlaceholder';
|
||||
import type { Column } from '@/ds-components/Table/types';
|
||||
import Tag from '@/ds-components/Tag';
|
||||
import TextLink from '@/ds-components/TextLink';
|
||||
import { Tooltip } from '@/ds-components/Tip';
|
||||
import useDocumentationUrl from '@/hooks/use-documentation-url';
|
||||
|
@ -68,7 +69,7 @@ function PermissionsTable({
|
|||
title: t('permissions.name_column'),
|
||||
dataIndex: 'name',
|
||||
colSpan: isApiColumnVisible ? 5 : 6,
|
||||
render: ({ name }) => <div className={styles.name}>{name}</div>,
|
||||
render: ({ name }) => <Tag variant="cell">{name}</Tag>,
|
||||
};
|
||||
|
||||
const descriptionColumn: Column<ScopeResponse> = {
|
||||
|
|
|
@ -10,7 +10,6 @@ import { buildUrl } from '@/utils/url';
|
|||
|
||||
import CreatePermissionModal from '../CreatePermissionModal';
|
||||
import TemplateTable, { pageSize } from '../TemplateTable';
|
||||
import * as styles from '../index.module.scss';
|
||||
|
||||
/**
|
||||
* Renders the permissions field that allows users to add, edit, and delete organization
|
||||
|
@ -59,7 +58,7 @@ function PermissionsField() {
|
|||
title: t('general.name'),
|
||||
dataIndex: 'name',
|
||||
colSpan: 4,
|
||||
render: ({ name }) => <div className={styles.permission}>{name}</div>,
|
||||
render: ({ name }) => <div>{name}</div>,
|
||||
},
|
||||
{
|
||||
title: t('general.description'),
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.permissions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: _.unit(2);
|
||||
}
|
|
@ -1,16 +1,18 @@
|
|||
import { type OrganizationRole } from '@logto/schemas';
|
||||
import { type OrganizationRoleWithScopes } from '@logto/schemas';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import DeleteButton from '@/components/DeleteButton';
|
||||
import FormField from '@/ds-components/FormField';
|
||||
import Tag from '@/ds-components/Tag';
|
||||
import useApi, { type RequestError } from '@/hooks/use-api';
|
||||
import { buildUrl } from '@/utils/url';
|
||||
|
||||
import CreateRoleModal from '../CreateRoleModal';
|
||||
import TemplateTable, { pageSize } from '../TemplateTable';
|
||||
import * as styles from '../index.module.scss';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
/**
|
||||
* Renders the roles field that allows users to add, edit, and delete organization
|
||||
|
@ -22,7 +24,7 @@ function RolesField() {
|
|||
data: response,
|
||||
error,
|
||||
mutate,
|
||||
} = useSWR<[OrganizationRole[], number], RequestError>(
|
||||
} = useSWR<[OrganizationRoleWithScopes[], number], RequestError>(
|
||||
buildUrl('api/organization-roles', {
|
||||
page: String(page),
|
||||
page_size: String(pageSize),
|
||||
|
@ -59,13 +61,24 @@ function RolesField() {
|
|||
title: t('general.name'),
|
||||
dataIndex: 'name',
|
||||
colSpan: 4,
|
||||
render: ({ name }) => <div className={styles.permission}>{name}</div>,
|
||||
render: ({ name }) => <div>{name}</div>,
|
||||
},
|
||||
{
|
||||
title: t('organizations.permission_other'),
|
||||
dataIndex: 'permissions',
|
||||
colSpan: 6,
|
||||
render: ({ description }) => description ?? '-',
|
||||
render: ({ scopes }) =>
|
||||
scopes.length === 0 ? (
|
||||
'-'
|
||||
) : (
|
||||
<div className={styles.permissions}>
|
||||
{scopes.map(({ id, name }) => (
|
||||
<Tag key={id} variant="cell">
|
||||
{name}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: null,
|
||||
|
|
|
@ -3,7 +3,3 @@
|
|||
.tabs {
|
||||
margin: _.unit(4) 0;
|
||||
}
|
||||
|
||||
.permission {
|
||||
@include _.tag;
|
||||
}
|
||||
|
|
|
@ -18,8 +18,6 @@ function Organizations() {
|
|||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { tab } = useParams();
|
||||
|
||||
console.log('tab', tab);
|
||||
|
||||
return (
|
||||
<div className={pageLayout.container}>
|
||||
<div className={pageLayout.headline}>
|
||||
|
|
|
@ -96,14 +96,3 @@
|
|||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/** Render a tag that has background color and border radius. */
|
||||
@mixin tag {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
vertical-align: bottom;
|
||||
padding: unit(1) unit(2);
|
||||
border-radius: 6px;
|
||||
background: var(--color-neutral-95);
|
||||
@include text-ellipsis;
|
||||
}
|
||||
|
|
|
@ -9,8 +9,12 @@ import {
|
|||
Users,
|
||||
OrganizationUserRelations,
|
||||
OrganizationRoleUserRelations,
|
||||
type OrganizationRoleKeys,
|
||||
type CreateOrganizationRole,
|
||||
type OrganizationRole,
|
||||
type OrganizationRoleWithScopes,
|
||||
} from '@logto/schemas';
|
||||
import { convertToIdentifiers } from '@logto/shared';
|
||||
import { conditionalSql, convertToIdentifiers } from '@logto/shared';
|
||||
import { sql, type CommonQueryMethods } from 'slonik';
|
||||
import { z } from 'zod';
|
||||
|
||||
|
@ -54,41 +58,88 @@ class UserRelationQueries extends RelationQueries<[typeof Organizations, typeof
|
|||
}
|
||||
|
||||
async getOrganizationsByUserId(userId: string): Promise<Readonly<OrganizationWithRoles[]>> {
|
||||
const organizationRoles = convertToIdentifiers(OrganizationRoles, true);
|
||||
const roles = convertToIdentifiers(OrganizationRoles, true);
|
||||
const organizations = convertToIdentifiers(Organizations, true);
|
||||
const { fields } = convertToIdentifiers(OrganizationUserRelations, true);
|
||||
const oruRelations = convertToIdentifiers(OrganizationRoleUserRelations, true);
|
||||
const relations = convertToIdentifiers(OrganizationRoleUserRelations, true);
|
||||
|
||||
return this.pool.any<OrganizationWithRoles>(sql`
|
||||
select
|
||||
${organizations.table}.*,
|
||||
json_agg(
|
||||
json_build_object(
|
||||
'id', ${organizationRoles.fields.id},
|
||||
'name', ${organizationRoles.fields.name})
|
||||
)
|
||||
as roles
|
||||
coalesce(
|
||||
json_agg(
|
||||
json_build_object(
|
||||
'id', ${roles.fields.id},
|
||||
'name', ${roles.fields.name}
|
||||
)
|
||||
) filter (where ${roles.fields.id} is not null), -- left join could produce nulls
|
||||
'[]'
|
||||
) as roles
|
||||
from ${this.table}
|
||||
join ${organizations.table}
|
||||
left join ${organizations.table}
|
||||
on ${fields.organizationId} = ${organizations.fields.id}
|
||||
left join ${oruRelations.table}
|
||||
on ${fields.userId} = ${oruRelations.fields.userId}
|
||||
and ${fields.organizationId} = ${oruRelations.fields.organizationId}
|
||||
left join ${organizationRoles.table}
|
||||
on ${oruRelations.fields.organizationRoleId} = ${organizationRoles.fields.id}
|
||||
left join ${relations.table}
|
||||
on ${fields.userId} = ${relations.fields.userId}
|
||||
and ${fields.organizationId} = ${relations.fields.organizationId}
|
||||
left join ${roles.table}
|
||||
on ${relations.fields.organizationRoleId} = ${roles.fields.id}
|
||||
where ${fields.userId} = ${userId}
|
||||
group by ${organizations.table}.id
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
class OrganizationRolesQueries extends SchemaQueries<
|
||||
OrganizationRoleKeys,
|
||||
CreateOrganizationRole,
|
||||
OrganizationRole
|
||||
> {
|
||||
async findAllWithScopes(
|
||||
limit: number,
|
||||
offset: number
|
||||
): Promise<Readonly<OrganizationRoleWithScopes[]>> {
|
||||
const { table, fields } = convertToIdentifiers(OrganizationRoles, true);
|
||||
const relations = convertToIdentifiers(OrganizationRoleScopeRelations, true);
|
||||
const scopes = convertToIdentifiers(OrganizationScopes, true);
|
||||
|
||||
return this.pool.any(sql`
|
||||
select
|
||||
${table}.*,
|
||||
coalesce(
|
||||
json_agg(
|
||||
json_build_object(
|
||||
'id', ${scopes.fields.id},
|
||||
'name', ${scopes.fields.name}
|
||||
)
|
||||
) filter (where ${scopes.fields.id} is not null),
|
||||
'[]'
|
||||
) as scopes -- left join could produce nulls as scopes
|
||||
from ${table}
|
||||
left join ${relations.table}
|
||||
on ${relations.fields.organizationRoleId} = ${fields.id}
|
||||
left join ${scopes.table}
|
||||
on ${relations.fields.organizationScopeId} = ${scopes.fields.id}
|
||||
group by ${fields.id}
|
||||
${conditionalSql(this.orderBy, ({ field, order }) => {
|
||||
return sql`order by ${fields[field]} ${order === 'desc' ? sql`desc` : sql`asc`}`;
|
||||
})}
|
||||
limit ${limit}
|
||||
offset ${offset}
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
export default class OrganizationQueries extends SchemaQueries<
|
||||
OrganizationKeys,
|
||||
CreateOrganization,
|
||||
Organization
|
||||
> {
|
||||
/** Queries for roles in the organization template. */
|
||||
roles = new SchemaQueries(this.pool, OrganizationRoles, { field: 'name', order: 'asc' });
|
||||
roles = new OrganizationRolesQueries(this.pool, OrganizationRoles, {
|
||||
field: 'name',
|
||||
order: 'asc',
|
||||
});
|
||||
|
||||
/** Queries for scopes in the organization template. */
|
||||
scopes = new SchemaQueries(this.pool, OrganizationScopes, { field: 'name', order: 'asc' });
|
||||
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
import { type CreateOrganizationRole, OrganizationRoles } from '@logto/schemas';
|
||||
import {
|
||||
type CreateOrganizationRole,
|
||||
OrganizationRoles,
|
||||
organizationRoleWithScopesGuard,
|
||||
} from '@logto/schemas';
|
||||
import { generateStandardId } from '@logto/shared';
|
||||
import { z } from 'zod';
|
||||
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import koaPagination from '#src/middleware/koa-pagination.js';
|
||||
import SchemaRouter from '#src/utils/SchemaRouter.js';
|
||||
|
||||
import { type AuthedRouter, type RouterInitArgs } from '../types.js';
|
||||
|
@ -23,10 +28,30 @@ export default function organizationRoleRoutes<T extends AuthedRouter>(
|
|||
]: RouterInitArgs<T>
|
||||
) {
|
||||
const router = new SchemaRouter(OrganizationRoles, roles, {
|
||||
disabled: { post: true },
|
||||
disabled: { get: true, post: true },
|
||||
errorHandler,
|
||||
});
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
koaPagination(),
|
||||
koaGuard({
|
||||
response: organizationRoleWithScopesGuard.array(),
|
||||
status: [200],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { limit, offset } = ctx.pagination;
|
||||
const [count, entities] = await Promise.all([
|
||||
roles.findTotalNumber(),
|
||||
roles.findAllWithScopes(limit, offset),
|
||||
]);
|
||||
|
||||
ctx.pagination.totalCount = count;
|
||||
ctx.body = entities;
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
/** Allows to carry an initial set of scopes for creating a new organization role. */
|
||||
type CreateOrganizationRolePayload = Omit<CreateOrganizationRole, 'id'> & {
|
||||
organizationScopeIds: string[];
|
||||
|
|
|
@ -33,7 +33,7 @@ export default class SchemaQueries<
|
|||
constructor(
|
||||
public readonly pool: CommonQueryMethods,
|
||||
public readonly schema: GeneratedSchema<Key | 'id', CreateSchema, Schema>,
|
||||
orderBy?: { field: Key | 'id'; order: 'asc' | 'desc' }
|
||||
protected readonly orderBy?: { field: Key | 'id'; order: 'asc' | 'desc' }
|
||||
) {
|
||||
this.#findTotalNumber = buildGetTotalRowCountWithPool(this.pool, this.schema.table);
|
||||
this.#findAll = buildFindAllEntitiesWithPool(this.pool)(this.schema, orderBy && [orderBy]);
|
||||
|
|
|
@ -20,3 +20,4 @@ export * from './dashboard.js';
|
|||
export * from './domain.js';
|
||||
export * from './sentinel.js';
|
||||
export * from './mfa.js';
|
||||
export * from './organization.js';
|
||||
|
|
20
packages/schemas/src/types/organization.ts
Normal file
20
packages/schemas/src/types/organization.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { type OrganizationRole, OrganizationRoles } from '../db-entries/index.js';
|
||||
|
||||
export type OrganizationRoleWithScopes = OrganizationRole & {
|
||||
scopes: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export const organizationRoleWithScopesGuard: z.ZodType<OrganizationRoleWithScopes> =
|
||||
OrganizationRoles.guard.extend({
|
||||
scopes: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
})
|
||||
.array(),
|
||||
});
|
Loading…
Reference in a new issue