Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(FE): fetch category from manager-api #1122

Merged
merged 12 commits into from
Dec 29, 2020
6 changes: 4 additions & 2 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@
"@ant-design/icons": "^4.0.0",
"@ant-design/pro-layout": "^6.0.0",
"@ant-design/pro-table": "2.6.3",
"@api7-dashboard/pluginchart": "^1.0.14",
"@api7-dashboard/ui": "^1.0.3",
"@mrblenny/react-flow-chart": "^0.0.14",
"@rjsf/antd": "2.2.0",
"@rjsf/core": "2.2.0",
"@uiw/react-codemirror": "^3.0.1",
Expand All @@ -73,13 +73,15 @@
"react-dom": "^16.8.6",
"react-helmet-async": "^1.0.4",
"start-server-and-test": "^1.11.5",
"styled-components": "^5.2.1",
"umi": "^3.1.2",
"umi-request": "^1.0.8",
"use-merge-value": "^1.0.1",
"uuid": "7.0.3"
},
"devDependencies": {
"@ant-design/pro-cli": "^2.0.2",
"@types/base-64": "^0.1.3",
"@types/classnames": "^2.2.7",
"@types/express": "^4.17.0",
"@types/history": "^4.7.2",
Expand All @@ -91,8 +93,8 @@
"@types/react": "^16.9.17",
"@types/react-dom": "^16.8.4",
"@types/react-helmet": "^5.0.13",
"@types/styled-components": "^5.1.7",
"@types/uuid": "7.0.4",
"@types/base-64": "^0.1.3",
"@umijs/fabric": "^2.2.0",
"@umijs/plugin-blocks": "^2.0.5",
"@umijs/plugin-esbuild": "^1.0.0-beta.2",
Expand Down
211 changes: 106 additions & 105 deletions web/src/components/Plugin/PluginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@
* limitations under the License.
*/
import React, { useEffect, useState } from 'react';
import { Anchor, Layout, Switch, Card, Tooltip, Button, notification, Avatar } from 'antd';
import { Anchor, Layout, Switch, Card, Tooltip, Button, notification } from 'antd';
import { SettingFilled } from '@ant-design/icons';
import { PanelSection } from '@api7-dashboard/ui';
import Ajv, { DefinedError } from 'ajv';

import { fetchSchema, getList } from './service';
import { fetchList } from './service';
import CodeMirrorDrawer from './CodeMirrorDrawer';

type Props = {
Expand Down Expand Up @@ -51,11 +51,23 @@ const PluginPage: React.FC<Props> = ({
schemaType = '',
onChange = () => {},
}) => {
const [pluginList, setPlugin] = useState<PluginComponent.Meta[][]>([]);
const [pluginList, setPluginList] = useState<PluginComponent.Meta[]>([]);
const [name, setName] = useState<string>(NEVER_EXIST_PLUGIN_FLAG);
const [typeList, setTypeList] = useState<string[]>([]);

const firstUpperCase = ([first, ...rest]: string) => first.toUpperCase() + rest.join('');
useEffect(() => {
getList().then(setPlugin);
fetchList().then((data) => {
setPluginList(data);

const categoryList: string[] = [];
data.forEach((item) => {
if (!categoryList.includes(firstUpperCase(item.type))) {
categoryList.push(firstUpperCase(item.type));
}
});
setTypeList(categoryList.sort());
});
}, []);

// NOTE: This function has side effect because it mutates the original schema data
Expand All @@ -73,48 +85,55 @@ const PluginPage: React.FC<Props> = ({
};

const validateData = (pluginName: string, value: PluginComponent.Data) => {
fetchSchema(pluginName, schemaType).then((schema) => {
if (schema.oneOf) {
(schema.oneOf || []).forEach((item: any) => {
injectDisableProperty(item);
});
} else {
injectDisableProperty(schema);
}
const plugin = pluginList.find((item) => item.name === pluginName);
let schema: any = {};

const validate = ajv.compile(schema);
if (schemaType === 'consumer' && plugin?.consumer_schema) {
schema = plugin.consumer_schema;
} else if (plugin?.schema) {
schema = plugin.schema;
}

if (validate(value)) {
setName(NEVER_EXIST_PLUGIN_FLAG);
onChange({ ...initialData, [pluginName]: value });
return;
}
if (schema.oneOf) {
(schema.oneOf || []).forEach((item: any) => {
injectDisableProperty(item);
});
} else {
injectDisableProperty(schema);
}

// eslint-disable-next-line
for (const err of validate.errors as DefinedError[]) {
let description = '';
switch (err.keyword) {
case 'enum':
description = `${err.dataPath} ${err.message}: ${err.params.allowedValues.join(', ')}`;
break;
case 'minItems':
case 'type':
description = `${err.dataPath} ${err.message}`;
break;
case 'oneOf':
case 'required':
description = err.message || '';
break;
default:
description = `${err.schemaPath} ${err.message}`;
}
notification.error({
message: 'Invalid plugin data',
description,
});
const validate = ajv.compile(schema);

if (validate(value)) {
setName(NEVER_EXIST_PLUGIN_FLAG);
onChange({ ...initialData, [pluginName]: value });
return;
}

// eslint-disable-next-line
for (const err of validate.errors as DefinedError[]) {
let description = '';
switch (err.keyword) {
case 'enum':
description = `${err.dataPath} ${err.message}: ${err.params.allowedValues.join(', ')}`;
break;
case 'minItems':
case 'type':
description = `${err.dataPath} ${err.message}`;
break;
case 'oneOf':
case 'required':
description = err.message || '';
break;
default:
description = `${err.schemaPath} ${err.message}`;
}
setName(pluginName);
});
notification.error({
message: 'Invalid plugin data',
description,
});
}
setName(pluginName);
};

return (
Expand All @@ -133,78 +152,60 @@ const PluginPage: React.FC<Props> = ({
<Layout>
<Sider theme="light">
<Anchor offsetTop={150}>
{pluginList.map((plugins) => {
const { category } = plugins[0];
return (
<Anchor.Link
href={`#plugin-category-${category}`}
title={category}
key={category}
/>
);
{typeList.map((type) => {
return <Anchor.Link href={`#plugin-category-${type}`} title={type} key={type} />;
})}
</Anchor>
</Sider>
<Content style={{ padding: '0 10px', backgroundColor: '#fff', minHeight: 1400 }}>
{pluginList.map((plugins) => {
const { category } = plugins[0];
{typeList.map((type) => {
return (
<PanelSection
title={category}
key={category}
title={type}
key={type}
style={PanelSectionStyle}
id={`plugin-category-${category}`}
id={`plugin-category-${type}`}
>
{plugins.map((item) => (
<Card
key={item.name}
title={[
item.avatar && (
<Avatar
key={1}
icon={item.avatar}
className="plugin-avatar"
style={{
marginRight: 5,
}}
/>
),
<span key={2}>{item.name}</span>,
]}
style={{ height: 66 }}
extra={[
<Tooltip title="Setting" key={`plugin-card-${item.name}-extra-tooltip-2`}>
<Button
shape="circle"
icon={<SettingFilled />}
style={{ marginRight: 10, marginLeft: 10 }}
size="middle"
onClick={() => {
setName(item.name);
{pluginList
.filter((item) => item.type === type.toLowerCase())
.map((item) => (
<Card
key={item.name}
title={[<span key={2}>{item.name}</span>]}
style={{ height: 66 }}
extra={[
<Tooltip title="Setting" key={`plugin-card-${item.name}-extra-tooltip-2`}>
<Button
shape="circle"
icon={<SettingFilled />}
style={{ marginRight: 10, marginLeft: 10 }}
size="middle"
onClick={() => {
setName(item.name);
}}
/>
</Tooltip>,
<Switch
defaultChecked={initialData[item.name] && !initialData[item.name].disable}
disabled={readonly}
onChange={(isChecked) => {
if (isChecked) {
validateData(item.name, {
...initialData[item.name],
disable: false,
});
} else {
onChange({
...initialData,
[item.name]: { ...initialData[item.name], disable: true },
});
}
}}
/>
</Tooltip>,
<Switch
defaultChecked={initialData[item.name] && !initialData[item.name].disable}
disabled={readonly}
onChange={(isChecked) => {
if (isChecked) {
validateData(item.name, {
...initialData[item.name],
disable: false,
});
} else {
onChange({
...initialData,
[item.name]: { ...initialData[item.name], disable: true },
});
}
}}
key={Math.random().toString(36).substring(7)}
/>,
]}
/>
))}
key={Math.random().toString(36).substring(7)}
/>,
]}
/>
))}
</PanelSection>
);
})}
Expand Down
53 changes: 4 additions & 49 deletions web/src/components/Plugin/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,55 +17,10 @@
import { omit } from 'lodash';
import { request } from 'umi';

import { PLUGIN_MAPPER_SOURCE } from './data';

enum Category {
'Limit traffic',
'Observability',
'Security',
'Authentication',
'Log',
'Other',
}

export const fetchList = () => request<Res<string[]>>('/plugins');

let cachedPluginNameList: string[] = [];
export const getList = async () => {
if (!cachedPluginNameList.length) {
cachedPluginNameList = (await fetchList()).data;
}
const names = cachedPluginNameList;
const data: Record<string, PluginComponent.Meta[]> = {};

names.forEach((name) => {
const plugin = PLUGIN_MAPPER_SOURCE[name] || {};
const { category = 'Other', hidden = false } = plugin;

// NOTE: assign it to Authentication plugin
if (name.includes('auth')) {
plugin.category = 'Authentication';
}

if (!data[category]) {
data[category] = [];
}

if (!hidden) {
data[category] = data[category].concat({
...plugin,
name,
});
}
});

return Object.keys(data)
.sort((a, b) => Category[a] - Category[b])
.map((category) => {
return data[category].sort((a, b) => {
return (a.priority || 9999) - (b.priority || 9999);
});
});
export const fetchList = () => {
return request<Res<PluginComponent.Meta[]>>('/plugins?all=true').then(data => {
return data.data;
})
};

/**
Expand Down
18 changes: 5 additions & 13 deletions web/src/components/Plugin/typing.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,12 @@ declare namespace PluginComponent {

type Schema = '' | 'route' | 'consumer' | 'service';

type Category =
| 'Security'
| 'Limit traffic'
| 'Log'
| 'Observability'
| 'Other'
| 'Authentication';

type Meta = {
name: string;
category: Category;
hidden?: boolean;
// Note: Plugins are sorted by priority under the same category in the frontend, the smaller the number, the higher the priority. The default value is 9999.
priority?: number;
avatar?: React.ReactNode;
priority: number;
schema: object;
type: string;
version: number;
consumer_schema?: object;
};
}
Loading