Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[Feature] [WRS-2078] Implement data connector authorization #218

Merged
merged 17 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
"validate-versions": "node validate_versions.cjs"
},
"dependencies": {
"@chili-publish/grafx-shared-components": "^0.83.2",
"@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",
Expand Down
9 changes: 8 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -83,7 +84,13 @@ function App({ projectConfig }: { projectConfig: ProjectConfig }) {
<UiThemeProvider theme="platform" mode={uiThemeMode}>
<NotificationManagerProvider>
<ShortcutProvider projectConfig={projectConfig}>
<MainContent authToken={authToken} updateToken={setAuthToken} projectConfig={projectConfig} />
<FeatureFlagProvider featureFlags={projectConfig.featureFlags}>
<MainContent
authToken={authToken}
updateToken={setAuthToken}
projectConfig={projectConfig}
/>
</FeatureFlagProvider>
</ShortcutProvider>
</NotificationManagerProvider>
</UiThemeProvider>
Expand Down
41 changes: 20 additions & 21 deletions src/MainContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,7 @@ import packageInfo from '../package.json';
import './App.css';
import { CanvasContainer, Container, MainContentContainer } from './App.styles';
import AnimationTimeline from './components/animationTimeline/AnimationTimeline';
import {
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';
Expand Down Expand Up @@ -78,14 +74,11 @@ function MainContent({ projectConfig, authToken, updateToken: setAuthToken }: Ma
);

const {
result: connectorAuthResult,
pendingAuthentication,
process: connectorAuthenticationProcess,
createProcess: createAuthenticationProcess,
connectorName,
} = useConnectorAuthentication();

useConnectorAuthenticationResult(connectorAuthResult);

useEffect(() => {
projectConfig
.onProjectInfoRequested(projectConfig.projectId)
Expand Down Expand Up @@ -143,11 +136,15 @@ 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 ?? '');
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,
);
return result;
}
} catch (error) {
Expand Down Expand Up @@ -334,13 +331,15 @@ function MainContent({ projectConfig, authToken, updateToken: setAuthToken }: Ma
) : null}
</CanvasContainer>
</MainContentContainer>
{connectorAuthenticationProcess && (
<ConnectorAuthenticationModal
name={connectorName}
onConfirm={() => connectorAuthenticationProcess.start()}
onCancel={() => connectorAuthenticationProcess.cancel()}
/>
)}
{pendingAuthentication.length &&
pendingAuthentication.map((authFlow) => (
<ConnectorAuthenticationModal
key={authFlow.connectorId}
name={authFlow.connectorName}
onConfirm={() => connectorAuthenticationProcess(authFlow.connectorId)?.start()}
onCancel={() => connectorAuthenticationProcess(authFlow.connectorId)?.cancel()}
/>
))}
</div>
</VariablePanelContextProvider>
</UiConfigContextProvider>
Expand Down
3 changes: 3 additions & 0 deletions src/components/connector-authentication/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { ConnectorAuthenticationResult } from 'src/types/ConnectorAuthenticationResult';

export type ConnectorAuthResult = { result: ConnectorAuthenticationResult | null; connectorName: string };
Original file line number Diff line number Diff line change
@@ -1,81 +1,98 @@
import { RefreshedAuthCredendentials } from '@chili-publish/studio-sdk';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useState } from 'react';
import { ConnectorAuthenticationResult } from '../../types/ConnectorAuthenticationResult';
import { useConnectorAuthenticationResult } from './useConnectorAuthenticationResult';

interface Executor {
handler: () => Promise<ConnectorAuthenticationResult>;
}

export type ConnectorAuthenticationFlow = {
alexandraFlavia9 marked this conversation as resolved.
Show resolved Hide resolved
alexandraFlavia9 marked this conversation as resolved.
Show resolved Hide resolved
connectorId: string;
connectorName: string;
authenticationResolvers: Omit<PromiseWithResolvers<RefreshedAuthCredendentials | null>, 'promise'> | null;
executor: Executor | null;
};

export const useConnectorAuthentication = () => {
const [authenticationResolvers, setAuthenticationResolvers] = useState<Omit<
// eslint-disable-next-line no-undef
PromiseWithResolvers<RefreshedAuthCredendentials | null>,
'promise'
> | null>(null);
const [executor, setExecutor] = useState<Executor | null>(null);
const [connectorName, setConnectorName] = useState<string>('');
const [result, setResult] = useState<ConnectorAuthenticationResult | null>(null);
const [pendingAuthentication, setpendingAuthentication] = useState<ConnectorAuthenticationFlow[]>([]);
alexandraFlavia9 marked this conversation as resolved.
Show resolved Hide resolved
const { showAuthNotification } = useConnectorAuthenticationResult();

const resetProcess = useCallback(() => {
setAuthenticationResolvers(null);
setExecutor(null);
const resetProcess = useCallback((id: string) => {
setpendingAuthentication((prev) => prev.filter((item) => item.connectorId !== id));
}, []);

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 process = useCallback(
(id: string) => {
const authenticationProcess = pendingAuthentication.find((item) => item.connectorId === id);
if (!authenticationProcess) return null;

if (authenticationProcess.authenticationResolvers && authenticationProcess.executor) {
return {
__resolvers: authenticationProcess.authenticationResolvers,
async start() {
try {
const executorResult = await authenticationProcess.executor?.handler().then((res) => {
showAuthNotification({
result: res,
connectorName: authenticationProcess.connectorName,
});

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 || null);
} catch (error) {
this.__resolvers.reject(error);
} finally {
resetProcess(id);
}
},
async cancel() {
this.__resolvers.resolve(null);
resetProcess(id);
},
};
}
return null;
},
[pendingAuthentication, resetProcess, showAuthNotification],
);

const createProcess = async (authorizationExecutor: Executor['handler'], name: string) => {
const createProcess = async (authorizationExecutor: Executor['handler'], name: string, id: string) => {
const authenticationAwaiter = Promise.withResolvers<RefreshedAuthCredendentials | null>();
setExecutor({
handler: authorizationExecutor,
});
setAuthenticationResolvers({
resolve: authenticationAwaiter.resolve,
reject: authenticationAwaiter.reject,
});
setConnectorName(name);

setpendingAuthentication((prev) => [
...prev,
{
executor: {
handler: authorizationExecutor,
},
authenticationResolvers: {
resolve: authenticationAwaiter.resolve,
reject: authenticationAwaiter.reject,
},
connectorName: name,
connectorId: id,
result: null,
alexandraFlavia9 marked this conversation as resolved.
Show resolved Hide resolved
},
]);
const promiseResult = await authenticationAwaiter.promise;
return promiseResult;
};

return {
pendingAuthentication,
createProcess,
process,
connectorName,
result,
};
};
Original file line number Diff line number Diff line change
@@ -1,28 +1,37 @@
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 = {
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 = () => {
const { addNotification } = useNotificationManager();

useEffect(() => {
if (result?.type === 'error') {
addNotification(authorizationFailedToast);
} else if (result?.type === 'timeout') {
addNotification(authorizationFailedTimeoutToast);
}
}, [result, 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,
};
};
74 changes: 74 additions & 0 deletions src/components/dataSource/DataSource.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
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';
import { getDataIdForSUI, getDataTestIdForSUI } from '../../utils/dataIds';

function DataSource() {
const { panel } = useTheme();
const [dataConnector, setDataConnector] = useState<ConnectorInstance | null>();
const [currentRow, setCurrentRow] = 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: 15 });
const firstRowData = pageInfoResponse.parsedData?.data?.[0];
setCurrentRow(firstRowData ? Object.values(firstRowData).join('|') : '');
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
} finally {
setIsLoading(false);
}
}, [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 ? (
<>
<PanelTitle panelTheme={panel}>Data source</PanelTitle>
<Input
type="text"
readOnly
disabled={isLoading}
dataId={getDataIdForSUI(`data-source-input`)}
dataTestId={getDataTestIdForSUI(`data-source-input`)}
dataIntercomId="data-source-input"
name="data-source-input"
value={currentRow}
placeholder="Select data row"
label={<Label translationKey="dataRow" value="Data row" />}
onClick={getDataConnectorFirstRow}
rightIcon={{
label: '',
icon: isLoading ? (
<LoadingIcon />
) : (
<Icon
dataId={getDataIdForSUI('data-source-input-icon')}
dataTestId={getDataTestIdForSUI('data-source-input-icon')}
icon={AvailableIcons.faTable}
/>
),
onClick: () => null,
}}
/>
</>
) : null;
}

export default DataSource;
Loading
Loading