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" >
- {error.general}} - /> Changing password for user @@ -117,7 +109,8 @@ const ChangePassword = ({ name="password" type="password" value={data.password} - helperText={error.password} + error={Boolean(error)} + helperText={error} onChange={updateField} variant="outlined" size="small" @@ -127,8 +120,6 @@ const ChangePassword = ({ name="confirm" type="password" value={data.confirm} - error={error.confirm !== undefined} - helperText={error.confirm} onChange={updateField} variant="outlined" size="small" diff --git a/frontend/src/component/admin/users/UsersList/UsersList.tsx b/frontend/src/component/admin/users/UsersList/UsersList.tsx index a8e61b6335..b22073a806 100644 --- a/frontend/src/component/admin/users/UsersList/UsersList.tsx +++ b/frontend/src/component/admin/users/UsersList/UsersList.tsx @@ -45,8 +45,7 @@ const UsersList = () => { const navigate = useNavigate(); const { users, roles, refetch, loading } = useUsers(); const { setToastData, setToastApiError } = useToast(); - const { removeUser, changePassword, userLoading, userApiErrors } = - useAdminUsersApi(); + const { removeUser, userLoading, userApiErrors } = useAdminUsersApi(); const [pwDialog, setPwDialog] = useState<{ open: boolean; user?: IUser }>({ open: false, }); @@ -320,7 +319,6 @@ const UsersList = () => { )} diff --git a/frontend/src/component/archive/ArchiveTable/ArchiveTable.tsx b/frontend/src/component/archive/ArchiveTable/ArchiveTable.tsx index 1978526139..3fc0127300 100644 --- a/frontend/src/component/archive/ArchiveTable/ArchiveTable.tsx +++ b/frontend/src/component/archive/ArchiveTable/ArchiveTable.tsx @@ -1,13 +1,6 @@ import { PageContent } from 'component/common/PageContent/PageContent'; import { PageHeader } from 'component/common/PageHeader/PageHeader'; -import { - SortableTableHeader, - Table, - TableBody, - TableCell, - TablePlaceholder, - TableRow, -} from 'component/common/Table'; +import { TablePlaceholder, VirtualizedTable } from 'component/common/Table'; import { SortingRule, useFlexLayout, useSortBy, useTable } from 'react-table'; import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; import { useMediaQuery } from '@mui/material'; @@ -17,13 +10,12 @@ import { HighlightCell } from 'component/common/Table/cells/HighlightCell/Highli import { DateCell } from 'component/common/Table/cells/DateCell/DateCell'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { Search } from 'component/common/Search/Search'; -import { FeatureTypeCell } from '../../common/Table/cells/FeatureTypeCell/FeatureTypeCell'; -import { FeatureSeenCell } from '../../common/Table/cells/FeatureSeenCell/FeatureSeenCell'; -import { LinkCell } from '../../common/Table/cells/LinkCell/LinkCell'; -import { FeatureStaleCell } from '../../feature/FeatureToggleList/FeatureStaleCell/FeatureStaleCell'; +import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell'; +import { FeatureSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureSeenCell'; +import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; +import { FeatureStaleCell } from 'component/feature/FeatureToggleList/FeatureStaleCell/FeatureStaleCell'; import { ReviveArchivedFeatureCell } from 'component/archive/ArchiveTable/ReviveArchivedFeatureCell/ReviveArchivedFeatureCell'; -import { useStyles } from '../../feature/FeatureToggleList/styles'; -import { featuresPlaceholder } from '../../feature/FeatureToggleList/FeatureToggleListTable'; +import { featuresPlaceholder } from 'component/feature/FeatureToggleList/FeatureToggleListTable'; import theme from 'themes/theme'; import { FeatureSchema } from 'openapi'; import { useFeatureArchiveApi } from 'hooks/api/actions/useFeatureArchiveApi/useReviveFeatureApi'; @@ -31,7 +23,6 @@ import useToast from 'hooks/useToast'; import { formatUnknownError } from 'utils/formatUnknownError'; import { useSearch } from 'hooks/useSearch'; import { FeatureArchivedCell } from './FeatureArchivedCell/FeatureArchivedCell'; -import { useVirtualizedRange } from 'hooks/useVirtualizedRange'; import { useSearchParams } from 'react-router-dom'; export interface IFeaturesArchiveTableProps { @@ -57,8 +48,6 @@ export const ArchiveTable = ({ title, projectId, }: IFeaturesArchiveTableProps) => { - const rowHeight = theme.shape.tableRowHeight; - const { classes } = useStyles(); const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg')); const { setToastData, setToastApiError } = useToast(); @@ -107,7 +96,7 @@ export const ArchiveTable = ({ align: 'center', }, { - Header: 'Feature toggle name', + Header: 'Name', accessor: 'name', searchable: true, minWidth: 100, @@ -152,7 +141,7 @@ export const ArchiveTable = ({ ] : []), { - Header: 'Status', + Header: 'State', accessor: 'stale', Cell: FeatureStaleCell, sortType: 'boolean', @@ -166,7 +155,6 @@ export const ArchiveTable = ({ align: 'center', maxWidth: 85, canSort: false, - disableGlobalFilter: true, Cell: ({ row: { original } }: any) => ( - - - - {rows.map((row, index) => { - const isVirtual = - index < firstRenderedIndex || - index > lastRenderedIndex; - - if (isVirtual) { - return null; - } - - prepareRow(row); - return ( - - {row.cells.map(cell => ( - - {cell.render('Cell')} - - ))} - - ); - })} - -
+
0} - show={ - - No feature toggles found matching “ - {searchValue}” - - } - /> - - None of the feature toggles where archived yet. - - } + condition={rows.length === 0} + show={() => ( + 0} + show={ + + No feature toggles found matching “ + {searchValue}” + + } + elseShow={ + + None of the feature toggles were archived yet. + + } + /> + )} /> ); diff --git a/frontend/src/component/archive/ArchiveTable/FeatureArchivedCell/FeatureArchivedCell.tsx b/frontend/src/component/archive/ArchiveTable/FeatureArchivedCell/FeatureArchivedCell.tsx index 75c94812f0..7cd1177199 100644 --- a/frontend/src/component/archive/ArchiveTable/FeatureArchivedCell/FeatureArchivedCell.tsx +++ b/frontend/src/component/archive/ArchiveTable/FeatureArchivedCell/FeatureArchivedCell.tsx @@ -1,6 +1,6 @@ import { VFC } from 'react'; import TimeAgo from 'react-timeago'; -import { Tooltip, Typography } from '@mui/material'; +import { Tooltip, Typography, useTheme } from '@mui/material'; import { formatDateYMD } from 'utils/formatDate'; import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; import { useLocationSettings } from 'hooks/useLocationSettings'; @@ -13,24 +13,37 @@ export const FeatureArchivedCell: VFC = ({ value: archivedAt, }) => { const { locationSettings } = useLocationSettings(); + const theme = useTheme(); - if (!archivedAt) return ; + if (!archivedAt) + return ( + + + not available + + + ); return ( - {archivedAt && ( - - - - - - )} + + + + + ); }; diff --git a/frontend/src/component/archive/ArchiveTable/ReviveArchivedFeatureCell/ReviveArchivedFeatureCell.tsx b/frontend/src/component/archive/ArchiveTable/ReviveArchivedFeatureCell/ReviveArchivedFeatureCell.tsx index 995810c38c..8a9b7c5029 100644 --- a/frontend/src/component/archive/ArchiveTable/ReviveArchivedFeatureCell/ReviveArchivedFeatureCell.tsx +++ b/frontend/src/component/archive/ArchiveTable/ReviveArchivedFeatureCell/ReviveArchivedFeatureCell.tsx @@ -19,7 +19,7 @@ export const ReviveArchivedFeatureCell: VFC = ({ onClick={onRevive} projectId={project} permission={UPDATE_FEATURE} - tooltipProps={{ title: 'Revive feature' }} + tooltipProps={{ title: 'Revive feature toggle' }} > diff --git a/frontend/src/component/archive/FeaturesArchiveTable.tsx b/frontend/src/component/archive/FeaturesArchiveTable.tsx index 23e57da625..1fde18d557 100644 --- a/frontend/src/component/archive/FeaturesArchiveTable.tsx +++ b/frontend/src/component/archive/FeaturesArchiveTable.tsx @@ -1,17 +1,18 @@ -import { useFeaturesArchive } from '../../hooks/api/getters/useFeaturesArchive/useFeaturesArchive'; +import { useFeaturesArchive } from 'hooks/api/getters/useFeaturesArchive/useFeaturesArchive'; import { ArchiveTable } from './ArchiveTable/ArchiveTable'; import { SortingRule } from 'react-table'; import { usePageTitle } from 'hooks/usePageTitle'; import { createLocalStorage } from 'utils/createLocalStorage'; -const defaultSort: SortingRule = { id: 'createdAt', desc: true }; +const defaultSort: SortingRule = { id: 'createdAt' }; const { value, setValue } = createLocalStorage( 'FeaturesArchiveTable:v1', defaultSort ); export const FeaturesArchiveTable = () => { - usePageTitle('Archived'); + usePageTitle('Archive'); + const { archivedFeatures = [], loading, @@ -20,7 +21,7 @@ export const FeaturesArchiveTable = () => { return ( = { id: 'archivedAt', desc: true }; +const defaultSort: SortingRule = { id: 'archivedAt' }; interface IProjectFeaturesTable { projectId: string; @@ -25,7 +25,7 @@ export const ProjectFeaturesArchiveTable = ({ return ( { item !== 'logs' && item !== 'metrics' && item !== 'copy' && - item !== 'strategies' && item !== 'features' && item !== 'features2' && item !== 'create-toggle' && diff --git a/frontend/src/component/common/FeatureStaleDialog/FeatureStaleDialog.tsx b/frontend/src/component/common/FeatureStaleDialog/FeatureStaleDialog.tsx index 1897eacf57..e73b71747e 100644 --- a/frontend/src/component/common/FeatureStaleDialog/FeatureStaleDialog.tsx +++ b/frontend/src/component/common/FeatureStaleDialog/FeatureStaleDialog.tsx @@ -1,5 +1,5 @@ import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi'; -import { DialogContentText } from '@mui/material'; +import { Typography } from '@mui/material'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { Dialogue } from 'component/common/Dialogue/Dialogue'; import React from 'react'; @@ -25,14 +25,13 @@ export const FeatureStaleDialog = ({ const { patchFeatureToggle } = useFeatureApi(); const toggleToStaleContent = ( - - Setting a toggle to stale marks it for cleanup - + Setting a toggle to stale marks it for cleanup ); + const toggleToActiveContent = ( - + Setting a toggle to active marks it as in active use - + ); const toggleActionText = isStale ? 'active' : 'stale'; @@ -68,17 +67,15 @@ export const FeatureStaleDialog = ({ open={isOpen} secondaryButtonText={'Cancel'} primaryButtonText={`Flip to ${toggleActionText}`} - title={`Set feature status to ${toggleActionText}`} + title={`Set feature state to ${toggleActionText}`} onClick={onSubmit} onClose={onClose} > - <> - - + ); }; diff --git a/frontend/src/component/common/HelpIcon/HelpIcon.tsx b/frontend/src/component/common/HelpIcon/HelpIcon.tsx index a3da995f80..502be077c5 100644 --- a/frontend/src/component/common/HelpIcon/HelpIcon.tsx +++ b/frontend/src/component/common/HelpIcon/HelpIcon.tsx @@ -17,7 +17,6 @@ export const HelpIcon = ({ tooltip, style }: IHelpIconProps) => { className={styles.container} style={style} tabIndex={0} - role="tooltip" aria-label="Help" > diff --git a/frontend/src/component/common/InstanceStatus/InstanceStatus.tsx b/frontend/src/component/common/InstanceStatus/InstanceStatus.tsx index 70e63bea00..4bb3626a65 100644 --- a/frontend/src/component/common/InstanceStatus/InstanceStatus.tsx +++ b/frontend/src/component/common/InstanceStatus/InstanceStatus.tsx @@ -5,11 +5,11 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit import { Dialogue } from 'component/common/Dialogue/Dialogue'; import { Typography } from '@mui/material'; import { useNavigate } from 'react-router-dom'; -import { IInstanceStatus, InstanceState } from 'interfaces/instance'; +import { IInstanceStatus } from 'interfaces/instance'; import { ADMIN } from 'component/providers/AccessProvider/permissions'; import AccessContext from 'contexts/AccessContext'; import useInstanceStatusApi from 'hooks/api/actions/useInstanceStatusApi/useInstanceStatusApi'; -import { hasTrialExpired } from 'utils/instanceTrial'; +import { trialHasExpired, canExtendTrial } from 'utils/instanceTrial'; import useToast from 'hooks/useToast'; import { formatUnknownError } from 'utils/formatUnknownError'; @@ -24,16 +24,25 @@ const TrialDialog: VFC = ({ }) => { const { hasAccess } = useContext(AccessContext); const navigate = useNavigate(); - const trialHasExpired = hasTrialExpired(instanceStatus); - const [dialogOpen, setDialogOpen] = useState(trialHasExpired); + const expired = trialHasExpired(instanceStatus); + const [dialogOpen, setDialogOpen] = useState(expired); + + const onClose = (event: React.SyntheticEvent, muiCloseReason?: string) => { + if (!muiCloseReason) { + setDialogOpen(false); + if (canExtendTrial(instanceStatus)) { + onExtendTrial().catch(console.error); + } + } + }; useEffect(() => { - setDialogOpen(trialHasExpired); + setDialogOpen(expired); const interval = setInterval(() => { - setDialogOpen(trialHasExpired); + setDialogOpen(expired); }, 60000); return () => clearInterval(interval); - }, [trialHasExpired]); + }, [expired]); if (hasAccess(ADMIN)) { return ( @@ -41,23 +50,15 @@ const TrialDialog: VFC = ({ open={dialogOpen} primaryButtonText="Upgrade trial" secondaryButtonText={ - instanceStatus?.trialExtended - ? 'Remind me later' - : 'Extend trial (5 days)' + canExtendTrial(instanceStatus) + ? 'Extend trial (5 days)' + : 'Remind me later' } onClick={() => { navigate('/admin/billing'); setDialogOpen(false); }} - onClose={(_: any, reason?: string) => { - if ( - reason !== 'backdropClick' && - reason !== 'escapeKeyDown' - ) { - onExtendTrial(); - setDialogOpen(false); - } - }} + onClose={onClose} title={`Your free ${instanceStatus.plan} trial has expired!`} > @@ -92,16 +93,11 @@ export const InstanceStatus: FC = ({ children }) => { const { setToastApiError } = useToast(); const onExtendTrial = async () => { - if ( - instanceStatus?.state === InstanceState.TRIAL && - !instanceStatus?.trialExtended - ) { - try { - await extendTrial(); - await refetchInstanceStatus(); - } catch (error: unknown) { - setToastApiError(formatUnknownError(error)); - } + try { + await extendTrial(); + await refetchInstanceStatus(); + } catch (error: unknown) { + setToastApiError(formatUnknownError(error)); } }; diff --git a/frontend/src/component/common/InstanceStatus/InstanceStatusBar.test.tsx b/frontend/src/component/common/InstanceStatus/InstanceStatusBar.test.tsx index 726e788b85..8791740e68 100644 --- a/frontend/src/component/common/InstanceStatus/InstanceStatusBar.test.tsx +++ b/frontend/src/component/common/InstanceStatus/InstanceStatusBar.test.tsx @@ -2,7 +2,7 @@ import { InstanceStatusBar } from 'component/common/InstanceStatus/InstanceStatu import { InstancePlan, InstanceState } from 'interfaces/instance'; import { render } from 'utils/testRenderer'; import { screen } from '@testing-library/react'; -import { addDays } from 'date-fns'; +import { addDays, subDays } from 'date-fns'; import { INSTANCE_STATUS_BAR_ID } from 'utils/testIds'; import { UNKNOWN_INSTANCE_STATUS } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus'; @@ -14,7 +14,22 @@ test('InstanceStatusBar should be hidden by default', async () => { ).not.toBeInTheDocument(); }); -test('InstanceStatusBar should be hidden when the trial is far from expired', async () => { +test('InstanceStatusBar should be hidden when state is active', async () => { + render( + + ); + + expect( + screen.queryByTestId(INSTANCE_STATUS_BAR_ID) + ).not.toBeInTheDocument(); +}); + +test('InstanceStatusBar should warn when the trial is far from expired', async () => { render(