From b091c46fd167a413ca668f08bf9f22d1662889ca Mon Sep 17 00:00:00 2001 From: Alexandra Bri Date: Thu, 31 Oct 2024 15:25:34 +0200 Subject: [PATCH 01/14] [Feature] [WRS-2078] Implement data connector authorization --- package.json | 4 +- src/MainContent.tsx | 8 ++- src/_dev-execution/bootstrap.ts | 24 +++++++ src/components/dataSource/DataSource.tsx | 68 +++++++++++++++++++ .../layout-panels/leftPanel/LeftPanel.tsx | 5 +- src/components/shared/Panel.styles.ts | 9 +++ src/components/variables/VariablesList.tsx | 5 +- src/tests/LeftPanel.test.tsx | 12 ++-- yarn.lock | 16 ++--- 9 files changed, 131 insertions(+), 20 deletions(-) create mode 100644 src/components/dataSource/DataSource.tsx create mode 100644 src/components/shared/Panel.styles.ts diff --git a/package.json b/package.json index b299da97..abc440e0 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,8 @@ "validate-versions": "node validate_versions.cjs" }, "dependencies": { - "@chili-publish/grafx-shared-components": "^0.83.1", - "@chili-publish/studio-sdk": "^1.15.0-rc.6", + "@chili-publish/grafx-shared-components": "^0.84.0", + "@chili-publish/studio-sdk": "^1.16.0-rc.12", "axios": "^1.6.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/src/MainContent.tsx b/src/MainContent.tsx index 63c219f0..827fd3d2 100644 --- a/src/MainContent.tsx +++ b/src/MainContent.tsx @@ -313,7 +313,13 @@ function MainContent({ projectConfig, authToken, updateToken: setAuthToken }: Ma )} - {!isMobileSize && } + {!isMobileSize && ( + + )} {isMobileSize && ( diff --git a/src/_dev-execution/bootstrap.ts b/src/_dev-execution/bootstrap.ts index 9bd01ab3..651c5b23 100644 --- a/src/_dev-execution/bootstrap.ts +++ b/src/_dev-execution/bootstrap.ts @@ -1,6 +1,7 @@ // This is an entry point when running standalone version of studio workspace in dev mode // It's not going to be bundled to the main `bundle.js` file +import axios from 'axios'; import { TokenManager } from './token-manager'; (async () => { @@ -64,6 +65,17 @@ import { TokenManager } from './token-manager'; } else { authToken = await tokenManager.getAccessToken(); } + + const onProjectInfoRequested = async () => { + return { + id: '', + name: '', + template: { + id: '', + }, + }; + }; + window.StudioUI.studioUILoaderConfig({ selector: 'sui-root', projectId, @@ -71,9 +83,21 @@ import { TokenManager } from './token-manager'; graFxStudioEnvironmentApiBaseUrl: `${baseUrl}`, authToken, editorLink: `https://stgrafxstudiodevpublic.blob.core.windows.net/editor/${engineSource}/web`, + sandboxMode: true, + uiTheme: 'dark', refreshTokenAction: () => tokenManager.refreshToken(), onConnectorAuthenticationRequested: (connectorId) => { return Promise.reject(new Error(`Authorization failed for ${connectorId}`)); }, + onProjectDocumentRequested: async () => { + const doc = await axios.get( + `https://cp-qeb-191.cpstaging.online/grafx/api/v1/environment/cp-qeb-191/templates/${projectId}/download`, + { + headers: { Authorization: `Bearer ${authToken}` }, + }, + ); + return doc.data || ''; + }, + onProjectInfoRequested, }); })(); diff --git a/src/components/dataSource/DataSource.tsx b/src/components/dataSource/DataSource.tsx new file mode 100644 index 00000000..c19b965a --- /dev/null +++ b/src/components/dataSource/DataSource.tsx @@ -0,0 +1,68 @@ +import { AvailableIcons, Icon, Input, Label, useTheme } from '@chili-publish/grafx-shared-components'; +import { useCallback, useEffect, useState } from 'react'; +import { ConnectorInstance, ConnectorType } from '@chili-publish/studio-sdk'; +import { PanelTitle } from '../shared/Panel.styles'; +import { getDataIdForSUI, getDataTestIdForSUI } from '../../utils/dataIds'; + +function DataSource() { + const { panel } = useTheme(); + const [dataConnector, setDataConnector] = useState(); + const [firstRowInfo, setFirstRowInfo] = useState(''); + + const getDataConnectorFirstRow = useCallback(async () => { + if (!dataConnector) return; + try { + const pageInfoResponse = await window.StudioUISDK.dataConnector.getPage(dataConnector.id, { limit: 1 }); + const firstRowData = pageInfoResponse.parsedData?.data?.[0]; + setFirstRowInfo(firstRowData ? Object.values(firstRowData).join('|') : ''); + } catch (error) { + // eslint-disable-next-line no-console + console.error(error); + // show err message + } + }, [dataConnector]); + + useEffect(() => { + const getDataConnector = async () => { + const dataConnectorsResponse = await window.StudioUISDK.connector.getAllByType(ConnectorType.data); + const defaultDataConnector = dataConnectorsResponse.parsedData?.[0] || null; + setDataConnector(defaultDataConnector); + }; + getDataConnector(); + }, []); + + useEffect(() => { + if (dataConnector) getDataConnectorFirstRow(); + }, [dataConnector, getDataConnectorFirstRow]); + + return dataConnector ? ( + <> + Data source + } + onClick={getDataConnectorFirstRow} + rightIcon={{ + label: '', + icon: ( + + ), + onClick: () => null, + }} + /> + + ) : null; +} + +export default DataSource; diff --git a/src/components/layout-panels/leftPanel/LeftPanel.tsx b/src/components/layout-panels/leftPanel/LeftPanel.tsx index f1d3f824..090e95a5 100644 --- a/src/components/layout-panels/leftPanel/LeftPanel.tsx +++ b/src/components/layout-panels/leftPanel/LeftPanel.tsx @@ -5,13 +5,15 @@ import ImagePanel from '../../imagePanel/ImagePanel'; import VariablesList from '../../variables/VariablesList'; import { useVariablePanelContext } from '../../../contexts/VariablePanelContext'; import { ContentType } from '../../../contexts/VariablePanelContext.types'; +import DataSource from '../../dataSource/DataSource'; interface LeftPanelProps { variables: Variable[]; + isSandboxMode: boolean; isDocumentLoaded: boolean; } -function LeftPanel({ variables, isDocumentLoaded }: LeftPanelProps) { +function LeftPanel({ variables, isSandboxMode, isDocumentLoaded }: LeftPanelProps) { const { contentType } = useVariablePanelContext(); const { panel } = useTheme(); @@ -23,6 +25,7 @@ function LeftPanel({ variables, isDocumentLoaded }: LeftPanelProps) { panelTheme={panel} > diff --git a/src/components/shared/Panel.styles.ts b/src/components/shared/Panel.styles.ts new file mode 100644 index 00000000..9a9aa0df --- /dev/null +++ b/src/components/shared/Panel.styles.ts @@ -0,0 +1,9 @@ +import { FontSizes, ITheme } from '@chili-publish/grafx-shared-components'; +import styled from 'styled-components'; + +export const PanelTitle = styled.h2<{ margin?: string; panelTheme: ITheme['panel'] }>` + font-size: ${FontSizes.heading2}; + font-weight: 500; + ${(props) => props.margin && `margin: ${props.margin};`}; + color: ${(props) => props.panelTheme.color}; +`; diff --git a/src/components/variables/VariablesList.tsx b/src/components/variables/VariablesList.tsx index 4432f075..94815fb8 100644 --- a/src/components/variables/VariablesList.tsx +++ b/src/components/variables/VariablesList.tsx @@ -2,9 +2,10 @@ import { useCallback, useEffect } from 'react'; import { DateVariable, DateVariable as DateVariableType, Variable, VariableType } from '@chili-publish/studio-sdk'; import { useTheme } from '@chili-publish/grafx-shared-components'; import VariablesComponents from '../variablesComponents/VariablesComponents'; -import { ComponentWrapper, VariablesListWrapper, VariablesPanelTitle } from './VariablesPanel.styles'; +import { ComponentWrapper, VariablesListWrapper } from './VariablesPanel.styles'; import { useVariablePanelContext } from '../../contexts/VariablePanelContext'; import { ContentType } from '../../contexts/VariablePanelContext.types'; +import { PanelTitle } from '../shared/Panel.styles'; interface VariablesListProps { variables: Variable[]; @@ -27,7 +28,7 @@ function VariablesList({ variables, isDocumentLoaded }: VariablesListProps) { return ( - Customize + Customize {variables.length > 0 && variables.map((variable: Variable) => { if (!variable.isVisible) return null; diff --git a/src/tests/LeftPanel.test.tsx b/src/tests/LeftPanel.test.tsx index 5347ef13..f18afb58 100644 --- a/src/tests/LeftPanel.test.tsx +++ b/src/tests/LeftPanel.test.tsx @@ -117,7 +117,7 @@ describe('Image Panel', () => { const { getByText, getByRole } = render( - + , { container: document.body.appendChild(APP_WRAPPER) }, @@ -143,7 +143,7 @@ describe('Image Panel', () => { const { getByText, getByTestId, getAllByText } = render( - + , { container: document.body.appendChild(APP_WRAPPER) }, @@ -169,7 +169,7 @@ describe('Image Panel', () => { const { getByText } = render( - + , { container: document.body.appendChild(APP_WRAPPER) }, @@ -192,7 +192,7 @@ describe('Image Panel', () => { const { getByRole } = render( - + , ); @@ -230,7 +230,7 @@ describe('Image Panel', () => { render( - + , { container: document.body.appendChild(APP_WRAPPER) }, @@ -255,7 +255,7 @@ describe('Image Panel', () => { const { getByTestId } = render( - + , { container: document.body.appendChild(APP_WRAPPER) }, diff --git a/yarn.lock b/yarn.lock index 4b1d53e6..3cb946ea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1225,10 +1225,10 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@chili-publish/grafx-shared-components@^0.83.1": - version "0.83.1" - resolved "https://npm.pkg.github.com/download/@chili-publish/grafx-shared-components/0.83.1/62327163443b2e9bb45d80d49b5e738c0f673fe4#62327163443b2e9bb45d80d49b5e738c0f673fe4" - integrity sha512-GpZiBG1+rjA4yIcvNG0Ny6IzxepVCxQuBY4lf70+SM2Q1p1d2t8SJqDvkwi+7a/4v43wzlHWaLNXnvi/7OPf/w== +"@chili-publish/grafx-shared-components@^0.84.0": + version "0.84.0" + resolved "https://npm.pkg.github.com/download/@chili-publish/grafx-shared-components/0.84.0/069a5181eef07b89d1e9a738f571a4b4d4f8c586#069a5181eef07b89d1e9a738f571a4b4d4f8c586" + integrity sha512-J1pmXArj67K772cs/OF0Hm6Yf7NdqN3t0iBP+G48NWp4UKV9UoX/d4/TZPYOXmwjIX6x4gd8jpgUluJUa6mRQA== dependencies: "@fortawesome/fontawesome-svg-core" "^6.4.0" "@fortawesome/pro-light-svg-icons" "^6.4.0" @@ -1239,10 +1239,10 @@ react-datepicker "^7.1.0" react-select "^5.6.1" -"@chili-publish/studio-sdk@^1.15.0-rc.6": - version "1.15.0-rc.6" - resolved "https://npm.pkg.github.com/download/@chili-publish/studio-sdk/1.15.0-rc.6/b251811d416c5b5e9a9a8e7fdafaa64dfd59879a#b251811d416c5b5e9a9a8e7fdafaa64dfd59879a" - integrity sha512-nEPYrWc2vvoo0lcezKk7Oa7Sw4mxnfceYFSHK5snyy4+L/UutVj+FTN3sKLl/OzvP2EnKAF5VYoBfC2yY4rsEQ== +"@chili-publish/studio-sdk@^1.16.0-rc.12": + version "1.16.0-rc.12" + resolved "https://npm.pkg.github.com/download/@chili-publish/studio-sdk/1.16.0-rc.12/5aed0681e13b1424a223f7cadebc5f3031d86a67#5aed0681e13b1424a223f7cadebc5f3031d86a67" + integrity sha512-3PItvQZpI61+Rtmxlavjcsxu+qHb0alNHBFPE7blx+FKbtOboLfolELYHQSZO8s6Y2i1wilxZc4zkVxtRvpu+w== dependencies: penpal "6.1.0" From 452149ee94d0a8f85593ed61671ef0452aa86010 Mon Sep 17 00:00:00 2001 From: Alexandra Bri Date: Thu, 31 Oct 2024 17:20:26 +0200 Subject: [PATCH 02/14] revert dev bootstrap --- src/_dev-execution/bootstrap.ts | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/src/_dev-execution/bootstrap.ts b/src/_dev-execution/bootstrap.ts index 651c5b23..9bd01ab3 100644 --- a/src/_dev-execution/bootstrap.ts +++ b/src/_dev-execution/bootstrap.ts @@ -1,7 +1,6 @@ // This is an entry point when running standalone version of studio workspace in dev mode // It's not going to be bundled to the main `bundle.js` file -import axios from 'axios'; import { TokenManager } from './token-manager'; (async () => { @@ -65,17 +64,6 @@ import { TokenManager } from './token-manager'; } else { authToken = await tokenManager.getAccessToken(); } - - const onProjectInfoRequested = async () => { - return { - id: '', - name: '', - template: { - id: '', - }, - }; - }; - window.StudioUI.studioUILoaderConfig({ selector: 'sui-root', projectId, @@ -83,21 +71,9 @@ import { TokenManager } from './token-manager'; graFxStudioEnvironmentApiBaseUrl: `${baseUrl}`, authToken, editorLink: `https://stgrafxstudiodevpublic.blob.core.windows.net/editor/${engineSource}/web`, - sandboxMode: true, - uiTheme: 'dark', refreshTokenAction: () => tokenManager.refreshToken(), onConnectorAuthenticationRequested: (connectorId) => { return Promise.reject(new Error(`Authorization failed for ${connectorId}`)); }, - onProjectDocumentRequested: async () => { - const doc = await axios.get( - `https://cp-qeb-191.cpstaging.online/grafx/api/v1/environment/cp-qeb-191/templates/${projectId}/download`, - { - headers: { Authorization: `Bearer ${authToken}` }, - }, - ); - return doc.data || ''; - }, - onProjectInfoRequested, }); })(); From 25c60fdedb8d0d98a443bcfd618809b1e3695dd6 Mon Sep 17 00:00:00 2001 From: Alexandra Bri Date: Fri, 1 Nov 2024 12:59:49 +0200 Subject: [PATCH 03/14] enable multiple connectors auth flow --- src/MainContent.tsx | 41 +++-- src/_dev-execution/bootstrap.ts | 122 ++++++++++++++- .../useConnectorAuthentication.ts | 142 ++++++++++++------ src/components/dataSource/DataSource.tsx | 1 + 4 files changed, 242 insertions(+), 64 deletions(-) diff --git a/src/MainContent.tsx b/src/MainContent.tsx index 827fd3d2..981cb0e3 100644 --- a/src/MainContent.tsx +++ b/src/MainContent.tsx @@ -78,13 +78,14 @@ function MainContent({ projectConfig, authToken, updateToken: setAuthToken }: Ma ); const { - result: connectorAuthResult, - process: connectorAuthenticationProcess, + // result: connectorAuthResult, + authentication: authenticationFlows, + getAuthenticationProcess: connectorAuthenticationProcess, createProcess: createAuthenticationProcess, - connectorName, + //connectorName, } = useConnectorAuthentication(); - useConnectorAuthenticationResult(connectorAuthResult); + // useConnectorAuthenticationResult(connectorAuthResult); useEffect(() => { projectConfig @@ -143,11 +144,17 @@ function MainContent({ projectConfig, authToken, updateToken: setAuthToken }: Ma // eslint-disable-next-line @typescript-eslint/no-unused-vars const [_, remoteConnectorId] = request.headerValue.split(';').map((i) => i.trim()); const connector = await window.StudioUISDK.next.connector.getById(request.connectorId); - const result = await createAuthenticationProcess(async () => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const res = await projectConfig.onConnectorAuthenticationRequested!(remoteConnectorId); - return res; - }, connector.parsedData?.name ?? ''); + console.log('here---', connector); + const result = await createAuthenticationProcess( + async () => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const res = await projectConfig.onConnectorAuthenticationRequested!(remoteConnectorId); + return res; + }, + connector.parsedData?.name ?? '', + remoteConnectorId, + ); + console.log('res', result); return result; } } catch (error) { @@ -287,6 +294,7 @@ function MainContent({ projectConfig, authToken, updateToken: setAuthToken }: Ma loadDocument(); }, [authToken, fetchedDocument]); + console.log('authenticationFlows', authenticationFlows); return ( @@ -340,13 +348,14 @@ function MainContent({ projectConfig, authToken, updateToken: setAuthToken }: Ma ) : null} - {connectorAuthenticationProcess && ( - connectorAuthenticationProcess.start()} - onCancel={() => connectorAuthenticationProcess.cancel()} - /> - )} + {authenticationFlows && + Object.entries(authenticationFlows).map(([connectorId, authFlow]) => ( + connectorAuthenticationProcess(connectorId)?.start()} + onCancel={() => connectorAuthenticationProcess(connectorId)?.cancel()} + /> + ))} diff --git a/src/_dev-execution/bootstrap.ts b/src/_dev-execution/bootstrap.ts index 9bd01ab3..76e90fc5 100644 --- a/src/_dev-execution/bootstrap.ts +++ b/src/_dev-execution/bootstrap.ts @@ -1,11 +1,14 @@ // This is an entry point when running standalone version of studio workspace in dev mode // It's not going to be bundled to the main `bundle.js` file +import axios from 'axios'; import { TokenManager } from './token-manager'; +import { Authentified, ConnectorAuthenticationResult } from '../types/ConnectorAuthenticationResult'; (async () => { const tokenManager = new TokenManager(); - + let popup: any; + let promiseExecutor: any; let urlParams = new URLSearchParams(window.location.search); // state after redirection @@ -64,16 +67,129 @@ import { TokenManager } from './token-manager'; } else { authToken = await tokenManager.getAccessToken(); } + + const handler = function (e: MessageEvent) { + // We're processing only events from redirect origin + if (e.origin && baseUrl.startsWith(e.origin)) { + try { + const result = JSON.parse(e.data); + if (result.type === 'AuthorizationComplete') { + popup?.close(); + promiseExecutor?.res({ + type: 'authentified', + }); + } else if (result.type === 'AuthorizationFailed') { + popup?.close(); + promiseExecutor?.res({ + type: 'error', + error: result.reason, + }); + } else { + promiseExecutor?.rej(`Something went wrong: ${JSON.stringify(result)}`); + } + } catch (error) { + promiseExecutor?.rej(`Something went wrong: ${error}`); + } + } + }; + + window.addEventListener('message', handler); + + const onProjectInfoRequested = async () => { + return { + id: '', + name: '', + template: { + id: '', + }, + }; + }; + + const buildRedirectUri = (baseUrl: string, connectorId: string) => { + return `${baseUrl}/connectors/${connectorId}/auth/oauth-authorization-code/redirect`; + }; + + const buildAuthentinticationUrl = ({ baseUrl, client_id, scope, redirect_uri, state }: any) => { + const url = new URL(baseUrl); + const query = new URLSearchParams({ + // eslint-disable-next-line camelcase + client_id, + // eslint-disable-next-line camelcase + redirect_uri, + scope, + // eslint-disable-next-line camelcase + response_type: 'code', + state, + }); + // eslint-disable-next-line no-restricted-syntax + for (const [paramKey, paramValue] of query) { + url.searchParams.append(paramKey, paramValue); + } + return url.toString(); + }; + + const runAuthentication = async (connectorId: string) => { + const connectorsAuthInfoRes = await axios.get( + `https://cp-qeb-191.cpstaging.online/grafx/api/v1/environment/cp-qeb-191/connectors/${connectorId}/auth/oauth-authorization-code`, + { + headers: { Authorization: `Bearer ${authToken}` }, + }, + ); + const userId = 'samlp|chili-publish-dev|5zPH9tpDkfYakh5ym0w-0DHe-mKIB8CiTYyqcnbAGEI'; + const subscriptionGuid = '57718ff6-81c8-4e9e-bbe8-3c4ec86cf184'; + const connectorsAuthInfo = connectorsAuthInfoRes.data; + const authorizationUrl = buildAuthentinticationUrl({ + baseUrl: connectorsAuthInfo.authorizationEndpoint, + client_id: connectorsAuthInfo.clientId, + redirect_uri: buildRedirectUri(baseUrl, connectorId), + scope: connectorsAuthInfo.scope, + state: window.btoa(JSON.stringify({ userId, subscriptionId: subscriptionGuid })), + }); + popup = window.open(authorizationUrl); + /*return { + type: 'authentified', + } as Authentified;*/ + }; + + const authenticate = (connectorId: string) => { + return Promise.race([ + new Promise((res, rej) => { + promiseExecutor = { res, rej }; + runAuthentication(connectorId); + }), + new Promise((res) => { + setTimeout(() => { + res({ + type: 'timeout', + }); + }, 60 * 1000); + }), + ]).finally(() => { + // resetParams(); + popup = null; + }); + }; + window.StudioUI.studioUILoaderConfig({ selector: 'sui-root', projectId, projectName: 'Dev Run', graFxStudioEnvironmentApiBaseUrl: `${baseUrl}`, authToken, + sandboxMode: true, + uiTheme: 'dark', editorLink: `https://stgrafxstudiodevpublic.blob.core.windows.net/editor/${engineSource}/web`, refreshTokenAction: () => tokenManager.refreshToken(), - onConnectorAuthenticationRequested: (connectorId) => { - return Promise.reject(new Error(`Authorization failed for ${connectorId}`)); + onConnectorAuthenticationRequested: authenticate, + onProjectDocumentRequested: async () => { + const doc = await axios.get( + `https://cp-qeb-191.cpstaging.online/grafx/api/v1/environment/cp-qeb-191/templates/${projectId}/download`, + { + headers: { Authorization: `Bearer ${authToken}` }, + }, + ); + return doc.data || ''; }, + onProjectInfoRequested, }); })(); diff --git a/src/components/connector-authentication/useConnectorAuthentication.ts b/src/components/connector-authentication/useConnectorAuthentication.ts index 8c0d0377..578a79ae 100644 --- a/src/components/connector-authentication/useConnectorAuthentication.ts +++ b/src/components/connector-authentication/useConnectorAuthentication.ts @@ -6,76 +6,128 @@ interface Executor { handler: () => Promise; } +type AuthenticationData = { + [connectorId: string]: { + connectorName: string; + authenticationResolvers: Omit< + // eslint-disable-next-line no-undef + PromiseWithResolvers, + 'promise' + > | null; + executor: Executor; + result: ConnectorAuthenticationResult | null; + }; +}; + export const useConnectorAuthentication = () => { - const [authenticationResolvers, setAuthenticationResolvers] = useState, 'promise' > | null>(null); const [executor, setExecutor] = useState(null); const [connectorName, setConnectorName] = useState(''); - const [result, setResult] = useState(null); + const [result, setResult] = useState(null);*/ - const resetProcess = useCallback(() => { - setAuthenticationResolvers(null); - setExecutor(null); + const [authentication, setAuthentication] = useState(); + + const resetProcess = useCallback((id: string) => { + setAuthentication((prev) => { + delete prev?.[id]; + return { ...prev }; + }); }, []); - const process = useMemo(() => { - if (authenticationResolvers && executor) { - return { - __resolvers: authenticationResolvers, - async start() { - setResult(null); - try { - const executorResult = await executor.handler().then((res) => { - setResult(res); - if (res.type === 'authentified') { - return new RefreshedAuthCredendentials(); - } - // eslint-disable-next-line no-console - console.warn(`There is a "${res.type}" issue with authentifying of the connector`); - if (res.type === 'error') { + const getAuthenticationProcess = useCallback( + (id: string) => { + const authenticationProcess = authentication?.[id]; + if (!authenticationProcess) return null; + + if (authenticationProcess.authenticationResolvers && authenticationProcess.executor) { + return { + __resolvers: authenticationProcess.authenticationResolvers, + async start() { + setAuthentication( + (prev) => + ({ ...prev, [id]: { ...(prev?.[id] || {}), result: null } } as AuthenticationData), + ); + // setResult(null); + try { + const executorResult = await authenticationProcess.executor.handler().then((res) => { + setAuthentication( + (prev) => + ({ + ...prev, + [id]: { ...(prev?.[id] || {}), result: res }, + } as AuthenticationData), + ); + + // setResult(res); + if (res.type === 'authentified') { + return new RefreshedAuthCredendentials(); + } // eslint-disable-next-line no-console - console.error(res.error); - } + console.warn(`There is a "${res.type}" issue with authentifying of the connector`); + if (res.type === 'error') { + // eslint-disable-next-line no-console + console.error(res.error); + } - return null; - }); - this.__resolvers.resolve(executorResult); - } catch (error) { - this.__resolvers.reject(error); - } finally { - resetProcess(); - } - }, - async cancel() { - this.__resolvers.resolve(null); - resetProcess(); - }, - }; - } - return null; - }, [authenticationResolvers, executor, resetProcess]); + return null; + }); + this.__resolvers.resolve(executorResult); + } catch (error) { + this.__resolvers.reject(error); + } finally { + resetProcess(id); + } + }, + async cancel() { + console.log('here', id); + this.__resolvers.resolve(null); + resetProcess(id); + }, + }; + } + return null; + }, + [authentication, resetProcess], + ); - const createProcess = async (authorizationExecutor: Executor['handler'], name: string) => { + const createProcess = async (authorizationExecutor: Executor['handler'], name: string, id: string) => { const authenticationAwaiter = Promise.withResolvers(); - setExecutor({ + /*setExecutor({ handler: authorizationExecutor, }); setAuthenticationResolvers({ resolve: authenticationAwaiter.resolve, reject: authenticationAwaiter.reject, }); - setConnectorName(name); + setConnectorName(name);*/ + + setAuthentication((prev) => ({ + ...prev, + [id]: { + executor: { + handler: authorizationExecutor, + }, + authenticationResolvers: { + resolve: authenticationAwaiter.resolve, + reject: authenticationAwaiter.reject, + }, + connectorName: name, + result: null, + }, + })); const promiseResult = await authenticationAwaiter.promise; return promiseResult; }; return { + authentication, createProcess, - process, - connectorName, - result, + getAuthenticationProcess, + //connectorName, + //result, }; }; diff --git a/src/components/dataSource/DataSource.tsx b/src/components/dataSource/DataSource.tsx index c19b965a..5421ceca 100644 --- a/src/components/dataSource/DataSource.tsx +++ b/src/components/dataSource/DataSource.tsx @@ -13,6 +13,7 @@ function DataSource() { if (!dataConnector) return; try { const pageInfoResponse = await window.StudioUISDK.dataConnector.getPage(dataConnector.id, { limit: 1 }); + const firstRowData = pageInfoResponse.parsedData?.data?.[0]; setFirstRowInfo(firstRowData ? Object.values(firstRowData).join('|') : ''); } catch (error) { From 8e51b39a853a8e2953f3b411ca810c235f3deb3f Mon Sep 17 00:00:00 2001 From: Alexandra Bri Date: Fri, 1 Nov 2024 15:28:26 +0200 Subject: [PATCH 04/14] Refactor connector authentication modals --- src/MainContent.tsx | 42 ++++--- .../useConnectorAuthentication.ts | 87 +++++--------- .../useConnectorAuthenticationResult.ts | 23 ++-- src/components/dataSource/DataSource.tsx | 1 - .../useConnectorAuthentication.test.ts | 111 +++++++++++------- 5 files changed, 140 insertions(+), 124 deletions(-) diff --git a/src/MainContent.tsx b/src/MainContent.tsx index 981cb0e3..31c13803 100644 --- a/src/MainContent.tsx +++ b/src/MainContent.tsx @@ -15,6 +15,7 @@ import './App.css'; import { CanvasContainer, Container, MainContentContainer } from './App.styles'; import AnimationTimeline from './components/animationTimeline/AnimationTimeline'; import { + ConnectorAuthenticationFlow, ConnectorAuthenticationModal, useConnectorAuthentication, useConnectorAuthenticationResult, @@ -43,6 +44,23 @@ interface MainContentProps { updateToken: (newValue: string) => void; } +interface AuthenticationFlowModalProps { + authenticationFlow: ConnectorAuthenticationFlow; + onConfirm: () => void; + onCancel: () => void; +} +function AuthenticationFlowModal({ authenticationFlow, onConfirm, onCancel }: AuthenticationFlowModalProps) { + useConnectorAuthenticationResult(authenticationFlow.connectorName, authenticationFlow.result); + + return ( + + ); +} + function MainContent({ projectConfig, authToken, updateToken: setAuthToken }: MainContentProps) { const [fetchedDocument, setFetchedDocument] = useState(''); const [variables, setVariables] = useState([]); @@ -78,15 +96,11 @@ function MainContent({ projectConfig, authToken, updateToken: setAuthToken }: Ma ); const { - // result: connectorAuthResult, - authentication: authenticationFlows, - getAuthenticationProcess: connectorAuthenticationProcess, + authenticationFlows, + process: connectorAuthenticationProcess, createProcess: createAuthenticationProcess, - //connectorName, } = useConnectorAuthentication(); - // useConnectorAuthenticationResult(connectorAuthResult); - useEffect(() => { projectConfig .onProjectInfoRequested(projectConfig.projectId) @@ -144,7 +158,6 @@ function MainContent({ projectConfig, authToken, updateToken: setAuthToken }: Ma // eslint-disable-next-line @typescript-eslint/no-unused-vars const [_, remoteConnectorId] = request.headerValue.split(';').map((i) => i.trim()); const connector = await window.StudioUISDK.next.connector.getById(request.connectorId); - console.log('here---', connector); const result = await createAuthenticationProcess( async () => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -154,7 +167,6 @@ function MainContent({ projectConfig, authToken, updateToken: setAuthToken }: Ma connector.parsedData?.name ?? '', remoteConnectorId, ); - console.log('res', result); return result; } } catch (error) { @@ -294,7 +306,6 @@ function MainContent({ projectConfig, authToken, updateToken: setAuthToken }: Ma loadDocument(); }, [authToken, fetchedDocument]); - console.log('authenticationFlows', authenticationFlows); return ( @@ -348,12 +359,13 @@ function MainContent({ projectConfig, authToken, updateToken: setAuthToken }: Ma ) : null} - {authenticationFlows && - Object.entries(authenticationFlows).map(([connectorId, authFlow]) => ( - connectorAuthenticationProcess(connectorId)?.start()} - onCancel={() => connectorAuthenticationProcess(connectorId)?.cancel()} + {authenticationFlows.length && + authenticationFlows.map((authFlow) => ( + connectorAuthenticationProcess(authFlow.connectorId)?.start()} + onCancel={() => connectorAuthenticationProcess(authFlow.connectorId)?.cancel()} /> ))} diff --git a/src/components/connector-authentication/useConnectorAuthentication.ts b/src/components/connector-authentication/useConnectorAuthentication.ts index 578a79ae..41ffe24c 100644 --- a/src/components/connector-authentication/useConnectorAuthentication.ts +++ b/src/components/connector-authentication/useConnectorAuthentication.ts @@ -1,68 +1,48 @@ import { RefreshedAuthCredendentials } from '@chili-publish/studio-sdk'; -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useState } from 'react'; import { ConnectorAuthenticationResult } from '../../types/ConnectorAuthenticationResult'; interface Executor { handler: () => Promise; } -type AuthenticationData = { - [connectorId: string]: { - connectorName: string; - authenticationResolvers: Omit< - // eslint-disable-next-line no-undef - PromiseWithResolvers, - 'promise' - > | null; - executor: Executor; - result: ConnectorAuthenticationResult | null; - }; +export type ConnectorAuthenticationFlow = { + connectorId: string; + connectorName: string; + authenticationResolvers: Omit, 'promise'> | null; + executor: Executor | null; + result: ConnectorAuthenticationResult | null; }; export const useConnectorAuthentication = () => { - /*const [authenticationResolvers, setAuthenticationResolvers] = useState, - 'promise' - > | null>(null); - const [executor, setExecutor] = useState(null); - const [connectorName, setConnectorName] = useState(''); - const [result, setResult] = useState(null);*/ - - const [authentication, setAuthentication] = useState(); + const [authenticationFlows, setAuthenticationFlows] = useState([]); const resetProcess = useCallback((id: string) => { - setAuthentication((prev) => { - delete prev?.[id]; - return { ...prev }; - }); + setAuthenticationFlows((prev) => + prev.map((item) => + item.connectorId === id ? { ...item, authenticationResolvers: null, executor: null } : item, + ), + ); }, []); - const getAuthenticationProcess = useCallback( + const process = useCallback( (id: string) => { - const authenticationProcess = authentication?.[id]; + const authenticationProcess = authenticationFlows.find((item) => item.connectorId === id); if (!authenticationProcess) return null; if (authenticationProcess.authenticationResolvers && authenticationProcess.executor) { return { __resolvers: authenticationProcess.authenticationResolvers, async start() { - setAuthentication( - (prev) => - ({ ...prev, [id]: { ...(prev?.[id] || {}), result: null } } as AuthenticationData), + setAuthenticationFlows((prev) => + prev.map((item) => (item.connectorId === id ? { ...item, result: null } : item)), ); - // setResult(null); try { - const executorResult = await authenticationProcess.executor.handler().then((res) => { - setAuthentication( - (prev) => - ({ - ...prev, - [id]: { ...(prev?.[id] || {}), result: res }, - } as AuthenticationData), + const executorResult = await authenticationProcess.executor?.handler().then((res) => { + setAuthenticationFlows((prev) => + prev.map((item) => (item.connectorId === id ? { ...item, result: res } : item)), ); - // setResult(res); if (res.type === 'authentified') { return new RefreshedAuthCredendentials(); } @@ -75,7 +55,7 @@ export const useConnectorAuthentication = () => { return null; }); - this.__resolvers.resolve(executorResult); + this.__resolvers.resolve(executorResult || null); } catch (error) { this.__resolvers.reject(error); } finally { @@ -83,31 +63,23 @@ export const useConnectorAuthentication = () => { } }, async cancel() { - console.log('here', id); this.__resolvers.resolve(null); + setAuthenticationFlows((prev) => prev.filter((item) => item.connectorId !== id)); resetProcess(id); }, }; } return null; }, - [authentication, resetProcess], + [authenticationFlows, resetProcess], ); const createProcess = async (authorizationExecutor: Executor['handler'], name: string, id: string) => { const authenticationAwaiter = Promise.withResolvers(); - /*setExecutor({ - handler: authorizationExecutor, - }); - setAuthenticationResolvers({ - resolve: authenticationAwaiter.resolve, - reject: authenticationAwaiter.reject, - }); - setConnectorName(name);*/ - setAuthentication((prev) => ({ + setAuthenticationFlows((prev) => [ ...prev, - [id]: { + { executor: { handler: authorizationExecutor, }, @@ -116,18 +88,17 @@ export const useConnectorAuthentication = () => { reject: authenticationAwaiter.reject, }, connectorName: name, + connectorId: id, result: null, }, - })); + ]); const promiseResult = await authenticationAwaiter.promise; return promiseResult; }; return { - authentication, + authenticationFlows, createProcess, - getAuthenticationProcess, - //connectorName, - //result, + process, }; }; diff --git a/src/components/connector-authentication/useConnectorAuthenticationResult.ts b/src/components/connector-authentication/useConnectorAuthenticationResult.ts index 7adaf163..d6e4c35a 100644 --- a/src/components/connector-authentication/useConnectorAuthenticationResult.ts +++ b/src/components/connector-authentication/useConnectorAuthenticationResult.ts @@ -3,26 +3,29 @@ import { useEffect } from 'react'; import { useNotificationManager } from '../../contexts/NotificantionManager/NotificationManagerContext'; import { ConnectorAuthenticationResult } from '../../types/ConnectorAuthenticationResult'; -const authorizationFailedToast = { +const authorizationFailedToast = (connectorName: string) => ({ id: 'connector-authorization-failed', - message: `Authorization failed`, + message: `Authorization failed for ${connectorName}.`, type: ToastVariant.NEGATIVE, -}; +}); -const authorizationFailedTimeoutToast = { +const authorizationFailedTimeoutToast = (connectorName: string) => ({ id: 'connector-authorization-failed-timeout', - message: `Authorization failed (timeout)`, + message: `Authorization failed (timeout) for ${connectorName}.`, type: ToastVariant.NEGATIVE, -}; +}); -export const useConnectorAuthenticationResult = (result: ConnectorAuthenticationResult | null) => { +export const useConnectorAuthenticationResult = ( + connectorName: string, + result: ConnectorAuthenticationResult | null, +) => { const { addNotification } = useNotificationManager(); useEffect(() => { if (result?.type === 'error') { - addNotification(authorizationFailedToast); + addNotification(authorizationFailedToast(connectorName)); } else if (result?.type === 'timeout') { - addNotification(authorizationFailedTimeoutToast); + addNotification(authorizationFailedTimeoutToast(connectorName)); } - }, [result, addNotification]); + }, [result, connectorName, addNotification]); }; diff --git a/src/components/dataSource/DataSource.tsx b/src/components/dataSource/DataSource.tsx index 5421ceca..74780a3f 100644 --- a/src/components/dataSource/DataSource.tsx +++ b/src/components/dataSource/DataSource.tsx @@ -19,7 +19,6 @@ function DataSource() { } catch (error) { // eslint-disable-next-line no-console console.error(error); - // show err message } }, [dataConnector]); diff --git a/src/tests/components/connector-authentication/useConnectorAuthentication.test.ts b/src/tests/components/connector-authentication/useConnectorAuthentication.test.ts index b0831a2d..c2c04ac5 100644 --- a/src/tests/components/connector-authentication/useConnectorAuthentication.test.ts +++ b/src/tests/components/connector-authentication/useConnectorAuthentication.test.ts @@ -17,8 +17,7 @@ describe('useConnectorAuthentication hook', () => { it('should create with default values', () => { const { result } = renderHook(() => useConnectorAuthentication()); - expect(result.current.connectorName).toEqual(''); - expect(result.current.process).toBeNull(); + expect(result.current.authenticationFlows.length).toEqual(0); }); it('should create "process" correctly', async () => { @@ -26,12 +25,12 @@ describe('useConnectorAuthentication hook', () => { const { result } = renderHook(() => useConnectorAuthentication()); act(() => { - result.current.createProcess(executor, 'connectorName'); + result.current.createProcess(executor, 'connectorName', 'connectorId'); }); - await waitFor(() => result.current.connectorName === 'connectorName'); + await waitFor(() => result.current.authenticationFlows[0].connectorName === 'connectorName'); - expect(result.current.process).toEqual({ + expect(result.current.process('connectorId')).toEqual({ __resolvers: { resolve: expect.any(Function), reject: expect.any(Function), @@ -41,39 +40,66 @@ describe('useConnectorAuthentication hook', () => { }); }); - it('should perform process correclty for none-authentified type', async () => { - const executor = jest.fn().mockResolvedValueOnce({ type: 'error' }); + it('should perform process correclty for different connectors', async () => { + const executor1 = jest.fn().mockResolvedValueOnce({ type: 'error' }); + const executor2 = jest.fn().mockResolvedValueOnce({ type: 'authentified' }); + const { result } = renderHook(() => useConnectorAuthentication()); - let processResult: RefreshedAuthCredendentials | null | undefined; + let processResultConnector1: RefreshedAuthCredendentials | null | undefined; act(() => { - result.current.createProcess(executor, 'connectorName').then( + result.current.createProcess(executor1, 'connectorName1', 'connectorId1').then( (res) => { - processResult = res; + processResultConnector1 = res; }, (err) => { - processResult = err; + processResultConnector1 = err; + }, + ); + }); + + let processResultConnector2: RefreshedAuthCredendentials | null | undefined; + act(() => { + result.current.createProcess(executor2, 'connectorName2', 'connectorId2').then( + (res) => { + processResultConnector2 = res; + }, + (err) => { + processResultConnector2 = err; }, ); }); - await waitFor(() => result.current.connectorName === 'connectorName'); + await waitFor(() => result.current.authenticationFlows[0].connectorName === 'connectorName1'); + await waitFor(() => result.current.authenticationFlows[1].connectorName === 'connectorName2'); await act(async () => { - await result.current.process?.start(); + await result.current.process?.('connectorId2')?.start(); }); - expect(processResult).toEqual(null); - expect(result.current.process).toBeNull(); + await act(async () => { + await result.current.process?.('connectorId1')?.start(); + }); + + expect(result.current.authenticationFlows.length).toEqual(2); + + expect(processResultConnector1).toEqual(null); + expect(result.current.authenticationFlows[0].authenticationResolvers).toBeNull(); + expect(result.current.authenticationFlows[0].executor).toBeNull(); + + expect(result.current.authenticationFlows[1].result).toEqual({ type: 'authentified' }); + expect(processResultConnector2).toEqual(new RefreshedAuthCredendentials()); + expect(result.current.authenticationFlows[1].authenticationResolvers).toBeNull(); + expect(result.current.authenticationFlows[1].executor).toBeNull(); }); - it('should perform process correclty for authentified type', async () => { + it('should perform process correclty for none-authentified type', async () => { const executor = jest.fn().mockResolvedValueOnce({ type: 'authentified' }); const { result } = renderHook(() => useConnectorAuthentication()); let processResult: RefreshedAuthCredendentials | null | undefined; act(() => { - result.current.createProcess(executor, 'connectorName').then( + result.current.createProcess(executor, 'connectorName', 'connectorId').then( (res) => { processResult = res; }, @@ -83,15 +109,17 @@ describe('useConnectorAuthentication hook', () => { ); }); - await waitFor(() => result.current.connectorName === 'connectorName'); + await waitFor(() => result.current.authenticationFlows[0].connectorName === 'connectorName'); await act(async () => { - await result.current.process?.start(); + await result.current.process?.('connectorId')?.start(); }); + expect(result.current.authenticationFlows.length).toEqual(1); + expect(result.current.authenticationFlows[0].result).toEqual({ type: 'authentified' }); expect(processResult).toEqual(new RefreshedAuthCredendentials()); - expect(result.current.result).toEqual({ type: 'authentified' }); - expect(result.current.process).toBeNull(); + expect(result.current.authenticationFlows[0].authenticationResolvers).toBeNull(); + expect(result.current.authenticationFlows[0].executor).toBeNull(); }); it('should perform process correclty when process return error state', async () => { @@ -100,7 +128,7 @@ describe('useConnectorAuthentication hook', () => { let processResult: RefreshedAuthCredendentials | null | undefined; act(() => { - result.current.createProcess(executor, 'connectorName').then( + result.current.createProcess(executor, 'connectorName', 'connectorId').then( (res) => { processResult = res; }, @@ -110,16 +138,17 @@ describe('useConnectorAuthentication hook', () => { ); }); - await waitFor(() => result.current.connectorName === 'connectorName'); + await waitFor(() => result.current.authenticationFlows[0].connectorName === 'connectorName'); await act(async () => { - await result.current.process?.start(); + await result.current.process?.('connectorId')?.start(); }); expect(processResult).toEqual(null); - expect(result.current.result).toEqual({ type: 'error', error: '[Error]: Occured' }); + expect(result.current.authenticationFlows[0].result).toEqual({ type: 'error', error: '[Error]: Occured' }); + expect(result.current.authenticationFlows[0].authenticationResolvers).toBeNull(); + expect(result.current.authenticationFlows[0].executor).toBeNull(); expect(window.console.error).toHaveBeenCalledWith('[Error]: Occured'); - expect(result.current.process).toBeNull(); }); it('should perform process correclty when process return timeout', async () => { @@ -128,7 +157,7 @@ describe('useConnectorAuthentication hook', () => { let processResult: RefreshedAuthCredendentials | null | undefined; act(() => { - result.current.createProcess(executor, 'connectorName').then( + result.current.createProcess(executor, 'connectorName', 'connectorId').then( (res) => { processResult = res; }, @@ -138,16 +167,17 @@ describe('useConnectorAuthentication hook', () => { ); }); - await waitFor(() => result.current.connectorName === 'connectorName'); + await waitFor(() => result.current.authenticationFlows[0].connectorName === 'connectorName'); await act(async () => { - await result.current.process?.start(); + await result.current.process?.('connectorId')?.start(); }); expect(processResult).toEqual(null); - expect(result.current.result).toEqual({ type: 'timeout' }); + expect(result.current.authenticationFlows[0].result).toEqual({ type: 'timeout' }); + expect(result.current.authenticationFlows[0].authenticationResolvers).toBeNull(); + expect(result.current.authenticationFlows[0].executor).toBeNull(); expect(window.console.error).not.toHaveBeenCalled(); - expect(result.current.process).toBeNull(); }); it('should perform process correclty when process rejected', async () => { @@ -156,7 +186,7 @@ describe('useConnectorAuthentication hook', () => { let processResult: RefreshedAuthCredendentials | null | undefined; act(() => { - result.current.createProcess(executor, 'connectorName').then( + result.current.createProcess(executor, 'connectorName', 'connectorId').then( (res) => { processResult = res; }, @@ -166,15 +196,16 @@ describe('useConnectorAuthentication hook', () => { ); }); - await waitFor(() => result.current.connectorName === 'connectorName'); + await waitFor(() => result.current.authenticationFlows[0].connectorName === 'connectorName'); await act(async () => { - await result.current.process?.start(); + await result.current.process?.('connectorId')?.start(); }); expect(processResult).toEqual({ message: 'TestError' }); - expect(result.current.result).toBeNull(); - expect(result.current.process).toBeNull(); + expect(result.current.authenticationFlows[0].result).toBeNull(); + expect(result.current.authenticationFlows[0].authenticationResolvers).toBeNull(); + expect(result.current.authenticationFlows[0].executor).toBeNull(); }); it('should perform process correclty when canceling', async () => { @@ -183,7 +214,7 @@ describe('useConnectorAuthentication hook', () => { let processResult: RefreshedAuthCredendentials | null | undefined; act(() => { - result.current.createProcess(executor, 'connectorName').then( + result.current.createProcess(executor, 'connectorName', 'connectorId').then( (res) => { processResult = res; }, @@ -193,14 +224,14 @@ describe('useConnectorAuthentication hook', () => { ); }); - await waitFor(() => result.current.connectorName === 'connectorName'); + await waitFor(() => result.current.authenticationFlows[0].connectorName === 'connectorName'); await act(async () => { - await result.current.process?.cancel(); + await result.current.process?.('connectorId')?.cancel(); }); expect(executor).not.toHaveBeenCalled(); expect(processResult).toEqual(null); - expect(result.current.process).toBeNull(); + expect(result.current.authenticationFlows.length).toBe(0); }); }); From ed7219f7684a6fe4340090408a8b03bbe133eb15 Mon Sep 17 00:00:00 2001 From: Alexandra Bri Date: Fri, 1 Nov 2024 15:55:34 +0200 Subject: [PATCH 05/14] test Datasource component --- .../components/dataSource/DataSource.test.tsx | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/tests/components/dataSource/DataSource.test.tsx diff --git a/src/tests/components/dataSource/DataSource.test.tsx b/src/tests/components/dataSource/DataSource.test.tsx new file mode 100644 index 00000000..87d0667d --- /dev/null +++ b/src/tests/components/dataSource/DataSource.test.tsx @@ -0,0 +1,47 @@ +import { screen, render } from '@testing-library/react'; +import { UiThemeProvider } from '@chili-publish/grafx-shared-components'; +import DataSource from '../../../components/dataSource/DataSource'; + +describe('DataSource test', () => { + it('Should display data connector first row', async () => { + window.StudioUISDK.dataConnector.getPage = jest.fn().mockResolvedValueOnce({ + parsedData: { data: [{ id: '1', name: 'Joe', age: 15 }] }, + }); + window.StudioUISDK.connector.getAllByType = jest.fn().mockResolvedValueOnce({ + parsedData: [ + { + id: '1', + name: 'Connector name', + }, + ], + }); + + render( + + + , + ); + + expect(await screen.findByDisplayValue('1|Joe|15')).toBeInTheDocument(); + }); + + it('Should display data connector first row', async () => { + window.StudioUISDK.dataConnector.getPage = jest.fn().mockRejectedValueOnce({}); + window.StudioUISDK.connector.getAllByType = jest.fn().mockResolvedValueOnce({ + parsedData: [ + { + id: '1', + name: 'Connector name', + }, + ], + }); + + render( + + + , + ); + + expect(await screen.findByPlaceholderText('Select data row')).toBeInTheDocument(); + }); +}); From 8ab225e36b046df84c98f8d4ba6846a99091cd04 Mon Sep 17 00:00:00 2001 From: Alexandra Bri Date: Fri, 1 Nov 2024 16:02:33 +0200 Subject: [PATCH 06/14] revert bootstrap --- src/_dev-execution/bootstrap.ts | 122 +------------------------------- 1 file changed, 3 insertions(+), 119 deletions(-) diff --git a/src/_dev-execution/bootstrap.ts b/src/_dev-execution/bootstrap.ts index 76e90fc5..9bd01ab3 100644 --- a/src/_dev-execution/bootstrap.ts +++ b/src/_dev-execution/bootstrap.ts @@ -1,14 +1,11 @@ // This is an entry point when running standalone version of studio workspace in dev mode // It's not going to be bundled to the main `bundle.js` file -import axios from 'axios'; import { TokenManager } from './token-manager'; -import { Authentified, ConnectorAuthenticationResult } from '../types/ConnectorAuthenticationResult'; (async () => { const tokenManager = new TokenManager(); - let popup: any; - let promiseExecutor: any; + let urlParams = new URLSearchParams(window.location.search); // state after redirection @@ -67,129 +64,16 @@ import { Authentified, ConnectorAuthenticationResult } from '../types/ConnectorA } else { authToken = await tokenManager.getAccessToken(); } - - const handler = function (e: MessageEvent) { - // We're processing only events from redirect origin - if (e.origin && baseUrl.startsWith(e.origin)) { - try { - const result = JSON.parse(e.data); - if (result.type === 'AuthorizationComplete') { - popup?.close(); - promiseExecutor?.res({ - type: 'authentified', - }); - } else if (result.type === 'AuthorizationFailed') { - popup?.close(); - promiseExecutor?.res({ - type: 'error', - error: result.reason, - }); - } else { - promiseExecutor?.rej(`Something went wrong: ${JSON.stringify(result)}`); - } - } catch (error) { - promiseExecutor?.rej(`Something went wrong: ${error}`); - } - } - }; - - window.addEventListener('message', handler); - - const onProjectInfoRequested = async () => { - return { - id: '', - name: '', - template: { - id: '', - }, - }; - }; - - const buildRedirectUri = (baseUrl: string, connectorId: string) => { - return `${baseUrl}/connectors/${connectorId}/auth/oauth-authorization-code/redirect`; - }; - - const buildAuthentinticationUrl = ({ baseUrl, client_id, scope, redirect_uri, state }: any) => { - const url = new URL(baseUrl); - const query = new URLSearchParams({ - // eslint-disable-next-line camelcase - client_id, - // eslint-disable-next-line camelcase - redirect_uri, - scope, - // eslint-disable-next-line camelcase - response_type: 'code', - state, - }); - // eslint-disable-next-line no-restricted-syntax - for (const [paramKey, paramValue] of query) { - url.searchParams.append(paramKey, paramValue); - } - return url.toString(); - }; - - const runAuthentication = async (connectorId: string) => { - const connectorsAuthInfoRes = await axios.get( - `https://cp-qeb-191.cpstaging.online/grafx/api/v1/environment/cp-qeb-191/connectors/${connectorId}/auth/oauth-authorization-code`, - { - headers: { Authorization: `Bearer ${authToken}` }, - }, - ); - const userId = 'samlp|chili-publish-dev|5zPH9tpDkfYakh5ym0w-0DHe-mKIB8CiTYyqcnbAGEI'; - const subscriptionGuid = '57718ff6-81c8-4e9e-bbe8-3c4ec86cf184'; - const connectorsAuthInfo = connectorsAuthInfoRes.data; - const authorizationUrl = buildAuthentinticationUrl({ - baseUrl: connectorsAuthInfo.authorizationEndpoint, - client_id: connectorsAuthInfo.clientId, - redirect_uri: buildRedirectUri(baseUrl, connectorId), - scope: connectorsAuthInfo.scope, - state: window.btoa(JSON.stringify({ userId, subscriptionId: subscriptionGuid })), - }); - popup = window.open(authorizationUrl); - /*return { - type: 'authentified', - } as Authentified;*/ - }; - - const authenticate = (connectorId: string) => { - return Promise.race([ - new Promise((res, rej) => { - promiseExecutor = { res, rej }; - runAuthentication(connectorId); - }), - new Promise((res) => { - setTimeout(() => { - res({ - type: 'timeout', - }); - }, 60 * 1000); - }), - ]).finally(() => { - // resetParams(); - popup = null; - }); - }; - window.StudioUI.studioUILoaderConfig({ selector: 'sui-root', projectId, projectName: 'Dev Run', graFxStudioEnvironmentApiBaseUrl: `${baseUrl}`, authToken, - sandboxMode: true, - uiTheme: 'dark', editorLink: `https://stgrafxstudiodevpublic.blob.core.windows.net/editor/${engineSource}/web`, refreshTokenAction: () => tokenManager.refreshToken(), - onConnectorAuthenticationRequested: authenticate, - onProjectDocumentRequested: async () => { - const doc = await axios.get( - `https://cp-qeb-191.cpstaging.online/grafx/api/v1/environment/cp-qeb-191/templates/${projectId}/download`, - { - headers: { Authorization: `Bearer ${authToken}` }, - }, - ); - return doc.data || ''; + onConnectorAuthenticationRequested: (connectorId) => { + return Promise.reject(new Error(`Authorization failed for ${connectorId}`)); }, - onProjectInfoRequested, }); })(); From 02489988e5c0be8eff42670db04da44a6c4322b1 Mon Sep 17 00:00:00 2001 From: Alexandra Bri Date: Fri, 1 Nov 2024 17:34:48 +0200 Subject: [PATCH 07/14] fix notifications --- src/MainContent.tsx | 28 +------- .../connector-authentication/types.ts | 3 + .../useConnectorAuthentication.ts | 22 +++--- .../useConnectorAuthenticationResult.ts | 32 +++++---- .../useConnectorAuthentication.test.ts | 67 +++++++++++++------ 5 files changed, 80 insertions(+), 72 deletions(-) create mode 100644 src/components/connector-authentication/types.ts diff --git a/src/MainContent.tsx b/src/MainContent.tsx index 31c13803..e15649cc 100644 --- a/src/MainContent.tsx +++ b/src/MainContent.tsx @@ -14,12 +14,7 @@ import packageInfo from '../package.json'; import './App.css'; import { CanvasContainer, Container, MainContentContainer } from './App.styles'; import AnimationTimeline from './components/animationTimeline/AnimationTimeline'; -import { - ConnectorAuthenticationFlow, - ConnectorAuthenticationModal, - useConnectorAuthentication, - useConnectorAuthenticationResult, -} from './components/connector-authentication'; +import { ConnectorAuthenticationModal, useConnectorAuthentication } from './components/connector-authentication'; import LeftPanel from './components/layout-panels/leftPanel/LeftPanel'; import { useSubscriberContext } from './contexts/Subscriber'; import { UiConfigContextProvider } from './contexts/UiConfigContext'; @@ -44,23 +39,6 @@ interface MainContentProps { updateToken: (newValue: string) => void; } -interface AuthenticationFlowModalProps { - authenticationFlow: ConnectorAuthenticationFlow; - onConfirm: () => void; - onCancel: () => void; -} -function AuthenticationFlowModal({ authenticationFlow, onConfirm, onCancel }: AuthenticationFlowModalProps) { - useConnectorAuthenticationResult(authenticationFlow.connectorName, authenticationFlow.result); - - return ( - - ); -} - function MainContent({ projectConfig, authToken, updateToken: setAuthToken }: MainContentProps) { const [fetchedDocument, setFetchedDocument] = useState(''); const [variables, setVariables] = useState([]); @@ -361,9 +339,9 @@ function MainContent({ projectConfig, authToken, updateToken: setAuthToken }: Ma {authenticationFlows.length && authenticationFlows.map((authFlow) => ( - connectorAuthenticationProcess(authFlow.connectorId)?.start()} onCancel={() => connectorAuthenticationProcess(authFlow.connectorId)?.cancel()} /> diff --git a/src/components/connector-authentication/types.ts b/src/components/connector-authentication/types.ts new file mode 100644 index 00000000..33f8b51a --- /dev/null +++ b/src/components/connector-authentication/types.ts @@ -0,0 +1,3 @@ +import { ConnectorAuthenticationResult } from 'src/types/ConnectorAuthenticationResult'; + +export type ConnectorAuthResult = { result: ConnectorAuthenticationResult | null; connectorName: string }; diff --git a/src/components/connector-authentication/useConnectorAuthentication.ts b/src/components/connector-authentication/useConnectorAuthentication.ts index 41ffe24c..1fe85745 100644 --- a/src/components/connector-authentication/useConnectorAuthentication.ts +++ b/src/components/connector-authentication/useConnectorAuthentication.ts @@ -1,6 +1,7 @@ import { RefreshedAuthCredendentials } from '@chili-publish/studio-sdk'; import { useCallback, useState } from 'react'; import { ConnectorAuthenticationResult } from '../../types/ConnectorAuthenticationResult'; +import { useConnectorAuthenticationResult } from './useConnectorAuthenticationResult'; interface Executor { handler: () => Promise; @@ -11,18 +12,14 @@ export type ConnectorAuthenticationFlow = { connectorName: string; authenticationResolvers: Omit, 'promise'> | null; executor: Executor | null; - result: ConnectorAuthenticationResult | null; }; export const useConnectorAuthentication = () => { const [authenticationFlows, setAuthenticationFlows] = useState([]); + const { showAuthNotification } = useConnectorAuthenticationResult(); const resetProcess = useCallback((id: string) => { - setAuthenticationFlows((prev) => - prev.map((item) => - item.connectorId === id ? { ...item, authenticationResolvers: null, executor: null } : item, - ), - ); + setAuthenticationFlows((prev) => prev.filter((item) => item.connectorId !== id)); }, []); const process = useCallback( @@ -34,14 +31,12 @@ export const useConnectorAuthentication = () => { return { __resolvers: authenticationProcess.authenticationResolvers, async start() { - setAuthenticationFlows((prev) => - prev.map((item) => (item.connectorId === id ? { ...item, result: null } : item)), - ); try { const executorResult = await authenticationProcess.executor?.handler().then((res) => { - setAuthenticationFlows((prev) => - prev.map((item) => (item.connectorId === id ? { ...item, result: res } : item)), - ); + showAuthNotification({ + result: res, + connectorName: authenticationProcess.connectorName, + }); if (res.type === 'authentified') { return new RefreshedAuthCredendentials(); @@ -64,14 +59,13 @@ export const useConnectorAuthentication = () => { }, async cancel() { this.__resolvers.resolve(null); - setAuthenticationFlows((prev) => prev.filter((item) => item.connectorId !== id)); resetProcess(id); }, }; } return null; }, - [authenticationFlows, resetProcess], + [authenticationFlows, resetProcess, showAuthNotification], ); const createProcess = async (authorizationExecutor: Executor['handler'], name: string, id: string) => { diff --git a/src/components/connector-authentication/useConnectorAuthenticationResult.ts b/src/components/connector-authentication/useConnectorAuthenticationResult.ts index d6e4c35a..5f3a8951 100644 --- a/src/components/connector-authentication/useConnectorAuthenticationResult.ts +++ b/src/components/connector-authentication/useConnectorAuthenticationResult.ts @@ -1,7 +1,7 @@ import { ToastVariant } from '@chili-publish/grafx-shared-components'; -import { useEffect } from 'react'; +import { useCallback } from 'react'; import { useNotificationManager } from '../../contexts/NotificantionManager/NotificationManagerContext'; -import { ConnectorAuthenticationResult } from '../../types/ConnectorAuthenticationResult'; +import { ConnectorAuthResult } from './types'; const authorizationFailedToast = (connectorName: string) => ({ id: 'connector-authorization-failed', @@ -15,17 +15,23 @@ const authorizationFailedTimeoutToast = (connectorName: string) => ({ type: ToastVariant.NEGATIVE, }); -export const useConnectorAuthenticationResult = ( - connectorName: string, - result: ConnectorAuthenticationResult | null, -) => { +export const useConnectorAuthenticationResult = () => { const { addNotification } = useNotificationManager(); - useEffect(() => { - if (result?.type === 'error') { - addNotification(authorizationFailedToast(connectorName)); - } else if (result?.type === 'timeout') { - addNotification(authorizationFailedTimeoutToast(connectorName)); - } - }, [result, connectorName, addNotification]); + const showAuthNotification = useCallback( + (authResult: ConnectorAuthResult) => { + if (!authResult) return; + + if (authResult.result?.type === 'error') { + addNotification(authorizationFailedToast(authResult.connectorName)); + } else if (authResult.result?.type === 'timeout') { + addNotification(authorizationFailedTimeoutToast(authResult.connectorName)); + } + }, + [addNotification], + ); + + return { + showAuthNotification, + }; }; diff --git a/src/tests/components/connector-authentication/useConnectorAuthentication.test.ts b/src/tests/components/connector-authentication/useConnectorAuthentication.test.ts index c2c04ac5..23684ab2 100644 --- a/src/tests/components/connector-authentication/useConnectorAuthentication.test.ts +++ b/src/tests/components/connector-authentication/useConnectorAuthentication.test.ts @@ -2,6 +2,7 @@ import { RefreshedAuthCredendentials } from '@chili-publish/studio-sdk'; import { renderHook, waitFor } from '@testing-library/react'; import { act } from 'react-dom/test-utils'; import { useConnectorAuthentication } from '../../../components/connector-authentication'; +import * as ConnectorAuthResultHook from '../../../components/connector-authentication/useConnectorAuthenticationResult'; describe('useConnectorAuthentication hook', () => { beforeEach(() => { @@ -41,6 +42,11 @@ describe('useConnectorAuthentication hook', () => { }); it('should perform process correclty for different connectors', async () => { + const showAuthNotificationFn = jest.fn(); + jest.spyOn(ConnectorAuthResultHook, 'useConnectorAuthenticationResult').mockImplementation(() => ({ + showAuthNotification: showAuthNotificationFn, + })); + const executor1 = jest.fn().mockResolvedValueOnce({ type: 'error' }); const executor2 = jest.fn().mockResolvedValueOnce({ type: 'authentified' }); @@ -81,19 +87,26 @@ describe('useConnectorAuthentication hook', () => { await result.current.process?.('connectorId1')?.start(); }); - expect(result.current.authenticationFlows.length).toEqual(2); + expect(result.current.authenticationFlows.length).toEqual(0); expect(processResultConnector1).toEqual(null); - expect(result.current.authenticationFlows[0].authenticationResolvers).toBeNull(); - expect(result.current.authenticationFlows[0].executor).toBeNull(); - - expect(result.current.authenticationFlows[1].result).toEqual({ type: 'authentified' }); + expect(showAuthNotificationFn).toHaveBeenCalledWith({ + connectorName: 'connectorName1', + result: { type: 'error' }, + }); expect(processResultConnector2).toEqual(new RefreshedAuthCredendentials()); - expect(result.current.authenticationFlows[1].authenticationResolvers).toBeNull(); - expect(result.current.authenticationFlows[1].executor).toBeNull(); + expect(showAuthNotificationFn).toHaveBeenCalledWith({ + connectorName: 'connectorName2', + result: { type: 'authentified' }, + }); }); it('should perform process correclty for none-authentified type', async () => { + const showAuthNotificationFn = jest.fn(); + jest.spyOn(ConnectorAuthResultHook, 'useConnectorAuthenticationResult').mockImplementation(() => ({ + showAuthNotification: showAuthNotificationFn, + })); + const executor = jest.fn().mockResolvedValueOnce({ type: 'authentified' }); const { result } = renderHook(() => useConnectorAuthentication()); @@ -115,14 +128,20 @@ describe('useConnectorAuthentication hook', () => { await result.current.process?.('connectorId')?.start(); }); - expect(result.current.authenticationFlows.length).toEqual(1); - expect(result.current.authenticationFlows[0].result).toEqual({ type: 'authentified' }); expect(processResult).toEqual(new RefreshedAuthCredendentials()); - expect(result.current.authenticationFlows[0].authenticationResolvers).toBeNull(); - expect(result.current.authenticationFlows[0].executor).toBeNull(); + expect(result.current.authenticationFlows.length).toBe(0); + expect(showAuthNotificationFn).toHaveBeenCalledWith({ + connectorName: 'connectorName', + result: { type: 'authentified' }, + }); }); it('should perform process correclty when process return error state', async () => { + const showAuthNotificationFn = jest.fn(); + jest.spyOn(ConnectorAuthResultHook, 'useConnectorAuthenticationResult').mockImplementation(() => ({ + showAuthNotification: showAuthNotificationFn, + })); + const executor = jest.fn().mockResolvedValue({ type: 'error', error: '[Error]: Occured' }); const { result } = renderHook(() => useConnectorAuthentication()); @@ -145,13 +164,20 @@ describe('useConnectorAuthentication hook', () => { }); expect(processResult).toEqual(null); - expect(result.current.authenticationFlows[0].result).toEqual({ type: 'error', error: '[Error]: Occured' }); - expect(result.current.authenticationFlows[0].authenticationResolvers).toBeNull(); - expect(result.current.authenticationFlows[0].executor).toBeNull(); + expect(result.current.authenticationFlows.length).toBe(0); expect(window.console.error).toHaveBeenCalledWith('[Error]: Occured'); + expect(showAuthNotificationFn).toHaveBeenCalledWith({ + connectorName: 'connectorName', + result: { type: 'error', error: '[Error]: Occured' }, + }); }); it('should perform process correclty when process return timeout', async () => { + const showAuthNotificationFn = jest.fn(); + jest.spyOn(ConnectorAuthResultHook, 'useConnectorAuthenticationResult').mockImplementation(() => ({ + showAuthNotification: showAuthNotificationFn, + })); + const executor = jest.fn().mockResolvedValue({ type: 'timeout' }); const { result } = renderHook(() => useConnectorAuthentication()); @@ -174,10 +200,13 @@ describe('useConnectorAuthentication hook', () => { }); expect(processResult).toEqual(null); - expect(result.current.authenticationFlows[0].result).toEqual({ type: 'timeout' }); - expect(result.current.authenticationFlows[0].authenticationResolvers).toBeNull(); - expect(result.current.authenticationFlows[0].executor).toBeNull(); + expect(result.current.authenticationFlows.length).toBe(0); expect(window.console.error).not.toHaveBeenCalled(); + + expect(showAuthNotificationFn).toHaveBeenCalledWith({ + connectorName: 'connectorName', + result: { type: 'timeout' }, + }); }); it('should perform process correclty when process rejected', async () => { @@ -203,9 +232,7 @@ describe('useConnectorAuthentication hook', () => { }); expect(processResult).toEqual({ message: 'TestError' }); - expect(result.current.authenticationFlows[0].result).toBeNull(); - expect(result.current.authenticationFlows[0].authenticationResolvers).toBeNull(); - expect(result.current.authenticationFlows[0].executor).toBeNull(); + expect(result.current.authenticationFlows.length).toBe(0); }); it('should perform process correclty when canceling', async () => { From 38a0e259357f0a41a5c7bc5fd0364d1135975c6d Mon Sep 17 00:00:00 2001 From: Alexandra Bri Date: Fri, 1 Nov 2024 17:56:39 +0200 Subject: [PATCH 08/14] implement loading icon for data source row --- src/components/dataSource/DataSource.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/dataSource/DataSource.tsx b/src/components/dataSource/DataSource.tsx index 74780a3f..9263479a 100644 --- a/src/components/dataSource/DataSource.tsx +++ b/src/components/dataSource/DataSource.tsx @@ -1,4 +1,4 @@ -import { AvailableIcons, Icon, Input, Label, useTheme } from '@chili-publish/grafx-shared-components'; +import { AvailableIcons, Icon, Input, Label, LoadingIcon, useTheme } from '@chili-publish/grafx-shared-components'; import { useCallback, useEffect, useState } from 'react'; import { ConnectorInstance, ConnectorType } from '@chili-publish/studio-sdk'; import { PanelTitle } from '../shared/Panel.styles'; @@ -8,9 +8,11 @@ function DataSource() { const { panel } = useTheme(); const [dataConnector, setDataConnector] = useState(); const [firstRowInfo, setFirstRowInfo] = useState(''); + const [isLoading, setIsLoading] = useState(false); const getDataConnectorFirstRow = useCallback(async () => { if (!dataConnector) return; + setIsLoading(true); try { const pageInfoResponse = await window.StudioUISDK.dataConnector.getPage(dataConnector.id, { limit: 1 }); @@ -19,6 +21,8 @@ function DataSource() { } catch (error) { // eslint-disable-next-line no-console console.error(error); + } finally { + setIsLoading(false); } }, [dataConnector]); @@ -41,6 +45,7 @@ function DataSource() { + ) : ( Date: Mon, 4 Nov 2024 12:18:40 +0200 Subject: [PATCH 09/14] add studio data source feature flag --- src/App.tsx | 9 ++++- src/MainContent.tsx | 8 +---- src/components/dataSource/DataSource.tsx | 3 +- .../layout-panels/leftPanel/LeftPanel.tsx | 7 ++-- src/contexts/FeatureFlagProvider.tsx | 35 +++++++++++++++++++ src/main.tsx | 8 +++++ src/tests/LeftPanel.test.tsx | 12 +++---- src/types/types.ts | 5 +++ 8 files changed, 68 insertions(+), 19 deletions(-) create mode 100644 src/contexts/FeatureFlagProvider.tsx diff --git a/src/App.tsx b/src/App.tsx index abd29326..b011eae5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import MainContent from './MainContent'; import { ProjectConfig } from './types/types'; import { Subscriber } from './utils/subscriber'; import ShortcutProvider from './contexts/ShortcutManager/ShortcutProvider'; +import FeatureFlagProvider from './contexts/FeatureFlagProvider'; function App({ projectConfig }: { projectConfig: ProjectConfig }) { const [authToken, setAuthToken] = useState(projectConfig.onAuthenticationRequested()); @@ -83,7 +84,13 @@ function App({ projectConfig }: { projectConfig: ProjectConfig }) { - + + + diff --git a/src/MainContent.tsx b/src/MainContent.tsx index e15649cc..fed6a12e 100644 --- a/src/MainContent.tsx +++ b/src/MainContent.tsx @@ -310,13 +310,7 @@ function MainContent({ projectConfig, authToken, updateToken: setAuthToken }: Ma )} - {!isMobileSize && ( - - )} + {!isMobileSize && } {isMobileSize && ( diff --git a/src/components/dataSource/DataSource.tsx b/src/components/dataSource/DataSource.tsx index 9263479a..be1d6918 100644 --- a/src/components/dataSource/DataSource.tsx +++ b/src/components/dataSource/DataSource.tsx @@ -14,8 +14,7 @@ function DataSource() { if (!dataConnector) return; setIsLoading(true); try { - const pageInfoResponse = await window.StudioUISDK.dataConnector.getPage(dataConnector.id, { limit: 1 }); - + const pageInfoResponse = await window.StudioUISDK.dataConnector.getPage(dataConnector.id, { limit: 15 }); const firstRowData = pageInfoResponse.parsedData?.data?.[0]; setFirstRowInfo(firstRowData ? Object.values(firstRowData).join('|') : ''); } catch (error) { diff --git a/src/components/layout-panels/leftPanel/LeftPanel.tsx b/src/components/layout-panels/leftPanel/LeftPanel.tsx index 090e95a5..93c7fca3 100644 --- a/src/components/layout-panels/leftPanel/LeftPanel.tsx +++ b/src/components/layout-panels/leftPanel/LeftPanel.tsx @@ -6,16 +6,17 @@ import VariablesList from '../../variables/VariablesList'; import { useVariablePanelContext } from '../../../contexts/VariablePanelContext'; import { ContentType } from '../../../contexts/VariablePanelContext.types'; import DataSource from '../../dataSource/DataSource'; +import { useFeatureFlagContext } from '../../../contexts/FeatureFlagProvider'; interface LeftPanelProps { variables: Variable[]; - isSandboxMode: boolean; isDocumentLoaded: boolean; } -function LeftPanel({ variables, isSandboxMode, isDocumentLoaded }: LeftPanelProps) { +function LeftPanel({ variables, isDocumentLoaded }: LeftPanelProps) { const { contentType } = useVariablePanelContext(); const { panel } = useTheme(); + const { featureFlags } = useFeatureFlagContext(); return ( diff --git a/src/contexts/FeatureFlagProvider.tsx b/src/contexts/FeatureFlagProvider.tsx new file mode 100644 index 00000000..6ea61019 --- /dev/null +++ b/src/contexts/FeatureFlagProvider.tsx @@ -0,0 +1,35 @@ +import { createContext, useContext, useMemo } from 'react'; +import { FeatureFlagsType } from 'src/types/types'; + +interface IFeatureFlagContext { + featureFlags?: FeatureFlagsType; +} + +export const FeatureFlagContextDefaultValues: IFeatureFlagContext = { + featureFlags: {}, +}; + +export const FeatureFlagContext = createContext(FeatureFlagContextDefaultValues); + +export const useFeatureFlagContext = () => { + return useContext(FeatureFlagContext); +}; + +function FeatureFlagProvider({ + featureFlags, + children, +}: { + featureFlags?: FeatureFlagsType; + children: React.ReactNode; +}) { + const data = useMemo( + () => ({ + featureFlags, + }), + [featureFlags], + ); + + return {children}; +} + +export default FeatureFlagProvider; diff --git a/src/main.tsx b/src/main.tsx index 9164184d..395dd578 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -100,6 +100,7 @@ export default class StudioUI { uiTheme: ITheme['mode'] | 'system', outputSettings: OutputSettings, sandboxMode: boolean, + featureFlags: Record | undefined, onSandboxModeToggle: (() => void) | undefined, onProjectInfoRequested: (projectId: string) => Promise, onProjectDocumentRequested: (projectId: string) => Promise, @@ -130,6 +131,7 @@ export default class StudioUI { outputSettings, graFxStudioEnvironmentApiBaseUrl, sandboxMode, + featureFlags, onSandboxModeToggle, onProjectInfoRequested, onProjectDocumentRequested, @@ -181,6 +183,7 @@ export default class StudioUI { outputSettings, userInterfaceID, sandboxMode, + featureFlags, onSandboxModeToggle, refreshTokenAction, onConnectorAuthenticationRequested, @@ -207,6 +210,7 @@ export default class StudioUI { uiTheme ?? 'light', outputSettings ?? defaultOutputSettings, sandboxMode || false, + featureFlags, onSandboxModeToggle, projectLoader.onProjectInfoRequested, projectLoader.onProjectDocumentRequested, @@ -252,6 +256,7 @@ export default class StudioUI { graFxStudioEnvironmentApiBaseUrl, authToken, sandboxMode, + featureFlags, onSandboxModeToggle, refreshTokenAction, onProjectInfoRequested, @@ -281,6 +286,7 @@ export default class StudioUI { uiTheme || 'light', outputSettings || defaultOutputSettings, sandboxMode || false, + featureFlags, onSandboxModeToggle, onProjectInfoRequested, onProjectDocumentRequested, @@ -330,6 +336,7 @@ export default class StudioUI { outputSettings, userInterfaceID, sandboxMode, + featureFlags, onSandboxModeToggle, refreshTokenAction, onProjectInfoRequested, @@ -363,6 +370,7 @@ export default class StudioUI { uiTheme || 'light', outputSettings ?? defaultOutputSettings, sandboxMode || false, + featureFlags, onSandboxModeToggle, onProjectInfoRequested ?? projectLoader.onProjectInfoRequested, onProjectDocumentRequested ?? projectLoader.onProjectDocumentRequested, diff --git a/src/tests/LeftPanel.test.tsx b/src/tests/LeftPanel.test.tsx index f18afb58..5347ef13 100644 --- a/src/tests/LeftPanel.test.tsx +++ b/src/tests/LeftPanel.test.tsx @@ -117,7 +117,7 @@ describe('Image Panel', () => { const { getByText, getByRole } = render( - + , { container: document.body.appendChild(APP_WRAPPER) }, @@ -143,7 +143,7 @@ describe('Image Panel', () => { const { getByText, getByTestId, getAllByText } = render( - + , { container: document.body.appendChild(APP_WRAPPER) }, @@ -169,7 +169,7 @@ describe('Image Panel', () => { const { getByText } = render( - + , { container: document.body.appendChild(APP_WRAPPER) }, @@ -192,7 +192,7 @@ describe('Image Panel', () => { const { getByRole } = render( - + , ); @@ -230,7 +230,7 @@ describe('Image Panel', () => { render( - + , { container: document.body.appendChild(APP_WRAPPER) }, @@ -255,7 +255,7 @@ describe('Image Panel', () => { const { getByTestId } = render( - + , { container: document.body.appendChild(APP_WRAPPER) }, diff --git a/src/types/types.ts b/src/types/types.ts index 33abae4c..7f3328ee 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -3,6 +3,8 @@ import { AxiosError, AxiosResponse } from 'axios'; import { ITheme } from '@chili-publish/grafx-shared-components'; import { ConnectorAuthenticationResult } from './ConnectorAuthenticationResult'; +export type FeatureFlagsType = Record; + export interface ProjectConfig { projectId: string; projectName: string; @@ -12,6 +14,7 @@ export interface ProjectConfig { userInterfaceID?: string; graFxStudioEnvironmentApiBaseUrl: string; sandboxMode: boolean; + featureFlags?: FeatureFlagsType; onSandboxModeToggle?: () => void; onProjectInfoRequested: (projectId: string) => Promise; onProjectDocumentRequested: (projectId: string) => Promise; @@ -39,6 +42,7 @@ export interface DefaultStudioConfig { projectId: string; graFxStudioEnvironmentApiBaseUrl: string; authToken: string; + featureFlags?: FeatureFlagsType; uiOptions?: UiOptions; uiTheme?: ITheme['mode'] | 'system'; outputSettings?: OutputSettings; @@ -170,6 +174,7 @@ export interface IStudioUILoaderConfig { projectDownloadUrl?: string; projectUploadUrl?: string; sandboxMode?: boolean; + featureFlags?: Record; onSandboxModeToggle?: () => void; onProjectInfoRequested?: (projectId: string) => Promise; onProjectDocumentRequested?: (projectId: string) => Promise; From 14a243c8fac25d448bc1ac2eed79f7ceda726b29 Mon Sep 17 00:00:00 2001 From: Alexandra Bri Date: Mon, 4 Nov 2024 12:27:12 +0200 Subject: [PATCH 10/14] code improvements --- src/MainContent.tsx | 6 ++-- .../useConnectorAuthentication.ts | 12 ++++---- src/components/dataSource/DataSource.tsx | 6 ++-- .../useConnectorAuthentication.test.ts | 30 +++++++++---------- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/MainContent.tsx b/src/MainContent.tsx index fed6a12e..5802e2c8 100644 --- a/src/MainContent.tsx +++ b/src/MainContent.tsx @@ -74,7 +74,7 @@ function MainContent({ projectConfig, authToken, updateToken: setAuthToken }: Ma ); const { - authenticationFlows, + pendingAuthentication, process: connectorAuthenticationProcess, createProcess: createAuthenticationProcess, } = useConnectorAuthentication(); @@ -331,8 +331,8 @@ function MainContent({ projectConfig, authToken, updateToken: setAuthToken }: Ma ) : null} - {authenticationFlows.length && - authenticationFlows.map((authFlow) => ( + {pendingAuthentication.length && + pendingAuthentication.map((authFlow) => ( { - const [authenticationFlows, setAuthenticationFlows] = useState([]); + const [pendingAuthentication, setpendingAuthentication] = useState([]); const { showAuthNotification } = useConnectorAuthenticationResult(); const resetProcess = useCallback((id: string) => { - setAuthenticationFlows((prev) => prev.filter((item) => item.connectorId !== id)); + setpendingAuthentication((prev) => prev.filter((item) => item.connectorId !== id)); }, []); const process = useCallback( (id: string) => { - const authenticationProcess = authenticationFlows.find((item) => item.connectorId === id); + const authenticationProcess = pendingAuthentication.find((item) => item.connectorId === id); if (!authenticationProcess) return null; if (authenticationProcess.authenticationResolvers && authenticationProcess.executor) { @@ -65,13 +65,13 @@ export const useConnectorAuthentication = () => { } return null; }, - [authenticationFlows, resetProcess, showAuthNotification], + [pendingAuthentication, resetProcess, showAuthNotification], ); const createProcess = async (authorizationExecutor: Executor['handler'], name: string, id: string) => { const authenticationAwaiter = Promise.withResolvers(); - setAuthenticationFlows((prev) => [ + setpendingAuthentication((prev) => [ ...prev, { executor: { @@ -91,7 +91,7 @@ export const useConnectorAuthentication = () => { }; return { - authenticationFlows, + pendingAuthentication, createProcess, process, }; diff --git a/src/components/dataSource/DataSource.tsx b/src/components/dataSource/DataSource.tsx index be1d6918..f2660070 100644 --- a/src/components/dataSource/DataSource.tsx +++ b/src/components/dataSource/DataSource.tsx @@ -7,7 +7,7 @@ import { getDataIdForSUI, getDataTestIdForSUI } from '../../utils/dataIds'; function DataSource() { const { panel } = useTheme(); const [dataConnector, setDataConnector] = useState(); - const [firstRowInfo, setFirstRowInfo] = useState(''); + const [currentRow, setCurrentRow] = useState(''); const [isLoading, setIsLoading] = useState(false); const getDataConnectorFirstRow = useCallback(async () => { @@ -16,7 +16,7 @@ function DataSource() { try { const pageInfoResponse = await window.StudioUISDK.dataConnector.getPage(dataConnector.id, { limit: 15 }); const firstRowData = pageInfoResponse.parsedData?.data?.[0]; - setFirstRowInfo(firstRowData ? Object.values(firstRowData).join('|') : ''); + setCurrentRow(firstRowData ? Object.values(firstRowData).join('|') : ''); } catch (error) { // eslint-disable-next-line no-console console.error(error); @@ -49,7 +49,7 @@ function DataSource() { dataTestId={getDataTestIdForSUI(`data-source-input`)} dataIntercomId="data-source-input" name="data-source-input" - value={firstRowInfo} + value={currentRow} placeholder="Select data row" label={