diff --git a/frontend/package.json b/frontend/package.json
index 527045faa8..2c0fe13bd5 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,7 +1,7 @@
{
"name": "unleash-frontend",
"description": "unleash your features",
- "version": "4.13.0-beta.1",
+ "version": "4.14.0-beta.0",
"keywords": [
"unleash",
"feature toggle",
@@ -29,6 +29,7 @@
"start": "vite",
"start:heroku": "UNLEASH_API=https://unleash.herokuapp.com yarn run start",
"start:enterprise": "UNLEASH_API=https://unleash4.herokuapp.com yarn run start",
+ "start:demo": "UNLEASH_BASE_PATH=/demo/ yarn start",
"test": "vitest",
"prepare": "yarn run build",
"fmt": "prettier src --write --loglevel warn",
@@ -39,27 +40,27 @@
"devDependencies": {
"@emotion/react": "11.9.3",
"@emotion/styled": "11.9.3",
- "@mui/icons-material": "5.8.3",
- "@mui/lab": "5.0.0-alpha.85",
- "@mui/material": "5.8.3",
+ "@mui/icons-material": "5.8.4",
+ "@mui/lab": "5.0.0-alpha.88",
+ "@mui/material": "5.8.6",
"@openapitools/openapi-generator-cli": "2.5.1",
- "@testing-library/dom": "8.13.0",
+ "@testing-library/dom": "8.14.0",
"@testing-library/jest-dom": "5.16.4",
"@testing-library/react": "12.1.5",
"@testing-library/react-hooks": "^7.0.2",
- "@testing-library/user-event": "14.2.0",
+ "@testing-library/user-event": "14.2.1",
"@types/debounce": "1.2.1",
"@types/deep-diff": "1.0.1",
"@types/jest": "27.5.2",
"@types/lodash.clonedeep": "4.5.7",
"@types/node": "17.0.18",
- "@types/react": "17.0.45",
+ "@types/react": "17.0.47",
"@types/react-dom": "17.0.17",
"@types/react-router-dom": "5.3.3",
"@types/react-table": "7.7.12",
"@types/react-test-renderer": "17.0.2",
"@types/react-timeago": "4.1.3",
- "@types/semver": "^7.3.9",
+ "@types/semver": "7.3.10",
"@vitejs/plugin-react": "1.3.2",
"chart.js": "3.8.0",
"chartjs-adapter-date-fns": "2.0.0",
@@ -69,17 +70,17 @@
"date-fns": "2.28.0",
"debounce": "1.2.1",
"deep-diff": "1.0.2",
- "eslint": "8.17.0",
+ "eslint": "8.18.0",
"eslint-config-react-app": "^7.0.1",
"fast-json-patch": "3.1.1",
"http-proxy-middleware": "2.0.6",
"immer": "9.0.15",
- "jsdom": "^19.0.0",
+ "jsdom": "20.0.0",
"lodash.clonedeep": "4.5.0",
- "msw": "0.42.1",
+ "msw": "0.42.3",
"pkginfo": "^0.4.1",
"plausible-tracker": "0.3.8",
- "prettier": "2.6.2",
+ "prettier": "2.7.1",
"prop-types": "15.8.1",
"react": "17.0.2",
"react-chartjs-2": "4.2.0",
@@ -89,16 +90,16 @@
"react-table": "7.8.0",
"react-test-renderer": "17.0.2",
"react-timeago": "7.1.0",
- "sass": "1.52.3",
+ "sass": "1.53.0",
"semver": "7.3.7",
"swr": "1.3.0",
"tss-react": "3.7.0",
- "typescript": "4.7.3",
- "vite": "2.9.12",
+ "typescript": "4.7.4",
+ "vite": "2.9.13",
"vite-plugin-env-compatible": "^1.1.1",
- "vite-plugin-svgr": "2.1.0",
+ "vite-plugin-svgr": "2.2.0",
"vite-tsconfig-paths": "3.5.0",
- "vitest": "0.14.2",
+ "vitest": "0.16.0",
"whatwg-fetch": "^3.6.2"
},
"jest": {
diff --git a/frontend/src/assets/img/envSplash2.png b/frontend/src/assets/img/envSplash2.png
deleted file mode 100644
index 95b1ba8227..0000000000
Binary files a/frontend/src/assets/img/envSplash2.png and /dev/null differ
diff --git a/frontend/src/assets/img/splashEnv1.svg b/frontend/src/assets/img/splashEnv1.svg
deleted file mode 100644
index 14f9e58480..0000000000
--- a/frontend/src/assets/img/splashEnv1.svg
+++ /dev/null
@@ -1,19 +0,0 @@
-
diff --git a/frontend/src/assets/img/splashEnv2.svg b/frontend/src/assets/img/splashEnv2.svg
deleted file mode 100644
index fb75b51e0d..0000000000
--- a/frontend/src/assets/img/splashEnv2.svg
+++ /dev/null
@@ -1,2 +0,0 @@
-
\ No newline at end of file
diff --git a/frontend/src/component/addons/AddonList/ConfiguredAddons/ConfiguredAddons.tsx b/frontend/src/component/addons/AddonList/ConfiguredAddons/ConfiguredAddons.tsx
index b6490cfffb..263d7d9d8f 100644
--- a/frontend/src/component/addons/AddonList/ConfiguredAddons/ConfiguredAddons.tsx
+++ b/frontend/src/component/addons/AddonList/ConfiguredAddons/ConfiguredAddons.tsx
@@ -53,7 +53,7 @@ export const ConfiguredAddons = () => {
type: 'success',
title: 'Success',
text: !addon.enabled
- ? 'Addon is now active'
+ ? 'Addon is now enabled'
: 'Addon is now disabled',
});
} catch (error: unknown) {
diff --git a/frontend/src/component/admin/apiToken/ApiTokenTable/ApiTokenTable.tsx b/frontend/src/component/admin/apiToken/ApiTokenTable/ApiTokenTable.tsx
index 90801c8197..273ca43376 100644
--- a/frontend/src/component/admin/apiToken/ApiTokenTable/ApiTokenTable.tsx
+++ b/frontend/src/component/admin/apiToken/ApiTokenTable/ApiTokenTable.tsx
@@ -56,24 +56,21 @@ export const ApiTokenTable = () => {
setHiddenColumns(hiddenColumns);
}, [setHiddenColumns, hiddenColumns]);
- const headerSearch = (
-
- );
-
- const headerActions = (
- <>
- {headerSearch}
-
-
- >
- );
-
return (
+
+
+
+ >
+ }
/>
}
>
diff --git a/frontend/src/component/admin/apiToken/CreateApiToken/CreateApiToken.tsx b/frontend/src/component/admin/apiToken/CreateApiToken/CreateApiToken.tsx
index 03430547e2..07b9c0eb09 100644
--- a/frontend/src/component/admin/apiToken/CreateApiToken/CreateApiToken.tsx
+++ b/frontend/src/component/admin/apiToken/CreateApiToken/CreateApiToken.tsx
@@ -11,6 +11,9 @@ import { ConfirmToken } from '../ConfirmToken/ConfirmToken';
import { useState } from 'react';
import { scrollToTop } from 'component/common/util';
import { formatUnknownError } from 'utils/formatUnknownError';
+import { usePageTitle } from 'hooks/usePageTitle';
+
+const pageTitle = 'Create API token';
export const CreateApiToken = () => {
const { setToastApiError } = useToast();
@@ -36,6 +39,8 @@ export const CreateApiToken = () => {
const { createToken, loading } = useApiTokensApi();
+ usePageTitle(pageTitle);
+
const handleSubmit = async (e: Event) => {
e.preventDefault();
if (!isValid()) {
@@ -76,7 +81,7 @@ export const CreateApiToken = () => {
return (
({
width: '100%',
@@ -27,10 +15,11 @@ interface IBillingInformationButtonProps {
export const BillingInformationButton: VFC = ({
update,
-}) => {
- return (
-
- {update ? 'Update billing information' : 'Add billing information'}
-
- );
-};
+}) => (
+
+ {update ? 'Update billing information' : 'Add billing information'}
+
+);
diff --git a/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingPlan.tsx b/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingPlan.tsx
index 569b37a4b7..8cd01433ff 100644
--- a/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingPlan.tsx
+++ b/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingPlan.tsx
@@ -9,7 +9,7 @@ import {
InstanceState,
InstancePlan,
} from 'interfaces/instance';
-import { hasTrialExpired } from 'utils/instanceTrial';
+import { trialHasExpired, isTrialInstance } from 'utils/instanceTrial';
import { GridRow } from 'component/common/GridRow/GridRow';
import { GridCol } from 'component/common/GridCol/GridCol';
import { GridColLink } from './GridColLink/GridColLink';
@@ -81,7 +81,7 @@ interface IBillingPlanProps {
export const BillingPlan: FC = ({ instanceStatus }) => {
const { users } = useUsers();
- const trialHasExpired = hasTrialExpired(instanceStatus);
+ const expired = trialHasExpired(instanceStatus);
const price = {
[InstancePlan.PRO]: 80,
@@ -124,18 +124,16 @@ export const BillingPlan: FC = ({ instanceStatus }) => {
{instanceStatus.plan}
({
- color: trialHasExpired
+ color: expired
? theme.palette.error.dark
: theme.palette.warning.dark,
})}
>
- {trialHasExpired
+ {expired
? 'Trial expired'
: instanceStatus.trialExtended
? 'Extended Trial'
diff --git a/frontend/src/component/admin/billing/FlaggedBillingRedirect/FlaggedBillingRedirect.tsx b/frontend/src/component/admin/billing/FlaggedBillingRedirect/FlaggedBillingRedirect.tsx
new file mode 100644
index 0000000000..0a9d17683e
--- /dev/null
+++ b/frontend/src/component/admin/billing/FlaggedBillingRedirect/FlaggedBillingRedirect.tsx
@@ -0,0 +1,19 @@
+import { Navigate } from 'react-router-dom';
+import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
+import InvoiceAdminPage from 'component/admin/invoice/InvoiceAdminPage';
+
+const FlaggedBillingRedirect = () => {
+ const { uiConfig, loading } = useUiConfig();
+
+ if (loading) {
+ return null;
+ }
+
+ if (!uiConfig.flags.UNLEASH_CLOUD) {
+ return ;
+ }
+
+ return ;
+};
+
+export default FlaggedBillingRedirect;
diff --git a/frontend/src/component/admin/billing/RedirectAdminInvoices/RedirectAdminInvoices.tsx b/frontend/src/component/admin/billing/RedirectAdminInvoices/RedirectAdminInvoices.tsx
deleted file mode 100644
index 07111c962e..0000000000
--- a/frontend/src/component/admin/billing/RedirectAdminInvoices/RedirectAdminInvoices.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import { Navigate } from 'react-router-dom';
-
-const RedirectAdminInvoices = () => {
- return ;
-};
-
-export default RedirectAdminInvoices;
diff --git a/frontend/src/component/admin/invoice/InvoiceAdminPage.tsx b/frontend/src/component/admin/invoice/InvoiceAdminPage.tsx
new file mode 100644
index 0000000000..f9197c0df6
--- /dev/null
+++ b/frontend/src/component/admin/invoice/InvoiceAdminPage.tsx
@@ -0,0 +1,25 @@
+import { useContext } from 'react';
+import InvoiceList from './InvoiceList';
+import AccessContext from 'contexts/AccessContext';
+import { ADMIN } from 'component/providers/AccessProvider/permissions';
+import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
+import { Alert } from '@mui/material';
+
+const InvoiceAdminPage = () => {
+ const { hasAccess } = useContext(AccessContext);
+ return (
+
+
}
+ elseShow={
+
+ You need to be instance admin to access this section.
+
+ }
+ />
+
+ );
+};
+
+export default InvoiceAdminPage;
diff --git a/frontend/src/component/admin/invoice/InvoiceList.tsx b/frontend/src/component/admin/invoice/InvoiceList.tsx
new file mode 100644
index 0000000000..136706ca32
--- /dev/null
+++ b/frontend/src/component/admin/invoice/InvoiceList.tsx
@@ -0,0 +1,122 @@
+import { useEffect, useState } from 'react';
+import {
+ Table,
+ TableHead,
+ TableBody,
+ TableRow,
+ TableCell,
+ Button,
+} from '@mui/material';
+import OpenInNew from '@mui/icons-material/OpenInNew';
+import { PageContent } from 'component/common/PageContent/PageContent';
+import { PageHeader } from 'component/common/PageHeader/PageHeader';
+import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
+import { formatApiPath } from 'utils/formatPath';
+import useInvoices from 'hooks/api/getters/useInvoices/useInvoices';
+import { IInvoice } from 'interfaces/invoice';
+import { useLocationSettings } from 'hooks/useLocationSettings';
+import { formatDateYMD } from 'utils/formatDate';
+
+const PORTAL_URL = formatApiPath('api/admin/invoices/portal');
+
+const InvoiceList = () => {
+ const { refetchInvoices, invoices } = useInvoices();
+ const [isLoaded, setLoaded] = useState(false);
+ const { locationSettings } = useLocationSettings();
+
+ useEffect(() => {
+ refetchInvoices();
+ setLoaded(true);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return (
+ 0}
+ show={
+ }
+ >
+ Billing portal
+
+ }
+ />
+ }
+ >
+
+
+
+
+ Amount
+ Status
+ Due date
+ PDF
+ Link
+
+
+
+ {invoices.map((item: IInvoice) => (
+
+
+ {item.amountFormatted}
+
+
+ {item.status}
+
+
+ {item.dueDate &&
+ formatDateYMD(
+ item.dueDate,
+ locationSettings.locale
+ )}
+
+
+ PDF
+
+
+
+ Payment link
+
+
+
+ ))}
+
+
+
+
+ }
+ elseShow={
{isLoaded && 'No invoices to show.'}
}
+ />
+ );
+};
+export default InvoiceList;
diff --git a/frontend/src/component/admin/users/UsersList/ChangePassword/ChangePassword.tsx b/frontend/src/component/admin/users/UsersList/ChangePassword/ChangePassword.tsx
index fbfc678c47..6521dd27c3 100644
--- a/frontend/src/component/admin/users/UsersList/ChangePassword/ChangePassword.tsx
+++ b/frontend/src/component/admin/users/UsersList/ChangePassword/ChangePassword.tsx
@@ -1,53 +1,49 @@
import React, { useState } from 'react';
import classnames from 'classnames';
-import { Avatar, TextField, Typography, Alert } from '@mui/material';
+import { Avatar, TextField, Typography } from '@mui/material';
import { trim } from 'component/common/util';
import { modalStyles } from 'component/admin/users/util';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
-import PasswordChecker from 'component/user/common/ResetPasswordForm/PasswordChecker/PasswordChecker';
+import PasswordChecker, {
+ PASSWORD_FORMAT_MESSAGE,
+} from 'component/user/common/ResetPasswordForm/PasswordChecker/PasswordChecker';
import { useThemeStyles } from 'themes/themeStyles';
import PasswordMatcher from 'component/user/common/ResetPasswordForm/PasswordMatcher/PasswordMatcher';
-import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { IUser } from 'interfaces/user';
+import useAdminUsersApi from 'hooks/api/actions/useAdminUsersApi/useAdminUsersApi';
interface IChangePasswordProps {
showDialog: boolean;
closeDialog: () => void;
- changePassword: (userId: number, password: string) => Promise;
user: IUser;
}
const ChangePassword = ({
showDialog,
closeDialog,
- changePassword,
user,
}: IChangePasswordProps) => {
const [data, setData] = useState>({});
- const [error, setError] = useState>({});
+ const [error, setError] = useState();
const [validPassword, setValidPassword] = useState(false);
const { classes: themeStyles } = useThemeStyles();
+ const { changePassword } = useAdminUsersApi();
const updateField: React.ChangeEventHandler = event => {
- setError({});
+ setError(undefined);
setData({ ...data, [event.target.name]: trim(event.target.value) });
};
const submit = async (event: React.SyntheticEvent) => {
event.preventDefault();
+ if (data.password !== data.confirm) {
+ return;
+ }
+
if (!validPassword) {
- if (!data.password || data.password.length < 8) {
- setError({
- password:
- 'You must specify a password with at least 8 chars.',
- });
- return;
- }
- if (!(data.password === data.confirm)) {
- setError({ confirm: 'Passwords does not match' });
- return;
- }
+ setError(PASSWORD_FORMAT_MESSAGE);
+ return;
}
try {
@@ -55,16 +51,15 @@ const ChangePassword = ({
setData({});
closeDialog();
} catch (error: unknown) {
- const msg =
- (error instanceof Error && error.message) ||
- 'Could not update password';
- setError({ general: msg });
+ console.warn(error);
+ setError(PASSWORD_FORMAT_MESSAGE);
}
};
const onCancel = (event: React.SyntheticEvent) => {
event.preventDefault();
setData({});
+ setError(undefined);
closeDialog();
};
@@ -77,6 +72,7 @@ const ChangePassword = ({
primaryButtonText="Save"
title="Update password"
secondaryButtonText="Cancel"
+ maxWidth="xs"
>