1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-23 00:22:19 +01:00

Merge branch 'main' into task/constraint_card_adjustmnets

This commit is contained in:
andreas-unleash 2022-07-26 13:10:45 +03:00 committed by GitHub
commit 3c23fb554e
15 changed files with 93 additions and 79 deletions

1
frontend/.gitignore vendored
View File

@ -49,6 +49,7 @@ build
.DS_Store .DS_Store
cypress/downloads/*
cypress/videos/* cypress/videos/*
cypress/downloads/* cypress/downloads/*
cypress/screenshots/* cypress/screenshots/*

View File

@ -1,7 +1,7 @@
{ {
"name": "unleash-frontend", "name": "unleash-frontend",
"description": "unleash your features", "description": "unleash your features",
"version": "4.14.0-beta.4", "version": "4.14.0-beta.5",
"keywords": [ "keywords": [
"unleash", "unleash",
"feature toggle", "feature toggle",

View File

@ -14,6 +14,7 @@ import { SplashPageRedirect } from 'component/splash/SplashPageRedirect/SplashPa
import { useStyles } from './App.styles'; import { useStyles } from './App.styles';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { Suspense } from 'react';
export const App = () => { export const App = () => {
const { classes: styles } = useStyles(); const { classes: styles } = useStyles();
@ -30,37 +31,39 @@ export const App = () => {
return ( return (
<SWRProvider isUnauthorized={!isLoggedIn}> <SWRProvider isUnauthorized={!isLoggedIn}>
<ConditionallyRender <Suspense fallback={<Loader />}>
condition={!hasFetchedAuth} <ConditionallyRender
show={<Loader />} condition={!hasFetchedAuth}
elseShow={ show={<Loader />}
<div className={styles.container}> elseShow={
<ToastRenderer /> <div className={styles.container}>
<LayoutPicker> <ToastRenderer />
<Routes> <LayoutPicker>
{availableRoutes.map(route => ( <Routes>
{availableRoutes.map(route => (
<Route
key={route.path}
path={route.path}
element={
<ProtectedRoute route={route} />
}
/>
))}
<Route <Route
key={route.path} path="/"
path={route.path}
element={ element={
<ProtectedRoute route={route} /> <Navigate to="/features" replace />
} }
/> />
))} <Route path="*" element={<NotFound />} />
<Route </Routes>
path="/" <FeedbackNPS openUrl="http://feedback.unleash.run" />
element={ <SplashPageRedirect />
<Navigate to="/features" replace /> </LayoutPicker>
} </div>
/> }
<Route path="*" element={<NotFound />} /> />
</Routes> </Suspense>
<FeedbackNPS openUrl="http://feedback.unleash.run" />
<SplashPageRedirect />
</LayoutPicker>
</div>
}
/>
</SWRProvider> </SWRProvider>
); );
}; };

View File

@ -1,6 +1,6 @@
import AdminMenu from '../menu/AdminMenu'; import AdminMenu from '../menu/AdminMenu';
import { PageContent } from 'component/common/PageContent/PageContent'; import { PageContent } from 'component/common/PageContent/PageContent';
import { useContext } from 'react'; import { useContext, useEffect } from 'react';
import { ADMIN } from 'component/providers/AccessProvider/permissions'; import { ADMIN } from 'component/providers/AccessProvider/permissions';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import AccessContext from 'contexts/AccessContext'; import AccessContext from 'contexts/AccessContext';
@ -12,14 +12,28 @@ import { BillingHistory } from './BillingHistory/BillingHistory';
import useInvoices from 'hooks/api/getters/useInvoices/useInvoices'; import useInvoices from 'hooks/api/getters/useInvoices/useInvoices';
export const Billing = () => { export const Billing = () => {
const { instanceStatus, isBilling } = useInstanceStatus(); const {
instanceStatus,
isBilling,
refetchInstanceStatus,
refresh,
loading,
} = useInstanceStatus();
const { invoices } = useInvoices(); const { invoices } = useInvoices();
const { hasAccess } = useContext(AccessContext); const { hasAccess } = useContext(AccessContext);
useEffect(() => {
const hardRefresh = async () => {
await refresh();
refetchInstanceStatus();
};
hardRefresh();
}, [refetchInstanceStatus, refresh]);
return ( return (
<div> <div>
<AdminMenu /> <AdminMenu />
<PageContent header="Billing"> <PageContent header="Billing" isLoading={loading}>
<ConditionallyRender <ConditionallyRender
condition={isBilling} condition={isBilling}
show={ show={

View File

@ -40,7 +40,7 @@ export const BillingInformation: FC<IBillingInformationProps> = ({
return ( return (
<Grid item xs={12} md={5}> <Grid item xs={12} md={5}>
<StyledInfoBox> <StyledInfoBox>
<StyledTitle variant="body1">Billing Information</StyledTitle> <StyledTitle variant="body1">Billing information</StyledTitle>
<ConditionallyRender <ConditionallyRender
condition={inactive} condition={inactive}
show={ show={
@ -54,7 +54,7 @@ export const BillingInformation: FC<IBillingInformationProps> = ({
<StyledInfoLabel> <StyledInfoLabel>
{inactive {inactive
? 'Once we have received your billing information we will upgrade your trial within 1 business day' ? 'Once we have received your billing information we will upgrade your trial within 1 business day'
: 'These changes may take up to 1 business day and they will be visible on your next invoice'} : 'Update your credit card and business information and change which email address we send invoices to'}
</StyledInfoLabel> </StyledInfoLabel>
<StyledDivider /> <StyledDivider />
<StyledInfoLabel> <StyledInfoLabel>

View File

@ -17,7 +17,7 @@ const StringTruncator = ({
}: IStringTruncatorProps) => { }: IStringTruncatorProps) => {
return ( return (
<ConditionallyRender <ConditionallyRender
condition={text.length > maxLength} condition={(text?.length ?? 0) > maxLength}
show={ show={
<Tooltip title={text} arrow> <Tooltip title={text} arrow>
<span <span

View File

@ -120,7 +120,14 @@ exports[`returns all baseRoutes 1`] = `
"type": "protected", "type": "protected",
}, },
{ {
"component": [Function], "component": {
"$$typeof": Symbol(react.lazy),
"_init": [Function],
"_payload": {
"_result": [Function],
"_status": -1,
},
},
"hidden": false, "hidden": false,
"menu": { "menu": {
"mobile": true, "mobile": true,
@ -473,7 +480,7 @@ exports[`returns all baseRoutes 1`] = `
}, },
"parent": "/admin", "parent": "/admin",
"path": "/admin-invoices", "path": "/admin-invoices",
"title": "Invoices", "title": "Billing & invoices",
"type": "protected", "type": "protected",
}, },
{ {

View File

@ -53,10 +53,10 @@ import { SegmentTable } from 'component/segments/SegmentTable/SegmentTable';
import FlaggedBillingRedirect from 'component/admin/billing/FlaggedBillingRedirect/FlaggedBillingRedirect'; import FlaggedBillingRedirect from 'component/admin/billing/FlaggedBillingRedirect/FlaggedBillingRedirect';
import { FeaturesArchiveTable } from '../archive/FeaturesArchiveTable'; import { FeaturesArchiveTable } from '../archive/FeaturesArchiveTable';
import { Billing } from 'component/admin/billing/Billing'; import { Billing } from 'component/admin/billing/Billing';
import { Playground } from 'component/playground/Playground/Playground';
import { Group } from 'component/admin/groups/Group/Group'; import { Group } from 'component/admin/groups/Group/Group';
import { CreateGroup } from 'component/admin/groups/CreateGroup/CreateGroup'; import { CreateGroup } from 'component/admin/groups/CreateGroup/CreateGroup';
import { EditGroup } from 'component/admin/groups/EditGroup/EditGroup'; import { EditGroup } from 'component/admin/groups/EditGroup/EditGroup';
import { LazyPlayground } from 'component/playground/Playground/LazyPlayground';
export const routes: IRoute[] = [ export const routes: IRoute[] = [
// Splash // Splash
@ -182,7 +182,7 @@ export const routes: IRoute[] = [
{ {
path: '/playground', path: '/playground',
title: 'Playground', title: 'Playground',
component: Playground, component: LazyPlayground,
hidden: false, hidden: false,
type: 'protected', type: 'protected',
menu: { mobile: true }, menu: { mobile: true },
@ -518,7 +518,7 @@ export const routes: IRoute[] = [
{ {
path: '/admin-invoices', path: '/admin-invoices',
parent: '/admin', parent: '/admin',
title: 'Invoices', title: 'Billing & invoices',
component: FlaggedBillingRedirect, component: FlaggedBillingRedirect,
type: 'protected', type: 'protected',
menu: { adminSettings: true, isEnterprise: true }, menu: { adminSettings: true, isEnterprise: true },

View File

@ -0,0 +1,3 @@
import { lazy } from 'react';
export const LazyPlayground = lazy(() => import('./Playground'));

View File

@ -212,3 +212,5 @@ export const Playground: VFC<{}> = () => {
</PageContent> </PageContent>
); );
}; };
export default Playground;

View File

@ -1,4 +1,4 @@
import { FormEvent, useEffect, useMemo, useState } from 'react'; import React, { FormEvent, useEffect, useMemo, useState } from 'react';
import { import {
Autocomplete, Autocomplete,
Button, Button,

View File

@ -32,6 +32,7 @@ import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { IUser } from 'interfaces/user'; import { IUser } from 'interfaces/user';
import { IGroup } from 'interfaces/group'; import { IGroup } from 'interfaces/group';
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
import { mapGroupUsers } from '../../../../hooks/api/getters/useGroup/useGroup';
const StyledAvatar = styled(Avatar)(({ theme }) => ({ const StyledAvatar = styled(Avatar)(({ theme }) => ({
width: theme.spacing(4), width: theme.spacing(4),
@ -339,7 +340,6 @@ export const ProjectAccessTable: VFC = () => {
setRemoveOpen(false); setRemoveOpen(false);
setSelectedRow(undefined); setSelectedRow(undefined);
}; };
return ( return (
<PageContent <PageContent
header={ header={

View File

@ -112,23 +112,6 @@ const useProjectApi = () => {
} }
}; };
const addUserToRole = async (
projectId: string,
roleId: number,
userId: number
) => {
const path = `api/admin/projects/${projectId}/users/${userId}/roles/${roleId}`;
const req = createRequest(path, { method: 'POST' });
try {
const res = await makeRequest(req.caller, req.id);
return res;
} catch (e) {
throw e;
}
};
const addAccessToProject = async ( const addAccessToProject = async (
projectId: string, projectId: string,
roleId: number, roleId: number,
@ -226,7 +209,6 @@ const useProjectApi = () => {
deleteProject, deleteProject,
addEnvironmentToProject, addEnvironmentToProject,
removeEnvironmentFromProject, removeEnvironmentFromProject,
addUserToRole,
addAccessToProject, addAccessToProject,
removeUserFromRole, removeUserFromRole,
removeGroupFromRole, removeGroupFromRole,

View File

@ -7,6 +7,7 @@ import { useEffect } from 'react';
export interface IUseInstanceStatusOutput { export interface IUseInstanceStatusOutput {
instanceStatus?: IInstanceStatus; instanceStatus?: IInstanceStatus;
refetchInstanceStatus: () => void; refetchInstanceStatus: () => void;
refresh: () => Promise<void>;
isBilling: boolean; isBilling: boolean;
loading: boolean; loading: boolean;
error?: Error; error?: Error;
@ -33,9 +34,14 @@ export const useInstanceStatus = (): IUseInstanceStatusOutput => {
InstancePlan.TEAM, InstancePlan.TEAM,
]; ];
const refresh = async (): Promise<void> => {
await fetch(formatApiPath('api/instance/refresh'));
};
return { return {
instanceStatus: data, instanceStatus: data,
refetchInstanceStatus: refetch, refetchInstanceStatus: refetch,
refresh,
isBilling: billingPlans.includes(data?.plan ?? InstancePlan.UNKNOWN), isBilling: billingPlans.includes(data?.plan ?? InstancePlan.UNKNOWN),
loading, loading,
error, error,

View File

@ -1,10 +1,12 @@
import useSWR, { mutate, SWRConfiguration } from 'swr'; import useSWR, { mutate, SWRConfiguration } from 'swr';
import { useState, useEffect } from 'react'; import { useState, useEffect, useMemo } from 'react';
import { formatApiPath } from 'utils/formatPath'; import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler'; import handleErrorResponses from '../httpErrorResponseHandler';
import { IProjectRole } from 'interfaces/role'; import { IProjectRole } from 'interfaces/role';
import { IGroup } from 'interfaces/group'; import { IGroup } from 'interfaces/group';
import { IUser } from 'interfaces/user'; import { IUser } from 'interfaces/user';
import { useGroups } from '../useGroups/useGroups';
import { mapGroupUsers } from '../useGroup/useGroup';
export enum ENTITY_TYPE { export enum ENTITY_TYPE {
USER = 'USERS', USER = 'USERS',
@ -45,11 +47,7 @@ const useProjectAccess = (
const CACHE_KEY = `api/admin/projects/${projectId}/users`; const CACHE_KEY = `api/admin/projects/${projectId}/users`;
const { data, error } = useSWR<IProjectAccessOutput>( const { data, error } = useSWR(CACHE_KEY, fetcher, options);
CACHE_KEY,
fetcher,
options
);
const [loading, setLoading] = useState(!error && !data); const [loading, setLoading] = useState(!error && !data);
@ -61,21 +59,19 @@ const useProjectAccess = (
setLoading(!error && !data); setLoading(!error && !data);
}, [data, error]); }, [data, error]);
// TODO: Remove this and replace `mockData` back for `data` @79. This mocks what a group looks like when returned along with the access. let access: IProjectAccessOutput = data
// const { groups } = useGroups(); ? {
// const mockData = useMemo( roles: data.roles,
// () => ({ users: data.users,
// ...data, groups:
// groups: groups?.map(group => ({ data?.groups.map((group: any) => ({
// ...group, ...group,
// roleId: 4, users: mapGroupUsers(group.users ?? []),
// })) as IProjectAccessGroup[], })) ?? [],
// }), }
// [data, groups] : { roles: [], users: [], groups: [] };
// );
return { return {
access: data ? data : { roles: [], users: [], groups: [] }, access: access,
error, error,
loading, loading,
refetchProjectAccess, refetchProjectAccess,