1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-06-04 01:18:20 +02:00

feat: application usage frontend (#4561)

This commit is contained in:
Jaanus Sellin 2023-08-24 13:13:02 +03:00 committed by GitHub
parent 2c3514954c
commit a97fabd415
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 246 additions and 44 deletions

View File

@ -58,10 +58,16 @@ yarn run e2e
The frontend uses an OpenAPI client generated from the backend's OpenAPI spec. The frontend uses an OpenAPI client generated from the backend's OpenAPI spec.
Whenever there are changes to the backend API, the client should be regenerated: Whenever there are changes to the backend API, the client should be regenerated:
For now we only use generated types (src/openapi/models).
We will use methods (src/openapi/apis) for new features soon.
``` ```
./scripts/generate-openapi.sh yarn gen:api
rm -rf src/openapi/apis
``` ```
clean up `src/openapi/index.ts` imports, only keep first line `export * from './models';`
This script assumes that you have a running instance of the enterprise backend at `http://localhost:4242`. This script assumes that you have a running instance of the enterprise backend at `http://localhost:4242`.
The new OpenAPI client will be generated from the runtime schema of this instance. The new OpenAPI client will be generated from the runtime schema of this instance.
The target URL can be changed by setting the `UNLEASH_OPENAPI_URL` env var. The target URL can be changed by setting the `UNLEASH_OPENAPI_URL` env var.

View File

@ -1,13 +1,5 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { import { Avatar, CircularProgress, Icon, Link } from '@mui/material';
Avatar,
CircularProgress,
Icon,
Link,
styled,
Typography,
useTheme,
} from '@mui/material';
import { Warning } from '@mui/icons-material'; import { Warning } from '@mui/icons-material';
import { styles as themeStyles } from 'component/common'; import { styles as themeStyles } from 'component/common';
import { PageContent } from 'component/common/PageContent/PageContent'; import { PageContent } from 'component/common/PageContent/PageContent';
@ -27,11 +19,11 @@ import { useGlobalFilter, useSortBy, useTable } from 'react-table';
import { sortTypes } from 'utils/sortTypes'; import { sortTypes } from 'utils/sortTypes';
import { IconCell } from 'component/common/Table/cells/IconCell/IconCell'; import { IconCell } from 'component/common/Table/cells/IconCell/IconCell';
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; import { ApplicationUsageCell } from './ApplicationUsageCell/ApplicationUsageCell';
import { ApplicationSchema } from '../../../openapi';
export const ApplicationList = () => { export const ApplicationList = () => {
const { applications: data, loading } = useApplications(); const { applications: data, loading } = useApplications();
const theme = useTheme();
const renderNoApplications = () => ( const renderNoApplications = () => (
<> <>
@ -100,16 +92,11 @@ export const ApplicationList = () => {
Header: 'Project(environment)', Header: 'Project(environment)',
accessor: 'usage', accessor: 'usage',
width: '50%', width: '50%',
Cell: () => ( Cell: ({
<TextCell> row: { original },
<Typography }: {
variant="body2" row: { original: ApplicationSchema };
color={theme.palette.text.secondary} }) => <ApplicationUsageCell usage={original.usage} />,
>
not connected
</Typography>
</TextCell>
),
sortType: 'alphanumeric', sortType: 'alphanumeric',
}, },
{ {

View File

@ -0,0 +1,34 @@
import { screen } from '@testing-library/react';
import { render } from 'utils/testRenderer';
import { ApplicationUsageCell } from './ApplicationUsageCell';
test('displays not connected if no usage found', () => {
render(<ApplicationUsageCell usage={[]} />);
expect(screen.getByText('not connected')).toBeInTheDocument();
});
test('display project and environments in correct manner', () => {
render(
<ApplicationUsageCell
usage={[
{ project: 'myProject', environments: ['dev', 'production'] },
]}
/>
);
const anchor = screen.getByRole('link');
expect(anchor).toHaveAttribute('href', '/projects/myProject');
expect(screen.getByText('(dev, production)')).toBeInTheDocument();
});
test('when no specific project is defined, do not create link', () => {
render(
<ApplicationUsageCell
usage={[{ project: '*', environments: ['dev', 'production'] }]}
/>
);
const anchor = screen.queryByRole('link');
expect(anchor).not.toBeInTheDocument();
});

View File

@ -0,0 +1,64 @@
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { styled, Typography, useTheme } from '@mui/material';
import { Link } from 'react-router-dom';
import { ApplicationUsageSchema } from '../../../../openapi';
export interface IApplicationUsageCellProps {
usage: ApplicationUsageSchema[] | undefined;
}
export interface IApplicationUsage {
project: string;
environments: string[];
}
const StyledLink = styled(Link)(() => ({
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
textDecoration: 'none',
'&:hover, &:focus': {
textDecoration: 'underline',
},
}));
const formatProject = (projectInfo: IApplicationUsage, index: number) => {
const separator = index !== 0 ? ', ' : '';
const projectElement =
projectInfo.project !== '*' ? (
<StyledLink to={`/projects/${projectInfo.project}`}>
{projectInfo.project}
</StyledLink>
) : (
projectInfo.project
);
const environments = ` (${projectInfo.environments.join(', ')})`;
return [separator, projectElement, environments];
};
export const ApplicationUsageCell = ({ usage }: IApplicationUsageCellProps) => {
const theme = useTheme();
const formattedProjects = usage?.flatMap((p, index) =>
formatProject(p, index)
);
return (
<TextCell>
<ConditionallyRender
condition={usage !== undefined && usage.length > 0}
show={
<Typography variant="body2">{formattedProjects}</Typography>
}
elseShow={
<Typography
variant="body2"
color={theme.palette.text.secondary}
>
not connected
</Typography>
}
/>
</TextCell>
);
};

View File

@ -3,6 +3,7 @@
* Do not edit manually. * Do not edit manually.
* See `gen:api` script in package.json * See `gen:api` script in package.json
*/ */
import type { ApplicationUsageSchema } from './applicationUsageSchema';
/** /**
* Data about an application that's connected to Unleash via an SDK. * Data about an application that's connected to Unleash via an SDK.
@ -22,4 +23,6 @@ export interface ApplicationSchema {
color?: string; color?: string;
/** An URL to an icon file to be used for the applications's entry in the application list */ /** An URL to an icon file to be used for the applications's entry in the application list */
icon?: string; icon?: string;
/** The list of projects the application has been using. */
usage?: ApplicationUsageSchema[];
} }

View File

@ -0,0 +1,15 @@
/**
* Generated by Orval
* Do not edit manually.
* See `gen:api` script in package.json
*/
/**
* Data about an project that have been used by applications.
*/
export interface ApplicationUsageSchema {
/** Name of the project */
project: string;
/** Which environments have been accessed in this project. */
environments: string[];
}

View File

@ -32,6 +32,8 @@ export interface ChangeRequestSchema {
segments: ChangeRequestSegmentChangeSchema[]; segments: ChangeRequestSegmentChangeSchema[];
/** A list of approvals that this change request has received. */ /** A list of approvals that this change request has received. */
approvals?: ChangeRequestApprovalSchema[]; approvals?: ChangeRequestApprovalSchema[];
/** A list of rejections that this change request has received. */
rejections?: ChangeRequestApprovalSchema[];
/** All comments that have been made on this change request. */ /** All comments that have been made on this change request. */
comments?: ChangeRequestCommentSchema[]; comments?: ChangeRequestCommentSchema[];
/** The user who created this change request. */ /** The user who created this change request. */

View File

@ -17,4 +17,5 @@ export const ChangeRequestSchemaState = {
Approved: 'Approved', Approved: 'Approved',
Applied: 'Applied', Applied: 'Applied',
Cancelled: 'Cancelled', Cancelled: 'Cancelled',
Rejected: 'Rejected',
} as const; } as const;

View File

@ -17,4 +17,5 @@ export const ChangeRequestStateSchemaState = {
Approved: 'Approved', Approved: 'Approved',
Applied: 'Applied', Applied: 'Applied',
Cancelled: 'Cancelled', Cancelled: 'Cancelled',
Rejected: 'Rejected',
} as const; } as const;

View File

@ -24,5 +24,4 @@ export interface ClientMetricsEnvSchema {
no?: number; no?: number;
/** How many times each variant was returned */ /** How many times each variant was returned */
variants?: ClientMetricsEnvSchemaVariants; variants?: ClientMetricsEnvSchemaVariants;
[key: string]: any;
} }

View File

@ -20,5 +20,4 @@ export interface CreateApplicationSchema {
color?: string; color?: string;
/** An URL to an icon file to be used for the applications's entry in the application list */ /** An URL to an icon file to be used for the applications's entry in the application list */
icon?: string; icon?: string;
[key: string]: any;
} }

View File

@ -19,5 +19,4 @@ export interface CreateGroupSchema {
rootRole?: number | null; rootRole?: number | null;
/** A list of users belonging to this group */ /** A list of users belonging to this group */
users?: CreateGroupSchemaUsersItem[]; users?: CreateGroupSchemaUsersItem[];
[key: string]: any;
} }

View File

@ -20,5 +20,4 @@ export interface CreateStrategyVariantSchema {
stickiness: string; stickiness: string;
/** Extra data configured for this variant */ /** Extra data configured for this variant */
payload?: CreateStrategyVariantSchemaPayload; payload?: CreateStrategyVariantSchemaPayload;
[key: string]: any;
} }

View File

@ -30,7 +30,7 @@ export interface CreateUserResponseSchema {
loginAttempts?: number; loginAttempts?: number;
/** Is the welcome email sent to the user or not */ /** Is the welcome email sent to the user or not */
emailSent?: boolean; emailSent?: boolean;
/** Which [root role](https://docs.getunleash.io/reference/rbac#standard-roles) this user is assigned. Usually a numeric role ID, but can be a string when returning newly created user with an explicit string role. */ /** Which [root role](https://docs.getunleash.io/reference/rbac#predefined-roles) this user is assigned. Usually a numeric role ID, but can be a string when returning newly created user with an explicit string role. */
rootRole?: CreateUserResponseSchemaRootRole; rootRole?: CreateUserResponseSchemaRootRole;
/** The last time this user logged in */ /** The last time this user logged in */
seenAt?: string | null; seenAt?: string | null;

View File

@ -5,7 +5,7 @@
*/ */
/** /**
* Which [root role](https://docs.getunleash.io/reference/rbac#standard-roles) this user is assigned. Usually a numeric role ID, but can be a string when returning newly created user with an explicit string role. * Which [root role](https://docs.getunleash.io/reference/rbac#predefined-roles) this user is assigned. Usually a numeric role ID, but can be a string when returning newly created user with an explicit string role.
*/ */
export type CreateUserResponseSchemaRootRole = export type CreateUserResponseSchemaRootRole =
| number | number

View File

@ -7,4 +7,4 @@
/** /**
* Extra associated data related to the event, such as feature toggle state, segment configuration, etc., if applicable. * Extra associated data related to the event, such as feature toggle state, segment configuration, etc., if applicable.
*/ */
export type EventSchemaData = { [key: string]: any } | null; export type EventSchemaData = { [key: string]: any };

View File

@ -7,4 +7,4 @@
/** /**
* Data relating to the previous state of the event's subject. * Data relating to the previous state of the event's subject.
*/ */
export type EventSchemaPreData = { [key: string]: any } | null; export type EventSchemaPreData = { [key: string]: any };

View File

@ -97,6 +97,7 @@ export const EventSchemaType = {
'change-added': 'change-added', 'change-added': 'change-added',
'change-discarded': 'change-discarded', 'change-discarded': 'change-discarded',
'change-edited': 'change-edited', 'change-edited': 'change-edited',
'change-request-rejected': 'change-request-rejected',
'change-request-approved': 'change-request-approved', 'change-request-approved': 'change-request-approved',
'change-request-approval-added': 'change-request-approval-added', 'change-request-approval-added': 'change-request-approval-added',
'change-request-cancelled': 'change-request-cancelled', 'change-request-cancelled': 'change-request-cancelled',

View File

@ -15,12 +15,10 @@ export type ExportQuerySchema =
environment: string; environment: string;
/** Whether to return a downloadable file */ /** Whether to return a downloadable file */
downloadFile?: boolean; downloadFile?: boolean;
[key: string]: any;
}) })
| (ExportQuerySchemaOneOfTwo & { | (ExportQuerySchemaOneOfTwo & {
/** The environment to export from */ /** The environment to export from */
environment: string; environment: string;
/** Whether to return a downloadable file */ /** Whether to return a downloadable file */
downloadFile?: boolean; downloadFile?: boolean;
[key: string]: any;
}); });

View File

@ -34,7 +34,10 @@ export interface FeatureSchema {
createdAt?: string | null; createdAt?: string | null;
/** The date the feature was archived */ /** The date the feature was archived */
archivedAt?: string | null; archivedAt?: string | null;
/** The date when metrics where last collected for the feature */ /**
* The date when metrics where last collected for the feature. This field is deprecated, use the one in featureEnvironmentSchema
* @deprecated
*/
lastSeenAt?: string | null; lastSeenAt?: string | null;
/** The list of environments where the feature can be used */ /** The list of environments where the feature can be used */
environments?: FeatureEnvironmentSchema[]; environments?: FeatureEnvironmentSchema[];

View File

@ -0,0 +1,14 @@
/**
* Generated by Orval
* Do not edit manually.
* See `gen:api` script in package.json
*/
export type GetApiTokensByName401 = {
/** The ID of the error instance */
id?: string;
/** The name of the error kind */
name?: string;
/** A description of what went wrong. */
message?: string;
};

View File

@ -0,0 +1,14 @@
/**
* Generated by Orval
* Do not edit manually.
* See `gen:api` script in package.json
*/
export type GetApiTokensByName403 = {
/** The ID of the error instance */
id?: string;
/** The name of the error kind */
name?: string;
/** A description of what went wrong. */
message?: string;
};

View File

@ -0,0 +1,14 @@
/**
* Generated by Orval
* Do not edit manually.
* See `gen:api` script in package.json
*/
export type GetRoleProjectAccess401 = {
/** The ID of the error instance */
id?: string;
/** The name of the error kind */
name?: string;
/** A description of what went wrong. */
message?: string;
};

View File

@ -0,0 +1,14 @@
/**
* Generated by Orval
* Do not edit manually.
* See `gen:api` script in package.json
*/
export type GetRoleProjectAccess403 = {
/** The ID of the error instance */
id?: string;
/** The name of the error kind */
name?: string;
/** A description of what went wrong. */
message?: string;
};

View File

@ -69,6 +69,7 @@ export * from './apiTokenSchema';
export * from './apiTokenSchemaType'; export * from './apiTokenSchemaType';
export * from './apiTokensSchema'; export * from './apiTokensSchema';
export * from './applicationSchema'; export * from './applicationSchema';
export * from './applicationUsageSchema';
export * from './applicationsSchema'; export * from './applicationsSchema';
export * from './archiveFeature401'; export * from './archiveFeature401';
export * from './archiveFeature403'; export * from './archiveFeature403';
@ -403,6 +404,8 @@ export * from './getAllFeatureTypes401';
export * from './getAllStrategies401'; export * from './getAllStrategies401';
export * from './getAllToggles401'; export * from './getAllToggles401';
export * from './getAllToggles403'; export * from './getAllToggles403';
export * from './getApiTokensByName401';
export * from './getApiTokensByName403';
export * from './getApplication404'; export * from './getApplication404';
export * from './getArchivedFeatures401'; export * from './getArchivedFeatures401';
export * from './getArchivedFeatures403'; export * from './getArchivedFeatures403';
@ -491,6 +494,8 @@ export * from './getRawFeatureMetrics404';
export * from './getRoleById400'; export * from './getRoleById400';
export * from './getRoleById401'; export * from './getRoleById401';
export * from './getRoleById404'; export * from './getRoleById404';
export * from './getRoleProjectAccess401';
export * from './getRoleProjectAccess403';
export * from './getRoles401'; export * from './getRoles401';
export * from './getRoles403'; export * from './getRoles403';
export * from './getSamlSettings400'; export * from './getSamlSettings400';
@ -637,6 +642,8 @@ export * from './projectCreatedSchemaMode';
export * from './projectEnvironmentSchema'; export * from './projectEnvironmentSchema';
export * from './projectOverviewSchema'; export * from './projectOverviewSchema';
export * from './projectOverviewSchemaMode'; export * from './projectOverviewSchemaMode';
export * from './projectRoleSchema';
export * from './projectRoleUsageSchema';
export * from './projectSchema'; export * from './projectSchema';
export * from './projectSchemaMode'; export * from './projectSchemaMode';
export * from './projectSettingsSchema'; export * from './projectSettingsSchema';

View File

@ -0,0 +1,21 @@
/**
* Generated by Orval
* Do not edit manually.
* See `gen:api` script in package.json
*/
/**
* An overview of how many users and groups are mapped to the specified project with the specified role.
*/
export interface ProjectRoleSchema {
/** The id of the project user and group count are counted for. */
project: string;
/** Id of the role the user and group count are counted for. */
role?: number;
/** Number of users mapped to this project. */
userCount?: number;
/** Number of service accounts mapped to this project. */
serviceAccountCount?: number;
/** Number of groups mapped to this project. */
groupCount?: number;
}

View File

@ -0,0 +1,14 @@
/**
* Generated by Orval
* Do not edit manually.
* See `gen:api` script in package.json
*/
import type { ProjectRoleSchema } from './projectRoleSchema';
/**
* A collection of projects with counts of users and groups mapped to them with specified roles.
*/
export interface ProjectRoleUsageSchema {
/** A collection of projects with counts of users and groups mapped to them with specified roles. */
projects?: ProjectRoleSchema[];
}

View File

@ -18,12 +18,7 @@ export interface SearchEventsSchema {
project?: string; project?: string;
/** Find events by feature toggle name (case-sensitive). */ /** Find events by feature toggle name (case-sensitive). */
feature?: string; feature?: string;
/** /** Find events by a free-text search query. The query will be matched against the event type, the username or email that created the event (if any), and the event data payload (if any). */
Find events by a free-text search query.
The query will be matched against the event type,
the username or email that created the event (if any),
and the event data payload (if any).
*/
query?: string; query?: string;
/** The maximum amount of events to return in the search result */ /** The maximum amount of events to return in the search result */
limit?: number; limit?: number;

View File

@ -97,6 +97,7 @@ export const SearchEventsSchemaType = {
'change-added': 'change-added', 'change-added': 'change-added',
'change-discarded': 'change-discarded', 'change-discarded': 'change-discarded',
'change-edited': 'change-edited', 'change-edited': 'change-edited',
'change-request-rejected': 'change-request-rejected',
'change-request-approved': 'change-request-approved', 'change-request-approved': 'change-request-approved',
'change-request-approval-added': 'change-request-approval-added', 'change-request-approval-added': 'change-request-approval-added',
'change-request-cancelled': 'change-request-cancelled', 'change-request-cancelled': 'change-request-cancelled',

View File

@ -44,5 +44,4 @@ export interface StateSchema {
segments?: SegmentSchema[]; segments?: SegmentSchema[];
/** A list of segment/strategy pairings */ /** A list of segment/strategy pairings */
featureStrategySegments?: FeatureStrategySegmentSchema[]; featureStrategySegments?: FeatureStrategySegmentSchema[];
[key: string]: any;
} }

View File

@ -10,5 +10,4 @@
export interface TokenStringListSchema { export interface TokenStringListSchema {
/** Tokens that we want to get access information about */ /** Tokens that we want to get access information about */
tokens: string[]; tokens: string[];
[key: string]: any;
} }

View File

@ -15,5 +15,4 @@ export interface UpdateUserSchema {
name?: string; name?: string;
/** The role to assign to the user. Can be either the role's ID or its unique name. */ /** The role to assign to the user. Can be either the role's ID or its unique name. */
rootRole?: UpdateUserSchemaRootRole; rootRole?: UpdateUserSchemaRootRole;
[key: string]: any;
} }

View File

@ -29,7 +29,7 @@ export interface UserSchema {
loginAttempts?: number; loginAttempts?: number;
/** Is the welcome email sent to the user or not */ /** Is the welcome email sent to the user or not */
emailSent?: boolean; emailSent?: boolean;
/** Which [root role](https://docs.getunleash.io/reference/rbac#standard-roles) this user is assigned */ /** Which [root role](https://docs.getunleash.io/reference/rbac#predefined-roles) this user is assigned */
rootRole?: number; rootRole?: number;
/** The last time this user logged in */ /** The last time this user logged in */
seenAt?: string | null; seenAt?: string | null;

View File

@ -12,6 +12,6 @@ import type { RoleSchema } from './roleSchema';
export interface UsersSchema { export interface UsersSchema {
/** A list of users in the Unleash instance. */ /** A list of users in the Unleash instance. */
users: UserSchema[]; users: UserSchema[];
/** A list of [root roles](https://docs.getunleash.io/reference/rbac#standard-roles) in the Unleash instance. */ /** A list of [root roles](https://docs.getunleash.io/reference/rbac#predefined-roles) in the Unleash instance. */
rootRoles?: RoleSchema[]; rootRoles?: RoleSchema[];
} }