nocobase/packages/core/client/src/pm/PluginManager.tsx
jack zhang 705b7449f0
feat: new plugin manager, supports adding plugins through UI (#2430)
* refactor: plugin manager page

* fix: bug

* feat: addByNpm api

* fix: improve the addByNpm

* feat: improve applicationPlugins:list api

* fix: re-download npm package when restart app

* fix: plugin delete api

* feat: plugin detail api

* feat: zipUrl add api

* fix: upload api bug

* fix: plugin detail info

* feat: upgrade api

* fix: upload api

* feat: handle plugin load error

* feat: support authToken

* feat: muti lang

* fix: build error

* fix: self review

* Update plugin-manager.ts

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: bugs

* fix: detail click and remove isOfficial

* fix: upgrade no refresh

* fix: file size and type check

* fix: bug

* fix: upgrade error

* fix: bug

* fix: bug

* fix: plugin card layout

* fix: handling exceptional cases

* fix: tgz file support

* fix: macos compress file

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: add upgrade npm type

* fix: bugs

* fix: bug

* fix: change plugins static expose url

* fix: api prefix

* fix: bug

* fix: add nginx `/static/plugin/` path

* fix: bugs and pr docker build no dts

* fix: bug

* fix: build tools bug

* fix: improve code

* fix: build bug

* feat: improve plugin info

* fix: ui bug

* fix: plugin document bug

* feat: improve code

* feat: improve code

* feat: process dev deps check

* feat: improve code

* feat: process.env.IS_DEV_CMD

* fix: do not delete the plugin package

* feat: plugin symlink

* fix: tsx watch --ignore=./storage/plugins/**

* fix: test error

* fix: improve code

* fix: improve code

* fix: emitStartedEvent

* fix: improve code

* fix: type error

* fix: test error

* test: console.log

* fix: createStoragePluginSymLink

* fix: clientStaticMiddleware rename to clientStaticUtils

* feat: build tools support plugins folder

* fix: 350px

* fix: error

* feat: client dev support plugin folder

* fix: clear cli options

* fix: typeError: Converting circular structure to JSON

* fix: plugin name

* chore: restart application after command

* feat: upgrade error & docs

* Update v14-changelog.md

* Update v14-changelog.md

* Update v14-changelog.md

* fix: gateway test

* refactor(plugin-workflow): add ready state for gracefully tearing down

* Revert "chore: restart application after command"

This reverts commit 5015274f8e4e06e506e15754b672330330e8c7f8.

* chore: stop application whe restart

* T 1218 change plugin folder (#2629)

* feat: change folder name

* feat: change `pm create` command

* feat:  revert plugin name change

* fix: delete samples

* feat: change plugins folder

* fix: pm create

* feat: update docs

* fix: link package error

* fix: docs

* fix: create command

* fix: pm add error

* fix: create  add build

* fix: pm creatre + add

* feat: add tar command

* fix: docs

* fix: bug

* fix: docs

---------

Co-authored-by: chenos <chenlinxh@gmail.com>

* feat: docs

* Update your-fisrt-plugin.md

* Update your-fisrt-plugin.md

* chore: application reload

* chore: test

* fix: pm add error

* chore: preset install skip exists plugin

* fix: createIfNotExists

---------

Co-authored-by: chenos <chenlinxh@gmail.com>
Co-authored-by: chareice <chareice@live.com>
Co-authored-by: Zhou <zhou.working@gmail.com>
Co-authored-by: mytharcher <mytharcher@gmail.com>
2023-09-12 22:39:23 +08:00

223 lines
6.1 KiB
TypeScript

export * from './PluginManagerLink';
import { PageHeader } from '@ant-design/pro-layout';
import { useDebounce } from 'ahooks';
import { Button, Divider, Input, Result, Space, Spin, Tabs } from 'antd';
import _ from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate, useParams } from 'react-router-dom';
import { css } from '@emotion/css';
import { useACLRoleContext } from '../acl/ACLProvider';
import { useRequest } from '../api-client';
import { useToken } from '../style';
import { PluginCard } from './PluginCard';
import { PluginAddModal } from './PluginForm/modal/PluginAddModal';
import { useStyles } from './style';
import { IPluginData } from './types';
export interface TData {
data: IPluginData[];
meta: IMetaData;
}
export interface IMetaData {
count: number;
page: number;
pageSize: number;
totalPage: number;
allowedActions: AllowedActions;
}
export interface AllowedActions {
view: number[];
update: number[];
destroy: number[];
}
const LocalPlugins = () => {
const { t } = useTranslation();
const { theme } = useStyles();
const { data, loading, refresh } = useRequest<TData>({
url: 'pm:list',
});
const filterList = useMemo(() => {
let list = data?.data || [];
list = list.reverse();
return [
{
type: 'All',
list: list,
},
{
type: 'Built-in',
list: _.filter(list, (item) => item.builtIn),
},
{
type: 'Enabled',
list: _.filter(list, (item) => item.enabled),
},
{
type: 'Not enabled',
list: _.filter(list, (item) => !item.enabled),
},
{
type: 'Problematic',
list: _.filter(list, (item) => !item.isCompatible),
},
];
}, [data?.data]);
const [filterIndex, setFilterIndex] = useState(0);
const [isShowAddForm, setShowAddForm] = useState(false);
const [searchValue, setSearchValue] = useState('');
const debouncedSearchValue = useDebounce(searchValue, { wait: 100 });
const pluginList = useMemo(() => {
let list = filterList[filterIndex]?.list || [];
if (debouncedSearchValue) {
list = _.filter(
list,
(item) =>
item.name?.includes(debouncedSearchValue) ||
item.description?.includes(debouncedSearchValue) ||
item.displayName?.includes(debouncedSearchValue) ||
item.packageName?.includes(debouncedSearchValue),
);
}
return list;
}, [filterIndex, filterList, debouncedSearchValue]);
const handleSearch = (value: string) => {
setSearchValue(value);
};
if (loading) {
return <Spin />;
}
return (
<>
<PluginAddModal
isShow={isShowAddForm}
onClose={(isRefresh) => {
setShowAddForm(false);
// if (isRefresh) refresh();
}}
/>
<div style={{ width: '100%' }}>
<div
style={{ marginBottom: theme.marginLG }}
className={css`
justify-content: space-between;
display: flex;
align-items: center;
`}
>
<div>
<Space size={theme.marginXXS} split={<Divider type="vertical" />}>
{filterList.map((item, index) => (
<a
onClick={() => setFilterIndex(index)}
key={item.type}
style={{ fontWeight: filterIndex === index ? 'bold' : 'normal' }}
>
{t(item.type)}({item.list?.length})
</a>
))}
<Input
allowClear
placeholder={t('Search plugin')}
onChange={(e) => handleSearch(e.currentTarget.value)}
/>
</Space>
</div>
<div>
<Space>
<Button onClick={() => setShowAddForm(true)} type="primary">
{t('Add new')}
</Button>
</Space>
</div>
</div>
<div
className={css`
--grid-gutter: ${theme.margin}px;
--extensions-card-width: 350px;
display: grid;
grid-column-gap: var(--grid-gutter);
grid-row-gap: var(--grid-gutter);
grid-template-columns: repeat(auto-fill, var(--extensions-card-width));
justify-content: center;
margin: auto;
`}
>
{pluginList.map((item) => (
<PluginCard key={item.name} data={item} />
))}
</div>
</div>
</>
);
};
const MarketplacePlugins = () => {
const { token } = useToken();
const { t } = useTranslation();
return <div style={{ fontSize: token.fontSizeXL, color: token.colorText }}>{t('Coming soon...')}</div>;
};
export const PluginManager = () => {
const params = useParams();
const navigate = useNavigate();
const { tabName = 'local' } = params;
const { t } = useTranslation();
const { snippets = [] } = useACLRoleContext();
const { styles } = useStyles();
useEffect(() => {
const { tabName } = params;
if (!tabName) {
navigate(`/admin/pm/list/local/`, { replace: true });
}
}, []);
return snippets.includes('pm') ? (
<div>
<PageHeader
className={styles.pageHeader}
ghost={false}
title={t('Plugin manager')}
footer={
<Tabs
activeKey={tabName}
onChange={(activeKey) => {
navigate(`/admin/pm/list/${activeKey}`);
}}
items={[
{
key: 'local',
label: t('Local'),
},
{
key: 'marketplace',
label: t('Marketplace'),
},
]}
/>
}
/>
<div className={styles.pageContent} style={{ display: 'flex', flexFlow: 'row wrap' }}>
{React.createElement(
{
local: LocalPlugins,
marketplace: MarketplacePlugins,
}[tabName],
)}
</div>
</div>
) : (
<Result status="404" title="404" subTitle="Sorry, the page you visited does not exist." />
);
};