mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-29 01:15:48 +02:00
feat: application usage new ui (#4528)
This commit is contained in:
parent
4ce6c96e04
commit
dbae2d1153
@ -1,41 +1,37 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { CircularProgress, Link } from '@mui/material';
|
import {
|
||||||
|
Avatar,
|
||||||
|
CircularProgress,
|
||||||
|
Icon,
|
||||||
|
Link,
|
||||||
|
styled,
|
||||||
|
Typography,
|
||||||
|
useTheme,
|
||||||
|
} from '@mui/material';
|
||||||
import { Warning } from '@mui/icons-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 { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
import useApplications from 'hooks/api/getters/useApplications/useApplications';
|
import useApplications from 'hooks/api/getters/useApplications/useApplications';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
|
||||||
import { Search } from 'component/common/Search/Search';
|
import { Search } from 'component/common/Search/Search';
|
||||||
import { safeRegExp } from '@server/util/escape-regex';
|
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||||
|
import {
|
||||||
type PageQueryType = Partial<Record<'search', string>>;
|
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 = () => {
|
export const ApplicationList = () => {
|
||||||
const { applications, loading } = useApplications();
|
const { applications: data, loading } = useApplications();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const theme = useTheme();
|
||||||
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 = () => (
|
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" />;
|
return <CircularProgress variant="indeterminate" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
let applicationCount =
|
|
||||||
filteredApplications.length < applications.length
|
|
||||||
? `${filteredApplications.length} of ${applications.length}`
|
|
||||||
: applications.length;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageContent
|
<PageContent
|
||||||
header={
|
header={
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={`Applications (${applicationCount})`}
|
title={`Applications (${rows.length})`}
|
||||||
actions={
|
actions={
|
||||||
<Search
|
<Search
|
||||||
initialValue={searchValue}
|
initialValue={globalFilter}
|
||||||
onChange={setSearchValue}
|
onChange={setGlobalFilter}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -82,8 +168,37 @@ export const ApplicationList = () => {
|
|||||||
>
|
>
|
||||||
<div className={themeStyles.fullwidth}>
|
<div className={themeStyles.fullwidth}>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={filteredApplications.length > 0}
|
condition={data.length > 0}
|
||||||
show={<AppsLinkList apps={filteredApplications} />}
|
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={
|
elseShow={
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={loading}
|
condition={loading}
|
||||||
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -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 />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -19,7 +19,6 @@ import EditProject from 'component/project/Project/EditProject/EditProject';
|
|||||||
import CreateFeature from 'component/feature/CreateFeature/CreateFeature';
|
import CreateFeature from 'component/feature/CreateFeature/CreateFeature';
|
||||||
import EditFeature from 'component/feature/EditFeature/EditFeature';
|
import EditFeature from 'component/feature/EditFeature/EditFeature';
|
||||||
import { ApplicationEdit } from 'component/application/ApplicationEdit/ApplicationEdit';
|
import { ApplicationEdit } from 'component/application/ApplicationEdit/ApplicationEdit';
|
||||||
import { ApplicationList } from 'component/application/ApplicationList/ApplicationList';
|
|
||||||
import ContextList from 'component/context/ContextList/ContextList/ContextList';
|
import ContextList from 'component/context/ContextList/ContextList/ContextList';
|
||||||
import RedirectFeatureView from 'component/feature/RedirectFeatureView/RedirectFeatureView';
|
import RedirectFeatureView from 'component/feature/RedirectFeatureView/RedirectFeatureView';
|
||||||
import { CreateAddon } from 'component/addons/CreateAddon/CreateAddon';
|
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 { LazyProject } from 'component/project/Project/LazyProject';
|
||||||
import { LoginHistory } from 'component/loginHistory/LoginHistory';
|
import { LoginHistory } from 'component/loginHistory/LoginHistory';
|
||||||
import { FeatureTypesList } from 'component/featureTypes/FeatureTypesList';
|
import { FeatureTypesList } from 'component/featureTypes/FeatureTypesList';
|
||||||
|
import { TemporaryApplicationListWrapper } from 'component/application/ApplicationList/TemporaryApplicationListWrapper';
|
||||||
|
|
||||||
export const routes: IRoute[] = [
|
export const routes: IRoute[] = [
|
||||||
// Splash
|
// Splash
|
||||||
@ -179,7 +179,7 @@ export const routes: IRoute[] = [
|
|||||||
{
|
{
|
||||||
path: '/applications',
|
path: '/applications',
|
||||||
title: 'Applications',
|
title: 'Applications',
|
||||||
component: ApplicationList,
|
component: TemporaryApplicationListWrapper,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
menu: { mobile: true, advanced: true },
|
menu: { mobile: true, advanced: true },
|
||||||
},
|
},
|
||||||
|
@ -58,6 +58,7 @@ export interface IFlags {
|
|||||||
segmentChangeRequests?: boolean;
|
segmentChangeRequests?: boolean;
|
||||||
changeRequestReject?: boolean;
|
changeRequestReject?: boolean;
|
||||||
lastSeenByEnvironment?: boolean;
|
lastSeenByEnvironment?: boolean;
|
||||||
|
newApplicationList?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IVersionInfo {
|
export interface IVersionInfo {
|
||||||
|
@ -30,7 +30,8 @@ export type IFlagKey =
|
|||||||
| 'lastSeenByEnvironment'
|
| 'lastSeenByEnvironment'
|
||||||
| 'segmentChangeRequests'
|
| 'segmentChangeRequests'
|
||||||
| 'changeRequestReject'
|
| 'changeRequestReject'
|
||||||
| 'customRootRolesKillSwitch';
|
| 'customRootRolesKillSwitch'
|
||||||
|
| 'newApplicationList';
|
||||||
|
|
||||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||||
|
|
||||||
|
@ -45,6 +45,7 @@ process.nextTick(async () => {
|
|||||||
frontendNavigationUpdate: true,
|
frontendNavigationUpdate: true,
|
||||||
lastSeenByEnvironment: true,
|
lastSeenByEnvironment: true,
|
||||||
segmentChangeRequests: true,
|
segmentChangeRequests: true,
|
||||||
|
newApplicationList: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
authentication: {
|
authentication: {
|
||||||
|
Loading…
Reference in New Issue
Block a user