From dae6dcb727c87838e74f410ff7af381bdcc92398 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Thu, 16 Mar 2023 11:45:32 +0200 Subject: [PATCH] Supported React plugins (#5801) ### Motivation and context ### How has this been tested? ### Checklist - [x] I submit my changes into the `develop` branch - [ ] I have added a description of my changes into the [CHANGELOG](https://github.com/opencv/cvat/blob/develop/CHANGELOG.md) file - [ ] I have updated the documentation accordingly - [ ] I have added tests to cover my changes - [ ] I have linked related issues (see [GitHub docs]( https://help.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword)) - [x] I have increased versions of npm packages if it is necessary ([cvat-canvas](https://github.com/opencv/cvat/tree/develop/cvat-canvas#versioning), [cvat-core](https://github.com/opencv/cvat/tree/develop/cvat-core#versioning), [cvat-data](https://github.com/opencv/cvat/tree/develop/cvat-data#versioning) and [cvat-ui](https://github.com/opencv/cvat/tree/develop/cvat-ui#versioning)) ### License - [x] I submit _my code changes_ under the same [MIT License]( https://github.com/opencv/cvat/blob/develop/LICENSE) that covers the project. Feel free to contact the maintainers if that's a concern. # How to add your own plugin **1. Write a ``PluginBuilder``** ``PluginBuilder`` is a function that accepts the following object as an argument: ```js { dispatch, REGISTER_ACTION, REMOVE_ACTION, core, } ``` This object is passed from the core application. Where: - ``dispatch`` is a redux function that can be used to add any React components - ``REGISTER_ACTION`` is action name to append component - ``REMOVE_ACTION`` is action name to remove component - ``core`` core library to access to server and interaction with any available API, e.g. ``core.tasks.get()``, ``core.server.request('dummy/url/...', { method: 'POST', data: { param1: 'value1', param2: 'value2' } })`` If you want to include authorization headers to the request, you must do the request via the core library. ``PluginBuilder`` must return the following object ``` { name: string; destructor: CallableFunction; } ``` - ``name`` is a plugin name - ``destructor`` is a function that removes plugin from storage and does any destructive actions ``PluginBuilder`` might register additional components this way: ```tsx const Component = () => ; dispatch({ type: REGISTER_ACTION, payload: { path: 'loginPage.loginForm', // path must be supported by the core application component: Component, data: { // optional method, define if necessary to render component conditionally, based on props, state of a target component shouldBeRendered: (targetComponentProps, targetComponentState) => { return true; } // optional field, define if necessary to affect rendering order weight: 5, } } }); ``` Destructor callback of a ``PluginBuilder`` must remove components this way: ```ts dispatch({ type: REMOVE_ACTION, payload: { path: 'loginPage.loginForm', // the same path as when register component: Component, // the same component as when register } }); ``` **2. Define plugin entrypoint** It must be in ``/src/ts/index.tsx``. Plugin entrypoint might register plugin in the core application when ``plugins.ready`` event is triggered. To achieve that, pass ``PluginBuilder`` to the exposed method: ``window.cvatUI.registerComponent(PluginBuilder)``. In general plugin can register itself anytime, but the above method must be available. Example code is below: ```ts function register() { if (Object.prototype.hasOwnProperty.call(window, 'cvatUI')) { (window as any as { cvatUI: { registerComponent: PluginEntryPoint } }) .cvatUI.registerComponent(PluginBuilder); } }; window.addEventListener('plugins.ready', register, { once: true }); ``` **3. Build/run core application together with plugins:** Just pass ``CLIENT_PLUGINS`` env variable to webpack. It can include multiple plugins: ```sh CLIENT_PLUGINS="path/to/plugin1:path/to/plugin2:path/to/plugin3" yarn start:cvat-ui CLIENT_PLUGINS="path/to/plugin1:path/to/plugin2:path/to/plugin3" yarn build:cvat-ui ``` Path may be defined in two formats: - relative to ``cvat-ui`` directory: ``plugins/plugin1``, ``../../another_place/plugin2`` - absolute, including entrypoint file: ``/home/user/some_path/plugin/src/ts/index.tsx`` **Webpack defines two aliases:** ``@modules`` - to use dependencies of the core application For example React may be imported this way: ```ts import React from '@modules/react'; ``` ``@root`` - to import something from the core application ```ts import { CombinedState } from '@root/reducers'; ``` You can install other dependencies to plugin directory if necessary. To avoid typescript errors in IDE and working with types, you can add ``tsconfig.json``. ```json { "compilerOptions": { "target": "es2020", "baseUrl": ".", "paths": { "@modules/*": ["/path/to/cvat/node_modules/*"], "@root/*": ["path/to/cvat/cvat-ui/src/*"] }, "moduleResolution": "node", "lib": ["dom", "dom.iterable", "esnext"], "jsx": "react", } } ``` --- Dockerfile.ui | 3 +- cvat-core/package.json | 2 +- cvat-core/src/api-implementation.ts | 30 ++----- cvat-core/src/api.ts | 38 ++------ cvat-core/src/auth-methods.ts | 68 --------------- cvat-core/src/server-proxy.ts | 68 ++------------- cvat-ui/package.json | 2 +- cvat-ui/src/actions/auth-actions.ts | 43 +-------- cvat-ui/src/actions/plugins-actions.ts | 21 ++++- cvat-ui/src/components/cvat-app.tsx | 26 +++--- .../src/components/login-page/login-form.tsx | 27 ++++-- .../src/components/login-page/login-page.tsx | 48 +--------- .../login-with-social-app.tsx | 56 ------------ .../login-with-sso-form.tsx | 87 ------------------- .../login-with-social-app/login-with-sso.tsx | 83 ------------------ cvat-ui/src/components/plugins-entrypoint.tsx | 56 ++++++++++++ .../signing-common/auth-provider-icon.tsx | 18 ---- .../signing-common/social-account-link.tsx | 48 ---------- .../src/components/signing-common/styles.scss | 66 -------------- cvat-ui/src/config.tsx | 2 - .../src/containers/login-page/login-page.tsx | 7 +- cvat-ui/src/cvat-core-wrapper.ts | 6 +- cvat-ui/src/index.tsx | 10 ++- cvat-ui/src/reducers/auth-reducer.ts | 53 +---------- cvat-ui/src/reducers/index.ts | 28 +++--- cvat-ui/src/reducers/notifications-reducer.ts | 16 ---- cvat-ui/src/reducers/plugins-reducer.ts | 79 ++++++++++++++++- cvat-ui/src/utils/git-utils.ts | 27 +++--- cvat-ui/src/utils/hooks.ts | 38 +++++++- cvat-ui/tsconfig.json | 2 +- cvat-ui/webpack.config.js | 23 ++++- 31 files changed, 318 insertions(+), 763 deletions(-) delete mode 100644 cvat-core/src/auth-methods.ts delete mode 100644 cvat-ui/src/components/login-with-social-app/login-with-social-app.tsx delete mode 100644 cvat-ui/src/components/login-with-social-app/login-with-sso-form.tsx delete mode 100644 cvat-ui/src/components/login-with-social-app/login-with-sso.tsx create mode 100644 cvat-ui/src/components/plugins-entrypoint.tsx delete mode 100644 cvat-ui/src/components/signing-common/auth-provider-icon.tsx delete mode 100644 cvat-ui/src/components/signing-common/social-account-link.tsx diff --git a/Dockerfile.ui b/Dockerfile.ui index 13a8a97f3852..7cf9aed0702e 100644 --- a/Dockerfile.ui +++ b/Dockerfile.ui @@ -6,6 +6,7 @@ ARG no_proxy ARG socks_proxy ARG WA_PAGE_VIEW_HIT ARG UI_APP_CONFIG +ARG CLIENT_PLUGINS ENV TERM=xterm \ http_proxy=${http_proxy} \ @@ -34,7 +35,7 @@ COPY cvat-core/ /tmp/cvat-core/ COPY cvat-canvas3d/ /tmp/cvat-canvas3d/ COPY cvat-canvas/ /tmp/cvat-canvas/ COPY cvat-ui/ /tmp/cvat-ui/ -RUN UI_APP_CONFIG="${UI_APP_CONFIG}" yarn run build:cvat-ui +RUN CLIENT_PLUGINS="${CLIENT_PLUGINS}" UI_APP_CONFIG="${UI_APP_CONFIG}" yarn run build:cvat-ui FROM nginx:mainline-alpine # Replace default.conf configuration to remove unnecessary rules diff --git a/cvat-core/package.json b/cvat-core/package.json index 32680da3e4cc..c2e4f2112093 100644 --- a/cvat-core/package.json +++ b/cvat-core/package.json @@ -1,6 +1,6 @@ { "name": "cvat-core", - "version": "8.2.0", + "version": "9.0.0", "description": "Part of Computer Vision Tool which presents an interface for client-side integration", "main": "src/api.ts", "scripts": { diff --git a/cvat-core/src/api-implementation.ts b/cvat-core/src/api-implementation.ts index 2301aa9a2fd3..b436c0ec64fb 100644 --- a/cvat-core/src/api-implementation.ts +++ b/cvat-core/src/api-implementation.ts @@ -3,7 +3,6 @@ // // SPDX-License-Identifier: MIT -import { SocialAuthMethod, SocialAuthMethodsRawType } from './auth-methods'; import config from './config'; import PluginRegistry from './plugins'; @@ -91,16 +90,6 @@ export default function implementAPI(cvat) { return result; }; - cvat.server.socialAuthentication.implementation = async () => { - const result: SocialAuthMethodsRawType = await serverProxy.server.socialAuthentication(); - return Object.entries(result).map(([provider, value]) => new SocialAuthMethod({ ...value, provider })); - }; - - cvat.server.selectSSOIdentityProvider.implementation = async (email?: string, iss?: string):Promise => { - const result: string = await serverProxy.server.selectSSOIdentityProvider(email, iss); - return result; - }; - cvat.server.changePassword.implementation = async (oldPassword, newPassword1, newPassword2) => { await serverProxy.server.changePassword(oldPassword, newPassword1, newPassword2); }; @@ -133,19 +122,18 @@ export default function implementAPI(cvat) { return result; }; - cvat.server.installedApps.implementation = async () => { - const result = await serverProxy.server.installedApps(); + cvat.server.setAuthData.implementation = async (response) => { + const result = await serverProxy.server.setAuthData(response); return result; }; - cvat.server.loginWithSocialAccount.implementation = async ( - tokenURL: string, - code: string, - authParams?: string, - process?: string, - scope?: string, - ) => { - const result = await serverProxy.server.loginWithSocialAccount(tokenURL, code, authParams, process, scope); + cvat.server.removeAuthData.implementation = async () => { + const result = await serverProxy.server.removeAuthData(); + return result; + }; + + cvat.server.installedApps.implementation = async () => { + const result = await serverProxy.server.installedApps(); return result; }; diff --git a/cvat-core/src/api.ts b/cvat-core/src/api.ts index 618119d645c1..af821afb8c17 100644 --- a/cvat-core/src/api.ts +++ b/cvat-core/src/api.ts @@ -81,18 +81,6 @@ function build() { const result = await PluginRegistry.apiWrapper(cvat.server.hasLimits, userId, orgId); return result; }, - async socialAuthentication() { - const result = await PluginRegistry.apiWrapper(cvat.server.socialAuthentication); - return result; - }, - async selectSSOIdentityProvider(email?: string, iss?: string) { - const result: string = await PluginRegistry.apiWrapper( - cvat.server.selectSSOIdentityProvider, - email, - iss, - ); - return result; - }, async changePassword(oldPassword, newPassword1, newPassword2) { const result = await PluginRegistry.apiWrapper( cvat.server.changePassword, @@ -134,20 +122,16 @@ function build() { const result = await PluginRegistry.apiWrapper(cvat.server.request, url, data); return result; }, - async installedApps() { - const result = await PluginRegistry.apiWrapper(cvat.server.installedApps); + async setAuthData(response) { + const result = await PluginRegistry.apiWrapper(cvat.server.setAuthData, response); return result; }, - async loginWithSocialAccount( - tokenURL: string, - code: string, - authParams?: string, - process?: string, - scope?: string, - ) { - const result = await PluginRegistry.apiWrapper( - cvat.server.loginWithSocialAccount, tokenURL, code, authParams, process, scope, - ); + async removeAuthData() { + const result = await PluginRegistry.apiWrapper(cvat.server.removeAuthData); + return result; + }, + async installedApps() { + const result = await PluginRegistry.apiWrapper(cvat.server.installedApps); return result; }, }, @@ -227,12 +211,6 @@ function build() { set backendAPI(value) { config.backendAPI = value; }, - get proxy() { - return config.proxy; - }, - set proxy(value) { - config.proxy = value; - }, get origin() { return config.origin; }, diff --git a/cvat-core/src/auth-methods.ts b/cvat-core/src/auth-methods.ts deleted file mode 100644 index aa4916e0b17a..000000000000 --- a/cvat-core/src/auth-methods.ts +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (C) 2022 CVAT.ai Corporation -// -// SPDX-License-Identifier: MIT - -export enum SelectionSchema { - EMAIL_ADDRESS = 'email_address', - LOWEST_WEIGHT = 'lowest_weight', -} -interface SocialAuthMethodCamelCase { - provider: string; - publicName: string; - isEnabled: boolean; - icon: string; - selectionSchema?: SelectionSchema; -} - -interface SocialAuthMethodSnakeCase { - public_name: string; - is_enabled: boolean; - icon: string; - provider?: string; - selection_schema?: SelectionSchema; -} - -export class SocialAuthMethod { - public provider: string; - public publicName: string; - public isEnabled: boolean; - public icon: string; - public selectionSchema: SelectionSchema; - - constructor(initialData: SocialAuthMethodSnakeCase) { - const data: SocialAuthMethodCamelCase = { - provider: initialData.provider, - publicName: initialData.public_name, - isEnabled: initialData.is_enabled, - icon: initialData.icon, - selectionSchema: initialData.selection_schema, - }; - - Object.defineProperties( - this, - Object.freeze({ - provider: { - get: () => data.provider, - }, - publicName: { - get: () => data.publicName, - }, - isEnabled: { - get: () => data.isEnabled, - }, - icon: { - get: () => data.icon, - }, - selectionSchema: { - get: () => data.selectionSchema, - }, - }), - ); - } -} - -export type SocialAuthMethodsRawType = { - [index: string]: SocialAuthMethodSnakeCase; -}; - -export type SocialAuthMethods = SocialAuthMethod[]; diff --git a/cvat-core/src/server-proxy.ts b/cvat-core/src/server-proxy.ts index 629a67ca03e9..cd3840841942 100644 --- a/cvat-core/src/server-proxy.ts +++ b/cvat-core/src/server-proxy.ts @@ -402,30 +402,6 @@ async function login(credential: string, password: string): Promise { setAuthData(authenticationResponse); } -async function loginWithSocialAccount( - tokenURL: string, - code: string, - authParams?: string, - process?: string, - scope?: string, -): Promise { - removeAuthData(); - const data = { - code, - ...(process ? { process } : {}), - ...(scope ? { scope } : {}), - ...(authParams ? { auth_params: authParams } : {}), - }; - let authenticationResponse = null; - try { - authenticationResponse = await Axios.post(tokenURL, data); - } catch (errorData) { - throw generateError(errorData); - } - - setAuthData(authenticationResponse); -} - async function logout(): Promise { try { await Axios.post(`${config.backendAPI}/auth/logout`); @@ -586,14 +562,13 @@ async function healthCheck( }); } -async function serverRequest(url: string, data: object): Promise { +async function serverRequest(url: string, data: object): Promise { try { - return ( - await Axios({ - url, - ...data, - }) - ).data; + const res = await Axios({ + url, + ...data, + }); + return res; } catch (errorData) { throw generateError(errorData); } @@ -2349,40 +2324,15 @@ async function receiveWebhookEvents(type: WebhookSourceType): Promise } } -async function socialAuthentication(): Promise { - const { backendAPI } = config; - try { - const response = await Axios.get(`${backendAPI}/auth/social`, { - validateStatus: (status) => status === 200 || status === 404, - }); - return (response.status === 200) ? response.data : {}; - } catch (errorData) { - throw generateError(errorData); - } -} - -async function selectSSOIdentityProvider(email?: string, iss?: string): Promise { - const { backendAPI } = config; - try { - const response = await Axios.get( - `${backendAPI}/auth/oidc/select-idp/`, { - params: { ...(email ? { email } : {}), ...(iss ? { iss } : {}) }, - }, - ); - return response.data; - } catch (errorData) { - throw generateError(errorData); - } -} - export default Object.freeze({ server: Object.freeze({ + setAuthData, + removeAuthData, about, share, formats, login, logout, - socialAuthentication, changePassword, requestPasswordReset, resetPassword, @@ -2392,8 +2342,6 @@ export default Object.freeze({ request: serverRequest, userAgreements, installedApps, - loginWithSocialAccount, - selectSSOIdentityProvider, hasLimits, }), diff --git a/cvat-ui/package.json b/cvat-ui/package.json index 226f11a1ca70..286fa567e48b 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.49.3", + "version": "1.50.0", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { diff --git a/cvat-ui/src/actions/auth-actions.ts b/cvat-ui/src/actions/auth-actions.ts index c07652630916..aaf8d9929789 100644 --- a/cvat-ui/src/actions/auth-actions.ts +++ b/cvat-ui/src/actions/auth-actions.ts @@ -1,5 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -7,7 +7,6 @@ import { ActionUnion, createAction, ThunkAction } from 'utils/redux'; import { UserConfirmation } from 'components/register-page/register-form'; import { getCore } from 'cvat-core-wrapper'; import isReachable from 'utils/url-checker'; -import { SocialAuthMethods } from '../cvat-core-wrapper'; const cvat = getCore(); @@ -36,12 +35,6 @@ export enum AuthActionTypes { LOAD_AUTH_ACTIONS = 'LOAD_AUTH_ACTIONS', LOAD_AUTH_ACTIONS_SUCCESS = 'LOAD_AUTH_ACTIONS_SUCCESS', LOAD_AUTH_ACTIONS_FAILED = 'LOAD_AUTH_ACTIONS_FAILED', - LOAD_SOCIAL_AUTHENTICATION = 'LOAD_SOCIAL_AUTHENTICATION', - LOAD_SOCIAL_AUTHENTICATION_SUCCESS = 'LOAD_SOCIAL_AUTHENTICATION_SUCCESS', - LOAD_SOCIAL_AUTHENTICATION_FAILED = 'LOAD_SOCIAL_AUTHENTICATION_FAILED', - SELECT_IDENTITY_PROVIDER = 'SELECT_IDENTITY_PROVIDER', - SELECT_IDENTITY_PROVIDER_SUCCESS = 'SELECT_IDENTITY_PROVIDER_SUCCESS', - SELECT_IDENTITY_PROVIDER_FAILED = 'SELECT_IDENTITY_PROVIDER_FAILED', } export const authActions = { @@ -78,20 +71,6 @@ export const authActions = { }) ), loadServerAuthActionsFailed: (error: any) => createAction(AuthActionTypes.LOAD_AUTH_ACTIONS_FAILED, { error }), - loadSocialAuth: () => createAction(AuthActionTypes.LOAD_SOCIAL_AUTHENTICATION), - loadSocialAuthSuccess: (methods: SocialAuthMethods) => ( - createAction(AuthActionTypes.LOAD_SOCIAL_AUTHENTICATION_SUCCESS, { methods }) - ), - loadSocialAuthFailed: (error: any) => ( - createAction(AuthActionTypes.LOAD_SOCIAL_AUTHENTICATION_FAILED, { error }) - ), - selectIdP: () => createAction(AuthActionTypes.SELECT_IDENTITY_PROVIDER), - selectIdPSuccess: (identityProviderID: string) => ( - createAction(AuthActionTypes.SELECT_IDENTITY_PROVIDER_SUCCESS, { identityProviderID }) - ), - selectIdPFailed: (error: any) => ( - createAction(AuthActionTypes.SELECT_IDENTITY_PROVIDER_FAILED, { error }) - ), }; export type AuthActions = ActionUnion; @@ -219,23 +198,3 @@ export const loadAuthActionsAsync = (): ThunkAction => async (dispatch) => { dispatch(authActions.loadServerAuthActionsFailed(error)); } }; - -export const loadSocialAuthAsync = (): ThunkAction => async (dispatch): Promise => { - dispatch(authActions.loadSocialAuth()); - try { - const methods: SocialAuthMethods = await cvat.server.socialAuthentication(); - dispatch(authActions.loadSocialAuthSuccess(methods)); - } catch (error) { - dispatch(authActions.loadSocialAuthFailed(error)); - } -}; - -export const selectIdPAsync = (email?: string, iss?: string): ThunkAction => async (dispatch): Promise => { - dispatch(authActions.selectIdP()); - try { - const identityProviderID: string = await cvat.server.selectSSOIdentityProvider(email, iss); - dispatch(authActions.selectIdPSuccess(identityProviderID)); - } catch (error) { - dispatch(authActions.selectIdPFailed(error)); - } -}; diff --git a/cvat-ui/src/actions/plugins-actions.ts b/cvat-ui/src/actions/plugins-actions.ts index 49006023a33b..c6a8ef9707fc 100644 --- a/cvat-ui/src/actions/plugins-actions.ts +++ b/cvat-ui/src/actions/plugins-actions.ts @@ -1,10 +1,12 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import { ActionUnion, createAction, ThunkAction } from 'utils/redux'; import { PluginsList } from 'reducers'; import { getCore } from 'cvat-core-wrapper'; +import React from 'react'; const core = getCore(); @@ -12,12 +14,29 @@ export enum PluginsActionTypes { GET_PLUGINS = 'GET_PLUGINS', GET_PLUGINS_SUCCESS = 'GET_PLUGINS_SUCCESS', GET_PLUGINS_FAILED = 'GET_PLUGINS_FAILED', + ADD_PLUGIN = 'ADD_PLUGIN', + ADD_UI_COMPONENT = 'ADD_UI_COMPONENT', + REMOVE_UI_COMPONENT = 'REMOVE_UI_COMPONENT', } -const pluginActions = { +export const pluginActions = { checkPlugins: () => createAction(PluginsActionTypes.GET_PLUGINS), checkPluginsSuccess: (list: PluginsList) => createAction(PluginsActionTypes.GET_PLUGINS_SUCCESS, { list }), checkPluginsFailed: (error: any) => createAction(PluginsActionTypes.GET_PLUGINS_FAILED, { error }), + addPlugin: (name: string, destructor: CallableFunction) => createAction( + PluginsActionTypes.ADD_PLUGIN, { name, destructor }, + ), + addUIComponent: ( + path: string, + component: React.Component, + data: { + weight?: number; + shouldBeRendered?: (props?: object, state?: object) => boolean; + } = {}, + ) => createAction(PluginsActionTypes.ADD_UI_COMPONENT, { path, component, data }), + removeUIComponent: (path: string, component: React.Component) => createAction( + PluginsActionTypes.REMOVE_UI_COMPONENT, { path, component }, + ), }; export type PluginActions = ActionUnion; diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx index a6ea9f155616..a61820da69ed 100644 --- a/cvat-ui/src/components/cvat-app.tsx +++ b/cvat-ui/src/components/cvat-app.tsx @@ -20,8 +20,6 @@ import 'antd/dist/antd.css'; import LogoutComponent from 'components/logout-component'; import LoginPageContainer from 'containers/login-page/login-page'; import LoginWithTokenComponent from 'components/login-with-token/login-with-token'; -import LoginWithSSOComponent from 'components/login-with-social-app/login-with-sso'; -import LoginWithSocialAppComponent from 'components/login-with-social-app/login-with-social-app'; import RegisterPageContainer from 'containers/register-page/register-page'; import ResetPasswordPageConfirmComponent from 'components/reset-password-confirm-page/reset-password-confirm-page'; import ResetPasswordPageComponent from 'components/reset-password-page/reset-password-page'; @@ -61,7 +59,7 @@ import UpdateWebhookPage from 'components/setup-webhook-pages/update-webhook-pag import AnnotationPageContainer from 'containers/annotation-page/annotation-page'; import { getCore } from 'cvat-core-wrapper'; import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react'; -import { NotificationsState } from 'reducers'; +import { NotificationsState, PluginsState } from 'reducers'; import { customWaViewHit } from 'utils/environment'; import showPlatformNotification, { platformInfo, @@ -109,6 +107,7 @@ interface CVATAppProps { notifications: NotificationsState; user: any; isModelPluginActive: boolean; + pluginComponents: PluginsState['components']; } interface CVATAppState { @@ -125,6 +124,7 @@ class CVATApplication extends React.PureComponent handler()); EventRecorder.log(event); }); + core.logger.configure(() => window.document.hasFocus, userActivityCallback); EventRecorder.initSave(); @@ -392,6 +393,7 @@ class CVATApplication extends React.PureComponent shouldBeRendered(this.props, this.state)) + .map(({ component: Component }) => Component()); + if (readyForRender) { if (user && user.isVerified) { return ( @@ -472,6 +478,7 @@ class CVATApplication extends React.PureComponent + { routesToRender } {isModelPluginActive && ( - - - + { routesToRender } 1 ? `/auth/login?next=${location.pathname}` : '/auth/login'} /> @@ -549,6 +546,7 @@ class CVATApplication extends React.PureComponent ); } + return ; } } diff --git a/cvat-ui/src/components/login-page/login-form.tsx b/cvat-ui/src/components/login-page/login-form.tsx index 20028b6dd903..fe9050dda2e7 100644 --- a/cvat-ui/src/components/login-page/login-form.tsx +++ b/cvat-ui/src/components/login-page/login-form.tsx @@ -1,21 +1,25 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import React, { useState } from 'react'; +import { Link } from 'react-router-dom'; + import Form from 'antd/lib/form'; import Button from 'antd/lib/button'; import Input from 'antd/lib/input'; +import { Col, Row } from 'antd/lib/grid'; +import Title from 'antd/lib/typography/Title'; +import Text from 'antd/lib/typography/Text'; import Icon from '@ant-design/icons'; import { BackArrowIcon, ClearIcon, } from 'icons'; -import { Col, Row } from 'antd/lib/grid'; -import Title from 'antd/lib/typography/Title'; -import Text from 'antd/lib/typography/Text'; -import { Link } from 'react-router-dom'; + import CVATSigningInput, { CVATInputType } from 'components/signing-common/cvat-signing-input'; +import { CombinedState } from 'reducers'; +import { usePlugins } from 'utils/hooks'; export interface LoginData { credential: string; @@ -25,16 +29,19 @@ export interface LoginData { interface Props { renderResetPassword: boolean; fetching: boolean; - socialAuthentication: JSX.Element | null; onSubmit(loginData: LoginData): void; } function LoginFormComponent(props: Props): JSX.Element { const { - fetching, onSubmit, renderResetPassword, socialAuthentication, + fetching, onSubmit, renderResetPassword, } = props; const [form] = Form.useForm(); const [credential, setCredential] = useState(''); + const pluginsToRender = usePlugins( + (state: CombinedState) => state.plugins.components.loginPage.loginForm, + props, { credential }, + ); const forgotPasswordLink = ( @@ -47,6 +54,7 @@ function LoginFormComponent(props: Props): JSX.Element { ); + return (
@@ -135,7 +143,7 @@ function LoginFormComponent(props: Props): JSX.Element { ) } { - credential || !socialAuthentication ? ( + !!credential && (
); diff --git a/cvat-ui/src/components/login-page/login-page.tsx b/cvat-ui/src/components/login-page/login-page.tsx index ade32ecb7550..0a0073a74d19 100644 --- a/cvat-ui/src/components/login-page/login-page.tsx +++ b/cvat-ui/src/components/login-page/login-page.tsx @@ -1,70 +1,33 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT -import React, { useEffect } from 'react'; +import React from 'react'; import { RouteComponentProps, useHistory } from 'react-router'; import { withRouter } from 'react-router-dom'; import { Row, Col } from 'antd/lib/grid'; import SigningLayout, { formSizes } from 'components/signing-common/signing-layout'; -import SocialAccountLink from 'components/signing-common/social-account-link'; - -import { getCore, SocialAuthMethods, SocialAuthMethod } from 'cvat-core-wrapper'; -import config from 'config'; import LoginForm, { LoginData } from './login-form'; -const cvat = getCore(); - interface LoginPageComponentProps { fetching: boolean; renderResetPassword: boolean; hasEmailVerificationBeenSent: boolean; - socialAuthMethods: SocialAuthMethods; onLogin: (credential: string, password: string) => void; - loadSocialAuthenticationMethods: () => void; } -const renderSocialAuthMethods = (methods: SocialAuthMethods): JSX.Element | null => { - const { backendAPI } = cvat.config; - const activeMethods = methods.filter((item: SocialAuthMethod) => item.isEnabled); - - if (!activeMethods.length) { - return null; - } - - return ( -
- {activeMethods.map((method: SocialAuthMethod) => ( - - {`Continue with ${method.publicName}`} - - ))} -
- ); -}; - function LoginPageComponent(props: LoginPageComponentProps & RouteComponentProps): JSX.Element { const history = useHistory(); const { - fetching, renderResetPassword, hasEmailVerificationBeenSent, - socialAuthMethods, onLogin, loadSocialAuthenticationMethods, + fetching, renderResetPassword, hasEmailVerificationBeenSent, onLogin, } = props; if (hasEmailVerificationBeenSent) { history.push('/auth/email-verification-sent'); } - useEffect(() => { - loadSocialAuthenticationMethods(); - }, []); - return ( @@ -73,11 +36,6 @@ function LoginPageComponent(props: LoginPageComponentProps & RouteComponentProps - {renderSocialAuthMethods(socialAuthMethods)} - - ) : null} onSubmit={(loginData: LoginData): void => { onLogin(loginData.credential, loginData.password); }} diff --git a/cvat-ui/src/components/login-with-social-app/login-with-social-app.tsx b/cvat-ui/src/components/login-with-social-app/login-with-social-app.tsx deleted file mode 100644 index d956a6ab58d2..000000000000 --- a/cvat-ui/src/components/login-with-social-app/login-with-social-app.tsx +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (C) 2022 CVAT.ai Corporation -// -// SPDX-License-Identifier: MIT - -import React, { useEffect } from 'react'; -import { Redirect, useLocation, useHistory } from 'react-router'; -import notification from 'antd/lib/notification'; -import Spin from 'antd/lib/spin'; - -import { getCore } from 'cvat-core-wrapper'; - -const cvat = getCore(); - -export default function LoginWithSocialAppComponent(): JSX.Element { - const location = useLocation(); - const history = useHistory(); - const search = new URLSearchParams(location.search); - - useEffect(() => { - const provider = search.get('provider'); - const code = search.get('code'); - const process = search.get('process'); - const scope = search.get('scope'); - const authParams = search.get('auth_params'); - - if (provider && code) { - const tokenURL = (location.pathname.includes('login-with-oidc')) ? - `${cvat.config.backendAPI}/auth/oidc/${provider}/login/token/` : - `${cvat.config.backendAPI}/auth/social/${provider}/login/token/`; - cvat.server.loginWithSocialAccount(tokenURL, code, authParams, process, scope) - .then(() => window.location.reload()) - .catch((exception: Error) => { - if (exception.message.includes('Unverified email')) { - history.push('/auth/email-verification-sent'); - return Promise.resolve(); - } - history.push('/auth/login'); - notification.error({ - message: 'Could not log in with social account', - description: 'Go to developer console', - }); - return Promise.reject(exception); - }); - } - }, []); - - if (localStorage.getItem('token')) { - return ; - } - - return ( -
- -
- ); -} diff --git a/cvat-ui/src/components/login-with-social-app/login-with-sso-form.tsx b/cvat-ui/src/components/login-with-social-app/login-with-sso-form.tsx deleted file mode 100644 index 6d986f0e68d7..000000000000 --- a/cvat-ui/src/components/login-with-social-app/login-with-sso-form.tsx +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (C) 2023 CVAT.ai Corporation -// -// SPDX-License-Identifier: MIT - -import React from 'react'; -import Form from 'antd/lib/form'; -import Button from 'antd/lib/button'; -import Icon from '@ant-design/icons'; -import Text from 'antd/lib/typography/Text'; -import { BackArrowIcon } from 'icons'; -import { Col, Row } from 'antd/lib/grid'; -import { Link } from 'react-router-dom'; -import Title from 'antd/lib/typography/Title'; -import CVATSigningInput from 'components/signing-common/cvat-signing-input'; - -export interface LoginWithSSOData { - email: string; -} - -interface Props { - fetching: boolean; - onSubmit(email: string): void; -} - -function LoginWithSSOFormComponent({ fetching, onSubmit }: Props): JSX.Element { - const [form] = Form.useForm(); - - return ( -
- - } - /> - - - - Login with SSO - - - - - Enter your company email - - -
{ - onSubmit(data.email); - }} - > - - form.setFieldsValue({ email: '' })} - /> - - - - - -
-
- ); -} - -export default React.memo(LoginWithSSOFormComponent); diff --git a/cvat-ui/src/components/login-with-social-app/login-with-sso.tsx b/cvat-ui/src/components/login-with-social-app/login-with-sso.tsx deleted file mode 100644 index 88b8041edf7d..000000000000 --- a/cvat-ui/src/components/login-with-social-app/login-with-sso.tsx +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (C) 2023 CVAT.ai Corporation -// -// SPDX-License-Identifier: MIT - -import React, { useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { useLocation } from 'react-router'; -import { Row, Col } from 'antd/lib/grid'; -import Spin from 'antd/lib/spin'; - -import { selectIdPAsync, loadSocialAuthAsync } from 'actions/auth-actions'; -import { CombinedState } from 'reducers'; -import SigningLayout, { formSizes } from 'components/signing-common/signing-layout'; -import { getCore, SocialAuthMethod, SelectionSchema } from 'cvat-core-wrapper'; -import config from 'config'; - -import LoginWithSSOForm from './login-with-sso-form'; - -const core = getCore(); - -function LoginWithSSOComponent(): JSX.Element { - const dispatch = useDispatch(); - const fetching = useSelector((state: CombinedState) => state.auth.ssoIDPSelectFetching); - const isIdPSelected = useSelector((state: CombinedState) => state.auth.ssoIDPSelected); - const selectedIdP = useSelector((state: CombinedState) => state.auth.ssoIDP); - const [SSOConfiguration] = useSelector((state: CombinedState) => state.auth.socialAuthMethods.filter( - (item: SocialAuthMethod) => item.provider === config.SSO_PROVIDER_KEY, - )); - - const location = useLocation(); - const search = new URLSearchParams(location.search); - - useEffect(() => { - const iss = search.get('iss'); - - if (!iss) { - dispatch(loadSocialAuthAsync()); - } else { - dispatch(selectIdPAsync(undefined, iss)); - } - }, []); - - useEffect(() => { - if (selectedIdP) { - window.open(`${core.config.backendAPI}/auth/oidc/${selectedIdP}/login/`, '_self'); - } - }, [selectedIdP]); - - useEffect(() => { - if (SSOConfiguration?.selectionSchema === SelectionSchema.LOWEST_WEIGHT) { - dispatch(selectIdPAsync()); - } - }, [SSOConfiguration?.selectionSchema]); - - if ( - (!fetching && !isIdPSelected && SSOConfiguration?.selectionSchema === SelectionSchema.EMAIL_ADDRESS) || - (isIdPSelected && !selectedIdP) - ) { - return ( - - - - - { - dispatch(selectIdPAsync(email)); - }} - /> - - - - - ); - } - return ( -
- -
- ); -} - -export default React.memo(LoginWithSSOComponent); diff --git a/cvat-ui/src/components/plugins-entrypoint.tsx b/cvat-ui/src/components/plugins-entrypoint.tsx new file mode 100644 index 000000000000..9db17ae01ae9 --- /dev/null +++ b/cvat-ui/src/components/plugins-entrypoint.tsx @@ -0,0 +1,56 @@ +// Copyright (C) 2023 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import React, { useEffect } from 'react'; +import { Dispatch, AnyAction } from 'redux'; +import { useDispatch } from 'react-redux'; + +import { PluginsActionTypes, pluginActions } from 'actions/plugins-actions'; +import { getCore } from 'cvat-core-wrapper'; + +const core = getCore(); + +export type ComponentBuilder = ({ + dispatch, + REGISTER_ACTION, + REMOVE_ACTION, + core, +}: { + dispatch: Dispatch, + REGISTER_ACTION: PluginsActionTypes.ADD_UI_COMPONENT, + REMOVE_ACTION: PluginsActionTypes.REMOVE_UI_COMPONENT + core: any, +}) => { + name: string; + destructor: CallableFunction; +}; + +export type PluginEntryPoint = (componentBuilder: ComponentBuilder) => void; + +function PluginEntrypoint(): null { + const dispatch = useDispatch(); + + useEffect(() => { + Object.defineProperty(window, 'cvatUI', { + value: Object.freeze({ + registerComponent: (componentBuilder: ComponentBuilder) => { + const { name, destructor } = componentBuilder({ + dispatch, + REGISTER_ACTION: PluginsActionTypes.ADD_UI_COMPONENT, + REMOVE_ACTION: PluginsActionTypes.REMOVE_UI_COMPONENT, + core, + }); + + dispatch(pluginActions.addPlugin(name, destructor)); + }, + }), + }); + + window.document.dispatchEvent(new CustomEvent('plugins.ready', { bubbles: true })); + }, []); + + return null; +} + +export default React.memo(PluginEntrypoint); diff --git a/cvat-ui/src/components/signing-common/auth-provider-icon.tsx b/cvat-ui/src/components/signing-common/auth-provider-icon.tsx deleted file mode 100644 index 1d688468f81c..000000000000 --- a/cvat-ui/src/components/signing-common/auth-provider-icon.tsx +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (C) 2023 CVAT.ai Corporation -// -// SPDX-License-Identifier: MIT - -import React from 'react'; - -export default function AuthenticationProviderIcon({ iconData, provider }: { - iconData: string, - provider: string -}): JSX.Element { - return ( - {provider} - ); -} diff --git a/cvat-ui/src/components/signing-common/social-account-link.tsx b/cvat-ui/src/components/signing-common/social-account-link.tsx deleted file mode 100644 index 3123f7877216..000000000000 --- a/cvat-ui/src/components/signing-common/social-account-link.tsx +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (C) 2022-2023 CVAT.ai Corporation -// -// SPDX-License-Identifier: MIT - -import './styles.scss'; -import React from 'react'; -import { Col, Row } from 'antd/lib/grid'; -import Button from 'antd/lib/button/button'; -import { LoginOutlined } from '@ant-design/icons'; -import AuthenticationProviderIcon from './auth-provider-icon'; - -export interface SocialAccountLinkProps { - children: string; - className?: string; - href: string; - icon: string; -} - -function SocialAccountLink(props: SocialAccountLinkProps): JSX.Element { - const { - children, className, href, icon, - } = props; - - return ( - - - - - - ); -} - -export default React.memo(SocialAccountLink); diff --git a/cvat-ui/src/components/signing-common/styles.scss b/cvat-ui/src/components/signing-common/styles.scss index e2c92245cafe..31302db6cdc3 100644 --- a/cvat-ui/src/components/signing-common/styles.scss +++ b/cvat-ui/src/components/signing-common/styles.scss @@ -339,69 +339,3 @@ $social-google-background: #4286f5; height: $grid-unit-size*76; } } - -.cvat-social-authentication-button { - @extend .cvat-credentials-action-button; - - .anticon { - margin-top: $grid-unit-size * 0.5; - } - - height: $grid-unit-size * 7; - letter-spacing: 1px; - margin-bottom: $grid-unit-size * 2; - display: flex; -} - -.cvat-social-authentication-google { - background: $social-google-background; - - &:hover, - &:focus { - background: $social-google-background; - color: $action-button-color-3; - } -} - -.cvat-login-with-sso-form-wrapper { - @extend .cvat-login-form-wrapper; - - h2 { - margin-bottom: 0; - } -} - -.cvat-login-with-sso-form { - @extend .cvat-signing-form; - - .cvat-credentials-form-item { - margin-bottom: $grid-unit-size*3; - } - - margin-top: $grid-unit-size*6; -} - -.cvat-social-authentication-row-with-icons { - height: $grid-unit-size * 35; - display: flex; - flex-direction: column; - justify-content: flex-end; - - img { - width: $grid-unit-size * 5; - height: $grid-unit-size * 5; - } -} - -.cvat-social-authentication-hr +.ant-typography { - position: absolute; - left: 50%; - transform: translate(-50%, -65%); - background: white; - padding: $grid-unit-size; - font-size: 20px; -} - -a.ant-btn.cvat-social-authentication-button { - padding-top: $grid-unit-size !important; // override "important" from .antd library -} diff --git a/cvat-ui/src/config.tsx b/cvat-ui/src/config.tsx index 6d89662dce8b..c1c4654dee09 100644 --- a/cvat-ui/src/config.tsx +++ b/cvat-ui/src/config.tsx @@ -112,7 +112,6 @@ const CVAT_BILLING_URL = process.env.CVAT_BILLING_HOST; const HEALTH_CHECK_RETRIES = 10; const HEALTH_CHECK_PERIOD = 3000; // ms const HEALTH_CHECK_REQUEST_TIMEOUT = 5000; // ms -const SSO_PROVIDER_KEY = 'sso'; const CONTROLS_LOGS_INTERVAL = 90000; // 1.5 min in ms @@ -161,6 +160,5 @@ export default { CANVAS_WORKSPACE_PADDING, CANVAS_WORKSPACE_DEFAULT_CONTEXT_HEIGHT, CONTROLS_LOGS_INTERVAL, - SSO_PROVIDER_KEY, RESET_NOTIFICATIONS_PATHS, }; diff --git a/cvat-ui/src/containers/login-page/login-page.tsx b/cvat-ui/src/containers/login-page/login-page.tsx index 24f182ff2ef9..71c39939f952 100644 --- a/cvat-ui/src/containers/login-page/login-page.tsx +++ b/cvat-ui/src/containers/login-page/login-page.tsx @@ -5,19 +5,16 @@ import { connect } from 'react-redux'; import LoginPageComponent from 'components/login-page/login-page'; import { CombinedState } from 'reducers'; -import { loginAsync, loadSocialAuthAsync } from 'actions/auth-actions'; -import { SocialAuthMethods } from 'cvat-core-wrapper'; +import { loginAsync } from 'actions/auth-actions'; interface StateToProps { fetching: boolean; renderResetPassword: boolean; hasEmailVerificationBeenSent: boolean; - socialAuthMethods: SocialAuthMethods; } interface DispatchToProps { onLogin: typeof loginAsync; - loadSocialAuthenticationMethods: typeof loadSocialAuthAsync; } function mapStateToProps(state: CombinedState): StateToProps { @@ -25,13 +22,11 @@ function mapStateToProps(state: CombinedState): StateToProps { fetching: state.auth.fetching, renderResetPassword: state.auth.allowResetPassword, hasEmailVerificationBeenSent: state.auth.hasEmailVerificationBeenSent, - socialAuthMethods: state.auth.socialAuthMethods, }; } const mapDispatchToProps: DispatchToProps = { onLogin: loginAsync, - loadSocialAuthenticationMethods: loadSocialAuthAsync, }; export default connect(mapStateToProps, mapDispatchToProps)(LoginPageComponent); diff --git a/cvat-ui/src/cvat-core-wrapper.ts b/cvat-ui/src/cvat-core-wrapper.ts index bb4aa2e811f8..9c97af759bea 100644 --- a/cvat-ui/src/cvat-core-wrapper.ts +++ b/cvat-ui/src/cvat-core-wrapper.ts @@ -20,7 +20,7 @@ import { import { Storage, StorageData } from 'cvat-core/src/storage'; import Issue from 'cvat-core/src/issue'; import Comment from 'cvat-core/src/comment'; -import { SocialAuthMethods, SocialAuthMethod, SelectionSchema } from 'cvat-core/src/auth-methods'; +import User from 'cvat-core/src/user'; const cvat: any = _cvat; @@ -45,14 +45,13 @@ export { LabelType, Storage, Webhook, - SocialAuthMethod, Issue, + User, Comment, MLModel, ModelKind, ModelProviders, ModelReturnType, - SelectionSchema, DimensionType, }; @@ -60,6 +59,5 @@ export type { SerializedAttribute, SerializedLabel, StorageData, - SocialAuthMethods, ModelProvider, }; diff --git a/cvat-ui/src/index.tsx b/cvat-ui/src/index.tsx index 2441ba007ac6..789825fb0256 100644 --- a/cvat-ui/src/index.tsx +++ b/cvat-ui/src/index.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -16,16 +17,18 @@ import { switchSettingsDialog } from 'actions/settings-actions'; import { shortcutsActions } from 'actions/shortcuts-actions'; import { getUserAgreementsAsync } from 'actions/useragreements-actions'; import CVATApplication from 'components/cvat-app'; +import PluginsEntrypoint from 'components/plugins-entrypoint'; import LayoutGrid from 'components/layout-grid/layout-grid'; import logger, { LogType } from 'cvat-logger'; import createCVATStore, { getCVATStore } from 'cvat-store'; import { KeyMap } from 'utils/mousetrap-react'; import createRootReducer from 'reducers/root-reducer'; import { getOrganizationsAsync } from 'actions/organization-actions'; -import { resetErrors, resetMessages } from './actions/notification-actions'; -import { CombinedState, NotificationsState } from './reducers'; +import { resetErrors, resetMessages } from 'actions/notification-actions'; +import { CombinedState, NotificationsState, PluginsState } from './reducers'; createCVATStore(createRootReducer); + const cvatStore = getCVATStore(); interface StateToProps { @@ -51,6 +54,7 @@ interface StateToProps { user: any; keyMap: KeyMap; isModelPluginActive: boolean; + pluginComponents: PluginsState['components']; } interface DispatchToProps { @@ -100,6 +104,7 @@ function mapStateToProps(state: CombinedState): StateToProps { notifications: state.notifications, user: auth.user, keyMap: shortcuts.keyMap, + pluginComponents: plugins.components, isModelPluginActive: plugins.list.MODELS, }; } @@ -126,6 +131,7 @@ const ReduxAppWrapper = connect(mapStateToProps, mapDispatchToProps)(CVATApplica ReactDOM.render( + diff --git a/cvat-ui/src/reducers/auth-reducer.ts b/cvat-ui/src/reducers/auth-reducer.ts index 28ede940b785..7d9527f3d62c 100644 --- a/cvat-ui/src/reducers/auth-reducer.ts +++ b/cvat-ui/src/reducers/auth-reducer.ts @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -16,12 +17,6 @@ const defaultState: AuthState = { showChangePasswordDialog: false, allowResetPassword: false, hasEmailVerificationBeenSent: false, - socialAuthFetching: false, - socialAuthInitialized: false, - socialAuthMethods: [], - ssoIDPSelectFetching: false, - ssoIDPSelected: false, - ssoIDP: null, }; export default function (state = defaultState, action: AuthActions | BoundariesActions): AuthState { @@ -160,52 +155,6 @@ export default function (state = defaultState, action: AuthActions | BoundariesA allowChangePassword: false, allowResetPassword: false, }; - case AuthActionTypes.LOAD_SOCIAL_AUTHENTICATION: { - return { - ...state, - socialAuthFetching: true, - socialAuthInitialized: false, - }; - } - case AuthActionTypes.LOAD_SOCIAL_AUTHENTICATION_SUCCESS: { - const { methods } = action.payload; - return { - ...state, - socialAuthFetching: false, - socialAuthInitialized: true, - socialAuthMethods: methods, - }; - } - case AuthActionTypes.LOAD_SOCIAL_AUTHENTICATION_FAILED: { - return { - ...state, - socialAuthFetching: false, - socialAuthInitialized: true, - }; - } - case AuthActionTypes.SELECT_IDENTITY_PROVIDER: { - return { - ...state, - ssoIDPSelectFetching: true, - ssoIDPSelected: false, - }; - } - case AuthActionTypes.SELECT_IDENTITY_PROVIDER_SUCCESS: { - const { identityProviderID } = action.payload; - return { - ...state, - ssoIDPSelectFetching: false, - ssoIDPSelected: true, - ssoIDP: identityProviderID, - }; - } - case AuthActionTypes.SELECT_IDENTITY_PROVIDER_FAILED: { - return { - ...state, - ssoIDPSelectFetching: false, - ssoIDPSelected: true, - }; - } case BoundariesActionTypes.RESET_AFTER_ERROR: { return { ...defaultState }; } diff --git a/cvat-ui/src/reducers/index.ts b/cvat-ui/src/reducers/index.ts index 9aaac0954ac8..16d264032e1e 100644 --- a/cvat-ui/src/reducers/index.ts +++ b/cvat-ui/src/reducers/index.ts @@ -5,9 +5,7 @@ import { Canvas3d } from 'cvat-canvas3d/src/typescript/canvas3d'; import { Canvas, RectDrawingMethod, CuboidDrawingMethod } from 'cvat-canvas-wrapper'; -import { - Webhook, SocialAuthMethods, MLModel, ModelProvider, -} from 'cvat-core-wrapper'; +import { Webhook, MLModel, ModelProvider } from 'cvat-core-wrapper'; import { IntelligentScissors } from 'utils/opencv-wrapper/intelligent-scissors'; import { KeyMap } from 'utils/mousetrap-react'; import { OpenCVTracker } from 'utils/opencv-wrapper/opencv-interfaces'; @@ -26,12 +24,6 @@ export interface AuthState { allowChangePassword: boolean; allowResetPassword: boolean; hasEmailVerificationBeenSent: boolean; - socialAuthFetching: boolean; - socialAuthInitialized: boolean; - socialAuthMethods: SocialAuthMethods; - ssoIDPSelectFetching: boolean; - ssoIDPSelected: boolean; - ssoIDP: string | null; } export interface ProjectsQuery { @@ -269,10 +261,27 @@ export type PluginsList = { [name in SupportedPlugins]: boolean; }; +export interface PluginComponent { + component: any; + data: { + weight: number; + shouldBeRendered: (props?: object, state?: object) => boolean; + }; +} + export interface PluginsState { fetching: boolean; initialized: boolean; list: PluginsList; + current: { + [index: string]: CallableFunction; + }, + components: { + loginPage: { + loginForm: PluginComponent[]; + } + router: PluginComponent[], + } } export interface AboutState { @@ -409,7 +418,6 @@ export interface NotificationsState { requestPasswordReset: null | ErrorState; resetPassword: null | ErrorState; loadAuthActions: null | ErrorState; - sso: null | ErrorState; }; projects: { fetching: null | ErrorState; diff --git a/cvat-ui/src/reducers/notifications-reducer.ts b/cvat-ui/src/reducers/notifications-reducer.ts index c8994af11911..1f62b3f8fbc8 100644 --- a/cvat-ui/src/reducers/notifications-reducer.ts +++ b/cvat-ui/src/reducers/notifications-reducer.ts @@ -37,7 +37,6 @@ const defaultState: NotificationsState = { requestPasswordReset: null, resetPassword: null, loadAuthActions: null, - sso: null, }, projects: { fetching: null, @@ -368,21 +367,6 @@ export default function (state = defaultState, action: AnyAction): Notifications }, }; } - case AuthActionTypes.SELECT_IDENTITY_PROVIDER_FAILED: { - return { - ...state, - errors: { - ...state.errors, - auth: { - ...state.errors.auth, - sso: { - message: 'Single sign-on (SSO) is not configured on your account', - reason: action.payload.error.toString(), - }, - }, - }, - }; - } case ExportActionTypes.EXPORT_DATASET_FAILED: { const { instance, instanceType } = action.payload; return { diff --git a/cvat-ui/src/reducers/plugins-reducer.ts b/cvat-ui/src/reducers/plugins-reducer.ts index 4c989b00be45..6ced19f5218e 100644 --- a/cvat-ui/src/reducers/plugins-reducer.ts +++ b/cvat-ui/src/reducers/plugins-reducer.ts @@ -5,7 +5,7 @@ import { PluginsActionTypes, PluginActions } from 'actions/plugins-actions'; import { registerGitPlugin } from 'utils/git-utils'; -import { PluginsState } from '.'; +import { PluginComponent, PluginsState } from '.'; const defaultState: PluginsState = { fetching: false, @@ -15,8 +15,37 @@ const defaultState: PluginsState = { ANALYTICS: false, MODELS: false, }, + current: {}, + components: { + loginPage: { + loginForm: [], + }, + router: [], + }, }; +function findContainerFromPath(path: string, state: PluginsState): PluginComponent[] { + const pathSegments = path.split('.'); + let updatedStateSegment: any = state.components; + for (const pathSegment of pathSegments) { + if (Array.isArray(updatedStateSegment[pathSegment])) { + updatedStateSegment[pathSegment] = [...updatedStateSegment[pathSegment]]; + } else { + updatedStateSegment[pathSegment] = { ...updatedStateSegment[pathSegment] }; + } + updatedStateSegment = updatedStateSegment[pathSegment]; + if (typeof updatedStateSegment === 'undefined') { + throw new Error('Could not add plugin component. Path is not supported by the core application'); + } + } + + if (!Array.isArray(updatedStateSegment)) { + throw new Error('Could not add plugin component. Target path is not array'); + } + + return updatedStateSegment; +} + export default function (state: PluginsState = defaultState, action: PluginActions): PluginsState { switch (action.type) { case PluginsActionTypes.GET_PLUGINS: { @@ -47,6 +76,54 @@ export default function (state: PluginsState = defaultState, action: PluginActio fetching: false, }; } + case PluginsActionTypes.ADD_UI_COMPONENT: { + const { path, component, data } = action.payload; + const updatedState = { + ...state, + components: { ...state.components }, + }; + + const container = findContainerFromPath(path, updatedState); + container.push({ + component, + data: { + weight: data.weight || Number.MAX_SAFE_INTEGER, + shouldBeRendered: (componentProps: object = {}, componentState: object = {}) => { + if (data.shouldBeRendered) { + return data.shouldBeRendered(Object.freeze(componentProps), Object.freeze(componentState)); + } + return true; + }, + }, + }); + + return updatedState; + } + case PluginsActionTypes.REMOVE_UI_COMPONENT: { + const { path, component } = action.payload; + const updatedState = { + ...state, + components: { ...state.components }, + }; + + const container = findContainerFromPath(path, updatedState); + const index = container.findIndex((el) => el.component === component); + if (index !== -1) { + container.splice(index, 1); + } + + return updatedState; + } + case PluginsActionTypes.ADD_PLUGIN: { + const { name, destructor } = action.payload; + return { + ...state, + current: { + ...state.current, + [name]: destructor, + }, + }; + } default: return state; } diff --git a/cvat-ui/src/utils/git-utils.ts b/cvat-ui/src/utils/git-utils.ts index 4863d773dac2..4c02d58c6c6c 100644 --- a/cvat-ui/src/utils/git-utils.ts +++ b/cvat-ui/src/utils/git-utils.ts @@ -1,4 +1,5 @@ // Copyright (C) 2019-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -42,27 +43,27 @@ interface ReposData { lfs: boolean } -function waitForClone(cloneResponse: any): Promise { +function waitForClone({ data: cloneResponse }: any): Promise { return new Promise((resolve, reject): void => { async function checkCallback(): Promise { core.server .request(`${baseURL}/git/repository/check/${cloneResponse.rq_id}`, { method: 'GET', }) - .then((response: any): void => { - if (['queued', 'started'].includes(response.status)) { + .then(({ data }: any): void => { + if (['queued', 'started'].includes(data.status)) { setTimeout(checkCallback, 1000); - } else if (response.status === 'finished') { + } else if (data.status === 'finished') { resolve(); - } else if (response.status === 'failed') { + } else if (data.status === 'failed') { let message = 'Repository status check failed. '; - if (response.stderr) { - message += response.stderr; + if (data.stderr) { + message += data.stderr; } reject(message); } else { - const message = `Repository status check returned the status "${response.status}"`; + const message = `Repository status check returned the status "${data.status}"`; reject(message); } }) @@ -143,9 +144,9 @@ export function registerGitPlugin(): void { } export async function getReposData(tid: number): Promise { - const response = await core.server.request(`${baseURL}/git/repository/get/${tid}`, { + const response = (await core.server.request(`${baseURL}/git/repository/get/${tid}`, { method: 'GET', - }); + })).data; if (!response.url.value) { return null; @@ -168,12 +169,12 @@ export function syncRepos(tid: number): Promise { .request(`${baseURL}/git/repository/push/${tid}`, { method: 'GET', }) - .then((syncResponse: any): void => { + .then(({ data: syncResponse }: any): void => { async function checkSync(): Promise { const id = syncResponse.rq_id; - const response = await core.server.request(`${baseURL}/git/repository/check/${id}`, { + const response = (await core.server.request(`${baseURL}/git/repository/check/${id}`, { method: 'GET', - }); + })).data; if (['queued', 'started'].includes(response.status)) { setTimeout(checkSync, 1000); diff --git a/cvat-ui/src/utils/hooks.ts b/cvat-ui/src/utils/hooks.ts index 5844775475fa..3de8de9df5d8 100644 --- a/cvat-ui/src/utils/hooks.ts +++ b/cvat-ui/src/utils/hooks.ts @@ -1,7 +1,13 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT -import { useRef, useEffect, useState } from 'react'; + +import { + useRef, useEffect, useState, useCallback, +} from 'react'; +import { useSelector } from 'react-redux'; +import { CombinedState, PluginComponent } from 'reducers'; // eslint-disable-next-line import/prefer-default-export export function usePrevious(value: T): T | undefined { @@ -12,6 +18,36 @@ export function usePrevious(value: T): T | undefined { return ref.current; } +export function useIsMounted(): () => boolean { + const ref = useRef(false); + + useEffect(() => { + ref.current = true; + return () => { + ref.current = false; + }; + }, []); + + return useCallback(() => ref.current, []); +} + +export function usePlugins( + getState: (state: CombinedState) => PluginComponent[], + props: object = {}, state: object = {}, +): any { + const components = useSelector(getState); + const filteredComponents = components.filter((component) => component.data.shouldBeRendered(props, state)); + const mappedComponents = filteredComponents.map(({ component }) => component); + const ref = useRef(mappedComponents); + + if (ref.current.length !== mappedComponents.length || + ref.current.some((comp, idx) => comp !== mappedComponents[idx])) { + ref.current = mappedComponents; + } + + return ref.current; +} + export interface ICardHeightHOC { numberOfRows: number; paddings: number; diff --git a/cvat-ui/tsconfig.json b/cvat-ui/tsconfig.json index 2cb57ed440c9..e70df27a42a7 100644 --- a/cvat-ui/tsconfig.json +++ b/cvat-ui/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es6", + "target": "es2020", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, diff --git a/cvat-ui/webpack.config.js b/cvat-ui/webpack.config.js index 14ded5ee2be6..3765651bce1a 100644 --- a/cvat-ui/webpack.config.js +++ b/cvat-ui/webpack.config.js @@ -12,12 +12,25 @@ const HtmlWebpackPlugin = require('html-webpack-plugin'); const Dotenv = require('dotenv-webpack'); const CopyPlugin = require('copy-webpack-plugin'); - module.exports = (env) => { - console.log() const defaultAppConfig = path.join(__dirname, 'src/config.tsx'); + const defaultPlugins = []; const appConfigFile = process.env.UI_APP_CONFIG ? process.env.UI_APP_CONFIG : defaultAppConfig; - console.log('Application config file is: ', appConfigFile); + const pluginsList = process.env.CLIENT_PLUGINS ? process.env.CLIENT_PLUGINS.split(':') + .map((s) => s.trim()).filter((s) => !!s) : defaultPlugins + + const transformedPlugins = pluginsList + .filter((plugin) => !!plugin).reduce((acc, _path, index) => ({ + ...acc, + [`plugin_${index}`]: { + dependOn: 'cvat-ui', + // path can be absolute, in this case it is accepted as is + // also the path can be relative to cvat-ui root directory + import: path.isAbsolute(_path) ? _path : path.join(__dirname, _path, 'src', 'ts', 'index.tsx'), + }, + }), {}); + + console.log('List of plugins: ', Object.values(transformedPlugins).map((plugin) => plugin.import)); return { target: 'web', @@ -25,6 +38,7 @@ module.exports = (env) => { devtool: 'source-map', entry: { 'cvat-ui': './src/index.tsx', + ...transformedPlugins, }, output: { path: path.resolve(__dirname, 'dist'), @@ -56,12 +70,13 @@ module.exports = (env) => { }, resolve: { extensions: ['.tsx', '.ts', '.jsx', '.js', '.json'], - fallback: { fs: false, }, alias: { config$: appConfigFile, + '@root': path.resolve(__dirname, 'src'), + '@modules': path.resolve(__dirname, '..', 'node_modules'), }, modules: [path.resolve(__dirname, 'src'), 'node_modules'], },