应用分组

pull/47/head
smallbun 2023-09-10 22:30:59 +08:00
parent 43af7e668f
commit 1f66d372a0
12 changed files with 653 additions and 39 deletions

View File

@ -134,26 +134,44 @@ export default [
},
],
},
//应用列表
//应用管理
{
name: 'app',
icon: 'AppstoreOutlined',
path: '/app',
component: './app/AppList',
},
//创建应用
{
name: 'app.create',
path: '/app/create',
hideInMenu: true,
component: './app/AppCreate',
},
//应用配置
{
name: 'app.config',
path: '/app/config',
hideInMenu: true,
component: './app/AppConfig',
routes: [
// 应用列表
{
path: '/app',
redirect: '/app/list',
},
// 应用列表
{
name: 'list',
path: '/app/list',
component: './app/AppList',
},
//创建应用
{
name: 'create',
path: '/app/list/create',
hideInMenu: true,
component: './app/AppCreate',
},
//应用配置
{
name: 'config',
path: '/app/list/config',
hideInMenu: true,
component: './app/AppConfig',
},
// 应用分组
{
name: 'group',
path: '/app/group',
component: './app/AppGroup',
},
],
},
//行为审计
{

View File

@ -57,7 +57,7 @@ const GlobalHeaderRight: React.FC = () => {
return (
<div className={styles.main}>
<Helmet>
<link rel="icon" href={initialState?.globalConfig?.appearance?.favicon} />
<link rel="icon" href={'/favicon.ico'} />
</Helmet>
<About />
<SelectLang className={styles.action} />

View File

@ -39,6 +39,8 @@ export default {
'menu.account.logout': '退出登录',
'menu.social-bind': '用户绑定',
'menu.app': '应用管理',
'menu.app.list': '应用列表',
'menu.app.group': '应用分组',
'menu.app.create': '创建应用',
'menu.app.config': '应用配置',
'menu.account': '账户管理',

View File

@ -81,7 +81,7 @@ export default () => {
history.push(`/account/identity-source`);
return;
}
if (!type) {
if (!type || !IdentitySourceDetailTabs[type]) {
setTabActiveKey(IdentitySourceDetailTabs.config);
history.push({
pathname: location.pathname,

View File

@ -15,7 +15,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { getApp, updateApp } from '@/services/app';
import { updateApp } from '@/services/app';
import { ProCard, ProDescriptions } from '@ant-design/pro-components';
import { useAsyncEffect } from 'ahooks';
@ -30,6 +30,7 @@ import classNames from 'classnames';
import { LoadingOutlined, PlusOutlined } from '@ant-design/icons';
import { uploadFile } from '@/services/upload';
import { GetApp } from '@/pages/app/AppConfig/data';
import { getApp } from '../../service';
const prefixCls = 'app-basic-info';
const AppBasic = (props: { appId: string }) => {

View File

@ -0,0 +1,152 @@
/*
* eiam-console - Employee Identity and Access Management
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { getPermissionResource, permissionResourceParamCheck } from '../../service';
import { ModalForm, ProFormText, ProFormTextArea } from '@ant-design/pro-components';
import { Form, Spin } from 'antd';
import React, { useState } from 'react';
import { useAsyncEffect } from 'ahooks';
import Paragraph from 'antd/es/typography/Paragraph';
type UpdateFormProps = {
/**
* ID
*/
id?: string;
/**
* SYSTEM
*/
appId: string;
/**
*
*/
open: boolean;
/**
*
*/
onCancel: (e?: React.MouseEvent | React.KeyboardEvent) => void;
/**
*
*/
onFinish?: (formData: Record<string, string>) => Promise<boolean | void>;
};
const UpdateResource: React.FC<UpdateFormProps> = (props) => {
const { open, onCancel, onFinish, id, appId } = props;
const [loading, setLoading] = useState<boolean>(false);
const [form] = Form.useForm();
useAsyncEffect(async () => {
if (open && id) {
setLoading(true);
const { success, result } = await getPermissionResource(id);
if (success && result) {
setLoading(false);
return;
}
}
}, [id, onCancel, open]);
return (
<ModalForm
title="修改资源"
width={'500px'}
open={open}
labelCol={{ span: 4 }}
wrapperCol={{ span: 20 }}
layout={'horizontal'}
labelAlign={'right'}
modalProps={{
maskClosable: true,
destroyOnClose: true,
onCancel: onCancel,
}}
form={form}
onFinish={async (values) => {
setLoading(true);
const result = await onFinish?.(values);
setLoading(false);
return result;
}}
>
<Spin spinning={loading}>
<ProFormText name={'id'} hidden />
<ProFormText
name="name"
label="资源名称"
placeholder="请输入资源名称"
rules={[
{
required: true,
message: '请输入资源名称',
},
{
validator: async (rule, value) => {
if (!value) {
return Promise.resolve();
}
setLoading(true);
const { success, result } = await permissionResourceParamCheck(
appId,
'NAME',
value,
id,
);
setLoading(false);
if (!success) {
return Promise.reject<any>();
}
if (!result) {
return Promise.reject<any>(new Error('资源名称已存在'));
}
},
validateTrigger: ['onBlur'],
},
]}
/>
<ProFormText
name="code"
label="资源编码"
placeholder="请输入资源编码"
proFieldProps={{
render: (value: string) => {
return (
value && (
<Paragraph copyable={{ text: value }} style={{ marginBottom: '0' }}>
<span
dangerouslySetInnerHTML={{
__html: `<span>${value}</span>`,
}}
/>
</Paragraph>
)
);
},
}}
readonly
extra="资源编码在当前应用中的唯一标识,不能重复,仅支持英文、数字、下划线,创建后不可修改。"
/>
<ProFormTextArea
name="desc"
fieldProps={{ rows: 2, maxLength: 20, showCount: false }}
label="资源描述"
placeholder="请输入资源描述"
/>
</Spin>
</ModalForm>
);
};
export default UpdateResource;

View File

@ -21,6 +21,7 @@ import {
ModalForm,
PageContainer,
ProCard,
ProFormSelect,
ProFormText,
ProFormTextArea,
ProList,
@ -38,6 +39,7 @@ import classnames from 'classnames';
import { ListTemplate } from './data.d';
import { AppType } from '@/constant';
import { createApp, getAppTemplateList } from './service';
import { getAllAppGroupList } from '@/services/app';
const { Paragraph } = Typography;
const prefixCls = 'topiam-create-app';
@ -106,7 +108,7 @@ const CreateApp = (props: {
onOk: () => {
successModal.destroy();
history.push(
`/app/config?id=${result.id}&name=${values.name}&protocol=${protocol}`,
`/app/list/config?id=${result.id}&name=${values.name}&protocol=${protocol}`,
);
},
});
@ -128,6 +130,23 @@ const CreateApp = (props: {
},
]}
/>
<ProFormSelect
name="groups"
mode="multiple"
label={'归属分组'}
request={async () => {
setLoading(true);
const { success, data } = await getAllAppGroupList({}, {}, {}).finally(() => {
setLoading(false);
});
if (success && data) {
return data.map((i) => {
return { label: i.name, value: i.id };
});
}
return [];
}}
/>
<ProFormTextArea
name="remark"
preserve={false}
@ -232,22 +251,20 @@ const AppCreate = () => {
);
}}
/>
{createAppTemplate && (
<CreateApp
code={createAppTemplate?.code}
name={createAppTemplate?.name}
protocol={createAppTemplate.protocol}
open={createAppOpen}
onCancel={() => {
setCreateAppOpen(false);
setCreateAppTemplate(undefined);
}}
/>
)}
</PageContainer>
{createAppTemplate && (
<CreateApp
code={createAppTemplate?.code}
name={createAppTemplate?.name}
protocol={createAppTemplate.protocol}
open={createAppOpen}
onCancel={() => {
setCreateAppOpen(false);
setCreateAppTemplate(undefined);
}}
/>
)}
</div>
);
};
export default () => {
return <AppCreate />;
};
export default AppCreate;

View File

@ -0,0 +1,221 @@
/*
* eiam-console - Employee Identity and Access Management
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import type { ActionType, ProColumns } from '@ant-design/pro-components';
import { PageContainer, ProTable } from '@ant-design/pro-components';
import { App, Button, Popconfirm, Tag } from 'antd';
import { useRef, useState } from 'react';
import CreateModal from './components/CreateModal';
import UpdateModal from './components/UpdateModal';
import { createAppGroup, removeAppGroup, updateAppGroup } from './service';
import { useIntl } from '@@/exports';
import { getAppGroupList } from '@/services/app';
export default () => {
const intl = useIntl();
const actionRef = useRef<ActionType>();
const [createModalOpen, setCreateModalOpen] = useState<boolean>(false);
const [updateModalOpen, setUpdateModalOpen] = useState<boolean>(false);
const [id, setId] = useState<string>();
const { message } = App.useApp();
const columns: ProColumns<AppAPI.AppGroupList>[] = [
{
title: intl.formatMessage({ id: 'pages.app_group.list.column.name' }),
dataIndex: 'name',
fixed: 'left',
},
{
title: intl.formatMessage({ id: 'pages.app_group.list.column.code' }),
dataIndex: 'code',
},
{
title: intl.formatMessage({ id: 'pages.app_group.list.column.app_count' }),
dataIndex: 'appCount',
search: false,
},
{
title: intl.formatMessage({ id: 'pages.app_group.list.column.type' }),
dataIndex: 'type',
valueEnum: {
default: {
text: intl.formatMessage({
id: 'pages.app_group.list.column.type.default',
}),
},
custom: {
text: intl.formatMessage({
id: 'pages.app_group.list.column.type.custom',
}),
},
},
render: (_, record) => (
<>
{record.type === 'custom' && (
<Tag color={'#108ee9'} key={'custom'}>
{intl.formatMessage({
id: 'pages.app_group.list.column.type.custom',
})}
</Tag>
)}
{record.type === 'default' && (
<Tag color={'#2db7f5'} key={'default'}>
{intl.formatMessage({
id: 'pages.app_group.list.column.type.default',
})}
</Tag>
)}
</>
),
},
{
title: intl.formatMessage({ id: 'pages.app_group.list.column.create_time' }),
dataIndex: 'createTime',
search: false,
align: 'center',
ellipsis: true,
},
{
title: intl.formatMessage({ id: 'pages.app_group.list.column.remark' }),
dataIndex: 'remark',
search: false,
ellipsis: true,
},
{
title: intl.formatMessage({ id: 'pages.app_group.list.column.option' }),
valueType: 'option',
key: 'option',
width: 100,
align: 'center',
render: (text, record) => [
<a
key="editable"
onClick={() => {
setId(record.id);
setUpdateModalOpen(true);
}}
>
{intl.formatMessage({ id: 'app.update' })}
</a>,
<Popconfirm
title={intl.formatMessage({
id: 'pages.app_group.list.actions.popconfirm.delete',
})}
placement="bottomRight"
icon={
<QuestionCircleOutlined
style={{
color: 'red',
}}
/>
}
onConfirm={async () => {
const { success } = await removeAppGroup(record.id);
if (success) {
message.success(intl.formatMessage({ id: 'app.operation_success' }));
actionRef.current?.reload();
return;
}
}}
okText={intl.formatMessage({ id: 'app.yes' })}
cancelText={intl.formatMessage({ id: 'app.no' })}
key="delete"
>
<a target="_blank" key="remove" style={{ color: 'red' }}>
{intl.formatMessage({ id: 'app.delete' })}
</a>
</Popconfirm>,
],
},
];
return (
<PageContainer>
<ProTable<AppAPI.AppGroupList>
columns={columns}
actionRef={actionRef}
request={getAppGroupList}
rowKey="id"
search={{
labelWidth: 'auto',
}}
scroll={{ x: 900 }}
form={{
syncToUrl: (values, type) => {
if (type === 'get') {
return {
...values,
};
}
return values;
},
}}
pagination={{
pageSize: 5,
}}
dateFormatter="string"
toolBarRender={() => [
<Button
key="button"
icon={<PlusOutlined />}
onClick={() => {
setCreateModalOpen(true);
}}
type="primary"
>
{intl.formatMessage({ id: 'pages.app_group.list.create' })}
</Button>,
]}
/>
<CreateModal
open={createModalOpen}
onCancel={() => {
setCreateModalOpen(false);
}}
onFinish={async (values) => {
const { result, success } = await createAppGroup(values);
if (success && result) {
message.success(intl.formatMessage({ id: 'app.create_success' }));
actionRef.current?.reload();
}
actionRef.current?.reload();
setCreateModalOpen(false);
return true;
}}
/>
{id && (
<UpdateModal
open={updateModalOpen}
id={id}
onCancel={() => {
setUpdateModalOpen(false);
}}
onFinish={async (values) => {
const { result, success } = await updateAppGroup(values);
if (success && result) {
message.success(intl.formatMessage({ id: 'app.create_success' }));
actionRef.current?.reload();
}
actionRef.current?.reload();
setUpdateModalOpen(false);
return true;
}}
/>
)}
</PageContainer>
);
};

View File

@ -0,0 +1,105 @@
/*
* eiam-console - Employee Identity and Access Management
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { ModalForm, ProFormText, ProFormTextArea } from '@ant-design/pro-components';
import { Form, Spin } from 'antd';
import React, { useState } from 'react';
import { useIntl } from '@@/exports';
import { getAppGroup } from '@/pages/app/AppGroup/service';
export default (props: {
id: string;
open: boolean;
onFinish: (formData: Record<string, string>) => Promise<boolean | void>;
onCancel: (e: React.MouseEvent<HTMLButtonElement>) => void;
}) => {
const { id, open, onCancel, onFinish } = props;
const [form] = Form.useForm();
const intl = useIntl();
const [loading, setLoading] = useState<boolean>(false);
return (
<ModalForm
title={intl.formatMessage({ id: 'pages.app_group.update.modal_form.title' })}
form={form}
open={open}
labelCol={{ span: 4 }}
wrapperCol={{ span: 20 }}
width={'500px'}
labelAlign={'right'}
preserve={false}
layout={'horizontal'}
autoFocusFirstInput
modalProps={{
maskClosable: true,
destroyOnClose: true,
onCancel: onCancel,
}}
onOpenChange={async (visible) => {
if (visible) {
setLoading(true);
const { result, success } = await getAppGroup(id).finally(() => {
setLoading(false);
});
if (success) {
form.setFieldsValue(result);
}
}
}}
onFinish={async (values) => {
setLoading(true);
await onFinish(values).finally(() => {
setLoading(false);
});
}}
>
<Spin spinning={loading}>
<ProFormText hidden name="id" />
<ProFormText
label={intl.formatMessage({ id: 'pages.app_group.modal_form.name' })}
name="name"
rules={[
{
required: true,
message: intl.formatMessage({
id: 'pages.app_group.modal_form.name.rule.0.message',
}),
},
]}
/>
<ProFormText
label={intl.formatMessage({ id: 'pages.app_group.modal_form.name' })}
name="code"
readonly
/>
<ProFormTextArea
label={intl.formatMessage({ id: 'pages.app_group.modal_form.remark' })}
name="remark"
fieldProps={{
placeholder: intl.formatMessage({
id: 'pages.app_group.modal_form.remark.placeholder',
}),
rows: 2,
maxLength: 20,
autoComplete: 'off',
showCount: true,
}}
/>
</Spin>
</ModalForm>
);
};

View File

@ -0,0 +1,17 @@
/*
* eiam-console - Employee Identity and Access Management
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

View File

@ -0,0 +1,62 @@
/*
* eiam-console - Employee Identity and Access Management
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { request } from '@umijs/max';
/**
*
*/
export async function createAppGroup(
params: Record<string, string>,
): Promise<API.ApiResult<Record<string, string>>> {
return request<API.ApiResult<Record<string, string>>>(`/api/v1/app/group/create`, {
method: 'POST',
data: params,
requestType: 'json',
});
}
/**
*
*/
export async function getAppGroup(id: string): Promise<API.ApiResult<Record<string, string>>> {
return request<API.ApiResult<Record<string, string>>>(`/api/v1/app/group/get/${id}`, {
method: 'GET',
});
}
/**
*
*/
export async function updateAppGroup(
params: Record<string, string>,
): Promise<API.ApiResult<Record<string, string>>> {
return request<API.ApiResult<Record<string, string>>>(`/api/v1/app/group/update`, {
method: 'PUT',
data: params,
requestType: 'json',
});
}
/**
* Remove App Group
*/
export async function removeAppGroup(id: string): Promise<API.ApiResult<boolean>> {
return request<API.ApiResult<boolean>>(`/api/v1/app/group/delete/${id}`, {
method: 'DELETE',
});
}

View File

@ -15,7 +15,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { getAppList } from '@/services/app';
import { getAllAppGroupList, getAppList } from '@/services/app';
import { PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import type { ActionType } from '@ant-design/pro-components';
import { PageContainer, ProList } from '@ant-design/pro-components';
@ -86,7 +86,7 @@ export default () => {
key={'create'}
type="primary"
onClick={() => {
history.push('/app/create');
history.push('/app/list/create');
}}
>
<PlusOutlined />
@ -102,7 +102,7 @@ export default () => {
<span
onClick={() => {
history.push(
`/app/config?id=${row.id}&protocol=${row.protocol}&name=${row.name}`,
`/app/list/config?id=${row.id}&protocol=${row.protocol}&name=${row.name}`,
);
}}
>
@ -177,7 +177,7 @@ export default () => {
key="config"
onClick={() => {
history.push(
`/app/config?id=${row.id}&protocol=${row.protocol}&name=${row.name}`,
`/app/list/config?id=${row.id}&protocol=${row.protocol}&name=${row.name}`,
);
}}
>
@ -213,6 +213,25 @@ export default () => {
</Popconfirm>,
],
},
status: {
// 自己扩展的字段,主要用于筛选,不在列表中显示
title: intl.formatMessage({
id: 'pages.app.list.metas.group',
}),
valueType: 'select',
fieldProps: {
mode: 'multiple',
},
request: async () => {
const { success, data } = await getAllAppGroupList({}, {}, {});
if (success && data) {
return data.map((i) => {
return { label: i.name, value: i.id };
});
}
return [];
},
},
}}
/>
</PageContainer>