mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-09 01:17:06 +02:00
Merge pull request #672 from Unleash/refactor/applications
refactor: application
This commit is contained in:
commit
b77c4a84f0
@ -0,0 +1,149 @@
|
|||||||
|
/* eslint react/no-multi-comp:off */
|
||||||
|
import { useContext, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Link,
|
||||||
|
Icon,
|
||||||
|
IconButton,
|
||||||
|
LinearProgress,
|
||||||
|
Typography,
|
||||||
|
} from '@material-ui/core';
|
||||||
|
import { Link as LinkIcon } from '@material-ui/icons';
|
||||||
|
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { formatDateWithLocale } from '../../common/util';
|
||||||
|
import { UPDATE_APPLICATION } from '../../providers/AccessProvider/permissions';
|
||||||
|
import { ApplicationView } from '../ApplicationView/ApplicationView';
|
||||||
|
import { ApplicationUpdate } from '../ApplicationUpdate/ApplicationUpdate';
|
||||||
|
import TabNav from '../../common/TabNav/TabNav';
|
||||||
|
import Dialogue from '../../common/Dialogue';
|
||||||
|
import PageContent from '../../common/PageContent';
|
||||||
|
import HeaderTitle from '../../common/HeaderTitle';
|
||||||
|
import AccessContext from '../../../contexts/AccessContext';
|
||||||
|
import useApplicationsApi from '../../../hooks/api/actions/useApplicationsApi/useApplicationsApi';
|
||||||
|
import useApplication from '../../../hooks/api/getters/useApplication/useApplication';
|
||||||
|
import { useHistory, useParams } from 'react-router-dom';
|
||||||
|
import { useLocationSettings } from '../../../hooks/useLocationSettings';
|
||||||
|
import useToast from '../../../hooks/useToast';
|
||||||
|
import PermissionButton from '../../common/PermissionButton/PermissionButton';
|
||||||
|
|
||||||
|
export const ApplicationEdit = () => {
|
||||||
|
const history = useHistory();
|
||||||
|
const { name } = useParams<{ name: string }>();
|
||||||
|
const { application, loading } = useApplication(name);
|
||||||
|
const { appName, url, description, icon = 'apps', createdAt } = application;
|
||||||
|
const { hasAccess } = useContext(AccessContext);
|
||||||
|
const { deleteApplication } = useApplicationsApi();
|
||||||
|
const { locationSettings } = useLocationSettings();
|
||||||
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
|
||||||
|
const [showDialog, setShowDialog] = useState(false);
|
||||||
|
|
||||||
|
const toggleModal = () => {
|
||||||
|
setShowDialog(!showDialog);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (v: string) =>
|
||||||
|
formatDateWithLocale(v, locationSettings.locale);
|
||||||
|
|
||||||
|
const onDeleteApplication = async (evt: Event) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
try {
|
||||||
|
await deleteApplication(appName);
|
||||||
|
setToastData({
|
||||||
|
title: 'Deleted Successfully',
|
||||||
|
text: 'Application deleted successfully',
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
history.push('/applications');
|
||||||
|
} catch (e: any) {
|
||||||
|
setToastApiError(e.toString());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderModal = () => (
|
||||||
|
<Dialogue
|
||||||
|
open={showDialog}
|
||||||
|
onClose={toggleModal}
|
||||||
|
onClick={onDeleteApplication}
|
||||||
|
title="Are you sure you want to delete this application?"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const tabData = [
|
||||||
|
{
|
||||||
|
label: 'Application overview',
|
||||||
|
component: <ApplicationView />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Edit application',
|
||||||
|
component: <ApplicationUpdate application={application} />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>Loading...</p>
|
||||||
|
<LinearProgress />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (!application) {
|
||||||
|
return <p>Application ({appName}) not found</p>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<PageContent
|
||||||
|
headerContent={
|
||||||
|
<HeaderTitle
|
||||||
|
title={
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar style={{ marginRight: '8px' }}>
|
||||||
|
<Icon>{icon || 'apps'}</Icon>
|
||||||
|
</Avatar>
|
||||||
|
{appName}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(url)}
|
||||||
|
show={
|
||||||
|
<IconButton component={Link} href={url}>
|
||||||
|
<LinkIcon />
|
||||||
|
</IconButton>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PermissionButton
|
||||||
|
title="Delete application"
|
||||||
|
onClick={toggleModal}
|
||||||
|
permission={UPDATE_APPLICATION}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</PermissionButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Typography variant="body1">{description || ''}</Typography>
|
||||||
|
<Typography variant="body2">
|
||||||
|
Created: <strong>{formatDate(createdAt)}</strong>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={hasAccess(UPDATE_APPLICATION)}
|
||||||
|
show={
|
||||||
|
<div>
|
||||||
|
{renderModal()}
|
||||||
|
<TabNav tabData={tabData} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,68 @@
|
|||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { CircularProgress } from '@material-ui/core';
|
||||||
|
import { Warning } from '@material-ui/icons';
|
||||||
|
|
||||||
|
import { AppsLinkList, styles as commonStyles } from '../../common';
|
||||||
|
import SearchField from '../../common/SearchField/SearchField';
|
||||||
|
import PageContent from '../../common/PageContent/PageContent';
|
||||||
|
import HeaderTitle from '../../common/HeaderTitle';
|
||||||
|
import useApplications from '../../../hooks/api/getters/useApplications/useApplications';
|
||||||
|
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||||
|
|
||||||
|
export const ApplicationList = () => {
|
||||||
|
const { applications, loading } = useApplications();
|
||||||
|
const [filter, setFilter] = useState('');
|
||||||
|
|
||||||
|
const filteredApplications = useMemo(() => {
|
||||||
|
const regExp = new RegExp(filter, 'i');
|
||||||
|
return filter
|
||||||
|
? applications?.filter(a => regExp.test(a.appName))
|
||||||
|
: applications;
|
||||||
|
}, [applications, filter]);
|
||||||
|
|
||||||
|
const renderNoApplications = () => (
|
||||||
|
<>
|
||||||
|
<section style={{ textAlign: 'center' }}>
|
||||||
|
<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{' '}
|
||||||
|
<a href="https://docs.getunleash.io/docs/sdks/">
|
||||||
|
documentation.
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!filteredApplications) {
|
||||||
|
return <CircularProgress variant="indeterminate" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={commonStyles.searchField}>
|
||||||
|
<SearchField value={filter} updateValue={setFilter} />
|
||||||
|
</div>
|
||||||
|
<PageContent headerContent={<HeaderTitle title="Applications" />}>
|
||||||
|
<div className={commonStyles.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,83 @@
|
|||||||
|
import { ChangeEvent, useState } from 'react';
|
||||||
|
import { TextField, Grid } from '@material-ui/core';
|
||||||
|
import { useCommonStyles } from '../../../common.styles';
|
||||||
|
import icons from '../icon-names';
|
||||||
|
import GeneralSelect from '../../common/GeneralSelect/GeneralSelect';
|
||||||
|
import useApplicationsApi from '../../../hooks/api/actions/useApplicationsApi/useApplicationsApi';
|
||||||
|
import useToast from '../../../hooks/useToast';
|
||||||
|
import { IApplication } from '../../../interfaces/application';
|
||||||
|
|
||||||
|
interface IApplicationUpdateProps {
|
||||||
|
application: IApplication;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ApplicationUpdate = ({ application }: IApplicationUpdateProps) => {
|
||||||
|
const { storeApplicationMetaData } = useApplicationsApi();
|
||||||
|
const { appName, icon, url, description } = application;
|
||||||
|
const [localUrl, setLocalUrl] = useState(url || '');
|
||||||
|
const [localDescription, setLocalDescription] = useState(description || '');
|
||||||
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
const commonStyles = useCommonStyles();
|
||||||
|
|
||||||
|
const handleChange = (
|
||||||
|
evt: ChangeEvent<{ name?: string | undefined; value: unknown }>,
|
||||||
|
field: string,
|
||||||
|
value: string
|
||||||
|
) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
try {
|
||||||
|
storeApplicationMetaData(appName, field, value);
|
||||||
|
setToastData({
|
||||||
|
type: 'success',
|
||||||
|
title: 'Updated Successfully',
|
||||||
|
text: `${field} successfully updated`,
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
setToastApiError(e.toString());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid container style={{ marginTop: '1rem' }}>
|
||||||
|
<Grid item sm={12} xs={12} className={commonStyles.contentSpacingY}>
|
||||||
|
<Grid item>
|
||||||
|
<GeneralSelect
|
||||||
|
name="iconSelect"
|
||||||
|
id="selectIcon"
|
||||||
|
label="Icon"
|
||||||
|
options={icons.map(v => ({ key: v, label: v }))}
|
||||||
|
value={icon || 'apps'}
|
||||||
|
onChange={e =>
|
||||||
|
handleChange(e, 'icon', e.target.value as string)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<TextField
|
||||||
|
value={localUrl}
|
||||||
|
onChange={e => setLocalUrl(e.target.value)}
|
||||||
|
label="Application URL"
|
||||||
|
placeholder="https://example.com"
|
||||||
|
type="url"
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
onBlur={e => handleChange(e, 'url', localUrl)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<TextField
|
||||||
|
value={localDescription}
|
||||||
|
label="Description"
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
rows={2}
|
||||||
|
onChange={e => setLocalDescription(e.target.value)}
|
||||||
|
onBlur={e =>
|
||||||
|
handleChange(e, 'description', localDescription)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
};
|
@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
import { useContext } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, useParams } from 'react-router-dom';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import {
|
import {
|
||||||
Grid,
|
Grid,
|
||||||
List,
|
List,
|
||||||
@ -9,30 +8,45 @@ import {
|
|||||||
ListItemAvatar,
|
ListItemAvatar,
|
||||||
Typography,
|
Typography,
|
||||||
} from '@material-ui/core';
|
} from '@material-ui/core';
|
||||||
import { Report, Extension, Timeline, FlagRounded } from '@material-ui/icons';
|
import {
|
||||||
|
Report,
|
||||||
import { shorten } from '../common';
|
Extension,
|
||||||
|
Timeline,
|
||||||
|
FlagRounded,
|
||||||
|
SvgIconComponent,
|
||||||
|
} from '@material-ui/icons';
|
||||||
|
import { shorten } from '../../common';
|
||||||
import {
|
import {
|
||||||
CREATE_FEATURE,
|
CREATE_FEATURE,
|
||||||
CREATE_STRATEGY,
|
CREATE_STRATEGY,
|
||||||
} from '../providers/AccessProvider/permissions';
|
} from '../../providers/AccessProvider/permissions';
|
||||||
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
||||||
import { getTogglePath } from '../../utils/route-path-helpers';
|
import { getTogglePath } from '../../../utils/route-path-helpers';
|
||||||
function ApplicationView({
|
import useApplication from '../../../hooks/api/getters/useApplication/useApplication';
|
||||||
seenToggles,
|
import AccessContext from '../../../contexts/AccessContext';
|
||||||
hasAccess,
|
import { formatFullDateTimeWithLocale } from '../../common/util';
|
||||||
strategies,
|
|
||||||
instances,
|
export const ApplicationView = () => {
|
||||||
formatFullDateTime,
|
const { hasAccess } = useContext(AccessContext);
|
||||||
}) {
|
const { name } = useParams<{ name: string }>();
|
||||||
const notFoundListItem = ({ createUrl, name, permission }) => (
|
const { application } = useApplication(name);
|
||||||
|
const { instances, strategies, seenToggles } = application;
|
||||||
|
const notFoundListItem = ({
|
||||||
|
createUrl,
|
||||||
|
name,
|
||||||
|
permission,
|
||||||
|
}: {
|
||||||
|
createUrl: string;
|
||||||
|
name: string;
|
||||||
|
permission: string;
|
||||||
|
}) => (
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
key={`not_found_conditional_${name}`}
|
key={`not_found_conditional_${name}`}
|
||||||
condition={hasAccess(permission)}
|
condition={hasAccess(permission)}
|
||||||
show={
|
show={
|
||||||
<ListItem key={`not_found_${name}`}>
|
<ListItem key={`not_found_${name}`}>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<Report style={{color: 'red'}} />
|
<Report style={{ color: 'red' }} />
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={<Link to={`${createUrl}`}>{name}</Link>}
|
primary={<Link to={`${createUrl}`}>{name}</Link>}
|
||||||
@ -54,13 +68,18 @@ function ApplicationView({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
// eslint-disable-next-line react/prop-types
|
|
||||||
const foundListItem = ({
|
const foundListItem = ({
|
||||||
viewUrl,
|
viewUrl,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
Icon,
|
Icon,
|
||||||
i,
|
i,
|
||||||
|
}: {
|
||||||
|
viewUrl: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
Icon: SvgIconComponent;
|
||||||
|
i: number;
|
||||||
}) => (
|
}) => (
|
||||||
<ListItem key={`found_${name}-${i}`}>
|
<ListItem key={`found_${name}-${i}`}>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
@ -83,10 +102,7 @@ function ApplicationView({
|
|||||||
<hr />
|
<hr />
|
||||||
<List>
|
<List>
|
||||||
{seenToggles.map(
|
{seenToggles.map(
|
||||||
(
|
({ name, description, notFound, project }, i) => (
|
||||||
{ name, description, notFound, project },
|
|
||||||
i
|
|
||||||
) => (
|
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
key={`toggle_conditional_${name}`}
|
key={`toggle_conditional_${name}`}
|
||||||
condition={notFound}
|
condition={notFound}
|
||||||
@ -97,7 +113,7 @@ function ApplicationView({
|
|||||||
i,
|
i,
|
||||||
})}
|
})}
|
||||||
elseShow={foundListItem({
|
elseShow={foundListItem({
|
||||||
viewUrl: getTogglePath(project, name),
|
viewUrl: getTogglePath(project, name, true),
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
Icon: FlagRounded,
|
Icon: FlagRounded,
|
||||||
@ -114,26 +130,26 @@ function ApplicationView({
|
|||||||
</Typography>
|
</Typography>
|
||||||
<hr />
|
<hr />
|
||||||
<List>
|
<List>
|
||||||
{strategies.map(({ name, description, notFound }, i) => (
|
{strategies.map(
|
||||||
<ConditionallyRender
|
({ name, description, notFound }, i: number) => (
|
||||||
key={`strategies_conditional_${name}`}
|
<ConditionallyRender
|
||||||
condition={notFound}
|
key={`strategies_conditional_${name}`}
|
||||||
show={notFoundListItem({
|
condition={notFound}
|
||||||
createUrl: '/strategies/create',
|
show={notFoundListItem({
|
||||||
name,
|
createUrl: '/strategies/create',
|
||||||
permission: CREATE_STRATEGY,
|
name,
|
||||||
i,
|
permission: CREATE_STRATEGY,
|
||||||
})}
|
})}
|
||||||
elseShow={foundListItem({
|
elseShow={foundListItem({
|
||||||
viewUrl: '/strategies/view',
|
viewUrl: '/strategies/view',
|
||||||
name,
|
name,
|
||||||
Icon: Extension,
|
Icon: Extension,
|
||||||
enabled: undefined,
|
description,
|
||||||
description,
|
i,
|
||||||
i,
|
})}
|
||||||
})}
|
/>
|
||||||
/>
|
)
|
||||||
))}
|
)}
|
||||||
</List>
|
</List>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xl={12} md={12}>
|
<Grid item xl={12} md={12}>
|
||||||
@ -143,7 +159,17 @@ function ApplicationView({
|
|||||||
<hr />
|
<hr />
|
||||||
<List>
|
<List>
|
||||||
{instances.map(
|
{instances.map(
|
||||||
({ instanceId, clientIp, lastSeen, sdkVersion }) => (
|
({
|
||||||
|
instanceId,
|
||||||
|
clientIp,
|
||||||
|
lastSeen,
|
||||||
|
sdkVersion,
|
||||||
|
}: {
|
||||||
|
instanceId: string;
|
||||||
|
clientIp: string;
|
||||||
|
lastSeen: string;
|
||||||
|
sdkVersion: string;
|
||||||
|
}) => (
|
||||||
<ListItem key={`${instanceId}`}>
|
<ListItem key={`${instanceId}`}>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<Timeline />
|
<Timeline />
|
||||||
@ -152,16 +178,22 @@ function ApplicationView({
|
|||||||
primary={
|
primary={
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
key={`${instanceId}_conditional`}
|
key={`${instanceId}_conditional`}
|
||||||
condition={sdkVersion}
|
condition={Boolean(sdkVersion)}
|
||||||
show={`${instanceId} (${sdkVersion})`}
|
show={
|
||||||
elseShow={instanceId}
|
<span>
|
||||||
|
{instanceId} {sdkVersion}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
elseShow={<span>{instanceId}</span>}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
secondary={
|
secondary={
|
||||||
<span>
|
<span>
|
||||||
{clientIp} last seen at{' '}
|
{clientIp} last seen at{' '}
|
||||||
<small>
|
<small>
|
||||||
{formatFullDateTime(lastSeen)}
|
{formatFullDateTimeWithLocale(
|
||||||
|
lastSeen
|
||||||
|
)}
|
||||||
</small>
|
</small>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
@ -173,17 +205,4 @@ function ApplicationView({
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
ApplicationView.propTypes = {
|
|
||||||
createUrl: PropTypes.string,
|
|
||||||
name: PropTypes.string,
|
|
||||||
permission: PropTypes.string,
|
|
||||||
instances: PropTypes.array.isRequired,
|
|
||||||
seenToggles: PropTypes.array.isRequired,
|
|
||||||
strategies: PropTypes.array.isRequired,
|
|
||||||
hasAccess: PropTypes.func.isRequired,
|
|
||||||
formatFullDateTime: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ApplicationView;
|
|
@ -22,223 +22,43 @@ exports[`renders correctly if no application 1`] = `
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`renders correctly with permissions 1`] = `
|
exports[`renders correctly with permissions 1`] = `
|
||||||
<div
|
<div>
|
||||||
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
<p>
|
||||||
style={
|
Loading...
|
||||||
Object {
|
</p>
|
||||||
"borderRadius": "10px",
|
|
||||||
"boxShadow": "none",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className="makeStyles-headerContainer-1"
|
className="MuiLinearProgress-root MuiLinearProgress-colorPrimary MuiLinearProgress-indeterminate"
|
||||||
|
role="progressbar"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-headerTitleContainer-5"
|
className="MuiLinearProgress-bar MuiLinearProgress-barColorPrimary MuiLinearProgress-bar1Indeterminate"
|
||||||
>
|
style={Object {}}
|
||||||
<div
|
/>
|
||||||
className=""
|
<div
|
||||||
data-loading={true}
|
className="MuiLinearProgress-bar MuiLinearProgress-bar2Indeterminate MuiLinearProgress-barColorPrimary"
|
||||||
>
|
style={Object {}}
|
||||||
<h2
|
/>
|
||||||
className="MuiTypography-root makeStyles-headerTitle-6 MuiTypography-h2"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"alignItems": "center",
|
|
||||||
"display": "flex",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault"
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"marginRight": "8px",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
aria-hidden={true}
|
|
||||||
className="material-icons MuiIcon-root"
|
|
||||||
>
|
|
||||||
apps
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
test-app
|
|
||||||
</span>
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="makeStyles-headerActions-7"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
aria-disabled={false}
|
|
||||||
className="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiButtonBase-root MuiIconButton-root MuiTypography-colorPrimary"
|
|
||||||
href="http://example.org"
|
|
||||||
onBlur={[Function]}
|
|
||||||
onDragLeave={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
onKeyUp={[Function]}
|
|
||||||
onMouseDown={[Function]}
|
|
||||||
onMouseLeave={[Function]}
|
|
||||||
onMouseUp={[Function]}
|
|
||||||
onTouchEnd={[Function]}
|
|
||||||
onTouchMove={[Function]}
|
|
||||||
onTouchStart={[Function]}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="MuiIconButton-label"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
aria-hidden={true}
|
|
||||||
className="MuiSvgIcon-root"
|
|
||||||
focusable="false"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="makeStyles-bodyContainer-2"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<p
|
|
||||||
className="MuiTypography-root MuiTypography-body1"
|
|
||||||
>
|
|
||||||
app description
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
className="MuiTypography-root MuiTypography-body2"
|
|
||||||
>
|
|
||||||
Created:
|
|
||||||
<strong>
|
|
||||||
Invalid Date
|
|
||||||
</strong>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`renders correctly without permission 1`] = `
|
exports[`renders correctly without permission 1`] = `
|
||||||
<div
|
<div>
|
||||||
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
<p>
|
||||||
style={
|
Loading...
|
||||||
Object {
|
</p>
|
||||||
"borderRadius": "10px",
|
|
||||||
"boxShadow": "none",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className="makeStyles-headerContainer-1"
|
className="MuiLinearProgress-root MuiLinearProgress-colorPrimary MuiLinearProgress-indeterminate"
|
||||||
|
role="progressbar"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-headerTitleContainer-5"
|
className="MuiLinearProgress-bar MuiLinearProgress-barColorPrimary MuiLinearProgress-bar1Indeterminate"
|
||||||
>
|
style={Object {}}
|
||||||
<div
|
/>
|
||||||
className=""
|
<div
|
||||||
data-loading={true}
|
className="MuiLinearProgress-bar MuiLinearProgress-bar2Indeterminate MuiLinearProgress-barColorPrimary"
|
||||||
>
|
style={Object {}}
|
||||||
<h2
|
/>
|
||||||
className="MuiTypography-root makeStyles-headerTitle-6 MuiTypography-h2"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"alignItems": "center",
|
|
||||||
"display": "flex",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault"
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"marginRight": "8px",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
aria-hidden={true}
|
|
||||||
className="material-icons MuiIcon-root"
|
|
||||||
>
|
|
||||||
apps
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
test-app
|
|
||||||
</span>
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="makeStyles-headerActions-7"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
aria-disabled={false}
|
|
||||||
className="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiButtonBase-root MuiIconButton-root MuiTypography-colorPrimary"
|
|
||||||
href="http://example.org"
|
|
||||||
onBlur={[Function]}
|
|
||||||
onDragLeave={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
onKeyUp={[Function]}
|
|
||||||
onMouseDown={[Function]}
|
|
||||||
onMouseLeave={[Function]}
|
|
||||||
onMouseUp={[Function]}
|
|
||||||
onTouchEnd={[Function]}
|
|
||||||
onTouchMove={[Function]}
|
|
||||||
onTouchStart={[Function]}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="MuiIconButton-label"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
aria-hidden={true}
|
|
||||||
className="MuiSvgIcon-root"
|
|
||||||
focusable="false"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="makeStyles-bodyContainer-2"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<p
|
|
||||||
className="MuiTypography-root MuiTypography-body1"
|
|
||||||
>
|
|
||||||
app description
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
className="MuiTypography-root MuiTypography-body2"
|
|
||||||
>
|
|
||||||
Created:
|
|
||||||
<strong>
|
|
||||||
Invalid Date
|
|
||||||
</strong>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
@ -1,26 +1,30 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { ThemeProvider } from '@material-ui/core';
|
import { ThemeProvider } from '@material-ui/core';
|
||||||
import ClientApplications from '../application-edit-component';
|
import { ApplicationEdit } from '../ApplicationEdit/ApplicationEdit';
|
||||||
import renderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { ADMIN } from '../../providers/AccessProvider/permissions';
|
import { ADMIN } from '../../providers/AccessProvider/permissions';
|
||||||
import theme from '../../../themes/main-theme';
|
import theme from '../../../themes/main-theme';
|
||||||
|
|
||||||
import { createFakeStore } from '../../../accessStoreFake';
|
import { createFakeStore } from '../../../accessStoreFake';
|
||||||
import AccessProvider from '../../providers/AccessProvider/AccessProvider';
|
import AccessProvider from '../../providers/AccessProvider/AccessProvider';
|
||||||
|
import UIProvider from '../../providers/UIProvider/UIProvider';
|
||||||
|
|
||||||
test('renders correctly if no application', () => {
|
test('renders correctly if no application', () => {
|
||||||
const tree = renderer
|
const tree = renderer
|
||||||
.create(
|
.create(
|
||||||
<AccessProvider store={createFakeStore([{ permission: ADMIN }])}>
|
<AccessProvider store={createFakeStore([{ permission: ADMIN }])}>
|
||||||
<ClientApplications
|
<ThemeProvider theme={theme}>
|
||||||
fetchApplication={() => Promise.resolve({})}
|
<UIProvider>
|
||||||
storeApplicationMetaData={jest.fn()}
|
<MemoryRouter initialEntries={['/test']}>
|
||||||
deleteApplication={jest.fn()}
|
<ApplicationEdit
|
||||||
history={{}}
|
fetchApplication={() => Promise.resolve({})}
|
||||||
locationSettings={{ locale: 'en-GB' }}
|
storeApplicationMetaData={jest.fn()}
|
||||||
/>
|
deleteApplication={jest.fn()}
|
||||||
|
history={{}}
|
||||||
|
locationSettings={{ locale: 'en-GB' }}
|
||||||
|
/>
|
||||||
|
</MemoryRouter>
|
||||||
|
</UIProvider>
|
||||||
|
</ThemeProvider>
|
||||||
</AccessProvider>
|
</AccessProvider>
|
||||||
)
|
)
|
||||||
.toJSON();
|
.toJSON();
|
||||||
@ -33,54 +37,56 @@ test('renders correctly without permission', () => {
|
|||||||
.create(
|
.create(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<AccessProvider store={createFakeStore([])}>
|
<UIProvider>
|
||||||
<ClientApplications
|
<AccessProvider store={createFakeStore([])}>
|
||||||
fetchApplication={() => Promise.resolve({})}
|
<ApplicationEdit
|
||||||
storeApplicationMetaData={jest.fn()}
|
fetchApplication={() => Promise.resolve({})}
|
||||||
deleteApplication={jest.fn()}
|
storeApplicationMetaData={jest.fn()}
|
||||||
history={{}}
|
deleteApplication={jest.fn()}
|
||||||
application={{
|
history={{}}
|
||||||
appName: 'test-app',
|
application={{
|
||||||
instances: [
|
appName: 'test-app',
|
||||||
{
|
instances: [
|
||||||
instanceId: 'instance-1',
|
{
|
||||||
clientIp: '123.123.123.123',
|
instanceId: 'instance-1',
|
||||||
lastSeen: '2017-02-23T15:56:49',
|
clientIp: '123.123.123.123',
|
||||||
sdkVersion: '4.0',
|
lastSeen: '2017-02-23T15:56:49',
|
||||||
},
|
sdkVersion: '4.0',
|
||||||
],
|
},
|
||||||
strategies: [
|
],
|
||||||
{
|
strategies: [
|
||||||
name: 'StrategyA',
|
{
|
||||||
description: 'A description',
|
name: 'StrategyA',
|
||||||
},
|
description: 'A description',
|
||||||
{
|
},
|
||||||
name: 'StrategyB',
|
{
|
||||||
description: 'B description',
|
name: 'StrategyB',
|
||||||
notFound: true,
|
description: 'B description',
|
||||||
},
|
notFound: true,
|
||||||
],
|
},
|
||||||
seenToggles: [
|
],
|
||||||
{
|
seenToggles: [
|
||||||
name: 'ToggleA',
|
{
|
||||||
description: 'this is A toggle',
|
name: 'ToggleA',
|
||||||
enabled: true,
|
description: 'this is A toggle',
|
||||||
project: 'default',
|
enabled: true,
|
||||||
},
|
project: 'default',
|
||||||
{
|
},
|
||||||
name: 'ToggleB',
|
{
|
||||||
description: 'this is B toggle',
|
name: 'ToggleB',
|
||||||
enabled: false,
|
description: 'this is B toggle',
|
||||||
notFound: true,
|
enabled: false,
|
||||||
project: 'default',
|
notFound: true,
|
||||||
},
|
project: 'default',
|
||||||
],
|
},
|
||||||
url: 'http://example.org',
|
],
|
||||||
description: 'app description',
|
url: 'http://example.org',
|
||||||
}}
|
description: 'app description',
|
||||||
locationSettings={{ locale: 'en-GB' }}
|
}}
|
||||||
/>
|
locationSettings={{ locale: 'en-GB' }}
|
||||||
</AccessProvider>
|
/>
|
||||||
|
</AccessProvider>
|
||||||
|
</UIProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
)
|
)
|
||||||
@ -94,56 +100,58 @@ test('renders correctly with permissions', () => {
|
|||||||
.create(
|
.create(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<AccessProvider
|
<UIProvider>
|
||||||
store={createFakeStore([{ permission: ADMIN }])}
|
<AccessProvider
|
||||||
>
|
store={createFakeStore([{ permission: ADMIN }])}
|
||||||
<ClientApplications
|
>
|
||||||
fetchApplication={() => Promise.resolve({})}
|
<ApplicationEdit
|
||||||
storeApplicationMetaData={jest.fn()}
|
fetchApplication={() => Promise.resolve({})}
|
||||||
history={{}}
|
storeApplicationMetaData={jest.fn()}
|
||||||
deleteApplication={jest.fn()}
|
history={{}}
|
||||||
application={{
|
deleteApplication={jest.fn()}
|
||||||
appName: 'test-app',
|
application={{
|
||||||
instances: [
|
appName: 'test-app',
|
||||||
{
|
instances: [
|
||||||
instanceId: 'instance-1',
|
{
|
||||||
clientIp: '123.123.123.123',
|
instanceId: 'instance-1',
|
||||||
lastSeen: '2017-02-23T15:56:49',
|
clientIp: '123.123.123.123',
|
||||||
sdkVersion: '4.0',
|
lastSeen: '2017-02-23T15:56:49',
|
||||||
},
|
sdkVersion: '4.0',
|
||||||
],
|
},
|
||||||
strategies: [
|
],
|
||||||
{
|
strategies: [
|
||||||
name: 'StrategyA',
|
{
|
||||||
description: 'A description',
|
name: 'StrategyA',
|
||||||
},
|
description: 'A description',
|
||||||
{
|
},
|
||||||
name: 'StrategyB',
|
{
|
||||||
description: 'B description',
|
name: 'StrategyB',
|
||||||
notFound: true,
|
description: 'B description',
|
||||||
},
|
notFound: true,
|
||||||
],
|
},
|
||||||
seenToggles: [
|
],
|
||||||
{
|
seenToggles: [
|
||||||
name: 'ToggleA',
|
{
|
||||||
description: 'this is A toggle',
|
name: 'ToggleA',
|
||||||
enabled: true,
|
description: 'this is A toggle',
|
||||||
project: 'default',
|
enabled: true,
|
||||||
},
|
project: 'default',
|
||||||
{
|
},
|
||||||
name: 'ToggleB',
|
{
|
||||||
description: 'this is B toggle',
|
name: 'ToggleB',
|
||||||
enabled: false,
|
description: 'this is B toggle',
|
||||||
notFound: true,
|
enabled: false,
|
||||||
project: 'default',
|
notFound: true,
|
||||||
},
|
project: 'default',
|
||||||
],
|
},
|
||||||
url: 'http://example.org',
|
],
|
||||||
description: 'app description',
|
url: 'http://example.org',
|
||||||
}}
|
description: 'app description',
|
||||||
locationSettings={{ locale: 'en-GB' }}
|
}}
|
||||||
/>
|
locationSettings={{ locale: 'en-GB' }}
|
||||||
</AccessProvider>
|
/>
|
||||||
|
</AccessProvider>
|
||||||
|
</UIProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
)
|
)
|
||||||
|
@ -1,196 +0,0 @@
|
|||||||
/* eslint react/no-multi-comp:off */
|
|
||||||
import React, { PureComponent } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Avatar,
|
|
||||||
Link,
|
|
||||||
Icon,
|
|
||||||
IconButton,
|
|
||||||
Button,
|
|
||||||
LinearProgress,
|
|
||||||
Typography,
|
|
||||||
} from '@material-ui/core';
|
|
||||||
import { Link as LinkIcon } from '@material-ui/icons';
|
|
||||||
|
|
||||||
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
|
|
||||||
import {
|
|
||||||
formatFullDateTimeWithLocale,
|
|
||||||
formatDateWithLocale,
|
|
||||||
} from '../common/util';
|
|
||||||
import { UPDATE_APPLICATION } from '../providers/AccessProvider/permissions';
|
|
||||||
import ApplicationView from './application-view';
|
|
||||||
import ApplicationUpdate from './application-update';
|
|
||||||
import TabNav from '../common/TabNav/TabNav';
|
|
||||||
import Dialogue from '../common/Dialogue';
|
|
||||||
import PageContent from '../common/PageContent';
|
|
||||||
import HeaderTitle from '../common/HeaderTitle';
|
|
||||||
import AccessContext from '../../contexts/AccessContext';
|
|
||||||
class ClientApplications extends PureComponent {
|
|
||||||
static contextType = AccessContext;
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
fetchApplication: PropTypes.func.isRequired,
|
|
||||||
appName: PropTypes.string,
|
|
||||||
application: PropTypes.object,
|
|
||||||
locationSettings: PropTypes.object.isRequired,
|
|
||||||
storeApplicationMetaData: PropTypes.func.isRequired,
|
|
||||||
deleteApplication: PropTypes.func.isRequired,
|
|
||||||
history: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super();
|
|
||||||
this.state = {
|
|
||||||
activeTab: 0,
|
|
||||||
loading: !props.application,
|
|
||||||
prompt: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props
|
|
||||||
.fetchApplication(this.props.appName)
|
|
||||||
.finally(() => this.setState({ loading: false }));
|
|
||||||
}
|
|
||||||
formatFullDateTime = v =>
|
|
||||||
formatFullDateTimeWithLocale(v, this.props.locationSettings.locale);
|
|
||||||
formatDate = v => formatDateWithLocale(v, this.props.locationSettings.locale);
|
|
||||||
|
|
||||||
deleteApplication = async evt => {
|
|
||||||
evt.preventDefault();
|
|
||||||
// if (window.confirm('Are you sure you want to remove this application?')) {
|
|
||||||
const { deleteApplication, appName } = this.props;
|
|
||||||
await deleteApplication(appName);
|
|
||||||
this.props.history.push('/applications');
|
|
||||||
// }
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.state.loading) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<p>Loading...</p>
|
|
||||||
<LinearProgress />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (!this.props.application) {
|
|
||||||
return <p>Application ({this.props.appName}) not found</p>;
|
|
||||||
}
|
|
||||||
const { hasAccess } = this.context;
|
|
||||||
const { application, storeApplicationMetaData } = this.props;
|
|
||||||
const {
|
|
||||||
appName,
|
|
||||||
instances,
|
|
||||||
strategies,
|
|
||||||
seenToggles,
|
|
||||||
url,
|
|
||||||
description,
|
|
||||||
icon = 'apps',
|
|
||||||
createdAt,
|
|
||||||
} = application;
|
|
||||||
|
|
||||||
const toggleModal = () => {
|
|
||||||
this.setState(prev => ({ ...prev, prompt: !prev.prompt }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderModal = () => (
|
|
||||||
<Dialogue
|
|
||||||
open={this.state.prompt}
|
|
||||||
onClose={toggleModal}
|
|
||||||
onClick={this.deleteApplication}
|
|
||||||
title="Are you sure you want to delete this application?"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const tabData = [
|
|
||||||
{
|
|
||||||
label: 'Application overview',
|
|
||||||
component: (
|
|
||||||
<ApplicationView
|
|
||||||
strategies={strategies}
|
|
||||||
instances={instances}
|
|
||||||
seenToggles={seenToggles}
|
|
||||||
hasAccess={hasAccess}
|
|
||||||
formatFullDateTime={this.formatFullDateTime}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Edit application',
|
|
||||||
component: (
|
|
||||||
<ApplicationUpdate
|
|
||||||
application={application}
|
|
||||||
storeApplicationMetaData={storeApplicationMetaData}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContent
|
|
||||||
headerContent={
|
|
||||||
<HeaderTitle
|
|
||||||
title={
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Avatar style={{ marginRight: '8px' }}>
|
|
||||||
<Icon>{icon || 'apps'}</Icon>
|
|
||||||
</Avatar>
|
|
||||||
{appName}
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
actions={
|
|
||||||
<>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={url}
|
|
||||||
show={
|
|
||||||
<IconButton component={Link} href={url}>
|
|
||||||
<LinkIcon />
|
|
||||||
</IconButton>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={hasAccess(UPDATE_APPLICATION)}
|
|
||||||
show={
|
|
||||||
<Button
|
|
||||||
color="secondary"
|
|
||||||
title="Delete application"
|
|
||||||
onClick={toggleModal}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<Typography variant="body1">{description || ''}</Typography>
|
|
||||||
<Typography variant="body2">
|
|
||||||
Created: <strong>{this.formatDate(createdAt)}</strong>
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={hasAccess(UPDATE_APPLICATION)}
|
|
||||||
show={
|
|
||||||
<div>
|
|
||||||
{renderModal()}
|
|
||||||
|
|
||||||
<TabNav tabData={tabData} />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</PageContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ClientApplications;
|
|
@ -1,30 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import ApplicationEdit from './application-edit-component';
|
|
||||||
import {
|
|
||||||
deleteApplication,
|
|
||||||
fetchApplication,
|
|
||||||
storeApplicationMetaData,
|
|
||||||
} from '../../store/application/actions';
|
|
||||||
import { useLocationSettings } from '../../hooks/useLocationSettings';
|
|
||||||
|
|
||||||
const ApplicationEditContainer = props => {
|
|
||||||
const { locationSettings } = useLocationSettings();
|
|
||||||
|
|
||||||
return <ApplicationEdit {...props} locationSettings={locationSettings} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => {
|
|
||||||
let application = state.applications.getIn(['apps', props.appName]);
|
|
||||||
if (application) {
|
|
||||||
application = application.toJS();
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
application,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, {
|
|
||||||
fetchApplication,
|
|
||||||
storeApplicationMetaData,
|
|
||||||
deleteApplication,
|
|
||||||
})(ApplicationEditContainer);
|
|
@ -1,69 +0,0 @@
|
|||||||
import React, { useEffect, useMemo, useState } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { CircularProgress } from '@material-ui/core';
|
|
||||||
import { Warning } from '@material-ui/icons';
|
|
||||||
|
|
||||||
import { AppsLinkList, styles as commonStyles } from '../common';
|
|
||||||
import SearchField from '../common/SearchField/SearchField';
|
|
||||||
import PageContent from '../common/PageContent/PageContent';
|
|
||||||
import HeaderTitle from '../common/HeaderTitle';
|
|
||||||
|
|
||||||
const Empty = () => (
|
|
||||||
<React.Fragment>
|
|
||||||
<section style={{ textAlign: 'center' }}>
|
|
||||||
<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{' '}
|
|
||||||
<a href="https://docs.getunleash.io/docs/sdks/">documentation.</a>
|
|
||||||
</section>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
|
|
||||||
const ClientStrategies = ({ fetchAll, applications }) => {
|
|
||||||
const [filter, setFilter] = useState('');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchAll();
|
|
||||||
}, [fetchAll]);
|
|
||||||
|
|
||||||
const filteredApplications = useMemo(() => {
|
|
||||||
const regExp = new RegExp(filter, 'i');
|
|
||||||
return filter
|
|
||||||
? applications?.filter(a => regExp.test(a.appName))
|
|
||||||
: applications;
|
|
||||||
}, [applications, filter]);
|
|
||||||
|
|
||||||
if (!filteredApplications) {
|
|
||||||
return <CircularProgress variant="indeterminate" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={commonStyles.searchField}>
|
|
||||||
<SearchField value={filter} updateValue={setFilter} />
|
|
||||||
</div>
|
|
||||||
<PageContent headerContent={<HeaderTitle title="Applications" />}>
|
|
||||||
<div className={commonStyles.fullwidth}>
|
|
||||||
{filteredApplications.length > 0 ? (
|
|
||||||
<AppsLinkList apps={filteredApplications} />
|
|
||||||
) : (
|
|
||||||
<Empty />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</PageContent>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
ClientStrategies.propTypes = {
|
|
||||||
applications: PropTypes.array,
|
|
||||||
fetchAll: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ClientStrategies;
|
|
@ -1,15 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import ApplicationList from './application-list-component';
|
|
||||||
import { fetchAll } from '../../store/application/actions';
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
applications: state.applications.get('list').toJS(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
fetchAll,
|
|
||||||
};
|
|
||||||
|
|
||||||
const Container = connect(mapStateToProps, mapDispatchToProps)(ApplicationList);
|
|
||||||
|
|
||||||
export default Container;
|
|
@ -1,72 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { TextField, Grid } from '@material-ui/core';
|
|
||||||
import { useCommonStyles } from '../../common.styles';
|
|
||||||
import icons from './icon-names';
|
|
||||||
import GeneralSelect from '../common/GeneralSelect/GeneralSelect';
|
|
||||||
|
|
||||||
function ApplicationUpdate({ application, storeApplicationMetaData }) {
|
|
||||||
const { appName, icon, url, description } = application;
|
|
||||||
const [localUrl, setLocalUrl] = useState(url);
|
|
||||||
const [localDescription, setLocalDescription] = useState(description);
|
|
||||||
const commonStyles = useCommonStyles();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Grid container style={{ marginTop: '1rem' }}>
|
|
||||||
<Grid item sm={12} xs={12} className={commonStyles.contentSpacingY}>
|
|
||||||
<Grid item>
|
|
||||||
<GeneralSelect
|
|
||||||
label="Icon"
|
|
||||||
options={icons.map(v => ({ key: v, label: v }))}
|
|
||||||
value={icon || 'apps'}
|
|
||||||
onChange={e =>
|
|
||||||
storeApplicationMetaData(
|
|
||||||
appName,
|
|
||||||
'icon',
|
|
||||||
e.target.value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid item>
|
|
||||||
<TextField
|
|
||||||
value={localUrl}
|
|
||||||
onChange={e => setLocalUrl(e.target.value)}
|
|
||||||
label="Application URL"
|
|
||||||
placeholder="https://example.com"
|
|
||||||
type="url"
|
|
||||||
variant="outlined"
|
|
||||||
size="small"
|
|
||||||
onBlur={() =>
|
|
||||||
storeApplicationMetaData(appName, 'url', localUrl)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid item>
|
|
||||||
<TextField
|
|
||||||
value={localDescription}
|
|
||||||
label="Description"
|
|
||||||
variant="outlined"
|
|
||||||
size="small"
|
|
||||||
rows={2}
|
|
||||||
onChange={e => setLocalDescription(e.target.value)}
|
|
||||||
onBlur={() =>
|
|
||||||
storeApplicationMetaData(
|
|
||||||
appName,
|
|
||||||
'description',
|
|
||||||
localDescription
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ApplicationUpdate.propTypes = {
|
|
||||||
application: PropTypes.object.isRequired,
|
|
||||||
storeApplicationMetaData: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ApplicationUpdate;
|
|
@ -7,7 +7,7 @@ import SearchIcon from '@material-ui/icons/Search';
|
|||||||
|
|
||||||
import { useStyles } from './styles';
|
import { useStyles } from './styles';
|
||||||
|
|
||||||
function SearchField({ updateValue, className }) {
|
function SearchField({ updateValue, className = '' }) {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
|
|
||||||
const [localValue, setLocalValue] = useState('');
|
const [localValue, setLocalValue] = useState('');
|
||||||
|
@ -13,7 +13,12 @@ const a11yProps = index => ({
|
|||||||
'aria-controls': `tabpanel-${index}`,
|
'aria-controls': `tabpanel-${index}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const TabNav = ({ tabData, className, navClass, startingTab = 0 }) => {
|
const TabNav = ({
|
||||||
|
tabData,
|
||||||
|
className = '',
|
||||||
|
navClass = '',
|
||||||
|
startingTab = 0,
|
||||||
|
}) => {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const [activeTab, setActiveTab] = useState(startingTab);
|
const [activeTab, setActiveTab] = useState(startingTab);
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
@ -6,8 +6,6 @@ import Strategies from '../../page/strategies';
|
|||||||
import HistoryPage from '../../page/history';
|
import HistoryPage from '../../page/history';
|
||||||
import HistoryTogglePage from '../../page/history/toggle';
|
import HistoryTogglePage from '../../page/history/toggle';
|
||||||
import { ArchiveListContainer } from '../archive/ArchiveListContainer';
|
import { ArchiveListContainer } from '../archive/ArchiveListContainer';
|
||||||
import Applications from '../../page/applications';
|
|
||||||
import ApplicationView from '../../page/applications/view';
|
|
||||||
import { TagTypeList } from '../tags/TagTypeList/TagTypeList';
|
import { TagTypeList } from '../tags/TagTypeList/TagTypeList';
|
||||||
import { AddonList } from '../addons/AddonList/AddonList';
|
import { AddonList } from '../addons/AddonList/AddonList';
|
||||||
import Admin from '../admin';
|
import Admin from '../admin';
|
||||||
@ -41,6 +39,8 @@ import EditProject from '../project/Project/EditProject/EditProject';
|
|||||||
import CreateProject from '../project/Project/CreateProject/CreateProject';
|
import CreateProject from '../project/Project/CreateProject/CreateProject';
|
||||||
import CreateFeature from '../feature/CreateFeature/CreateFeature';
|
import CreateFeature from '../feature/CreateFeature/CreateFeature';
|
||||||
import EditFeature from '../feature/EditFeature/EditFeature';
|
import EditFeature from '../feature/EditFeature/EditFeature';
|
||||||
|
import { ApplicationEdit } from '../application/ApplicationEdit/ApplicationEdit';
|
||||||
|
import { ApplicationList } from '../application/ApplicationList/ApplicationList';
|
||||||
import ContextList from '../context/ContextList/ContextList';
|
import ContextList from '../context/ContextList/ContextList';
|
||||||
import RedirectFeatureView from '../feature/RedirectFeatureView/RedirectFeatureView';
|
import RedirectFeatureView from '../feature/RedirectFeatureView/RedirectFeatureView';
|
||||||
import { CreateAddon } from '../addons/CreateAddon/CreateAddon';
|
import { CreateAddon } from '../addons/CreateAddon/CreateAddon';
|
||||||
@ -193,7 +193,7 @@ export const routes = [
|
|||||||
path: '/applications/:name',
|
path: '/applications/:name',
|
||||||
title: ':name',
|
title: ':name',
|
||||||
parent: '/applications',
|
parent: '/applications',
|
||||||
component: ApplicationView,
|
component: ApplicationEdit,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
layout: 'main',
|
layout: 'main',
|
||||||
menu: {},
|
menu: {},
|
||||||
@ -201,7 +201,7 @@ export const routes = [
|
|||||||
{
|
{
|
||||||
path: '/applications',
|
path: '/applications',
|
||||||
title: 'Applications',
|
title: 'Applications',
|
||||||
component: Applications,
|
component: ApplicationList,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
layout: 'main',
|
layout: 'main',
|
||||||
menu: { mobile: true, advanced: true },
|
menu: { mobile: true, advanced: true },
|
||||||
|
@ -0,0 +1,50 @@
|
|||||||
|
import useAPI from '../useApi/useApi';
|
||||||
|
|
||||||
|
const useApplicationsApi = () => {
|
||||||
|
const { makeRequest, createRequest, errors, loading } = useAPI({
|
||||||
|
propagateErrors: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const URI = 'api/admin/metrics/applications';
|
||||||
|
|
||||||
|
const storeApplicationMetaData = async (
|
||||||
|
appName: string,
|
||||||
|
key: string,
|
||||||
|
value: string
|
||||||
|
) => {
|
||||||
|
const data: { [key: string]: any } = {};
|
||||||
|
data[key] = value;
|
||||||
|
const path = `${URI}/${appName}`;
|
||||||
|
const req = createRequest(path, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const res = await makeRequest(req.caller, req.id);
|
||||||
|
return res;
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteApplication = async (appName: string) => {
|
||||||
|
const path = `${URI}/${appName}`;
|
||||||
|
const req = createRequest(path, { method: 'DELETE' });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await makeRequest(req.caller, req.id);
|
||||||
|
return res;
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
storeApplicationMetaData,
|
||||||
|
deleteApplication,
|
||||||
|
errors,
|
||||||
|
loading,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useApplicationsApi;
|
@ -0,0 +1,63 @@
|
|||||||
|
import useSWR, { mutate, SWRConfiguration } from 'swr';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { formatApiPath } from '../../../../utils/format-path';
|
||||||
|
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||||
|
import { IApplication } from '../../../../interfaces/application';
|
||||||
|
|
||||||
|
interface IUseApplicationOutput {
|
||||||
|
application: IApplication;
|
||||||
|
refetchApplication: () => void;
|
||||||
|
loading: boolean;
|
||||||
|
error?: Error;
|
||||||
|
APPLICATION_CACHE_KEY: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useApplication = (
|
||||||
|
name: string,
|
||||||
|
options: SWRConfiguration = {}
|
||||||
|
): IUseApplicationOutput => {
|
||||||
|
const path = formatApiPath(`api/admin/metrics/applications/${name}`);
|
||||||
|
|
||||||
|
const fetcher = async () => {
|
||||||
|
return fetch(path, {
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
.then(handleErrorResponses('Application'))
|
||||||
|
.then(res => res.json());
|
||||||
|
};
|
||||||
|
|
||||||
|
const APPLICATION_CACHE_KEY = `api/admin/metrics/applications/${name}`;
|
||||||
|
|
||||||
|
const { data, error } = useSWR(APPLICATION_CACHE_KEY, fetcher, {
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(!error && !data);
|
||||||
|
|
||||||
|
const refetchApplication = () => {
|
||||||
|
mutate(APPLICATION_CACHE_KEY);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(!error && !data);
|
||||||
|
}, [data, error]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
application: data || {
|
||||||
|
appName: name,
|
||||||
|
color: '',
|
||||||
|
createdAt: '2022-02-02T21:04:00.268Z',
|
||||||
|
descriotion: '',
|
||||||
|
instances: [],
|
||||||
|
strategies: [],
|
||||||
|
seenToggles: [],
|
||||||
|
url: '',
|
||||||
|
},
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
refetchApplication,
|
||||||
|
APPLICATION_CACHE_KEY,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useApplication;
|
@ -0,0 +1,53 @@
|
|||||||
|
import useSWR, { mutate, SWRConfiguration } from 'swr';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { formatApiPath } from '../../../../utils/format-path';
|
||||||
|
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||||
|
import { IApplication } from '../../../../interfaces/application';
|
||||||
|
|
||||||
|
const path = formatApiPath('api/admin/metrics/applications');
|
||||||
|
|
||||||
|
interface IUseApplicationsOutput {
|
||||||
|
applications: IApplication[];
|
||||||
|
refetchApplications: () => void;
|
||||||
|
loading: boolean;
|
||||||
|
error?: Error;
|
||||||
|
APPLICATIONS_CACHE_KEY: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useApplications = (
|
||||||
|
options: SWRConfiguration = {}
|
||||||
|
): IUseApplicationsOutput => {
|
||||||
|
const fetcher = async () => {
|
||||||
|
return fetch(path, {
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
.then(handleErrorResponses('Applications data'))
|
||||||
|
.then(res => res.json());
|
||||||
|
};
|
||||||
|
|
||||||
|
const APPLICATIONS_CACHE_KEY = 'api/admin/metrics/applications';
|
||||||
|
|
||||||
|
const { data, error } = useSWR(APPLICATIONS_CACHE_KEY, fetcher, {
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(!error && !data);
|
||||||
|
|
||||||
|
const refetchApplications = () => {
|
||||||
|
mutate(APPLICATIONS_CACHE_KEY);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(!error && !data);
|
||||||
|
}, [data, error]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
applications: data?.applications || {},
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
refetchApplications,
|
||||||
|
APPLICATIONS_CACHE_KEY,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useApplications;
|
12
frontend/src/interfaces/application.ts
Normal file
12
frontend/src/interfaces/application.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export interface IApplication {
|
||||||
|
appName: string;
|
||||||
|
color: string;
|
||||||
|
createdAt: string;
|
||||||
|
description: string;
|
||||||
|
icon: string;
|
||||||
|
instances: [];
|
||||||
|
links: object;
|
||||||
|
seenToggles: [];
|
||||||
|
strategies: [];
|
||||||
|
url: string;
|
||||||
|
}
|
@ -1,6 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import ApplicationListConmponent from '../../component/application/application-list-container';
|
|
||||||
|
|
||||||
const render = () => <ApplicationListConmponent />;
|
|
||||||
|
|
||||||
export default render;
|
|
@ -1,12 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import ApplicationEditComponent from '../../component/application/application-edit-container';
|
|
||||||
|
|
||||||
const render = ({ match: { params }, history }) => <ApplicationEditComponent appName={params.name} history={history} />;
|
|
||||||
|
|
||||||
render.propTypes = {
|
|
||||||
match: PropTypes.object.isRequired,
|
|
||||||
history: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default render;
|
|
Loading…
Reference in New Issue
Block a user