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:
commit
3c23fb554e
1
frontend/.gitignore
vendored
1
frontend/.gitignore
vendored
@ -49,6 +49,7 @@ build
|
|||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
cypress/downloads/*
|
||||||
cypress/videos/*
|
cypress/videos/*
|
||||||
cypress/downloads/*
|
cypress/downloads/*
|
||||||
cypress/screenshots/*
|
cypress/screenshots/*
|
||||||
|
@ -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",
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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={
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -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 },
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
import { lazy } from 'react';
|
||||||
|
|
||||||
|
export const LazyPlayground = lazy(() => import('./Playground'));
|
@ -212,3 +212,5 @@ export const Playground: VFC<{}> = () => {
|
|||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default Playground;
|
||||||
|
@ -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,
|
||||||
|
@ -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={
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user