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

feat: show archived toggles on a project level (#942)

* feat: show archived toggles on a project level

* Update src/component/feature/FeatureToggleList/FeatureToggleListActions/FeatureToggleListActions.tsx

Co-authored-by: Fredrik Strand Oseberg <fredrik.no@gmail.com>

* refactor: adapt code to PR comments, clarity

Co-authored-by: Fredrik Strand Oseberg <fredrik.no@gmail.com>
This commit is contained in:
Nuno Góis 2022-05-03 15:27:43 +01:00 committed by GitHub
parent b4eed811a1
commit 9ffc421252
20 changed files with 219 additions and 139 deletions

View File

@ -0,0 +1,58 @@
import { FC } from 'react';
import { useProjectFeaturesArchive } from 'hooks/api/getters/useProjectFeaturesArchive/useProjectFeaturesArchive';
import { FeatureToggleList } from '../feature/FeatureToggleList/FeatureToggleList';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { useFeaturesFilter } from 'hooks/useFeaturesFilter';
import { useFeatureArchiveApi } from 'hooks/api/actions/useFeatureArchiveApi/useReviveFeatureApi';
import useToast from 'hooks/useToast';
import { useFeaturesSort } from 'hooks/useFeaturesSort';
interface IProjectFeaturesArchiveList {
projectId: string;
}
export const ProjectFeaturesArchiveList: FC<IProjectFeaturesArchiveList> = ({
projectId,
}) => {
const { setToastData, setToastApiError } = useToast();
const { uiConfig } = useUiConfig();
const { reviveFeature } = useFeatureArchiveApi();
const {
archivedFeatures = [],
refetchArchived,
loading,
} = useProjectFeaturesArchive(projectId);
const { filtered, filter, setFilter } = useFeaturesFilter(archivedFeatures);
const { sorted, sort, setSort } = useFeaturesSort(filtered);
const onRevive = (feature: string) => {
reviveFeature(feature)
.then(refetchArchived)
.then(() =>
setToastData({
type: 'success',
title: "And we're back!",
text: 'The feature toggle has been revived.',
confetti: true,
})
)
.catch(e => setToastApiError(e.toString()));
};
return (
<FeatureToggleList
features={sorted}
loading={loading}
onRevive={onRevive}
flags={uiConfig.flags}
filter={filter}
setFilter={setFilter}
sort={sort}
setSort={setSort}
isArchive
inProject={Boolean(projectId)}
/>
);
};

View File

@ -28,6 +28,7 @@ interface IFeatureToggleListProps {
sort: IFeaturesSort;
setSort: Dispatch<SetStateAction<IFeaturesSort>>;
onRevive?: (feature: string) => void;
inProject?: boolean;
isArchive?: boolean;
}
@ -52,6 +53,7 @@ const loadingFeaturesPlaceholder: FeatureSchema[] = Array(10)
export const FeatureToggleList: VFC<IFeatureToggleListProps> = ({
features,
onRevive,
inProject,
isArchive,
loading,
flags,
@ -95,6 +97,7 @@ export const FeatureToggleList: VFC<IFeatureToggleListProps> = ({
onRevive={onRevive}
hasAccess={hasAccess}
flags={flags}
inProject={inProject}
/>
))}
elseShow={
@ -128,12 +131,18 @@ export const FeatureToggleList: VFC<IFeatureToggleListProps> = ({
: '';
const headerTitle = isArchive
? `Archived Features ${searchResultsHeader}`
? inProject
? `Project Archived Features ${searchResultsHeader}`
: `Archived Features ${searchResultsHeader}`
: `Features ${searchResultsHeader}`;
return (
<div>
<div className={styles.searchBarContainer}>
<div
className={classnames(styles.searchBarContainer, {
dense: inProject,
})}
>
<SearchField
initialValue={filter.query}
updateValue={setFilterQuery}
@ -164,6 +173,7 @@ export const FeatureToggleList: VFC<IFeatureToggleListProps> = ({
sort={sort}
setSort={setSort}
loading={loading}
inProject={inProject}
/>
}
/>

View File

@ -13,7 +13,7 @@ import {
import { useStyles } from './styles';
import { IFeaturesFilter } from 'hooks/useFeaturesFilter';
const sortOptions = createFeaturesFilterSortOptions();
let sortOptions = createFeaturesFilterSortOptions();
interface IFeatureToggleListActionsProps {
filter: IFeaturesFilter;
@ -21,6 +21,7 @@ interface IFeatureToggleListActionsProps {
sort: IFeaturesSort;
setSort: Dispatch<SetStateAction<IFeaturesSort>>;
loading?: boolean;
inProject?: boolean;
}
export const FeatureToggleListActions: VFC<IFeatureToggleListActionsProps> = ({
@ -29,6 +30,7 @@ export const FeatureToggleListActions: VFC<IFeatureToggleListActionsProps> = ({
sort,
setSort,
loading = false,
inProject,
}) => {
const { classes: styles } = useStyles();
const { uiConfig } = useUiConfig();
@ -46,6 +48,10 @@ export const FeatureToggleListActions: VFC<IFeatureToggleListActionsProps> = ({
const selectedOption =
sortOptions.find(o => o.type === sort.type) || sortOptions[0];
if (inProject) {
sortOptions = sortOptions.filter(option => option.type !== 'project');
}
const renderSortingOptions = () =>
sortOptions.map(option => (
<MenuItem
@ -73,7 +79,7 @@ export const FeatureToggleListActions: VFC<IFeatureToggleListActionsProps> = ({
data-loading
/>
<ConditionallyRender
condition={uiConfig.flags.P}
condition={uiConfig.flags.P && !inProject}
show={
<ProjectSelect
currentProjectId={filter.project}

View File

@ -23,11 +23,20 @@ interface IFeatureToggleListItemProps {
onRevive?: (id: string) => void;
hasAccess: IAccessContext['hasAccess'];
flags?: IFlags;
inProject?: boolean;
className?: string;
}
export const FeatureToggleListItem = memo<IFeatureToggleListItemProps>(
({ feature, onRevive, hasAccess, flags = {}, className, ...rest }) => {
({
feature,
onRevive,
hasAccess,
flags = {},
inProject,
className,
...rest
}) => {
const { classes: styles } = useStyles();
const { projects } = useProjects();
@ -153,21 +162,30 @@ export const FeatureToggleListItem = memo<IFeatureToggleListItemProps>(
)}
>
<StatusChip stale={Boolean(stale)} showActive={false} />
<ConditionallyRender
condition={!inProject}
show={
<Link
to={`/projects/${project}`}
style={{ textDecoration: 'none' }}
className={classnames({
[`${styles.disabledLink}`]: !projectExists(),
[`${styles.disabledLink}`]:
!projectExists(),
})}
>
<Chip
color="primary"
variant="outlined"
style={{ marginLeft: '8px', cursor: 'pointer' }}
style={{
marginLeft: '8px',
cursor: 'pointer',
}}
title={`Project: ${project}`}
label={project}
/>
</Link>
}
/>
</span>
<ConditionallyRender
condition={isArchive}

View File

@ -17,6 +17,9 @@ export const useStyles = makeStyles()(theme => ({
[theme.breakpoints.down('sm')]: {
display: 'block',
},
'&.dense': {
marginBottom: '1rem',
},
},
searchBar: {
minWidth: '450px',

View File

@ -11,6 +11,7 @@ import useQueryParams from 'hooks/useQueryParams';
import { useEffect } from 'react';
import { ProjectAccess } from '../ProjectAccess/ProjectAccess';
import ProjectEnvironment from '../ProjectEnvironment/ProjectEnvironment';
import { ProjectFeaturesArchive } from './ProjectFeaturesArchive/ProjectFeaturesArchive';
import ProjectOverview from './ProjectOverview';
import ProjectHealth from './ProjectHealth/ProjectHealth';
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
@ -52,6 +53,12 @@ const Project = () => {
path: `${basePath}/environments`,
name: 'environments',
},
{
title: 'Archive',
component: <ProjectFeaturesArchive projectId={id} />,
path: `${basePath}/archive`,
name: 'archive',
},
];
const activeTabIdx = activeTab

View File

@ -0,0 +1,14 @@
import { ProjectFeaturesArchiveList } from 'component/archive/ProjectFeaturesArchiveList';
import { usePageTitle } from 'hooks/usePageTitle';
interface IProjectFeaturesArchiveProps {
projectId: string;
}
export const ProjectFeaturesArchive = ({
projectId,
}: IProjectFeaturesArchiveProps) => {
usePageTitle('Project Archived Features');
return <ProjectFeaturesArchiveList projectId={projectId} />;
};

View File

@ -1,7 +1,10 @@
import useSWR, { SWRConfiguration, mutate } from 'swr';
import { useCallback } from 'react';
type CacheKey = 'apiAdminFeaturesGet' | 'apiAdminArchiveFeaturesGet';
type CacheKey =
| 'apiAdminFeaturesGet'
| 'apiAdminArchiveFeaturesGet'
| ['apiAdminArchiveFeaturesGet', string?];
interface IUseApiGetterOutput<T> {
data?: T;

View File

@ -0,0 +1,33 @@
import { openApiAdmin } from 'utils/openapiClient';
import { FeatureSchema } from 'openapi';
import { useApiGetter } from 'hooks/api/getters/useApiGetter/useApiGetter';
export interface IUseProjectFeaturesArchiveOutput {
archivedFeatures?: FeatureSchema[];
refetchArchived: () => void;
loading: boolean;
error?: Error;
}
export const useProjectFeaturesArchive = (
projectId: string
): IUseProjectFeaturesArchiveOutput => {
const { data, refetch, loading, error } = useApiGetter(
['apiAdminArchiveFeaturesGet', projectId],
() => {
if (projectId) {
return openApiAdmin.apiAdminArchiveFeaturesProjectIdGet({
projectId,
});
}
return openApiAdmin.apiAdminArchiveFeaturesGet();
}
);
return {
archivedFeatures: data?.features,
refetchArchived: refetch,
loading,
error,
};
};

View File

@ -4,7 +4,7 @@
* Unleash API
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 4.10.0-beta.1
* The version of the OpenAPI document: 4.10.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
@ -15,9 +15,6 @@
import * as runtime from '../runtime';
import {
ChangeProjectSchema,
ChangeProjectSchemaFromJSON,
ChangeProjectSchemaToJSON,
CreateFeatureSchema,
CreateFeatureSchemaFromJSON,
CreateFeatureSchemaToJSON,
@ -29,10 +26,8 @@ import {
FeaturesSchemaToJSON,
} from '../models';
export interface ApiAdminProjectsProjectIdFeaturesFeatureNameChangeProjectPostRequest {
export interface ApiAdminArchiveFeaturesProjectIdGetRequest {
projectId: string;
featureName: string;
changeProjectSchema: ChangeProjectSchema;
}
export interface ApiAdminProjectsProjectIdFeaturesFeatureNameGetRequest {
@ -82,6 +77,38 @@ export class AdminApi extends runtime.BaseAPI {
return await response.value();
}
/**
*/
async apiAdminArchiveFeaturesProjectIdGetRaw(requestParameters: ApiAdminArchiveFeaturesProjectIdGetRequest, initOverrides?: RequestInit): Promise<runtime.ApiResponse<FeaturesSchema>> {
if (requestParameters.projectId === null || requestParameters.projectId === undefined) {
throw new runtime.RequiredError('projectId','Required parameter requestParameters.projectId was null or undefined when calling apiAdminArchiveFeaturesProjectIdGet.');
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
if (this.configuration && this.configuration.apiKey) {
headerParameters["Authorization"] = this.configuration.apiKey("Authorization"); // apiKey authentication
}
const response = await this.request({
path: `/api/admin/archive/features/{projectId}`.replace(`{${"projectId"}}`, encodeURIComponent(String(requestParameters.projectId))),
method: 'GET',
headers: headerParameters,
query: queryParameters,
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => FeaturesSchemaFromJSON(jsonValue));
}
/**
*/
async apiAdminArchiveFeaturesProjectIdGet(requestParameters: ApiAdminArchiveFeaturesProjectIdGetRequest, initOverrides?: RequestInit): Promise<FeaturesSchema> {
const response = await this.apiAdminArchiveFeaturesProjectIdGetRaw(requestParameters, initOverrides);
return await response.value();
}
/**
*/
async apiAdminFeaturesGetRaw(initOverrides?: RequestInit): Promise<runtime.ApiResponse<FeaturesSchema>> {
@ -110,48 +137,6 @@ export class AdminApi extends runtime.BaseAPI {
return await response.value();
}
/**
*/
async apiAdminProjectsProjectIdFeaturesFeatureNameChangeProjectPostRaw(requestParameters: ApiAdminProjectsProjectIdFeaturesFeatureNameChangeProjectPostRequest, initOverrides?: RequestInit): Promise<runtime.ApiResponse<void>> {
if (requestParameters.projectId === null || requestParameters.projectId === undefined) {
throw new runtime.RequiredError('projectId','Required parameter requestParameters.projectId was null or undefined when calling apiAdminProjectsProjectIdFeaturesFeatureNameChangeProjectPost.');
}
if (requestParameters.featureName === null || requestParameters.featureName === undefined) {
throw new runtime.RequiredError('featureName','Required parameter requestParameters.featureName was null or undefined when calling apiAdminProjectsProjectIdFeaturesFeatureNameChangeProjectPost.');
}
if (requestParameters.changeProjectSchema === null || requestParameters.changeProjectSchema === undefined) {
throw new runtime.RequiredError('changeProjectSchema','Required parameter requestParameters.changeProjectSchema was null or undefined when calling apiAdminProjectsProjectIdFeaturesFeatureNameChangeProjectPost.');
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
headerParameters['Content-Type'] = 'application/json';
if (this.configuration && this.configuration.apiKey) {
headerParameters["Authorization"] = this.configuration.apiKey("Authorization"); // apiKey authentication
}
const response = await this.request({
path: `/api/admin/projects/{projectId}/features/{featureName}/changeProject`.replace(`{${"projectId"}}`, encodeURIComponent(String(requestParameters.projectId))).replace(`{${"featureName"}}`, encodeURIComponent(String(requestParameters.featureName))),
method: 'POST',
headers: headerParameters,
query: queryParameters,
body: ChangeProjectSchemaToJSON(requestParameters.changeProjectSchema),
}, initOverrides);
return new runtime.VoidApiResponse(response);
}
/**
*/
async apiAdminProjectsProjectIdFeaturesFeatureNameChangeProjectPost(requestParameters: ApiAdminProjectsProjectIdFeaturesFeatureNameChangeProjectPostRequest, initOverrides?: RequestInit): Promise<void> {
await this.apiAdminProjectsProjectIdFeaturesFeatureNameChangeProjectPostRaw(requestParameters, initOverrides);
}
/**
*/
async apiAdminProjectsProjectIdFeaturesFeatureNameGetRaw(requestParameters: ApiAdminProjectsProjectIdFeaturesFeatureNameGetRequest, initOverrides?: RequestInit): Promise<runtime.ApiResponse<FeatureSchema>> {

View File

@ -1,56 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* Unleash API
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 4.10.0-beta.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { exists, mapValues } from '../runtime';
/**
*
* @export
* @interface ChangeProjectSchema
*/
export interface ChangeProjectSchema {
/**
*
* @type {string}
* @memberof ChangeProjectSchema
*/
newProjectId: string;
}
export function ChangeProjectSchemaFromJSON(json: any): ChangeProjectSchema {
return ChangeProjectSchemaFromJSONTyped(json, false);
}
export function ChangeProjectSchemaFromJSONTyped(json: any, ignoreDiscriminator: boolean): ChangeProjectSchema {
if ((json === undefined) || (json === null)) {
return json;
}
return {
'newProjectId': json['newProjectId'],
};
}
export function ChangeProjectSchemaToJSON(value?: ChangeProjectSchema | null): any {
if (value === undefined) {
return undefined;
}
if (value === null) {
return null;
}
return {
'newProjectId': value.newProjectId,
};
}

View File

@ -4,7 +4,7 @@
* Unleash API
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 4.10.0-beta.1
* The version of the OpenAPI document: 4.10.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@ -4,7 +4,7 @@
* Unleash API
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 4.10.0-beta.1
* The version of the OpenAPI document: 4.10.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@ -4,7 +4,7 @@
* Unleash API
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 4.10.0-beta.1
* The version of the OpenAPI document: 4.10.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@ -4,7 +4,7 @@
* Unleash API
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 4.10.0-beta.1
* The version of the OpenAPI document: 4.10.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@ -4,7 +4,7 @@
* Unleash API
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 4.10.0-beta.1
* The version of the OpenAPI document: 4.10.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@ -4,7 +4,7 @@
* Unleash API
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 4.10.0-beta.1
* The version of the OpenAPI document: 4.10.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@ -4,7 +4,7 @@
* Unleash API
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 4.10.0-beta.1
* The version of the OpenAPI document: 4.10.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@ -1,6 +1,5 @@
/* tslint:disable */
/* eslint-disable */
export * from './ChangeProjectSchema';
export * from './ConstraintSchema';
export * from './CreateFeatureSchema';
export * from './FeatureSchema';

View File

@ -4,7 +4,7 @@
* Unleash API
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 4.10.0-beta.1
* The version of the OpenAPI document: 4.10.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
@ -80,21 +80,21 @@ export class BaseAPI {
let fetchParams = { url, init };
for (const middleware of this.middleware) {
if (middleware.pre) {
fetchParams = (await middleware.pre({
fetchParams = await middleware.pre({
fetch: this.fetchApi,
...fetchParams,
})) || fetchParams;
}) || fetchParams;
}
}
let response = await (this.configuration.fetchApi || fetch)(fetchParams.url, fetchParams.init);
for (const middleware of this.middleware) {
if (middleware.post) {
response = (await middleware.post({
response = await middleware.post({
fetch: this.fetchApi,
url: fetchParams.url,
init: fetchParams.init,
response: response.clone(),
})) || response;
}) || response;
}
}
return response;