1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01: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.
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`.
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.

View File

@ -1,13 +1,5 @@
import { useMemo } from 'react';
import {
Avatar,
CircularProgress,
Icon,
Link,
styled,
Typography,
useTheme,
} from '@mui/material';
import { Avatar, CircularProgress, Icon, Link } from '@mui/material';
import { Warning } from '@mui/icons-material';
import { styles as themeStyles } from 'component/common';
import { PageContent } from 'component/common/PageContent/PageContent';
@ -27,11 +19,11 @@ import { useGlobalFilter, useSortBy, useTable } from 'react-table';
import { sortTypes } from 'utils/sortTypes';
import { IconCell } from 'component/common/Table/cells/IconCell/IconCell';
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 = () => {
const { applications: data, loading } = useApplications();
const theme = useTheme();
const renderNoApplications = () => (
<>
@ -100,16 +92,11 @@ export const ApplicationList = () => {
Header: 'Project(environment)',
accessor: 'usage',
width: '50%',
Cell: () => (
<TextCell>
<Typography
variant="body2"
color={theme.palette.text.secondary}
>
not connected
</Typography>
</TextCell>
),
Cell: ({
row: { original },
}: {
row: { original: ApplicationSchema };
}) => <ApplicationUsageCell usage={original.usage} />,
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.
* See `gen:api` script in package.json
*/
import type { ApplicationUsageSchema } from './applicationUsageSchema';
/**
* Data about an application that's connected to Unleash via an SDK.
@ -22,4 +23,6 @@ export interface ApplicationSchema {
color?: string;
/** An URL to an icon file to be used for the applications's entry in the application list */
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[];
/** A list of approvals that this change request has received. */
approvals?: ChangeRequestApprovalSchema[];
/** A list of rejections that this change request has received. */
rejections?: ChangeRequestApprovalSchema[];
/** All comments that have been made on this change request. */
comments?: ChangeRequestCommentSchema[];
/** The user who created this change request. */

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -30,7 +30,7 @@ export interface CreateUserResponseSchema {
loginAttempts?: number;
/** Is the welcome email sent to the user or not */
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;
/** The last time this user logged in */
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 =
| number

View File

@ -7,4 +7,4 @@
/**
* 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.
*/
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-discarded': 'change-discarded',
'change-edited': 'change-edited',
'change-request-rejected': 'change-request-rejected',
'change-request-approved': 'change-request-approved',
'change-request-approval-added': 'change-request-approval-added',
'change-request-cancelled': 'change-request-cancelled',

View File

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

View File

@ -34,7 +34,10 @@ export interface FeatureSchema {
createdAt?: string | null;
/** The date the feature was archived */
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;
/** The list of environments where the feature can be used */
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 './apiTokensSchema';
export * from './applicationSchema';
export * from './applicationUsageSchema';
export * from './applicationsSchema';
export * from './archiveFeature401';
export * from './archiveFeature403';
@ -403,6 +404,8 @@ export * from './getAllFeatureTypes401';
export * from './getAllStrategies401';
export * from './getAllToggles401';
export * from './getAllToggles403';
export * from './getApiTokensByName401';
export * from './getApiTokensByName403';
export * from './getApplication404';
export * from './getArchivedFeatures401';
export * from './getArchivedFeatures403';
@ -491,6 +494,8 @@ export * from './getRawFeatureMetrics404';
export * from './getRoleById400';
export * from './getRoleById401';
export * from './getRoleById404';
export * from './getRoleProjectAccess401';
export * from './getRoleProjectAccess403';
export * from './getRoles401';
export * from './getRoles403';
export * from './getSamlSettings400';
@ -637,6 +642,8 @@ export * from './projectCreatedSchemaMode';
export * from './projectEnvironmentSchema';
export * from './projectOverviewSchema';
export * from './projectOverviewSchemaMode';
export * from './projectRoleSchema';
export * from './projectRoleUsageSchema';
export * from './projectSchema';
export * from './projectSchemaMode';
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;
/** Find events by feature toggle name (case-sensitive). */
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;
/** The maximum amount of events to return in the search result */
limit?: number;

View File

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

View File

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

View File

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

View File

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

View File

@ -29,7 +29,7 @@ export interface UserSchema {
loginAttempts?: number;
/** Is the welcome email sent to the user or not */
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;
/** The last time this user logged in */
seenAt?: string | null;

View File

@ -12,6 +12,6 @@ import type { RoleSchema } from './roleSchema';
export interface UsersSchema {
/** A list of users in the Unleash instance. */
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[];
}