diff --git a/frontend/src/component/admin/Admin.tsx b/frontend/src/component/admin/Admin.tsx index be50b4963fd5..cfea95936672 100644 --- a/frontend/src/component/admin/Admin.tsx +++ b/frontend/src/component/admin/Admin.tsx @@ -19,6 +19,7 @@ import NotFound from 'component/common/NotFound/NotFound'; import { AdminIndex } from './AdminIndex'; import { AdminTabsMenu } from './menu/AdminTabsMenu'; import { Banners } from './banners/Banners'; +import { License } from './license/License'; export const Admin = () => { return ( @@ -38,6 +39,7 @@ export const Admin = () => { } /> } /> } /> + } /> } /> } /> ({ + display: 'grid', + gap: theme.spacing(4), +})); + +const StyledDataCollectionPropertyRow = styled('div')(() => ({ + display: 'table-row', +})); + +const StyledPropertyName = styled('p')(({ theme }) => ({ + display: 'table-cell', + fontWeight: theme.fontWeight.bold, + paddingTop: theme.spacing(2), +})); + +const StyledPropertyDetails = styled('p')(({ theme }) => ({ + display: 'table-cell', + paddingTop: theme.spacing(2), + paddingLeft: theme.spacing(4), +})); + + + + + +export const License = () => { + const { setToastData, setToastApiError } = useToast(); + const { license, error, refetchLicense } = useLicense(); + const { reCheckLicense } = useLicenseCheck(); + const { loading } = useUiConfig(); + const { locationSettings } = useLocationSettings(); + const [token, setToken] = useState(''); + const { updateLicenseKey } = useUpdateLicenseKeyApi(); + + const updateToken = (event: React.ChangeEvent) => { + setToken(event.target.value.trim()); + }; + + if (loading || !license) { + return null; + } + + const onSubmit = async (event: React.SyntheticEvent) => { + event.preventDefault(); + + try { + await updateLicenseKey(token); + setToastData({ + title: 'License key updated', + type: 'success', + }); + refetchLicense(); + reCheckLicense (); + } catch (error: unknown) { + setToastApiError(formatUnknownError(error)); + } + }; + + return ( + }> + + {license.token ?
+ + Customer + + {license?.customer} + + + + Plan + + {license?.plan} + + + + Seats + + {license?.seats} + + + + Expire at + + {formatDateYMD(license.expireAt, locationSettings.locale)} + + +
:

You do not have a registered Unleash Enterprise License.

} + + +
+ +

+ + + + {' '} +

+ + {error?.message} + +

+
+
+ + +
+
+ ); +}; diff --git a/frontend/src/component/banners/externalBanners/ExternalBanners.tsx b/frontend/src/component/banners/externalBanners/ExternalBanners.tsx index a8c768c2e17d..37f2b53b7c6e 100644 --- a/frontend/src/component/banners/externalBanners/ExternalBanners.tsx +++ b/frontend/src/component/banners/externalBanners/ExternalBanners.tsx @@ -1,10 +1,12 @@ import { Banner } from 'component/banners/Banner/Banner'; +import { useLicenseCheck } from 'hooks/api/getters/useLicense/useLicense'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import { useVariant } from 'hooks/useVariant'; import { IBanner } from 'interfaces/banner'; export const ExternalBanners = () => { - const { uiConfig } = useUiConfig(); + const { uiConfig, isEnterprise } = useUiConfig(); + const licenseInfo = useLicenseCheck(); const bannerVariantFromMessageBannerFlag = useVariant( uiConfig.flags.messageBanner, @@ -19,6 +21,17 @@ export const ExternalBanners = () => { const banners: IBanner[] = Array.isArray(bannerVariant) ? bannerVariant : [bannerVariant]; + + // Only for enterprise + if(isEnterprise()) { + if(licenseInfo && !licenseInfo.isValid && !licenseInfo.loading && !licenseInfo.error) { + banners.push({ + message: licenseInfo.message || 'You have an invalid Unleash license.', + variant: 'error', + sticky: true, + }); + } + } return ( <> diff --git a/frontend/src/hooks/api/actions/useLicenseAPI/useLicenseApi.ts b/frontend/src/hooks/api/actions/useLicenseAPI/useLicenseApi.ts new file mode 100644 index 000000000000..aefeb10c55d4 --- /dev/null +++ b/frontend/src/hooks/api/actions/useLicenseAPI/useLicenseApi.ts @@ -0,0 +1,37 @@ +import { Dispatch, SetStateAction } from 'react'; +import useAPI from '../useApi/useApi'; + +export const handleBadRequest = async ( + setErrors?: Dispatch>, + res?: Response, +) => { + if (!setErrors) return; + if (res) { + const data = await res.json(); + setErrors({ message: data.message }); + throw new Error(data.message); + } + + throw new Error(); +}; + +const useUpdateLicenseKeyApi = () => { + const { makeRequest, createRequest, errors, loading } = useAPI({ + propagateErrors: true, + handleBadRequest, + }); + + const updateLicenseKey = async (token: string): Promise => { + const path = `api/admin/license`; + const req = createRequest(path, { + method: 'POST', + body: JSON.stringify({ token }), + }); + + await makeRequest(req.caller, req.id); + }; + + return { updateLicenseKey, errors, loading }; +}; + +export default useUpdateLicenseKeyApi; diff --git a/frontend/src/hooks/api/getters/useLicense/useLicense.ts b/frontend/src/hooks/api/getters/useLicense/useLicense.ts new file mode 100644 index 000000000000..531abd49c8ca --- /dev/null +++ b/frontend/src/hooks/api/getters/useLicense/useLicense.ts @@ -0,0 +1,70 @@ +import useSWR from 'swr'; +import { useMemo } from 'react'; +import { formatApiPath } from 'utils/formatPath'; +import handleErrorResponses from '../httpErrorResponseHandler'; +import useUiConfig from '../useUiConfig/useUiConfig'; +import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR'; +import { useEnterpriseSWR } from '../useEnterpriseSWR/useEnterpriseSWR'; + +export interface LicenseInfo { + isValid: boolean; + message?: string; + loading: boolean; + reCheckLicense: () => void; + error?: Error; +} + +const fallback = { + isValid: true, + message: '', + loading: false, +}; + +export interface License { + license?: { + token: string; + customer: string; + plan: string; + seats: number; + expireAt: Date; + }; + loading: boolean; + refetchLicense: () => void; + error?: Error; +} + +export const useLicenseCheck = (): LicenseInfo => { + const { data, error, mutate } = useEnterpriseSWR( + fallback, + formatApiPath(`api/admin/license/check`), + fetcher, + ); + + return { + isValid: data?.isValid, + message: data?.message, + loading: !error && !data, + reCheckLicense: () => mutate(), + error, + }; +}; + +export const useLicense = (): License => { + const { data, error, mutate } = useSWR( + formatApiPath(`api/admin/license`), + fetcher, + ); + + return { + license: { ...data }, + loading: !error && !data, + refetchLicense: () => mutate(), + error, + }; +}; + +const fetcher = (path: string) => { + return fetch(path) + .then(handleErrorResponses('License')) + .then((res) => res.json()); +}; diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts index ea518783e9c6..b10bd4b63726 100644 --- a/frontend/src/interfaces/uiConfig.ts +++ b/frontend/src/interfaces/uiConfig.ts @@ -67,6 +67,10 @@ export type UiFlags = { scheduledConfigurationChanges?: boolean; featureSearchAPI?: boolean; featureSearchFrontend?: boolean; + internalMessageBanners?: boolean; + disableEnvsOnRevive?: boolean; + playgroundImprovements?: boolean; + enableLicense?: boolean; }; export interface IVersionInfo {