1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-03-27 00:19:39 +01:00

feat: application usage new ui ()

This commit is contained in:
Jaanus Sellin 2023-08-18 14:55:23 +03:00 committed by GitHub
parent 4ce6c96e04
commit dbae2d1153
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 297 additions and 44 deletions

View File

@ -1,41 +1,37 @@
import { useEffect, useMemo, useState } from 'react';
import { CircularProgress, Link } from '@mui/material';
import { useMemo } from 'react';
import {
Avatar,
CircularProgress,
Icon,
Link,
styled,
Typography,
useTheme,
} from '@mui/material';
import { Warning } from '@mui/icons-material';
import { AppsLinkList, styles as themeStyles } from 'component/common';
import { styles as themeStyles } from 'component/common';
import { PageContent } from 'component/common/PageContent/PageContent';
import { PageHeader } from 'component/common/PageHeader/PageHeader';
import useApplications from 'hooks/api/getters/useApplications/useApplications';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useSearchParams } from 'react-router-dom';
import { Search } from 'component/common/Search/Search';
import { safeRegExp } from '@server/util/escape-regex';
type PageQueryType = Partial<Record<'search', string>>;
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import {
SortableTableHeader,
Table,
TableBody,
TableCell,
TableRow,
} from 'component/common/Table';
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';
export const ApplicationList = () => {
const { applications, loading } = useApplications();
const [searchParams, setSearchParams] = useSearchParams();
const [searchValue, setSearchValue] = useState(
searchParams.get('search') || ''
);
useEffect(() => {
const tableState: PageQueryType = {};
if (searchValue) {
tableState.search = searchValue;
}
setSearchParams(tableState, {
replace: true,
});
}, [searchValue, setSearchParams]);
const filteredApplications = useMemo(() => {
const regExp = safeRegExp(searchValue, 'i');
return searchValue
? applications?.filter(a => regExp.test(a.appName))
: applications;
}, [applications, searchValue]);
const { applications: data, loading } = useApplications();
const theme = useTheme();
const renderNoApplications = () => (
<>
@ -56,25 +52,115 @@ export const ApplicationList = () => {
</>
);
if (!filteredApplications) {
const initialState = useMemo(
() => ({
sortBy: [{ id: 'name', desc: false }],
hiddenColumns: ['description', 'sortOrder'],
}),
[]
);
const columns = useMemo(
() => [
{
id: 'Icon',
Cell: ({
row: {
original: { icon },
},
}: any) => (
<IconCell
icon={
<Avatar>
<Icon>{icon}</Icon>
</Avatar>
}
/>
),
disableGlobalFilter: true,
},
{
Header: 'Name',
accessor: 'appName',
width: '50%',
Cell: ({
row: {
original: { appName, description },
},
}: any) => (
<LinkCell
title={appName}
to={`/applications/${appName}`}
subtitle={description}
/>
),
sortType: 'alphanumeric',
},
{
Header: 'Project(environment)',
accessor: 'usage',
width: '50%',
Cell: () => (
<TextCell>
<Typography
variant="body2"
color={theme.palette.text.secondary}
>
not connected
</Typography>
</TextCell>
),
sortType: 'alphanumeric',
},
{
accessor: 'description',
disableSortBy: true,
},
{
accessor: 'sortOrder',
disableGlobalFilter: true,
sortType: 'number',
},
],
[]
);
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
state: { globalFilter },
setGlobalFilter,
} = useTable(
{
columns: columns as any[], // TODO: fix after `react-table` v8 update
data,
initialState,
sortTypes,
autoResetGlobalFilter: false,
autoResetSortBy: false,
disableSortRemove: true,
},
useGlobalFilter,
useSortBy
);
if (!data) {
return <CircularProgress variant="indeterminate" />;
}
let applicationCount =
filteredApplications.length < applications.length
? `${filteredApplications.length} of ${applications.length}`
: applications.length;
return (
<>
<PageContent
header={
<PageHeader
title={`Applications (${applicationCount})`}
title={`Applications (${rows.length})`}
actions={
<Search
initialValue={searchValue}
onChange={setSearchValue}
initialValue={globalFilter}
onChange={setGlobalFilter}
/>
}
/>
@ -82,8 +168,37 @@ export const ApplicationList = () => {
>
<div className={themeStyles.fullwidth}>
<ConditionallyRender
condition={filteredApplications.length > 0}
show={<AppsLinkList apps={filteredApplications} />}
condition={data.length > 0}
show={
<SearchHighlightProvider value={globalFilter}>
<Table {...getTableProps()}>
<SortableTableHeader
headerGroups={headerGroups}
/>
<TableBody {...getTableBodyProps()}>
{rows.map(row => {
prepareRow(row);
return (
<TableRow
hover
{...row.getRowProps()}
>
{row.cells.map(cell => (
<TableCell
{...cell.getCellProps()}
>
{cell.render(
'Cell'
)}
</TableCell>
))}
</TableRow>
);
})}
</TableBody>
</Table>
</SearchHighlightProvider>
}
elseShow={
<ConditionallyRender
condition={loading}

View File

@ -0,0 +1,99 @@
import { useEffect, useMemo, useState } from 'react';
import { CircularProgress, Link } from '@mui/material';
import { Warning } from '@mui/icons-material';
import { AppsLinkList, styles as themeStyles } from 'component/common';
import { PageContent } from 'component/common/PageContent/PageContent';
import { PageHeader } from 'component/common/PageHeader/PageHeader';
import useApplications from 'hooks/api/getters/useApplications/useApplications';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useSearchParams } from 'react-router-dom';
import { Search } from 'component/common/Search/Search';
import { safeRegExp } from '@server/util/escape-regex';
type PageQueryType = Partial<Record<'search', string>>;
export const OldApplicationList = () => {
const { applications, loading } = useApplications();
const [searchParams, setSearchParams] = useSearchParams();
const [searchValue, setSearchValue] = useState(
searchParams.get('search') || ''
);
useEffect(() => {
const tableState: PageQueryType = {};
if (searchValue) {
tableState.search = searchValue;
}
setSearchParams(tableState, {
replace: true,
});
}, [searchValue, setSearchParams]);
const filteredApplications = useMemo(() => {
const regExp = safeRegExp(searchValue, 'i');
return searchValue
? applications?.filter(a => regExp.test(a.appName))
: applications;
}, [applications, searchValue]);
const renderNoApplications = () => (
<>
<section style={{ textAlign: 'center' }}>
<Warning titleAccess="Warning" /> <br />
<br />
Oh snap, it does not seem like you have connected any
applications. To connect your application to Unleash you will
require a Client SDK.
<br />
<br />
You can read more about how to use Unleash in your application
in the{' '}
<Link href="https://docs.getunleash.io/docs/sdks/">
documentation.
</Link>
</section>
</>
);
if (!filteredApplications) {
return <CircularProgress variant="indeterminate" />;
}
let applicationCount =
filteredApplications.length < applications.length
? `${filteredApplications.length} of ${applications.length}`
: applications.length;
return (
<>
<PageContent
header={
<PageHeader
title={`Applications (${applicationCount})`}
actions={
<Search
initialValue={searchValue}
onChange={setSearchValue}
/>
}
/>
}
>
<div className={themeStyles.fullwidth}>
<ConditionallyRender
condition={filteredApplications.length > 0}
show={<AppsLinkList apps={filteredApplications} />}
elseShow={
<ConditionallyRender
condition={loading}
show={<div>...loading</div>}
elseShow={renderNoApplications()}
/>
}
/>
</div>
</PageContent>
</>
);
};

View File

@ -0,0 +1,36 @@
import { useMemo } from 'react';
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';
import { PageHeader } from 'component/common/PageHeader/PageHeader';
import useApplications from 'hooks/api/getters/useApplications/useApplications';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { Search } from 'component/common/Search/Search';
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import {
SortableTableHeader,
Table,
TableBody,
TableCell,
TableRow,
} from '../../common/Table';
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 useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
import { ApplicationList } from './ApplicationList';
import { OldApplicationList } from './OldApplicationList';
export const TemporaryApplicationListWrapper = () => {
const { uiConfig } = useUiConfig();
return (
<ConditionallyRender
condition={Boolean(uiConfig.flags.newApplicationList)}
show={<ApplicationList />}
elseShow={<OldApplicationList />}
/>
);
};

View File

@ -19,7 +19,6 @@ import EditProject from 'component/project/Project/EditProject/EditProject';
import CreateFeature from 'component/feature/CreateFeature/CreateFeature';
import EditFeature from 'component/feature/EditFeature/EditFeature';
import { ApplicationEdit } from 'component/application/ApplicationEdit/ApplicationEdit';
import { ApplicationList } from 'component/application/ApplicationList/ApplicationList';
import ContextList from 'component/context/ContextList/ContextList/ContextList';
import RedirectFeatureView from 'component/feature/RedirectFeatureView/RedirectFeatureView';
import { CreateAddon } from 'component/addons/CreateAddon/CreateAddon';
@ -44,6 +43,7 @@ import { LazyAdmin } from 'component/admin/LazyAdmin';
import { LazyProject } from 'component/project/Project/LazyProject';
import { LoginHistory } from 'component/loginHistory/LoginHistory';
import { FeatureTypesList } from 'component/featureTypes/FeatureTypesList';
import { TemporaryApplicationListWrapper } from 'component/application/ApplicationList/TemporaryApplicationListWrapper';
export const routes: IRoute[] = [
// Splash
@ -179,7 +179,7 @@ export const routes: IRoute[] = [
{
path: '/applications',
title: 'Applications',
component: ApplicationList,
component: TemporaryApplicationListWrapper,
type: 'protected',
menu: { mobile: true, advanced: true },
},

View File

@ -58,6 +58,7 @@ export interface IFlags {
segmentChangeRequests?: boolean;
changeRequestReject?: boolean;
lastSeenByEnvironment?: boolean;
newApplicationList?: boolean;
}
export interface IVersionInfo {

View File

@ -30,7 +30,8 @@ export type IFlagKey =
| 'lastSeenByEnvironment'
| 'segmentChangeRequests'
| 'changeRequestReject'
| 'customRootRolesKillSwitch';
| 'customRootRolesKillSwitch'
| 'newApplicationList';
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;

View File

@ -45,6 +45,7 @@ process.nextTick(async () => {
frontendNavigationUpdate: true,
lastSeenByEnvironment: true,
segmentChangeRequests: true,
newApplicationList: true,
},
},
authentication: {