diff --git a/package.json b/package.json index d8b554670..c09dbad66 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@equinor/amplify-components", - "version": "4.2.2", + "version": "3.9.1", "description": "Frontend Typescript components for the Amplify team", "main": "dist/amplify-components.cjs", "module": "dist/esm/index.js", diff --git a/src/api/core/OpenAPI.ts b/src/api/core/OpenAPI.ts index c856a7312..57f162db1 100644 --- a/src/api/core/OpenAPI.ts +++ b/src/api/core/OpenAPI.ts @@ -3,13 +3,26 @@ /* eslint-disable */ import type { ApiRequestOptions } from './ApiRequestOptions'; import { environment, auth } from 'src/utils'; +import { CancelablePromise, TokenService } from 'src/api'; +import { + getLocalStorage, + updateLocalStorage, +} from 'src/hooks/useLocalStorage'; +import { JwtPayload } from 'jwt-decode'; +import jwtDecode from 'jwt-decode'; -const { getApiUrl } = environment; +const { getApiUrl, getEnvironmentName } = environment; const { GRAPH_REQUESTS_BACKEND, acquireToken, msalApp } = auth; type Resolver = (options: ApiRequestOptions) => Promise; type Headers = Record; +const environmentName = getEnvironmentName( + import.meta.env.VITE_ENVIRONMENT_NAME +); +const noLocalhostEnvironmentName = + environmentName === 'localhost' ? 'development' : environmentName; + export type OpenAPIConfig = { BASE: string; VERSION: string; @@ -22,7 +35,7 @@ export type OpenAPIConfig = { ENCODE_PATH?: (path: string) => string; }; -const getToken = async () => { +const getApplicationToken = async () => { return ( await acquireToken( msalApp(environment.getClientId(import.meta.env.VITE_CLIENT_ID)), @@ -33,12 +46,69 @@ const getToken = async () => { ).accessToken; }; +const isJwtTokenExpired = (token: string) => { + const decodedToken: JwtPayload = jwtDecode(token); + const todayInSecUnix = new Date().getTime() / 1000; + return decodedToken.exp && todayInSecUnix < decodedToken.exp; +}; + +const getToken = async ( + localStorageKey: string, + tokenRequest: () => CancelablePromise +) => { + const localStorageToken = getLocalStorage(localStorageKey, ''); + if (localStorageToken.length !== 0 && isJwtTokenExpired(localStorageToken)) { + return localStorageToken; + } else { + const requestToken = await tokenRequest(); + updateLocalStorage(localStorageKey, requestToken); + return requestToken; + } +}; + +const getPortalToken = async () => { + return getToken( + `amplify-portal-${environmentName}`, + TokenService.getAmplifyPortalToken + ); +}; + +const getPortalProdToken = async () => { + return getToken( + `amplify-portal-production`, + TokenService.getAmplifyPortalProductionToken + ); +}; + export const OpenAPI: OpenAPIConfig = { BASE: getApiUrl(import.meta.env.VITE_API_URL), VERSION: '1.0', WITH_CREDENTIALS: false, CREDENTIALS: 'include', - TOKEN: getToken, + TOKEN: getApplicationToken, + USERNAME: undefined, + PASSWORD: undefined, + HEADERS: undefined, + ENCODE_PATH: undefined, +}; + +export const OpenAPI_Portal: OpenAPIConfig = { + BASE: `https://api-amplify-portal-${noLocalhostEnvironmentName}.radix.equinor.com`, + VERSION: '1.0', + WITH_CREDENTIALS: false, + CREDENTIALS: 'include', + TOKEN: getPortalToken, + USERNAME: undefined, + PASSWORD: undefined, + HEADERS: undefined, + ENCODE_PATH: undefined, +}; +export const OpenAPI_Portal_Prod: OpenAPIConfig = { + BASE: `https://api-amplify-portal-production.radix.equinor.com`, + VERSION: '1.0', + WITH_CREDENTIALS: false, + CREDENTIALS: 'include', + TOKEN: getPortalProdToken, USERNAME: undefined, PASSWORD: undefined, HEADERS: undefined, diff --git a/src/api/index.ts b/src/api/index.ts index b9d12ad68..a5f9fe9fb 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -3,7 +3,14 @@ /* eslint-disable */ export { ApiError } from './core/ApiError'; export { CancelablePromise, CancelError } from './core/CancelablePromise'; -export { OpenAPI } from './core/OpenAPI'; +export { OpenAPI, OpenAPI_Portal, OpenAPI_Portal_Prod } from './core/OpenAPI'; +export { request } from './core/request'; export type { OpenAPIConfig } from './core/OpenAPI'; export { TokenService } from './services/TokenService'; +export { PortalService } from './services/PortalService'; + +export type { Feature } from './models/Feature'; +export type { FeatureToggleDto } from './models/FeatureToggleDto'; +export type { GraphUser } from './models/GraphUser'; +export type { ServiceNowIncidentRequestDto } from './models/ServiceNowIncidentRequestDto'; diff --git a/src/api/models/Feature.ts b/src/api/models/Feature.ts new file mode 100644 index 000000000..e2512ff70 --- /dev/null +++ b/src/api/models/Feature.ts @@ -0,0 +1,13 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { GraphUser } from './GraphUser'; + +export type Feature = { + uuid?: string | null; + featureKey?: string | null; + description?: string | null; + activeUsers?: Array | null; + activeEnvironments?: Array | null; +}; diff --git a/src/api/models/FeatureToggleDto.ts b/src/api/models/FeatureToggleDto.ts new file mode 100644 index 000000000..c01d8bb32 --- /dev/null +++ b/src/api/models/FeatureToggleDto.ts @@ -0,0 +1,10 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { Feature } from './Feature'; + +export type FeatureToggleDto = { + applicationName?: string | null; + features?: Array | null; +}; diff --git a/src/api/models/GraphUser.ts b/src/api/models/GraphUser.ts new file mode 100644 index 000000000..7c45b9ad4 --- /dev/null +++ b/src/api/models/GraphUser.ts @@ -0,0 +1,10 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type GraphUser = { + id?: string | null; + displayName?: string | null; + mail?: string | null; + userPrincipalName?: string | null; +}; diff --git a/src/api/models/ServiceNowIncidentRequestDto.ts b/src/api/models/ServiceNowIncidentRequestDto.ts new file mode 100644 index 000000000..0f0d7fd32 --- /dev/null +++ b/src/api/models/ServiceNowIncidentRequestDto.ts @@ -0,0 +1,10 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type ServiceNowIncidentRequestDto = { + configurationItem: string; + title: string; + description: string; + callerEmail: string; +}; diff --git a/src/api/services/PortalService.ts b/src/api/services/PortalService.ts new file mode 100644 index 000000000..fe1fc5b34 --- /dev/null +++ b/src/api/services/PortalService.ts @@ -0,0 +1,66 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { CancelablePromise } from '../.'; +import { + OpenAPI_Portal, + request as __request, + ServiceNowIncidentRequestDto, + FeatureToggleDto, + OpenAPI_Portal_Prod, +} from '../.'; +export class PortalService { + /** + * @param requestBody + * @returns any Success + * @throws ApiError + */ + public static createIncident( + requestBody?: ServiceNowIncidentRequestDto + ): CancelablePromise { + return __request(OpenAPI_Portal, { + method: 'POST', + url: '/api/v1/ServiceNow/incident', + body: requestBody, + mediaType: 'application/json-patch+json', + }); + } + + /** + * Uploads file to slack and links it to a channel defined in config + * @param formData + * @returns any Success + * @throws ApiError + */ + + public static fileUpload(formData?: FormData): CancelablePromise { + return __request(OpenAPI_Portal, { + method: 'POST', + url: '/api/v1/Slack/fileUpload', + body: formData, + }); + } + + /** + * Gets a Feature Toggle from Application name + * @param applicationName name + * @returns FeatureToggleDto Success + * @throws ApiError + */ + public static getFeatureToggleFromApplicationName( + applicationName: string + ): CancelablePromise { + return __request(OpenAPI_Portal_Prod, { + method: 'GET', + url: '/api/v1/FeatureToggle/{applicationName}', + path: { + applicationName: applicationName, + }, + errors: { + 400: `Bad Request`, + 404: `Not Found`, + 500: `Server Error`, + }, + }); + } +} diff --git a/src/api/services/TokenService.ts b/src/api/services/TokenService.ts index 1f7e14faf..380291dd7 100644 --- a/src/api/services/TokenService.ts +++ b/src/api/services/TokenService.ts @@ -7,23 +7,6 @@ import { request as __request } from '../core/request'; export class TokenService { /** - * Gets a token for the application with the provided clientId - * @param clientId - * @returns string Success - * @throws ApiError - */ - public static getToken(clientId: string): CancelablePromise { - return __request(OpenAPI, { - method: 'GET', - url: '/api/v1/Token/{clientId}', - path: { - clientId: clientId, - }, - }); - } - - /** - * Gets a Token for Amplify Portal for the Current Radix Environemnt (development/staging/production) * @returns string Success * @throws ApiError */ @@ -35,7 +18,6 @@ export class TokenService { } /** - * Gets a token for Amplify Portal in Production * @returns string Success * @throws ApiError */ diff --git a/src/components/Navigation/TopBar/Feedback/FeedbackForm/ConsentCheckbox.tsx b/src/components/Navigation/TopBar/Feedback/FeedbackForm/ConsentCheckbox.tsx index 68cf0b74d..5766cd882 100644 --- a/src/components/Navigation/TopBar/Feedback/FeedbackForm/ConsentCheckbox.tsx +++ b/src/components/Navigation/TopBar/Feedback/FeedbackForm/ConsentCheckbox.tsx @@ -4,8 +4,8 @@ import { FileWithPath } from 'react-dropzone'; import { Checkbox, Typography } from '@equinor/eds-core-react'; import { tokens } from '@equinor/eds-tokens'; -import { SeverityOption } from './FeedbackDetails'; import { FeedbackContentType, FeedbackEnum } from './FeedbackForm'; +import { SeverityOption } from './FeedbackFormInner'; import styled from 'styled-components'; diff --git a/src/components/Navigation/TopBar/Feedback/FeedbackForm/FeedbackForm.tsx b/src/components/Navigation/TopBar/Feedback/FeedbackForm/FeedbackForm.tsx index 1e52d5e16..df058750d 100644 --- a/src/components/Navigation/TopBar/Feedback/FeedbackForm/FeedbackForm.tsx +++ b/src/components/Navigation/TopBar/Feedback/FeedbackForm/FeedbackForm.tsx @@ -1,17 +1,13 @@ import { FC, useState } from 'react'; import { FileWithPath } from 'react-dropzone'; -import { useMsal } from '@azure/msal-react'; -import { useMutation, useQuery } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; import { createSlackMessage } from '../Feedback.utils'; -import FeedbackDetails, { SeverityOption } from './FeedbackDetails'; +import FeedbackFormInner, { SeverityOption } from './FeedbackFormInner'; import SelectType from './SelectType'; +import { PortalService, ServiceNowIncidentRequestDto } from 'src/api'; import { useAuth } from 'src/providers/AuthProvider/AuthProvider'; -import { auth, environment } from 'src/utils'; - -const { getEnvironmentName, getApiUrl, getApiScope } = environment; -const { GRAPH_REQUESTS_BACKEND, acquireToken } = auth; export enum FeedbackEnum { ERROR = 'error', @@ -32,11 +28,8 @@ interface FeedbackFormProps { } const FeedbackForm: FC = ({ onClose }) => { - const { instance } = useMsal(); const { account } = useAuth(); const userEmail = account?.username; - const environment = getEnvironmentName(import.meta.env.VITE_ENVIRONMENT_NAME); - const apiUrl = getApiUrl(import.meta.env.VITE_API_URL); const [selectedType, setSelectedType] = useState( undefined @@ -46,76 +39,16 @@ const FeedbackForm: FC = ({ onClose }) => { description: '', consent: false, }); - console.log(feedbackContent.consent); - const { data: portalToken } = useQuery( - ['getPortalTokenForCurrentEnvironment'], - async () => { - const authResult = await acquireToken( - instance, - GRAPH_REQUESTS_BACKEND(getApiScope(import.meta.env.VITE_API_SCOPE)) - ); - return await fetch(`${apiUrl}/api/v1/Token/AmplifyPortal`, { - method: 'GET', - headers: { - Authorization: 'Bearer ' + authResult.accessToken, - 'Content-type': 'application/json', - }, - }) - .then((res) => { - return res.json(); - }) - .catch((error) => { - throw new Error(error); - }); - } - ); - - const { mutate: slackFileUpload } = useMutation( + const { mutateAsync: slackFileUpload } = useMutation( ['slackFileUpload', feedbackContent], - async () => { - const formData = new FormData(); - if (feedbackContent.attachments && feedbackContent.attachments[0]) { - await formData.append('file', feedbackContent.attachments[0]); - } - await formData.append( - 'comment', - createSlackMessage(feedbackContent, selectedType, userEmail) - ); - - await fetch( - `https://api-amplify-portal-${environment}.radix.equinor.com/api/v1/Slack/fileUpload`, - { - method: 'POST', - headers: { - Authorization: 'Bearer ' + portalToken, - }, - body: formData, - } - ); - } + (formData: FormData) => PortalService.fileUpload(formData) ); - const { mutate: serviceNowIncident } = useMutation( + const { mutateAsync: serviceNowIncident } = useMutation( ['serviceNowIncident', feedbackContent], - async () => { - await fetch( - `https://api-amplify-portal-${environment}.radix.equinor.com/api/v1/ServiceNow/incident`, - { - method: 'POST', - headers: { - Authorization: 'Bearer ' + portalToken, - 'Content-type': 'application/json', - }, - body: JSON.stringify({ - configurationItem: '117499', // TODO: use individual IDs for all apps with this as a fallback for "Amplify Applications" - title: feedbackContent.title, - description: feedbackContent.description, - callerEmail: userEmail, - }), - } - ); - } + async (serviceNowDto: ServiceNowIncidentRequestDto) => + PortalService.createIncident(serviceNowDto) ); const updateFeedback = ( @@ -125,17 +58,32 @@ const FeedbackForm: FC = ({ onClose }) => { setFeedbackContent({ ...feedbackContent, [key]: newValue }); }; - const handleSave = () => { - if (selectedType === FeedbackEnum.ERROR) { - serviceNowIncident(); + const handleSave = async () => { + if (selectedType === FeedbackEnum.ERROR && userEmail) { + const serviceNowObject: ServiceNowIncidentRequestDto = { + configurationItem: '117499', // TODO: use individual IDs for all apps with this as a fallback for "Amplify Applications" + title: feedbackContent.title, + description: feedbackContent.description, + callerEmail: userEmail, + }; + await serviceNowIncident(serviceNowObject); + } + + const formData = new FormData(); + if (feedbackContent.attachments && feedbackContent.attachments[0]) { + formData.append('file', feedbackContent.attachments[0]); } - slackFileUpload(); + formData.append( + 'comment', + createSlackMessage(feedbackContent, selectedType, userEmail) + ); + await slackFileUpload(formData); onClose(); }; if (!selectedType) return ; return ( - void; } -const FeedbackDetails: FC = ({ +const FeedbackFormInner: FC = ({ selectedType, setSelectedType, feedbackContent, @@ -148,4 +148,4 @@ const FeedbackDetails: FC = ({ ); }; -export default FeedbackDetails; +export default FeedbackFormInner; diff --git a/src/components/Navigation/TopBar/Feedback/FeedbackForm/UploadFile.tsx b/src/components/Navigation/TopBar/Feedback/FeedbackForm/UploadFile.tsx index ee527d0d1..9648c8158 100644 --- a/src/components/Navigation/TopBar/Feedback/FeedbackForm/UploadFile.tsx +++ b/src/components/Navigation/TopBar/Feedback/FeedbackForm/UploadFile.tsx @@ -4,10 +4,10 @@ import { FileRejection, FileWithPath } from 'react-dropzone'; import { Typography } from '@equinor/eds-core-react'; import { tokens } from '@equinor/eds-tokens'; -import FileProgress from '../../../../Feedback/Progress/FileProgress'; -import FileUploadArea from '../../../../Inputs/FileUploadArea'; -import { SeverityOption } from './FeedbackDetails'; import { FeedbackContentType } from './FeedbackForm'; +import { SeverityOption } from './FeedbackFormInner'; +import FileProgress from 'src/components/Feedback/Progress/FileProgress'; +import FileUploadArea from 'src/components/Inputs/FileUploadArea'; import styled from 'styled-components'; @@ -53,14 +53,10 @@ const UploadFile: FC = ({ acceptedFiles: FileWithPath[], fileRejections: FileRejection[] ) => { - console.log(acceptedFiles); const cleanedOfHiddenFiles = acceptedFiles.filter( (file) => file.name[0] !== '.' ); const reader = new FileReader(); - reader.onloadend = () => { - console.log(reader.result); - }; reader.readAsDataURL(acceptedFiles[0]); setRejectedFiles(fileRejections); updateFeedback('attachments', cleanedOfHiddenFiles); diff --git a/src/hooks/useFeatureToggling.ts b/src/hooks/useFeatureToggling.ts index 879b237ee..e78fbedc3 100644 --- a/src/hooks/useFeatureToggling.ts +++ b/src/hooks/useFeatureToggling.ts @@ -1,52 +1,18 @@ import { useMemo } from 'react'; -import { useMsal } from '@azure/msal-react'; import { useQuery } from '@tanstack/react-query'; -import { useAuth } from '../providers/AuthProvider/AuthProvider'; -import { auth, environment } from '../utils'; +import { FeatureToggleDto, GraphUser, PortalService } from 'src/api'; +import { useAuth } from 'src/providers/AuthProvider/AuthProvider'; +import { environment } from 'src/utils'; -const { - getAppName, - getEnvironmentName, - getApiUrl, - getPortalProdClientId, - getApiScope, -} = environment; -const { GRAPH_REQUESTS_BACKEND, acquireToken } = auth; - -// These three types (FeatureToggleDto, Feature, GraphUser) are from the swagger generated types in the portal API - -export type FeatureToggleDto = { - applicationName?: string | null; - features?: Array | null; -}; - -export type Feature = { - uuid?: string | null; - featureKey?: string | null; - description?: string | null; - activeUsers?: Array | null; - activeEnvironments?: Array | null; -}; - -export type GraphUser = { - id?: string | null; - displayName?: string | null; - mail?: string | null; - userPrincipalName?: string | null; -}; +const { getAppName, getEnvironmentName } = environment; export function useFeatureToggling(featureKey: string) { - const { instance } = useMsal(); const { account } = useAuth(); const username = `${account?.username}`; const applicationName = getAppName(import.meta.env.VITE_NAME); const environment = getEnvironmentName(import.meta.env.VITE_ENVIRONMENT_NAME); - const apiUrl = getApiUrl(import.meta.env.VITE_API_URL); - const portalProdClientId = getPortalProdClientId( - import.meta.env.VITE_PORTAL_PROD_CLIENT_ID - ); const isUserInActiveUserArray = ( username: string, @@ -60,47 +26,10 @@ export function useFeatureToggling(featureKey: string) { return false; }; - const { data: portalToken } = useQuery( - ['getPortalProdToken'], - async () => { - const authResult = await acquireToken( - instance, - GRAPH_REQUESTS_BACKEND(getApiScope(import.meta.env.VITE_API_SCOPE)) - ); - return await fetch(`${apiUrl}/api/v1/Token/${portalProdClientId}`, { - method: 'GET', - headers: { - Authorization: 'Bearer ' + authResult.accessToken, - 'Content-type': 'application/json', - }, - }) - .then((res) => { - return res.json(); - }) - .catch((error) => { - throw new Error(error); - }); - } - ); - const { data: featureToggle, isLoading } = useQuery( ['getFeatureToggleFromAppName'], async () => - await fetch( - `https://api-amplify-portal-production.radix.equinor.com/api/v1/FeatureToggle/${applicationName}`, - { - method: 'GET', - headers: { - Authorization: 'Bearer ' + portalToken, - 'Content-type': 'application/json', - }, - } - ) - .then((res) => res.json()) - .catch((error) => { - throw new Error(error); - }), - { enabled: portalToken !== undefined } + PortalService.getFeatureToggleFromApplicationName(applicationName) ); const feature = featureToggle?.features?.find( @@ -111,7 +40,7 @@ export function useFeatureToggling(featureKey: string) { if (feature) { if (isUserInActiveUserArray(username, feature.activeUsers)) { return true; - } else return feature.activeEnvironments?.includes(environment) ?? true; + } else return feature.activeEnvironments?.includes(environment); } else { return true; } diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts index 18b62bacb..abd1d4546 100644 --- a/src/hooks/useLocalStorage.ts +++ b/src/hooks/useLocalStorage.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; -const getLocalStorage = (key: string, defaultState: T): T => { +export const getLocalStorage = (key: string, defaultState: T): T => { const localStorageData = localStorage.getItem(key); if (localStorageData) { diff --git a/tsconfig.json b/tsconfig.json index 2cda327f2..8fb23f203 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,6 +18,6 @@ "jsx": "react-jsx", "sourceMap": true }, - "include": ["src/**/*"], + "include": ["src/**/*", "vite.config.ts"], "exclude": ["node_modules"] }