Skip to content

Commit

Permalink
Supported React plugins (#5801)
Browse files Browse the repository at this point in the history
<!-- Raise an issue to propose your change
(https://github.com/opencv/cvat/issues).
It helps to avoid duplication of efforts from multiple independent
contributors.
Discuss your ideas with maintainers to be sure that changes will be
approved and merged.
Read the [Contribution
guide](https://opencv.github.io/cvat/docs/contributing/). -->

<!-- Provide a general summary of your changes in the Title above -->

### Motivation and context
<!-- Why is this change required? What problem does it solve? If it
fixes an open
issue, please link to the issue here. Describe your changes in detail,
add
screenshots. -->

### How has this been tested?
<!-- Please describe in detail how you tested your changes.
Include details of your testing environment, and the tests you ran to
see how your change affects other areas of the code, etc. -->

### Checklist
<!-- Go over all the following points, and put an `x` in all the boxes
that apply.
If an item isn't applicable for some reason, then ~~explicitly
strikethrough~~ the whole
line. If you don't do that, GitHub will show incorrect progress for the
pull request.
If you're unsure about any of these, don't hesitate to ask. We're here
to help! -->
- [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 = () => <Button> Press me </Button>;

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 ``<plugin_directory>/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",
    }
}

```
  • Loading branch information
bsekachev authored Mar 16, 2023
1 parent e5bf3ec commit dae6dcb
Show file tree
Hide file tree
Showing 31 changed files with 318 additions and 763 deletions.
3 changes: 2 additions & 1 deletion Dockerfile.ui
Original file line number Diff line number Diff line change
Expand Up @@ -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} \
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion cvat-core/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
30 changes: 9 additions & 21 deletions cvat-core/src/api-implementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
//
// SPDX-License-Identifier: MIT

import { SocialAuthMethod, SocialAuthMethodsRawType } from './auth-methods';
import config from './config';

import PluginRegistry from './plugins';
Expand Down Expand Up @@ -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<string> => {
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);
};
Expand Down Expand Up @@ -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;
};

Expand Down
38 changes: 8 additions & 30 deletions cvat-core/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
},
},
Expand Down Expand Up @@ -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;
},
Expand Down
68 changes: 0 additions & 68 deletions cvat-core/src/auth-methods.ts

This file was deleted.

68 changes: 8 additions & 60 deletions cvat-core/src/server-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,30 +402,6 @@ async function login(credential: string, password: string): Promise<void> {
setAuthData(authenticationResponse);
}

async function loginWithSocialAccount(
tokenURL: string,
code: string,
authParams?: string,
process?: string,
scope?: string,
): Promise<void> {
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<void> {
try {
await Axios.post(`${config.backendAPI}/auth/logout`);
Expand Down Expand Up @@ -586,14 +562,13 @@ async function healthCheck(
});
}

async function serverRequest(url: string, data: object): Promise<void> {
async function serverRequest(url: string, data: object): Promise<any> {
try {
return (
await Axios({
url,
...data,
})
).data;
const res = await Axios({
url,
...data,
});
return res;
} catch (errorData) {
throw generateError(errorData);
}
Expand Down Expand Up @@ -2349,40 +2324,15 @@ async function receiveWebhookEvents(type: WebhookSourceType): Promise<string[]>
}
}

async function socialAuthentication(): Promise<any> {
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<string> {
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,
Expand All @@ -2392,8 +2342,6 @@ export default Object.freeze({
request: serverRequest,
userAgreements,
installedApps,
loginWithSocialAccount,
selectSSOIdentityProvider,
hasLimits,
}),

Expand Down
2 changes: 1 addition & 1 deletion cvat-ui/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
43 changes: 1 addition & 42 deletions cvat-ui/src/actions/auth-actions.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
// Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// Copyright (C) 2022-2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT

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();

Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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<typeof authActions>;
Expand Down Expand Up @@ -219,23 +198,3 @@ export const loadAuthActionsAsync = (): ThunkAction => async (dispatch) => {
dispatch(authActions.loadServerAuthActionsFailed(error));
}
};

export const loadSocialAuthAsync = (): ThunkAction => async (dispatch): Promise<void> => {
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<void> => {
dispatch(authActions.selectIdP());
try {
const identityProviderID: string = await cvat.server.selectSSOIdentityProvider(email, iss);
dispatch(authActions.selectIdPSuccess(identityProviderID));
} catch (error) {
dispatch(authActions.selectIdPFailed(error));
}
};
Loading

0 comments on commit dae6dcb

Please sign in to comment.