mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-07 06:29:25 +08:00
397 lines
11 KiB
TypeScript
397 lines
11 KiB
TypeScript
import { DeleteOutlined, SettingOutlined } from '@ant-design/icons';
|
|
import { css } from '@emotion/css';
|
|
import { Avatar, Card, Layout, Menu, message, PageHeader, Popconfirm, Spin, Switch, Tabs } from 'antd';
|
|
import React, { createContext, useContext, useMemo } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Redirect, useHistory, useRouteMatch } from 'react-router-dom';
|
|
import { ACLPane } from '../acl';
|
|
import { useAPIClient, useRequest } from '../api-client';
|
|
import { CollectionManagerPane } from '../collection-manager';
|
|
import { useDocumentTitle } from '../document-title';
|
|
import { Icon } from '../icon';
|
|
import { RouteSwitchContext } from '../route-switch';
|
|
import { useCompile } from '../schema-component';
|
|
import { BlockTemplatesPane } from '../schema-templates';
|
|
import { SystemSettingsPane } from '../system-settings';
|
|
|
|
export const SettingsCenterContext = createContext<any>({});
|
|
|
|
const PluginCard = (props) => {
|
|
const history = useHistory<any>();
|
|
const { data = {} } = props;
|
|
const api = useAPIClient();
|
|
const { t } = useTranslation();
|
|
return (
|
|
<Card
|
|
bordered={false}
|
|
style={{ width: 'calc(20% - 24px)', marginRight: 24, marginBottom: 24 }}
|
|
actions={[
|
|
data.enabled ? (
|
|
<SettingOutlined
|
|
onClick={() => {
|
|
history.push(`/admin/settings/${data.name}`);
|
|
}}
|
|
/>
|
|
) : null,
|
|
<Popconfirm
|
|
title={t('Are you sure to delete this plugin?')}
|
|
onConfirm={async () => {
|
|
await api.request({
|
|
url: `pm:remove/${data.name}`,
|
|
});
|
|
message.success(t('插件删除成功'));
|
|
window.location.reload();
|
|
}}
|
|
onCancel={() => {}}
|
|
okText={t('Yes')}
|
|
cancelText={t('No')}
|
|
>
|
|
<DeleteOutlined />
|
|
</Popconfirm>,
|
|
<Switch
|
|
size={'small'}
|
|
onChange={async (checked) => {
|
|
await api.request({
|
|
url: `pm:${checked ? 'enable' : 'disable'}/${data.name}`,
|
|
});
|
|
message.success(checked ? t('插件激活成功') : t('插件禁用成功'));
|
|
window.location.reload();
|
|
}}
|
|
defaultChecked={data.enabled}
|
|
></Switch>,
|
|
].filter(Boolean)}
|
|
>
|
|
<Card.Meta
|
|
className={css`
|
|
.ant-card-meta-avatar {
|
|
margin-top: 8px;
|
|
.ant-avatar {
|
|
border-radius: 2px;
|
|
}
|
|
}
|
|
`}
|
|
avatar={<Avatar />}
|
|
description={data.description}
|
|
title={
|
|
<span>
|
|
{data.name}
|
|
<span
|
|
className={css`
|
|
display: block;
|
|
color: rgba(0, 0, 0, 0.45);
|
|
font-weight: normal;
|
|
font-size: 13px;
|
|
// margin-left: 8px;
|
|
`}
|
|
>
|
|
{data.version}
|
|
</span>
|
|
</span>
|
|
}
|
|
/>
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
const BuiltInPluginCard = (props) => {
|
|
const { data } = props;
|
|
return (
|
|
<Card
|
|
bordered={false}
|
|
style={{ width: 'calc(20% - 24px)', marginRight: 24, marginBottom: 24 }}
|
|
// actions={[<a>Settings</a>, <a>Remove</a>, <Switch size={'small'} defaultChecked={true}></Switch>]}
|
|
>
|
|
<Card.Meta
|
|
className={css`
|
|
.ant-card-meta-avatar {
|
|
margin-top: 8px;
|
|
.ant-avatar {
|
|
border-radius: 2px;
|
|
}
|
|
}
|
|
`}
|
|
avatar={<Avatar />}
|
|
description={data.description}
|
|
title={
|
|
<span>
|
|
{data.name}
|
|
<span
|
|
className={css`
|
|
display: block;
|
|
color: rgba(0, 0, 0, 0.45);
|
|
font-weight: normal;
|
|
font-size: 13px;
|
|
// margin-left: 8px;
|
|
`}
|
|
>
|
|
{data.version}
|
|
</span>
|
|
</span>
|
|
}
|
|
/>
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
const LocalPlugins = () => {
|
|
const { data, loading } = useRequest({
|
|
url: 'applicationPlugins:list',
|
|
params: {
|
|
filter: {
|
|
'builtIn.$isFalsy': true,
|
|
},
|
|
sort: 'name',
|
|
},
|
|
});
|
|
if (loading) {
|
|
return <Spin />;
|
|
}
|
|
return (
|
|
<>
|
|
{data?.data?.map((item) => {
|
|
return <PluginCard key={item.id} data={item} />;
|
|
})}
|
|
</>
|
|
);
|
|
};
|
|
|
|
const BuiltinPlugins = () => {
|
|
const { data, loading } = useRequest({
|
|
url: 'applicationPlugins:list',
|
|
params: {
|
|
filter: {
|
|
'builtIn.$isTruly': true,
|
|
},
|
|
sort: 'name',
|
|
},
|
|
});
|
|
if (loading) {
|
|
return <Spin />;
|
|
}
|
|
return (
|
|
<>
|
|
{data?.data?.map((item) => {
|
|
return <BuiltInPluginCard data={item} />;
|
|
})}
|
|
</>
|
|
);
|
|
};
|
|
|
|
const MarketplacePlugins = () => {
|
|
const { t } = useTranslation();
|
|
return <div style={{ fontSize: 18 }}>{t('Coming soon...')}</div>;
|
|
};
|
|
|
|
const PluginList = (props) => {
|
|
const match = useRouteMatch<any>();
|
|
const history = useHistory<any>();
|
|
const { tabName = 'local' } = match.params || {};
|
|
const { setTitle } = useDocumentTitle();
|
|
const { t } = useTranslation();
|
|
|
|
return (
|
|
<div>
|
|
<PageHeader
|
|
ghost={false}
|
|
title={t('Plugin manager')}
|
|
footer={
|
|
<Tabs
|
|
activeKey={tabName}
|
|
onChange={(activeKey) => {
|
|
history.push(`/admin/pm/list/${activeKey}`);
|
|
}}
|
|
>
|
|
<Tabs.TabPane tab={t('Local')} key={'local'} />
|
|
<Tabs.TabPane tab={t('Built-in')} key={'built-in'} />
|
|
<Tabs.TabPane tab={t('Marketplace')} key={'marketplace'} />
|
|
</Tabs>
|
|
}
|
|
/>
|
|
<div style={{ margin: 24, display: 'flex', flexFlow: 'row wrap' }}>
|
|
{React.createElement(
|
|
{
|
|
local: LocalPlugins,
|
|
'built-in': BuiltinPlugins,
|
|
marketplace: MarketplacePlugins,
|
|
}[tabName],
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const settings = {
|
|
acl: {
|
|
title: '{{t("ACL")}}',
|
|
icon: 'LockOutlined',
|
|
tabs: {
|
|
roles: {
|
|
title: '{{t("Roles & Permissions")}}',
|
|
component: ACLPane,
|
|
},
|
|
},
|
|
},
|
|
'block-templates': {
|
|
title: '{{t("Block templates")}}',
|
|
icon: 'LayoutOutlined',
|
|
tabs: {
|
|
list: {
|
|
title: '{{t("Block templates")}}',
|
|
component: BlockTemplatesPane,
|
|
},
|
|
},
|
|
},
|
|
'collection-manager': {
|
|
icon: 'DatabaseOutlined',
|
|
title: '{{t("Collection manager")}}',
|
|
tabs: {
|
|
collections: {
|
|
title: '{{t("Collections & Fields")}}',
|
|
component: CollectionManagerPane,
|
|
},
|
|
},
|
|
},
|
|
'system-settings': {
|
|
icon: 'SettingOutlined',
|
|
title: '{{t("System settings")}}',
|
|
tabs: {
|
|
'system-settings': {
|
|
title: '{{t("System settings")}}',
|
|
component: SystemSettingsPane,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const SettingsCenter = (props) => {
|
|
const match = useRouteMatch<any>();
|
|
const history = useHistory<any>();
|
|
const items = useContext(SettingsCenterContext);
|
|
const compile = useCompile();
|
|
const firstUri = useMemo(() => {
|
|
const keys = Object.keys(items).sort();
|
|
const pluginName = keys.shift();
|
|
const tabName = Object.keys(items?.[pluginName]?.tabs || {}).shift();
|
|
return `/admin/settings/${pluginName}/${tabName}`;
|
|
}, [items]);
|
|
const { pluginName, tabName } = match.params || {};
|
|
if (!pluginName) {
|
|
return <Redirect to={firstUri} />;
|
|
}
|
|
if (!items[pluginName]) {
|
|
return <Redirect to={firstUri} />;
|
|
}
|
|
if (!tabName) {
|
|
const firstTabName = Object.keys(items[pluginName]?.tabs).shift();
|
|
return <Redirect to={`/admin/settings/${pluginName}/${firstTabName}`} />;
|
|
}
|
|
const component = items[pluginName]?.tabs?.[tabName]?.component;
|
|
const menuItems: any = Object.keys(items)
|
|
.sort()
|
|
.map((key) => {
|
|
const item = items[key];
|
|
const tabKey = Object.keys(item.tabs).shift();
|
|
return {
|
|
label: compile(item.title),
|
|
key: key,
|
|
icon: item.icon ? <Icon type={item.icon} /> : null,
|
|
};
|
|
});
|
|
return (
|
|
<Layout>
|
|
<div
|
|
style={
|
|
{
|
|
'--side-menu-width': '200px',
|
|
} as Record<string, string>
|
|
}
|
|
className={css`
|
|
width: var(--side-menu-width);
|
|
overflow: hidden;
|
|
flex: 0 0 var(--side-menu-width);
|
|
max-width: var(--side-menu-width);
|
|
min-width: var(--side-menu-width);
|
|
pointer-events: none;
|
|
`}
|
|
></div>
|
|
<Layout.Sider
|
|
className={css`
|
|
height: 100%;
|
|
position: fixed;
|
|
padding-top: 46px;
|
|
left: 0;
|
|
top: 0;
|
|
background: rgba(0, 0, 0, 0);
|
|
z-index: 100;
|
|
`}
|
|
theme={'light'}
|
|
>
|
|
<Menu
|
|
selectedKeys={[pluginName]}
|
|
style={{ height: 'calc(100vh - 46px)', overflowY: 'auto', overflowX: 'hidden' }}
|
|
onClick={(e) => {
|
|
const item = items[e.key];
|
|
const tabKey = Object.keys(item.tabs).shift();
|
|
history.push(`/admin/settings/${e.key}/${tabKey}`);
|
|
}}
|
|
items={menuItems as any}
|
|
/>
|
|
</Layout.Sider>
|
|
<Layout.Content>
|
|
<PageHeader
|
|
ghost={false}
|
|
title={compile(items[pluginName]?.title)}
|
|
footer={
|
|
<Tabs
|
|
activeKey={tabName}
|
|
onChange={(activeKey) => {
|
|
history.push(`/admin/settings/${pluginName}/${activeKey}`);
|
|
}}
|
|
>
|
|
{Object.keys(items[pluginName]?.tabs).map((tabKey) => {
|
|
const tab = items[pluginName].tabs?.[tabKey];
|
|
return <Tabs.TabPane tab={compile(tab?.title)} key={tabKey} />;
|
|
})}
|
|
</Tabs>
|
|
}
|
|
/>
|
|
<div style={{ margin: 24 }}>{component && React.createElement(component)}</div>
|
|
</Layout.Content>
|
|
</Layout>
|
|
);
|
|
};
|
|
|
|
export const SettingsCenterProvider = (props) => {
|
|
const { settings = {} } = props;
|
|
const items = useContext(SettingsCenterContext);
|
|
return (
|
|
<SettingsCenterContext.Provider value={{ ...items, ...settings }}>{props.children}</SettingsCenterContext.Provider>
|
|
);
|
|
};
|
|
|
|
export const PMProvider = (props) => {
|
|
const { routes, ...others } = useContext(RouteSwitchContext);
|
|
routes[1].routes.unshift(
|
|
{
|
|
type: 'route',
|
|
path: '/admin/pm/list/:tabName?',
|
|
component: PluginList,
|
|
},
|
|
{
|
|
type: 'route',
|
|
path: '/admin/settings/:pluginName?/:tabName?',
|
|
component: SettingsCenter,
|
|
uiSchemaUid: routes[1].uiSchemaUid,
|
|
},
|
|
);
|
|
return (
|
|
<SettingsCenterProvider settings={settings}>
|
|
<RouteSwitchContext.Provider value={{ ...others, routes }}>{props.children}</RouteSwitchContext.Provider>
|
|
</SettingsCenterProvider>
|
|
);
|
|
};
|
|
|
|
export default PMProvider;
|
|
|
|
export * from './PluginManagerLink';
|