0
Fork 0
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:
Gao Sun 2023-10-19 14:57:28 +08:00
parent 8754d86610
commit d582fdf284
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
13 changed files with 144 additions and 48 deletions

View file

@ -19,10 +19,6 @@
}
}
.name {
@include _.tag;
}
.description {
@include _.text-ellipsis;
}

View file

@ -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> = {

View file

@ -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'),

View file

@ -0,0 +1,7 @@
@use '@/scss/underscore' as _;
.permissions {
display: flex;
flex-wrap: wrap;
gap: _.unit(2);
}

View file

@ -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,

View file

@ -3,7 +3,3 @@
.tabs {
margin: _.unit(4) 0;
}
.permission {
@include _.tag;
}

View file

@ -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}>

View file

@ -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;
}

View file

@ -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' });

View file

@ -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[];

View file

@ -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]);

View file

@ -20,3 +20,4 @@ export * from './dashboard.js';
export * from './domain.js';
export * from './sentinel.js';
export * from './mfa.js';
export * from './organization.js';

View 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(),
});