mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 08:08:32 +00:00
feat(plugin-manager): better plugin manager experience (#1927)
This commit is contained in:
parent
82e6c7bb40
commit
4296db5859
397
packages/core/client/src/pm/Card.tsx
Normal file
397
packages/core/client/src/pm/Card.tsx
Normal file
@ -0,0 +1,397 @@
|
||||
import React, { useEffect, useMemo, useState, useCallback, MouseEventHandler } from 'react';
|
||||
import { useAPIClient, useRequest } from '../api-client';
|
||||
import {
|
||||
Avatar,
|
||||
Card,
|
||||
message,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Spin,
|
||||
Switch,
|
||||
Tabs,
|
||||
TabsProps,
|
||||
Tag,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { css } from '@emotion/css';
|
||||
import cls from 'classnames';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DeleteOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
import { useParseMarkdown } from '../schema-component/antd/markdown/util';
|
||||
import type { IPluginData } from '.';
|
||||
|
||||
interface PluginDocumentProps {
|
||||
path: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface ICommonCard {
|
||||
onClick: () => void;
|
||||
name: string;
|
||||
description: string;
|
||||
title: string;
|
||||
displayName: string;
|
||||
actions?: JSX.Element[];
|
||||
}
|
||||
|
||||
interface IPluginDetail {
|
||||
plugin: any;
|
||||
onCancel: () => void;
|
||||
items: TabsProps['items'];
|
||||
}
|
||||
|
||||
/**
|
||||
* get color by string
|
||||
* TODO: real avatar
|
||||
* @param str
|
||||
*/
|
||||
const stringToColor = function (str: string) {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
let color = '#';
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const value = (hash >> (i * 8)) & 0xff;
|
||||
color += ('00' + value.toString(16)).substr(-2);
|
||||
}
|
||||
return color;
|
||||
};
|
||||
|
||||
const PluginDocument: React.FC<PluginDocumentProps> = (props) => {
|
||||
const [docLang, setDocLang] = useState('');
|
||||
const { name, path } = props;
|
||||
const { data, loading, error } = useRequest(
|
||||
{
|
||||
url: '/plugins:getTabInfo',
|
||||
params: {
|
||||
filterByTk: name,
|
||||
path: path,
|
||||
locale: docLang,
|
||||
},
|
||||
},
|
||||
{
|
||||
refreshDeps: [name, path, docLang],
|
||||
},
|
||||
);
|
||||
const { html, loading: parseLoading } = useParseMarkdown(data?.data?.content);
|
||||
|
||||
const htmlWithOutRelativeDirect = useMemo(() => {
|
||||
if (html) {
|
||||
const pattern = /<a\s+href="\..*?\/([^/]+)"/g;
|
||||
return html.replace(pattern, (match, $1) => match + `onclick="return false;"`); // prevent the default event of <a/>
|
||||
}
|
||||
}, [html]);
|
||||
|
||||
const handleSwitchDocLang = useCallback((e: MouseEvent) => {
|
||||
const lang = (e.target as HTMLDivElement).innerHTML;
|
||||
if (lang.trim() === '中文') {
|
||||
setDocLang('zh-CN');
|
||||
} else if (lang.trim() === 'English') {
|
||||
setDocLang('en-US');
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const md = document.getElementById('pm-md-preview');
|
||||
md.addEventListener('click', handleSwitchDocLang);
|
||||
return () => {
|
||||
removeEventListener('click', handleSwitchDocLang);
|
||||
};
|
||||
}, [handleSwitchDocLang]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={css`
|
||||
background: #ffffff;
|
||||
padding: var(--nb-spacing); // if the antd can upgrade to v5.0, theme token will be better
|
||||
height: 60vh;
|
||||
overflow-y: auto;
|
||||
`}
|
||||
id="pm-md-preview"
|
||||
>
|
||||
{loading || parseLoading ? (
|
||||
<Spin />
|
||||
) : (
|
||||
<div className="nb-markdown" dangerouslySetInnerHTML={{ __html: error ? '' : htmlWithOutRelativeDirect }}></div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function PluginDetail(props: IPluginDetail) {
|
||||
const { plugin, onCancel, items } = props;
|
||||
return (
|
||||
<Modal
|
||||
footer={false}
|
||||
className={css`
|
||||
.ant-modal-header {
|
||||
background: #f0f2f5;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
background: #f0f2f5;
|
||||
.plugin-desc {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
}
|
||||
`}
|
||||
width="70%"
|
||||
title={
|
||||
<Typography.Title level={2} style={{ margin: 0 }}>
|
||||
{plugin?.displayName || plugin?.name}
|
||||
<Tag
|
||||
className={css`
|
||||
vertical-align: middle;
|
||||
margin-top: -3px;
|
||||
margin-left: 8px;
|
||||
`}
|
||||
>
|
||||
v{plugin?.version}
|
||||
</Tag>
|
||||
</Typography.Title>
|
||||
}
|
||||
open={!!plugin}
|
||||
onCancel={onCancel}
|
||||
destroyOnClose
|
||||
>
|
||||
{plugin?.description && <div className={'plugin-desc'}>{plugin?.description}</div>}
|
||||
<Tabs items={items}></Tabs>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function CommonCard(props: ICommonCard) {
|
||||
const { onClick, name, displayName, actions, description, title } = props;
|
||||
return (
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{ width: 'calc(20% - 24px)', marginRight: 24, marginBottom: 24, transition: 'all 0.35s ease-in-out' }}
|
||||
onClick={onClick}
|
||||
className={cls(css`
|
||||
&:hover {
|
||||
border: 1px solid var(--antd-wave-shadow-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
border: 1px solid transparent;
|
||||
`)}
|
||||
actions={actions}
|
||||
// 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 style={{ background: `${stringToColor(name)}` }}>{name?.[0]}</Avatar>}
|
||||
description={
|
||||
<Tooltip title={description} placement="bottom">
|
||||
<div
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
{description || '-'}
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
title={
|
||||
<span>
|
||||
{displayName || name}
|
||||
<span
|
||||
className={css`
|
||||
display: block;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
font-weight: normal;
|
||||
font-size: 13px;
|
||||
// margin-left: 8px;
|
||||
`}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export const PluginCard = (props: { data: IPluginData }) => {
|
||||
const history = useHistory<any>();
|
||||
const { data } = props;
|
||||
const api = useAPIClient();
|
||||
const { t } = useTranslation();
|
||||
const { enabled, name, displayName, id, description, version } = data;
|
||||
const [plugin, setPlugin] = useState<any>(null);
|
||||
const { data: tabsData, run } = useRequest(
|
||||
{
|
||||
url: '/plugins:getTabs',
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
},
|
||||
);
|
||||
const items = useMemo<TabsProps['items']>(() => {
|
||||
return tabsData?.data?.tabs.map((item) => {
|
||||
return {
|
||||
label: item.title,
|
||||
key: item.path,
|
||||
children: React.createElement(PluginDocument, {
|
||||
name: tabsData?.data.filterByTk,
|
||||
path: item.path,
|
||||
}),
|
||||
};
|
||||
});
|
||||
}, [tabsData?.data]);
|
||||
|
||||
const actions = useMemo(
|
||||
() =>
|
||||
[
|
||||
enabled ? (
|
||||
<SettingOutlined
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
history.push(`/admin/settings/${name}`);
|
||||
}}
|
||||
/>
|
||||
) : null,
|
||||
<Popconfirm
|
||||
key={id}
|
||||
title={t('Are you sure to delete this plugin?')}
|
||||
onConfirm={async (e) => {
|
||||
e.stopPropagation();
|
||||
await api.request({
|
||||
url: `pm:remove/${name}`,
|
||||
});
|
||||
message.success(t('插件删除成功'));
|
||||
window.location.reload();
|
||||
}}
|
||||
onCancel={(e) => e.stopPropagation()}
|
||||
okText={t('Yes')}
|
||||
cancelText={t('No')}
|
||||
>
|
||||
<DeleteOutlined onClick={(e) => e.stopPropagation()} />
|
||||
</Popconfirm>,
|
||||
<Switch
|
||||
key={id}
|
||||
size={'small'}
|
||||
onChange={async (checked, e) => {
|
||||
e.stopPropagation();
|
||||
Modal.warn({
|
||||
title: checked ? t('Plugin staring') : t('Plugin stopping'),
|
||||
content: t('The application is reloading, please do not close the page.'),
|
||||
okButtonProps: {
|
||||
style: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
await api.request({
|
||||
url: `pm:${checked ? 'enable' : 'disable'}/${name}`,
|
||||
});
|
||||
window.location.reload();
|
||||
// message.success(checked ? t('插件激活成功') : t('插件禁用成功'));
|
||||
}}
|
||||
defaultChecked={enabled}
|
||||
></Switch>,
|
||||
].filter(Boolean),
|
||||
[api, enabled, history, id, name, t],
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<PluginDetail plugin={plugin} onCancel={() => setPlugin(null)} items={items} />
|
||||
<CommonCard
|
||||
onClick={() => {
|
||||
setPlugin(data);
|
||||
run({
|
||||
params: {
|
||||
filterByTk: name,
|
||||
},
|
||||
});
|
||||
}}
|
||||
name={name}
|
||||
description={description}
|
||||
title={version}
|
||||
actions={actions}
|
||||
displayName={displayName}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const BuiltInPluginCard = (props: { data: IPluginData }) => {
|
||||
const {
|
||||
data: { description, name, version, displayName },
|
||||
data,
|
||||
} = props;
|
||||
const history = useHistory();
|
||||
const [plugin, setPlugin] = useState<any>(null);
|
||||
const { data: tabsData, run } = useRequest(
|
||||
{
|
||||
url: '/plugins:getTabs',
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
},
|
||||
);
|
||||
const items = useMemo(() => {
|
||||
return tabsData?.data?.tabs.map((item) => {
|
||||
return {
|
||||
label: item.title,
|
||||
key: item.path,
|
||||
children: React.createElement(PluginDocument, {
|
||||
name: tabsData?.data.filterByTk,
|
||||
path: item.path,
|
||||
}),
|
||||
};
|
||||
});
|
||||
}, [tabsData?.data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PluginDetail plugin={plugin} onCancel={() => setPlugin(null)} items={items} />
|
||||
<CommonCard
|
||||
onClick={() => {
|
||||
setPlugin(data);
|
||||
run({
|
||||
params: {
|
||||
filterByTk: name,
|
||||
},
|
||||
});
|
||||
}}
|
||||
name={name}
|
||||
displayName={displayName}
|
||||
description={description}
|
||||
title={version}
|
||||
actions={[
|
||||
<div key="placeholder-comp"></div>,
|
||||
<SettingOutlined
|
||||
key="settings"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
history.push(`/admin/settings/${name}`);
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
function useCallabck(arg0: () => void, arg1: undefined[]) {
|
||||
throw new Error('Function not implemented.');
|
||||
}
|
@ -16,7 +16,7 @@ export const PluginManagerLink = () => {
|
||||
icon={<ApiOutlined />}
|
||||
title={t('Plugin manager')}
|
||||
onClick={() => {
|
||||
history.push('/admin/pm/list');
|
||||
history.push('/admin/pm/list/');
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
@ -1,313 +1,105 @@
|
||||
import { css } from '@emotion/css';
|
||||
import {
|
||||
Layout,
|
||||
Menu,
|
||||
message,
|
||||
Modal,
|
||||
PageHeader,
|
||||
Popconfirm,
|
||||
Result,
|
||||
Space,
|
||||
Spin,
|
||||
Table,
|
||||
TableProps,
|
||||
Tabs,
|
||||
TabsProps,
|
||||
Tag,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { Layout, Menu, PageHeader, Result, Spin, Tabs } from 'antd';
|
||||
import { sortBy } from 'lodash';
|
||||
import React, { createContext, useContext, useMemo, useState } from 'react';
|
||||
import React, { createContext, useContext, useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Redirect, useHistory, useRouteMatch } from 'react-router-dom';
|
||||
import { ACLPane } from '../acl';
|
||||
import { useACLRoleContext } from '../acl/ACLProvider';
|
||||
import { useAPIClient, useRequest } from '../api-client';
|
||||
import { 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, useTableSize } from '../schema-component';
|
||||
import { useParseMarkdown } from '../schema-component/antd/markdown/util';
|
||||
import { useCompile } from '../schema-component';
|
||||
import { BlockTemplatesPane } from '../schema-templates';
|
||||
import { SystemSettingsPane } from '../system-settings';
|
||||
const { Link } = Typography;
|
||||
import { BuiltInPluginCard, PluginCard } from './Card';
|
||||
|
||||
export interface TData {
|
||||
data: IPluginData[];
|
||||
meta: IMetaData;
|
||||
}
|
||||
|
||||
export interface IPluginData {
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
name: string;
|
||||
displayName: string;
|
||||
version: string;
|
||||
enabled: boolean;
|
||||
installed: boolean;
|
||||
builtIn: boolean;
|
||||
options: Record<string, unknown>;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface IMetaData {
|
||||
count: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
totalPage: number;
|
||||
allowedActions: AllowedActions;
|
||||
}
|
||||
|
||||
export interface AllowedActions {
|
||||
view: number[];
|
||||
update: number[];
|
||||
destroy: number[];
|
||||
}
|
||||
|
||||
// TODO: refactor card/built-int card
|
||||
|
||||
export const SettingsCenterContext = createContext<any>({});
|
||||
|
||||
interface PluginTableProps {
|
||||
filter: any;
|
||||
builtIn?: boolean;
|
||||
}
|
||||
|
||||
interface PluginDocumentProps {
|
||||
path: string;
|
||||
name: string;
|
||||
}
|
||||
const PluginDocument: React.FC<PluginDocumentProps> = (props) => {
|
||||
const { data, loading, error } = useRequest(
|
||||
{
|
||||
url: '/plugins:getTabInfo',
|
||||
params: {
|
||||
filterByTk: props.name,
|
||||
path: props.path,
|
||||
},
|
||||
},
|
||||
{
|
||||
refreshDeps: [props.name, props.path],
|
||||
},
|
||||
);
|
||||
const { html, loading: parseLoading } = useParseMarkdown(data?.data?.content);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={css`
|
||||
background: #ffffff;
|
||||
padding: var(--nb-spacing);
|
||||
height: 60vh;
|
||||
overflow-y: auto;
|
||||
`}
|
||||
>
|
||||
{loading || parseLoading ? (
|
||||
<Spin />
|
||||
) : (
|
||||
<div className="nb-markdown" dangerouslySetInnerHTML={{ __html: error ? '' : html }}></div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PluginTable: React.FC<PluginTableProps> = (props) => {
|
||||
const { builtIn } = props;
|
||||
const history = useHistory<any>();
|
||||
const api = useAPIClient();
|
||||
const [plugin, setPlugin] = useState<any>(null);
|
||||
const { t, i18n } = useTranslation();
|
||||
const settingItems = useContext(SettingsCenterContext);
|
||||
const { data, loading } = useRequest({
|
||||
const LocalPlugins = () => {
|
||||
// TODO: useRequest types for data ts type
|
||||
const { data, loading }: { data: TData; loading: boolean } = useRequest<TData>({
|
||||
url: 'applicationPlugins:list',
|
||||
params: {
|
||||
filter: props.filter,
|
||||
filter: {
|
||||
'builtIn.$isFalsy': true,
|
||||
},
|
||||
sort: 'name',
|
||||
paginate: false,
|
||||
},
|
||||
});
|
||||
|
||||
const { data: tabsData, run } = useRequest(
|
||||
{
|
||||
url: '/plugins:getTabs',
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
},
|
||||
);
|
||||
|
||||
const columns = useMemo<TableProps<any>['columns']>(() => {
|
||||
return [
|
||||
{
|
||||
title: t('Plugin name'),
|
||||
dataIndex: 'displayName',
|
||||
width: 300,
|
||||
render: (displayName, record) => displayName || record.name,
|
||||
},
|
||||
{
|
||||
title: t('Description'),
|
||||
dataIndex: 'description',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: t('Version'),
|
||||
dataIndex: 'version',
|
||||
width: 300,
|
||||
},
|
||||
// {
|
||||
// title: t('Author'),
|
||||
// dataIndex: 'author',
|
||||
// width: 200,
|
||||
// },
|
||||
{
|
||||
title: t('Actions'),
|
||||
width: 300,
|
||||
render(data) {
|
||||
return (
|
||||
<Space>
|
||||
<Link
|
||||
onClick={() => {
|
||||
setPlugin(data);
|
||||
run({
|
||||
params: {
|
||||
filterByTk: data.name,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('View')}
|
||||
</Link>
|
||||
{data.enabled && settingItems[data.name] ? (
|
||||
<Link
|
||||
onClick={() => {
|
||||
history.push(`/admin/settings/${data.name}`);
|
||||
}}
|
||||
>
|
||||
{t('Setting')}
|
||||
</Link>
|
||||
) : null}
|
||||
{!builtIn ? (
|
||||
<>
|
||||
<Link
|
||||
onClick={async () => {
|
||||
const checked = !data.enabled;
|
||||
Modal.warn({
|
||||
title: checked ? t('Plugin starting') : t('Plugin stopping'),
|
||||
content: t('The application is reloading, please do not close the page.'),
|
||||
okButtonProps: {
|
||||
style: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
await api.request({
|
||||
url: `pm:${checked ? 'enable' : 'disable'}/${data.name}`,
|
||||
});
|
||||
window.location.reload();
|
||||
// message.success(checked ? t('插件激活成功') : t('插件禁用成功'));
|
||||
}}
|
||||
>
|
||||
{t(data.enabled ? 'Disable' : 'Enable')}
|
||||
</Link>
|
||||
<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')}
|
||||
>
|
||||
<Link>{t('Delete')}</Link>
|
||||
</Popconfirm>
|
||||
</>
|
||||
) : null}
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}, [t, builtIn]);
|
||||
|
||||
const items = useMemo<TabsProps['items']>(() => {
|
||||
return tabsData?.data?.tabs.map((item) => {
|
||||
return {
|
||||
label: item.title,
|
||||
key: item.path,
|
||||
children: React.createElement(PluginDocument, {
|
||||
name: tabsData?.data.filterByTk,
|
||||
path: item.path,
|
||||
}),
|
||||
};
|
||||
});
|
||||
}, [tabsData?.data]);
|
||||
|
||||
const { height, tableSizeRefCallback } = useTableSize();
|
||||
if (loading) {
|
||||
return <Spin />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={css`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
padding: var(--nb-spacing);
|
||||
`}
|
||||
>
|
||||
<Modal
|
||||
footer={false}
|
||||
className={css`
|
||||
.ant-modal-header {
|
||||
background: #f0f2f5;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
background: #f0f2f5;
|
||||
.plugin-desc {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
}
|
||||
`}
|
||||
width="70%"
|
||||
title={
|
||||
<Typography.Title level={2} style={{ margin: 0 }}>
|
||||
{plugin?.displayName || plugin?.name}
|
||||
<Tag
|
||||
className={css`
|
||||
vertical-align: middle;
|
||||
margin-top: -3px;
|
||||
margin-left: 8px;
|
||||
`}
|
||||
>
|
||||
v{plugin?.version}
|
||||
</Tag>
|
||||
</Typography.Title>
|
||||
}
|
||||
open={!!plugin}
|
||||
onCancel={() => setPlugin(null)}
|
||||
>
|
||||
{plugin?.description && <div className={'plugin-desc'}>{plugin?.description}</div>}
|
||||
<Tabs items={items}></Tabs>
|
||||
</Modal>
|
||||
<Table
|
||||
ref={tableSizeRefCallback}
|
||||
pagination={false}
|
||||
className={css`
|
||||
.ant-spin-nested-loading {
|
||||
height: 100%;
|
||||
.ant-spin-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.ant-table {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
height: 100%;
|
||||
`}
|
||||
scroll={{
|
||||
y: height,
|
||||
}}
|
||||
dataSource={data?.data || []}
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const LocalPlugins = () => {
|
||||
return (
|
||||
<PluginTable
|
||||
filter={{
|
||||
'builtIn.$isFalsy': true,
|
||||
}}
|
||||
></PluginTable>
|
||||
<>
|
||||
{data?.data?.map((item) => {
|
||||
const { id } = item;
|
||||
return <PluginCard data={item} key={id} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const BuiltinPlugins = () => {
|
||||
return (
|
||||
<PluginTable
|
||||
builtIn
|
||||
filter={{
|
||||
const { data, loading } = useRequest({
|
||||
url: 'applicationPlugins:list',
|
||||
params: {
|
||||
filter: {
|
||||
'builtIn.$isTruly': true,
|
||||
}}
|
||||
></PluginTable>
|
||||
},
|
||||
sort: 'name',
|
||||
paginate: false,
|
||||
},
|
||||
});
|
||||
if (loading) {
|
||||
return <Spin />;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{data?.data?.map((item) => {
|
||||
const { id } = item;
|
||||
return <BuiltInPluginCard data={item} key={id} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -324,15 +116,15 @@ const PluginList = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const { snippets = [] } = useACLRoleContext();
|
||||
|
||||
useEffect(() => {
|
||||
const { tabName } = match.params;
|
||||
if (!tabName) {
|
||||
history.replace(`/admin/pm/list/local/`);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return snippets.includes('pm') ? (
|
||||
<div
|
||||
className={css`
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
`}
|
||||
>
|
||||
<div>
|
||||
<PageHeader
|
||||
ghost={false}
|
||||
title={t('Plugin manager')}
|
||||
@ -340,7 +132,7 @@ const PluginList = (props) => {
|
||||
<Tabs
|
||||
activeKey={tabName}
|
||||
onChange={(activeKey) => {
|
||||
history.push(`/admin/pm/list/${activeKey}`);
|
||||
history.push(`/admin/pm/list/${activeKey}/`);
|
||||
}}
|
||||
>
|
||||
<Tabs.TabPane tab={t('Local')} key={'local'} />
|
||||
@ -349,7 +141,7 @@ const PluginList = (props) => {
|
||||
</Tabs>
|
||||
}
|
||||
/>
|
||||
<div style={{ margin: 'var(--nb-spacing)', flex: 1, display: 'flex', flexFlow: 'row wrap' }}>
|
||||
<div className={'m24'} style={{ margin: 24, display: 'flex', flexFlow: 'row wrap' }}>
|
||||
{React.createElement(
|
||||
{
|
||||
local: LocalPlugins,
|
||||
@ -529,7 +321,7 @@ const SettingsCenter = (props) => {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<div style={{ margin: 'var(--nb-spacing)' }}>
|
||||
<div className={'m24'} style={{ margin: 24 }}>
|
||||
{aclPluginTabCheck ? (
|
||||
component && React.createElement(component)
|
||||
) : (
|
||||
@ -555,7 +347,7 @@ export const PMProvider = (props) => {
|
||||
routes[1].routes.unshift(
|
||||
{
|
||||
type: 'route',
|
||||
path: '/admin/pm/list/:tabName?',
|
||||
path: '/admin/pm/list/:tabName?/:mdfile?',
|
||||
component: PluginList,
|
||||
},
|
||||
{
|
||||
|
@ -3,7 +3,7 @@
|
||||
"displayName": "ACL",
|
||||
"displayName.zh-CN": "权限控制",
|
||||
"description": "A simple access control based on roles, resources and actions",
|
||||
"description.zh-CN": "基于角色、资源和操作的权限控制插件",
|
||||
"description.zh-CN": "基于角色、资源和操作的权限控制。",
|
||||
"version": "0.9.4-alpha.2",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "./lib/index.js",
|
||||
|
@ -1,6 +1,10 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-audit-logs",
|
||||
"version": "0.9.4-alpha.2",
|
||||
"displayName": "audit-logs",
|
||||
"displayName.zh-CN": "审计日志",
|
||||
"description": "audit logs plugin",
|
||||
"description.zh-CN": "审计日志。",
|
||||
"main": "./lib/server/index.js",
|
||||
"types": "./lib/server/index.d.ts",
|
||||
"license": "AGPL-3.0",
|
||||
|
@ -1,5 +1,9 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-charts",
|
||||
"displayName": "charts",
|
||||
"displayName.zh-CN": "图表",
|
||||
"description": "Out-of-the-box, feature-rich chart plugins.",
|
||||
"description.zh-CN": "开箱即用、丰富的报表。",
|
||||
"version": "0.9.4-alpha.2",
|
||||
"main": "lib/server/index.js",
|
||||
"devDependencies": {
|
||||
|
@ -1,6 +1,10 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-china-region",
|
||||
"version": "0.9.4-alpha.2",
|
||||
"displayName": "china-region",
|
||||
"displayName.zh-CN": "中国行政区",
|
||||
"description": "Chinese Administrative Division Plugin, including all administrative regions of China.",
|
||||
"description.zh-CN": "中国行政区划插件,内含所有中国行政区域。",
|
||||
"main": "lib/index.js",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
|
@ -1,5 +1,9 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-client",
|
||||
"displayName": "client",
|
||||
"displayName.zh-CN": "客户端",
|
||||
"description": "client",
|
||||
"description.zh-CN": "客户端。",
|
||||
"version": "0.9.4-alpha.2",
|
||||
"main": "lib/index.js",
|
||||
"license": "AGPL-3.0",
|
||||
|
@ -1,5 +1,9 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-collection-manager",
|
||||
"displayName": "collection manager plugin",
|
||||
"displayName.zh-CN": "数据库管理",
|
||||
"description": " database management plugin designed to simplify the process of managing and operating databases. It seamlessly integrates with various relational database systems such as MySQL and PostgreSQL, and provides an intuitive user interface for performing various database tasks.",
|
||||
"description.zh-CN": "可以与多种关系型数据库系统(如MySQL、PostgreSQL)无缝集成,并提供直观的用户界面来执行各种数据库任务。",
|
||||
"version": "0.9.4-alpha.2",
|
||||
"main": "lib/index.js",
|
||||
"license": "AGPL-3.0",
|
||||
|
@ -1,7 +1,10 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-duplicator",
|
||||
"displayName": "duplicator",
|
||||
"displayName.zh-CN": "NocoBase应用备份与还原",
|
||||
"description": "Backup and Restore Plugin for NocoBase applications, used for scenarios such as application replication, migration, and upgrade.",
|
||||
"description.zh-CN": "NocoBase 应用的备份与还原插件,可用于应用的复制、迁移、升级等场景。",
|
||||
"version": "0.9.4-alpha.2",
|
||||
"description": "",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
|
@ -1,7 +1,10 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-error-handler",
|
||||
"displayName": "error handler",
|
||||
"displayName.zh-CN": "错误处理",
|
||||
"description": "Managing and handling errors and exceptions in an application.",
|
||||
"description.zh-CN": "管理和处理应用程序中的错误和异常。",
|
||||
"version": "0.9.4-alpha.2",
|
||||
"description": "",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
|
@ -1,7 +1,10 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-export",
|
||||
"displayName": "export",
|
||||
"displayName.zh-CN": "导出",
|
||||
"description": "export data",
|
||||
"description.zh-CN": "导出数据。",
|
||||
"version": "0.9.4-alpha.2",
|
||||
"description": "",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
|
@ -1,7 +1,10 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-file-manager",
|
||||
"version": "0.9.4-alpha.2",
|
||||
"description": "",
|
||||
"displayName": "file manager",
|
||||
"displayName.zh-CN": "文件管理",
|
||||
"description": "file management plugin.",
|
||||
"description.zh-CN": "文件管理。",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
|
@ -1,7 +1,10 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-formula-field",
|
||||
"displayName": "formula-field",
|
||||
"displayName.zh-CN": "字段",
|
||||
"description": "formula-field",
|
||||
"description.zh-CN": "字段。",
|
||||
"version": "0.9.4-alpha.2",
|
||||
"description": "",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
|
@ -1,7 +1,10 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-graph-collection-manager",
|
||||
"displayName": "graph collection manager",
|
||||
"displayName.zh-CN": "数据库可视化管理",
|
||||
"description": "database collection manage",
|
||||
"description.zh-CN": "数据库管理。",
|
||||
"version": "0.9.4-alpha.2",
|
||||
"description": "",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
|
@ -1,7 +1,10 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-iframe-block",
|
||||
"displayName": "iframe",
|
||||
"displayName.zh-CN": "iframe",
|
||||
"description": "create and manage IFrame blocks on the page for embedding and displaying external web pages or content.",
|
||||
"description.zh-CN": "在页面上创建和管理iframe,用于嵌入和展示外部网页或内容。",
|
||||
"version": "0.9.4-alpha.2",
|
||||
"description": "",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
|
@ -1,7 +1,10 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-import",
|
||||
"displayName": "import",
|
||||
"displayName.zh-CN": "导入",
|
||||
"description": "import data.",
|
||||
"description.zh-CN": "导入数据。",
|
||||
"version": "0.9.4-alpha.2",
|
||||
"description": "",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
|
@ -1,7 +1,10 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-math-formula-field",
|
||||
"displayName": "math formula field",
|
||||
"displayName.zh-CN": "数学公式字段",
|
||||
"description": "A powerful mathematical formula field plugin designed to provide flexible mathematical and formula calculation capabilities for databases or data management systems. It seamlessly integrates with various database systems such as MySQL, PostgreSQL, and offers an intuitive user interface for defining and executing mathematical formula fields.",
|
||||
"description.zh-CN": "一个功能强大的数学公式字段插件,旨在为数据库或数据管理系统提供灵活的数学计算和公式计算功能。它可以与各种数据库系统(如MySQL、PostgreSQL)无缝集成,并提供直观的用户界面来定义和执行数学公式字段。",
|
||||
"version": "0.9.4-alpha.2",
|
||||
"description": "",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
|
@ -1,7 +1,10 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-multi-app-manager",
|
||||
"displayName": "multi app manager",
|
||||
"displayName.zh-CN": "多应用管理",
|
||||
"description": "manage app",
|
||||
"description.zh-CN": "管理应用",
|
||||
"version": "0.9.4-alpha.2",
|
||||
"description": "",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
|
@ -1,5 +1,9 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-multi-app-share-collection",
|
||||
"displayName": "multi app share collection",
|
||||
"displayName.zh-CN": "多应用数据共享",
|
||||
"description": "multi app share collection",
|
||||
"description.zh-CN": "多应用数据共享",
|
||||
"version": "0.9.4-alpha.2",
|
||||
"main": "lib/server/index.js",
|
||||
"devDependencies": {
|
||||
|
@ -1,7 +1,10 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-oidc",
|
||||
"displayName": "OpenID Connect",
|
||||
"displayName.zh-CN": "OpenID Connect",
|
||||
"description": " provide OIDC authentication and authorization functionality for applications or authentication systems.",
|
||||
"description.zh-CN": "在为应用程序或身份验证系统提供OIDC认证和授权功能。",
|
||||
"version": "0.9.4-alpha.2",
|
||||
"description": "",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
|
@ -1,7 +1,10 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-saml",
|
||||
"displayName": "saml",
|
||||
"displayName.zh-CN": "saml",
|
||||
"description": "provide SAML authentication and authorization functionality for applications or authentication systems.",
|
||||
"description.zh-CN": "为应用程序或身份验证系统提供SAML认证和授权功能。",
|
||||
"version": "0.9.4-alpha.2",
|
||||
"description": "",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
|
@ -1,7 +1,10 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-sequence-field",
|
||||
"displayName": "sequence field",
|
||||
"displayName.zh-CN": "字段序列",
|
||||
"description": "provide the functionality of automatically generating unique sequence values for databases or data management systems.",
|
||||
"description.zh-CN": "为数据库或数据管理系统提供自动生成唯一序列值的功能。",
|
||||
"version": "0.9.4-alpha.2",
|
||||
"description": "",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
|
@ -1,7 +1,10 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-snapshot-field",
|
||||
"displayName": "snapshot field",
|
||||
"displayName.zh-CN": "关系数据库快照",
|
||||
"description": "provide fast and reliable database backup and recovery functionality for database systems.",
|
||||
"description.zh-CN": "为数据库系统提供快速、可靠的数据库备份和恢复功能。。",
|
||||
"version": "0.9.4-alpha.2",
|
||||
"description": "",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
|
@ -1,7 +1,10 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-system-settings",
|
||||
"displayName": "system settings",
|
||||
"displayName.zh-CN": "系统配置",
|
||||
"description": "A plugin specifically designed to adjust the system-level settings of NocoBase.",
|
||||
"description.zh-CN": "用于调整 nocobase 的系统级设置。",
|
||||
"version": "0.9.4-alpha.2",
|
||||
"description": "",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
|
@ -1,7 +1,10 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-ui-routes-storage",
|
||||
"displayName": "ui routes storage",
|
||||
"displayName.zh-CN": "路由管理",
|
||||
"description": "manage the routes of nocobase",
|
||||
"description.zh-CN": "管理页面路由。",
|
||||
"version": "0.9.4-alpha.2",
|
||||
"description": "",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
|
@ -1,7 +1,10 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-ui-schema-storage",
|
||||
"displayName": "UI schema storage",
|
||||
"displayName.zh-CN": "UI schema",
|
||||
"description": "A plugin used for managing page UI schemas.",
|
||||
"description.zh-CN": "UI schema 配置。",
|
||||
"version": "0.9.4-alpha.2",
|
||||
"description": "",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
|
@ -1,7 +1,10 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-users",
|
||||
"displayName": "users",
|
||||
"displayName.zh-CN": "用户管理",
|
||||
"description": "manage the uses of nocobase system",
|
||||
"description.zh-CN": "管理系统用户",
|
||||
"version": "0.9.4-alpha.2",
|
||||
"description": "",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
|
@ -1,7 +1,10 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-verification",
|
||||
"displayName": "verification",
|
||||
"displayName.zh-CN": "验证码",
|
||||
"description": "verification setting.",
|
||||
"description.zh-CN": "验证码配置。",
|
||||
"version": "0.9.4-alpha.2",
|
||||
"description": "",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
|
@ -1,7 +1,10 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-workflow",
|
||||
"displayName": "workflow",
|
||||
"displayName.zh-CN": "工作流",
|
||||
"description": " a powerful workflow plugin designed to support business process management and automation. .",
|
||||
"description.zh-CN": "工作流插件,为业务流程管理和自动化提供支持。",
|
||||
"version": "0.9.4-alpha.2",
|
||||
"description": "",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
|
@ -2,6 +2,10 @@
|
||||
"name": "@nocobase/plugin-sample-hello",
|
||||
"version": "0.9.4-alpha.2",
|
||||
"main": "lib/server/index.js",
|
||||
"displayName": "Sample-Hello",
|
||||
"displayName.zh-CN": "插件demo-hello",
|
||||
"description": "simple plugin demo",
|
||||
"description.zh-CN": "这就是一个简单的插件demo",
|
||||
"devDependencies": {
|
||||
"@nocobase/client": "0.9.4-alpha.2",
|
||||
"@nocobase/server": "0.9.4-alpha.2",
|
||||
|
Loading…
Reference in New Issue
Block a user