Skip to content

Commit

Permalink
feat(web): 支持多个UI扩展 (#1215)
Browse files Browse the repository at this point in the history
支持同时启用多个UI扩展
  • Loading branch information
ddadaal authored Apr 22, 2024
1 parent 00d8f28 commit 94aa24c
Show file tree
Hide file tree
Showing 24 changed files with 289 additions and 109 deletions.
9 changes: 9 additions & 0 deletions .changeset/giant-dolphins-vanish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@scow/portal-web": patch
"@scow/mis-web": patch
"@scow/config": patch
"@scow/lib-web": patch
"@scow/docs": patch
---

支持同时配置多个 UI 扩展。UI 扩展的实现有破坏性变更,请参考文档。
2 changes: 1 addition & 1 deletion apps/mis-web/src/layouts/BaseLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export const BaseLayout =
basePath={publicConfig.BASE_PATH}
userLinks={publicConfig.USER_LINKS}
from="mis"
extension={uiExtensionStore.config}
extensionStoreData={uiExtensionStore.data}
languageId={languageId}
headerRightContent={(
<>
Expand Down
2 changes: 1 addition & 1 deletion apps/mis-web/src/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ function MyApp({ Component, pageProps, extra }: Props) {
return store;
});

const uiExtensionStore = useConstant(() => createStore(UiExtensionStore, publicConfig.UI_EXTENSION?.url));
const uiExtensionStore = useConstant(() => createStore(UiExtensionStore, publicConfig.UI_EXTENSION));

return (
<>
Expand Down
2 changes: 1 addition & 1 deletion apps/mis-web/src/pages/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
*/

import { moneyToNumber } from "@scow/lib-decimal";
import { Head } from "@scow/lib-web/build/components/head";
import { Money } from "@scow/protos/build/common/money";
import { AccountStatus } from "@scow/protos/build/server/user";
import { Divider } from "antd";
Expand All @@ -29,7 +30,6 @@ import { JobsSection } from "src/pageComponents/dashboard/JobsSection";
import { getUserStatus, GetUserStatusSchema } from "src/pages/api/dashboard/status";
import { UserStore } from "src/stores/UserStore";
import { ensureNotUndefined } from "src/utils/checkNull";
import { Head } from "src/utils/head";


export type AccountInfo = Omit<AccountStatus, "balance" | "jobChargeLimit" | "usedJobCharge"> & {
Expand Down
7 changes: 4 additions & 3 deletions apps/mis-web/src/pages/extensions/[[...path]].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,24 @@ export const ExtensionPage: NextPage = () => {

const i18n = useI18n();

if (uiExtensionStore.configLoading) {
if (uiExtensionStore.isLoading) {
return (
<Spin />
);
}

if (!uiExtensionStore.config) {
if (!uiExtensionStore.data) {
return (
<NotFoundPage />
);
}

return (
<LibExtensionPage
uiExtensionStoreConfig={uiExtensionStore.config}
uiExtensionStoreConfig={uiExtensionStore.data}
user={userStore.user}
currentLanguageId={i18n.currentLanguage.id}
NotFoundPageComponent={NotFoundPage}
/>
);

Expand Down
3 changes: 2 additions & 1 deletion apps/mis-web/src/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type { ClusterTextsConfigSchema } from "@scow/config/build/clusterTexts";
import { I18nStringType, SystemLanguageConfig } from "@scow/config/build/i18n";
import type { MisConfigSchema } from "@scow/config/build/mis";
import type { UiConfigSchema } from "@scow/config/build/ui";
import { UiExtensionConfigSchema } from "@scow/config/build/uiExtensions";
import { UserLink } from "@scow/lib-web/build/layouts/base/types";
import { getI18nConfigCurrentText } from "@scow/lib-web/build/utils/systemLanguage";
import getConfig from "next/config";
Expand Down Expand Up @@ -106,7 +107,7 @@ export interface PublicRuntimeConfig {
alarmLogs: { enabled: boolean | undefined }
},

UI_EXTENSION?: { url: string; }
UI_EXTENSION?: UiExtensionConfigSchema;

CHANGE_JOB_LIMIT: { allowUser: boolean}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,5 @@
* See the Mulan PSL v2 for more details.
*/

import NextHead from "next/head";
export { Head } from "@scow/lib-web/build/components/head";

type Props = React.PropsWithChildren<{
title: string;
}>;

export const Head: React.FC<Props> = ({ title, children }) => {
return (
<NextHead>
<title>{`${title} - scow`}</title>
{children}
</NextHead>
);
};
3 changes: 2 additions & 1 deletion apps/portal-web/config/portal.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,5 @@ shell: true
# children: []

uiExtension:
url: http://localhost:16566
- name: test1
url: http://localhost:16566
2 changes: 1 addition & 1 deletion apps/portal-web/src/layouts/BaseLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export const BaseLayout = ({ footerText, versionTag, initialLanguage, children }
basePath={publicConfig.BASE_PATH}
userLinks={publicConfig.USER_LINKS}
languageId={languageId}
extension={uiExtensionStore.config}
extensionStoreData={uiExtensionStore.data}
from="portal"
headerRightContent={(
<>
Expand Down
2 changes: 1 addition & 1 deletion apps/portal-web/src/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ function MyApp({ Component, pageProps, extra }: Props) {
extra.initialLanguage));

const defaultClusterStore = useConstant(() => createStore(DefaultClusterStore));
const uiExtensionStore = useConstant(() => createStore(UiExtensionStore, publicConfig.UI_EXTENSION?.url));
const uiExtensionStore = useConstant(() => createStore(UiExtensionStore, publicConfig.UI_EXTENSION));

// Use the layout defined at the page level, if available
return (
Expand Down
7 changes: 4 additions & 3 deletions apps/portal-web/src/pages/extensions/[[...path]].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,24 @@ export const ExtensionPage: NextPage = () => {

const i18n = useI18n();

if (uiExtensionStore.configLoading) {
if (uiExtensionStore.isLoading) {
return (
<Spin />
);
}

if (!uiExtensionStore.config) {
if (!uiExtensionStore.data) {
return (
<NotFoundPage />
);
}

return (
<LibExtensionPage
uiExtensionStoreConfig={uiExtensionStore.config}
uiExtensionStoreConfig={uiExtensionStore.data}
user={userStore.user}
currentLanguageId={i18n.currentLanguage.id}
NotFoundPageComponent={NotFoundPage}
/>
);

Expand Down
3 changes: 2 additions & 1 deletion apps/portal-web/src/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type { ClusterConfigSchema } from "@scow/config/build/cluster";
import { I18nStringType, SystemLanguageConfig } from "@scow/config/build/i18n";
import type { PortalConfigSchema } from "@scow/config/build/portal";
import type { UiConfigSchema } from "@scow/config/build/ui";
import { UiExtensionConfigSchema } from "@scow/config/build/uiExtensions";
import { UserLink } from "@scow/lib-web/build/layouts/base/types";
import { getI18nConfigCurrentText } from "@scow/lib-web/build/utils/systemLanguage";
import getConfig from "next/config";
Expand Down Expand Up @@ -99,7 +100,7 @@ export interface PublicRuntimeConfig {

SYSTEM_LANGUAGE_CONFIG: SystemLanguageConfig;

UI_EXTENSION?: { url: string; }
UI_EXTENSION?: UiExtensionConfigSchema;
}

export const runtimeConfig: ServerRuntimeConfig = getConfig().serverRuntimeConfig;
Expand Down
13 changes: 13 additions & 0 deletions apps/portal-web/src/utils/head.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy
* SCOW is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
* http://license.coscl.org.cn/MulanPSL2
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/

export { Head } from "@scow/lib-web/build/components/head";
2 changes: 1 addition & 1 deletion apps/portal-web/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,5 @@
"next-env.d.ts",
"**/*.ts",
"**/*.tsx"
]
, "../../libs/web/src/components/head.tsx" ]
}
53 changes: 36 additions & 17 deletions docs/docs/integration/ui-extension/develop.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,35 @@ sidebar_position: 2
title: 开发UI扩展
---

## 开发UI扩展
# 开发UI扩展

要使用UI扩展,您首先需要开发并构建一个从外界可以访问的网站(称为**扩展站**)。您可以使用任何技术开发、维护和部署此网站,只需要满足您的用户可以通过直接公网访问即可。

### 扩展页
## 上下文参数

当用户开启了UI扩展功能,并访问SCOW部署路径的`/extensions/*`的路径时,SCOW将会显示SCOW的基础导航结构,并在主要内容部分通过一个`iframe`显示您的**扩展站**`/extensions/*`下的内容,并将用户访问的query参数,以及以下三个query参数,以query参数的形式传给iframe
SCOW会在访问扩展页和调用某些配置接口时,将以下参数作为查询字符串(querystring)加入访问的URL,UI扩展可以通过这些参数获取当前SCOW系统的信息。具体哪些页面/API会传递这些参数会在具体章节里提到

| 参数 | 取值 | 解释 |
| --------------- | --------------------- | ------------------------------------------------------------- |
| `scowDark` | `"true" \| "false"` | 当前SCOW是否以黑暗主题显示 |
| `scowUserToken` | `string \| undefined` | 当前SCOW的登录用户的token。可通过SCOW认证系统接口查询登录用户 |
| `scowLangId` | `string` | 当前SCOW显示所使用的语言ID |

例如,假设SCOW部署于`https://myscow.com/scow`,您的扩展站部署于`https://myscowext.com/ext`,当用户访问`https://myscow/scow/extensions/parent/child?test=123`时,SCOW将会显示一个iframe,其URL为`https://myscowext.com/ext/extensions/parent/child?test=123&scowDark={当前SCOW是否以黑暗模式显示}&scowUserToken={用户token}&scowLangId={当前SCOW显示语言ID}`
## 扩展页

### 配置接口
UI扩展的功能应实现为标准的网页。当访问SCOW的扩展路径时,SCOW将会在外层显示SCOW的基础导航结构,并在页面主要部分使用一个`<iframe>`组件将扩展页的内容显示出来。[上下文参数](#上下文参数)中的参数也将会传递给`<iframe>`

若只设置了一个UI扩展,当用户访问SCOW部署路径的`/extensions/*`的路径时,`<iframe>`将会显示UI扩展`/extensions/*`下的内容。

若设置了多个UI扩展,当用户访问SCOW部署路径的`/extensions/{name}/*`的路径时,`<iframe>`将会显示`{name}`部分对应的UI扩展的`/extensions/*`下的内容。

例如,假设SCOW部署于`https://myscow.com/scow`,您的扩展站1部署于`https://myscowext1.com/ext1`,扩展站2部署于`https://myscowext2.com/ext2`

- 若用户在配置中使用单个UI扩展配置语法时,当用户访问`https://myscow/scow/extensions/parent/child?test=123`时,SCOW将会显示一个iframe,其URL为`https://myscowext1.com/ext1/extensions/parent/child?test=123&scowDark={当前SCOW是否以黑暗模式显示}&scowUserToken={用户token}&scowLangId={当前SCOW显示语言ID}`
- 若用户在配置中使用多个UI扩展配置语法,但是只配置了扩展站1时,起名称为`extname1`,当用户访问`https://myscow/scow/extensions/extname1/parent/child?test=123`时,SCOW将会显示一个iframe,其URL为`https://myscowext1.com/ext1/extensions/parent/child?test=123&scowDark={当前SCOW是否以黑暗模式显示}&scowUserToken={用户token}&scowLangId={当前SCOW显示语言ID}`
- 若用户在配置中使用多个UI扩展配置语法,配置了扩展站1和2,名称分别为`extname1``extname2`,当用户访问`https://myscow/scow/extensions/extname1/parent/child?test=123`时,SCOW将会显示一个iframe,其URL为`https://myscowext1.com/ext1/extensions/parent/child?test=123&scowDark={当前SCOW是否以黑暗模式显示}&scowUserToken={用户token}&scowLangId={当前SCOW显示语言ID}`

## 配置接口

除此之外,UI扩展站需要实现以下的配置接口。SCOW会在需要的使用调用以下接口获取响应配置。所有配置接口以`/api`开头。

Expand Down Expand Up @@ -49,11 +61,11 @@ title: 开发UI扩展
}
```

#### 重写门户系统的导航项:POST /api/portal/rewriteNavigations
### 重写门户系统的导航项:POST /api/portal/rewriteNavigations

重写门户系统的导航项。若您在`GET /api/manifests`中返回的`portal.rewriteNavigations``true`,则必须实现此接口。

SCOW将会在body中传入默认情况下SCOW将会显示的导航项。下表为传入的JSON参数的属性
SCOW将会在body中传入默认情况下SCOW将会显示的导航项。下表为传入的JSON参数的属性。除此表之外,[上下文参数](#上下文参数)同样也会被作为查询字符串传入。

| JSON属性路径 | 类型 | 是否必须 | 解释 |
| ---------------------- | -------------------------------------- | -------- | ----------------------------------------------------------------------------------------- |
Expand All @@ -64,28 +76,35 @@ SCOW将会在body中传入默认情况下SCOW将会显示的导航项。下表
| `navs[].openInNewPage` | 布尔值 || 此导航项的页面是否在新窗口中打开 |
| `navs[].children` | 对象数组,类型与`navs`数组的每一项相同 || 此导航项的子项。 |

同时,SCOW会传入以下参数作为query参数。

| 参数 | 取值 | 解释 |
| --------------- | --------------------- | ------------------------------------------------------------- |
| `scowDark` | `"true" \| "false"` | 当前SCOW是否以黑暗主题显示 |
| `scowUserToken` | `string \| undefined` | 当前SCOW的登录用户的token。可通过SCOW认证系统接口查询登录用户 |
| `scowLangId` | `string` | 当前SCOW显示所使用的语言ID |

您需要返回以下类型的JSON,表示重写后的门户系统的导航项。您可以重写系统默认导航项的属性。

| JSON属性路径 | 类型 | 是否必须 | 解释 |
| ---------------------- | -------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `navs` | 对象数组 || 导航项 |
| `navs[].path` | 字符串 || 此导航项的路径。此路径不包括base path。若当前浏览器的pathname以此开头,则此导航项将会高亮 |
| `navs[].clickToPath` | 字符串 || 点击此导航项将会导航至的路径。如果不填,则使用`path`属性 |
| `navs[].path` | 字符串 || 此导航项的路径,请参考表格下**关于返回的路径的的说明** |
| `navs[].clickToPath` | 字符串 || 点击此导航项将会导航至的路径,规则同上。如果不填,则使用`path`属性 |
| `navs[].text` | 字符串 || 导航项的文本 |
| `navs[].icon` | 对象 || 导航项的图标信息。如果不填,将显示默认导航项中具有相同的path的导航项的图标。如果不存在具有相同path的导航项,将显示[Ant Design Icon](https://ant.design/components/icon-cn)`LinkOutlined` |
| `navs[].icon.src` | 图标URL || 导航项的图标地址。必须是完整的、可公开访问的URL |
| `navs[].icon.alt` | 布尔值 || 导航项的图标alt属性。可不填 |
| `navs[].openInNewPage` | 布尔值 || 此导航项的页面是否在新窗口中打开 |
| `navs[].children` | 对象数组,类型与`navs`数组的每一项相同 || 此导航项的子项。 |

关于返回的路径的说明:

- 如果
- 返回的路径在调用这个扩展的此接口之前已经存在(即在调用此扩展的此接口时的某个已有的导航项具有和返回的路径相同的路径),或者
- 此路径是一个有效的URL
- 检查方法:使用`new URL(输入)`,若不抛出异常则为有效的URL
-
- 这个路径将会保留原状,直接写入为`<a>`标签的`href`属性
- 否则
- 此路径为相当于扩展UI的`/extensions`下的路径,即
- 当系统采用单个UI扩展配置语法时,对应的导航项的路径为:`{SCOW URL}/extensions/{path}`
- 当系统采用多个个UI扩展配置语法时,对应的导航项的路径为:`{SCOW URL}/extensions/{name}/{path}`
- 若当前浏览器的pathname以此开头,则此导航项将会高亮

如果配置了多个UI扩展,那么SCOW将会按照配置中的顺序依次调用每个需要重写导航项的UI扩展的此接口,并将上一个UI扩展的输出作为下一个UI扩展的输入,并将最终结果作为SCOW的导航项。

#### 重写管理系统的导航项:POST /api/mis/rewriteNavigations

Expand Down
29 changes: 26 additions & 3 deletions docs/docs/integration/ui-extension/ui-extension.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
---
sidebar_position: 1
title: UI扩展
title: UI扩展 (Beta)
---

# UI扩展
# UI扩展 (Beta)

:::caution

实验性功能!

:::

如果您需要在SCOW的界面中增加更多的页面,您可以开发自己的UI,并通过**UI扩展**功能将您的UI集成进SCOW的UI中。这样,您的用户在访问您自己的页面时,也可以获得与访问SCOW的功能一致的体验。

Expand All @@ -22,8 +28,25 @@ title: UI扩展

当您的扩展站开发并部署完成后,请在`config/portal.yaml`和/或`config/mis.yaml`中增加以下内容,以开启SCOW UI扩展。

如果您在门户系统或者管理系统中仅使用一个UI扩展,可使用以下配置。您将可以使用`{SCOW URL}/extensions`访问到此UI扩展。

```yaml title="config/{portal,mis}.yaml"
uiExtension:
# 您的UI扩展页部署URL
# 您的UI扩展页部署URL
url: http://localhost:16566/basepath
```
如果您在门户系统或者管理系统中需使用多个UI扩展,请使用以下配置。您将可以使用`{SCOW URL}/extensions/{name}`访问到对应名称的UI扩展。同一份配置文件中的多个UI扩展名称之间的名称不可重复。

```yaml title="config/{portal,mis}.yaml"
uiExtension:
- # 这个UI扩展的名称
name: extension1
# 您的此UI扩展页部署URL
url: http://localhost:16566/basepath
- # 这个UI扩展的名称
name: extension2
# 您的此UI扩展页部署URL
url: http://localhost:16567/basepath
```
Loading

0 comments on commit 94aa24c

Please sign in to comment.