Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • andersharrisson/ce-deploy-ui
  • ccce/dev/ce-deploy-ui
2 results
Show changes
Commits on Source (26)
Showing
with 202 additions and 783 deletions
......@@ -34,9 +34,9 @@
},
"scripts": {
"start": "react-scripts start",
"test": "npm run testComponents",
"startMock": "PORT=3007 REACT_APP_MOCK_CCCE_API=YES react-scripts start",
"build": "react-scripts build",
"test": "DEBUG_PRINT_LIMIT=20000 react-scripts test",
"testComponents": "npx cypress run --component --browser chrome",
"coverage": "DEBUG_PRINT_LIMIT=20000 react-scripts test --env=jsdom --watchAll=false --coverage --passWithNoTests",
"eject": "react-scripts eject",
......
import React from "react";
import { loadFeature, defineFeature } from "jest-cucumber";
import { render, screen /* , findByText*/ } from "@testing-library/react";
import App from "../App";
import { createMemoryHistory } from "history";
// mock BrowserRouter (this should probably live somewhere else)
const reactRouterDom = require("react-router-dom");
reactRouterDom.BrowserRouter = ({ children }) => <div>{children}</div>;
const Router = reactRouterDom.Router;
const feature = loadFeature("features/deployments/browse-deployments.feature");
defineFeature(feature, (test) => {
test("User wants to browse IOC deployments", ({ given, when, then }) => {
given(
"the user is not logged in and navigates to the deployments page",
async () => {
const history = createMemoryHistory();
history.push("/deployments");
render(
<Router history={history}>
<App />
</Router>
);
await screen.findByText(/Running/i);
}
);
then("they should not get Access deniend", async () => {
expect(await screen.findByText(/IOC name/i)).toBeInTheDocument();
});
});
test("User wants to see relevant IOC deployments", ({
given,
when,
then,
and
}) => {
given("the user is on the deployments page", () => {});
when("the user clicks on the My deployments toogle button", () => {});
then(
"the table should display only deployments which the user has made",
() => {}
);
and("recent deployments should be at the top", () => {});
});
test("User wants to navigate to a specific IOC deployment", ({
given,
when,
then
}) => {
given(
"the user is not logged in and tries to navigate any deployment details page",
async () => {
const history = createMemoryHistory();
history.push("/deployments/1");
render(
<Router history={history}>
<App />
</Router>
);
await screen.findByText(/deployment details/i);
}
);
then("they should not get Access deniend", async () => {
expect(
await screen.findByText(/Running Undeployment/i)
).toBeInTheDocument();
});
});
/* test('Logged in user wants to navigate to a specific IOC deployment', ({ given, when, then }) => {
given('the user is on the deployments page', async () => {
const history = createMemoryHistory();
history.push('/deployments')
const { container } = render(
<Router history={history}>
<App />
</Router>
)
await findByText(container, /my deployments/i);
});
when('the user clicks on the deployment in the table', async () => {
const history = createMemoryHistory();
history.push('/deployments/1')
render(
<Router history={history}>
<App />
</Router>
)
await screen.findByTestId("deployment-details-container")
});
then('the user should be redirected to the deployment details page.', async () => {
expect(await screen.findByText(/the deployment is queued/i)).toBeInTheDocument()
});
});*/
});
import React from "react";
import { loadFeature, defineFeature } from "jest-cucumber";
import {
render,
screen /* , act, fireEvent, waitForElementToBeRemoved*/
} from "@testing-library/react";
import App from "../App";
import { server } from "../mocks/server";
import { rest } from "msw";
import { createMemoryHistory } from "history";
// mock BrowserRouter (this should probably live somewhere else)
const reactRouterDom = require("react-router-dom");
reactRouterDom.BrowserRouter = ({ children }) => <div>{children}</div>;
const Router = reactRouterDom.Router;
const feature = loadFeature("features/hosts/browse-hosts.feature");
defineFeature(feature, (test) => {
test("User wants to browse IOC hosts", ({ given, when, then }) => {
given("the user is logged in", () => {
const history = createMemoryHistory();
history.push("/hosts");
render(
<Router history={history}>
<App />
</Router>
);
});
when("the user navigates to the hosts page", async () => {
await screen.findByText("CE deploy & monitor / IOC hosts");
});
then(
"they should be presented with a table of hosts from the iocs group in CSEntry",
async () => {
expect(
await screen.findByText("CE deploy & monitor / IOC hosts")
).toBeInTheDocument();
expect(await screen.findByText("Status")).toBeInTheDocument();
expect(await screen.findByText("Host")).toBeInTheDocument();
expect(await screen.findByText("Network")).toBeInTheDocument();
expect(await screen.findByText("Scope")).toBeInTheDocument();
}
);
});
test("User wants to navigate to a specific IOC Host", ({
given,
when,
then,
and
}) => {
const history = createMemoryHistory();
given("the user is on the hosts page", async () => {
server.use(
rest.get("*/hosts", (req, res, ctx) => {
return res(
ctx.json({
totalCount: 0,
listSize: 1,
pageNumber: 0,
limit: 100,
csEntryHosts: [
{
csEntryHost: {
id: 3057,
fqdn: "ccce-demo-01.tn.esss.lu.se",
name: "ccce-demo-01",
scope: "TechnicalNetwork",
description: null,
user: "johnsparger",
interfaces: [
{
id: 3494,
name: "ccce-demo-01",
network: "ChannelAccess-ACC",
domain: "tn.esss.lu.se",
ip: "172.16.100.61",
mac: "02:42:42:1f:38:53",
host: "ccce-demo-01",
cnames: [],
is_main: true
}
],
is_ioc: true,
device_type: "VirtualMachine",
created_at: "2021-02-12T12:07:00.000+00:00",
ansible_vars: {
vm_owners: ["johnsparger"]
},
ansible_groups: []
},
assigned: false,
status: "AVAILABLE"
}
]
})
);
}),
rest.get("*/hosts/:hostId", (req, res, ctx) => {
const { hostId } = req.params;
return res(
ctx.json({
csEntryHosts: [
{
csEntryHost: {
id: hostId,
fqdn: "ccce-demo-01.tn.esss.lu.se",
name: "ccce-demo-01",
scope: "TechnicalNetwork",
description: null,
user: "johnsparger",
interfaces: [
{
id: 3494,
name: "ccce-demo-01",
network: "ChannelAccess-ACC",
domain: "tn.esss.lu.se",
ip: "172.16.100.61",
mac: "02:42:42:1f:38:53",
host: "ccce-demo-01",
cnames: [],
is_main: true
}
],
is_ioc: true,
deviceType: "VirtualMachine",
created_at: "2021-02-12T12:07:00.000+00:00",
ansible_vars: {
vm_owners: ["johnsparger"]
},
ansible_groups: []
},
assigned: false,
status: "AVAILABLE"
}
],
assigned: false,
status: "AVAILABLE"
})
);
})
);
history.push("/hosts");
render(
<Router history={history}>
<App />
</Router>
);
// await screen.findByText('ccce-demo-01.tn.esss.lu.se');
});
when("the user clicks (exactly) on the host url link", async () => {
/* act(() => {
fireEvent.click(screen.getByText('ccce-demo-01.tn.esss.lu.se'));
});
await waitForElementToBeRemoved(() => screen.getByText('Status'));
history.push('/hosts/3057')
render(
<Router history={history}>
<App />
</Router>
)
await screen.findByTestId("host-details-container")*/
});
then("the user should be redirected to the Host Details page", async () => {
// expect(await screen.findByText('Syslog info')).toBeInTheDocument()
});
});
test("User wants to search for an IOC host", ({ given, when, then }) => {
given("the user is on the hosts page", () => {});
when("the user types a query in the search bar", () => {});
then("the hosts table should be updated with the search results", () => {});
});
test("There are more hosts than can be displayed", ({
given,
when,
then
}) => {
given("the user is on the hosts page", () => {});
when(
"there are more hosts matching the search query than can be displayed",
() => {}
);
then(
"a warning should be shown letting the user know that all hosts are not rendered",
() => {}
);
});
});
import React from "react";
import { loadFeature, defineFeature } from "jest-cucumber";
// import { render, screen, fireEvent, act, waitForElementToBeRemoved } from "@testing-library/react"
// import App from '../App';
// import { server } from "../mocks/server";
// import { rest } from 'msw'
// import { createMemoryHistory } from 'history'
// mock BrowserRouter (this should probably live somewhere else)
const reactRouterDom = require("react-router-dom");
reactRouterDom.BrowserRouter = ({ children }) => <div>{children}</div>;
// const Router = reactRouterDom.Router;
const feature = loadFeature("features/iocs/ioc-creation.feature");
defineFeature(feature, (test) => {
test("Logged in user opens IOC creation form", ({ given, when, then }) => {
given("the user is on the Home page", () => {});
when("the user clicks the create IOC button", () => {});
then("the user should be presented with a form to create an IOC", () => {});
});
// test('User opens IOC creation form', ({ given, when, then }) => {
// given('the user is on the Home page', () => {
// const history = createMemoryHistory()
// history.push('/home')
// render(
// <Router history={history}>
// <App />
// </Router>
// )
// });
// when('the user clicks the create IOC button', async () => {
// await screen.findByText(/new ioc/i);
// act(() => {
// fireEvent.click(screen.getByText(/new ioc/i));
// });
// await screen.findByTestId("create-dialog-title");
// });
// then('the user should be presented with a form to create an IOC', async () => {
// expect(await screen.findByText(/create new ioc/i)).toBeInTheDocument()
// });
// });
// test('User submits IOC creation form successfully', ({ given, and, when, then }) => {
// var page;
// const history = createMemoryHistory()
// given('the user has filled out the IOC creation form', async () => {
// history.push('/home')
// page = render(
// <Router history={history}>
// <App />
// </Router>
// )
// await screen.findByText(/new ioc/i);
// act(() => {
// fireEvent.click(screen.getByText(/new ioc/i));
// });
// });
// and('the information is valid', async () => {
// await screen.findByTestId("create-dialog-title")
// const iocName = page.getByLabelText(/ioc name/i);
// act(() => {
// fireEvent.change(iocName, {target: {value: 'TEST-IOC-1'}});
// });
// const git = page.getByLabelText(/git repository/i);
// act(() => {
// fireEvent.change(git, {target: {value: 'http://test-ioc.git'}});
// });
// });
// when('the user clicks on the Create IOC button', async () => {
// server.use(
// rest.post("*/iocs", (req, res, ctx) => {
// return res(ctx.status(201), ctx.json({
// description: "TS2 PSS IOC",
// owner: "testuser",
// id: 1,
// status: null,
// latestVersion: {
// id: 1,
// version: 1,
// createdAt: "2021-08-04T14:35:41.037+00:00",
// namingName: "TEST-IOC-1",
// sourceUrl: "http://test-ioc.git",
// sourceVersion: null
// },
// activeDeployment: null,
// hasLocalCommits: null,
// active: true,
// dirty: null
// })
// );
// }),
// rest.get("*/iocs/:iocId", (req, res, ctx) => {
// const { iocId } = req.params
// return res(ctx.json({
// description: "TS2 PSS IOC",
// owner: "testuser",
// id: iocId,
// status: null,
// latestVersion: {
// id: 1,
// version: 1,
// createdAt: "2021-08-04T14:35:41.037+00:00",
// namingName: "TEST-IOC-1",
// sourceUrl: "http://test-ioc.git",
// sourceVersion: null
// },
// activeDeployment: null,
// hasLocalCommits: null,
// active: true,
// dirty: null
// })
// );
// })
// );
// act(() => {
// fireEvent.click(screen.getByText('Create'));
// });
// await waitForElementToBeRemoved(() => screen.getByText('Create'));
// history.push('/iocs/1')
// render(
// <Router history={history}>
// <App />
// </Router>
// )
// await screen.findByTestId("ioc-details-container")
// });
// then('the user should be redirected to the new IOC details page', async () => {
// expect(await screen.findByText(/manage deployment/i)).toBeInTheDocument();
// });
// });
});
import React from "react";
import { loadFeature, defineFeature } from "jest-cucumber";
import { render, screen /* fireEvent, act*/ } from "@testing-library/react";
import App from "../App";
import { server } from "../mocks/server";
import { rest } from "msw";
import { createMemoryHistory } from "history";
// import { Router } from 'react-router-dom'
// mock BrowserRouter (this should probably live somewhere else)
const reactRouterDom = require("react-router-dom");
reactRouterDom.BrowserRouter = ({ children }) => <div>{children}</div>;
const Router = reactRouterDom.Router;
// TODO: Some of these tests don't do anything yet. They should probably fail insted of passing.
// But they do demonstrate how you can use the features in tests.
const feature = loadFeature("features/auth/login.feature");
defineFeature(feature, (test) => {
test("Opening application when not authenticated", async ({
given,
when,
then
}) => {
console.debug(server);
given("the user is not authenticated", () => {
// let's set a handler which returns 401 "Unauthorized" when fetching userInfo
// this is one of the first pieces of info the app tries to fetch
server.use(
rest.get("*/api/v1/git_helper/user_info", (req, res, ctx) => {
return res(ctx.status(401));
})
);
});
when("the user opens the application", async () => {
const history = createMemoryHistory();
history.push("/iocs");
render(
<Router history={history}>
<App />
</Router>
);
});
then("login button should appear", async () => {
// check that the login Button is shown
expect(await screen.findByText(/login/i)).toBeInTheDocument();
});
});
// test('Opening application root or login when already authenticated', ({ given, when, then }) => {
// given('the user is already authenticated', () => {
// // default mock api setup should return 200 and example data for userInfo
// });
// when('the user opens the application to the root or login page', () => {
// const history = createMemoryHistory()
// history.push('/')
// render(
// <Router history={history}>
// <App />
// </Router>
// )
// });
// then('the user should redirected to the IOCs list', async () => {
// expect(await screen.findByText(/new ioc/i)).toBeInTheDocument()
// });
// });
// test('Opening application path when already authenticated', ({ given, when, then }) => {
// given('the user is already authenticated', () => {
// // default api mock should be good enough to make the app think it is logged in.
// });
// when('the user opens the application with a valid path in the URL', () => {
// const history = createMemoryHistory()
// history.push('/deployments')
// render(
// <Router history={history}>
// <App />
// </Router>
// )
// });
// then('then the content at that path should be displayed without prompting for login', async () => {
// expect(await screen.findByText(/my deployments/i)).toBeInTheDocument()
// });
// });
// test('Entering an incorrect username or password into Login form', ({ given, when, then }) => {
// given('the user is on the login page', () => {
// const history = createMemoryHistory()
// history.push('/login')
// render(
// <Router history={history}>
// <App />
// </Router>
// )
// server.use(
// rest.post("*/login", (req, res, ctx) => {
// return res(ctx.status(500), ctx.json({
// description: 'Bad username/password'
// })
// );
// })
// );
// });
// when('the user enters a username or password which are not valid', async () => {
// await screen.findByText(/login/i);
// act(() => {
// fireEvent.click(screen.getByText(/login/i));
// });
// });
// then('the user should see a descriptive error message', async () => {
// //expect(await screen.findByText(/bad username/i)).toBeInTheDocument()
// });
// });
});
const jobMessages = {
queued: "job is queued on job server",
queued: "job is queued on the server",
running: "job is running",
failed: "job failed",
failed: "job has failed",
successful: "job was successful"
};
const typeMap = {
......@@ -31,7 +31,7 @@ export class AWXJobDetails {
return this.job?.status.toLowerCase();
}
message() {
const stem = this.job ? `The ${this.typeLabel()}: ` : "";
const stem = this.job ? `${this.typeLabel()} ` : "";
const info = this.job
? jobMessages[this.job.status.toLowerCase()]
: "Fetching data";
......
......@@ -26,7 +26,8 @@ export default function ChangeHostAdmin({
ioc,
getIOC,
resetTab,
buttonDisabled
buttonDisabled,
setButtonDisabled
}) {
const initHost = useMemo(
() => ({
......@@ -72,16 +73,18 @@ export default function ChangeHostAdmin({
useEffect(() => {
if (updateHostError) {
setButtonDisabled(false);
setError(updateHostError?.message);
}
}, [updateHostError, setError]);
}, [updateHostError, setError, setButtonDisabled]);
useEffect(() => {
if (updatedIoc) {
getIOC();
resetTab();
setButtonDisabled(false);
}
}, [updatedIoc, getIOC, resetTab]);
}, [updatedIoc, getIOC, resetTab, setButtonDisabled]);
useEffect(() => {
getHosts({ query: transformHostQuery(`${query}`) });
......@@ -93,6 +96,7 @@ export default function ChangeHostAdmin({
}, [setOpen, initHost]);
const onConfirm = useCallback(() => {
setButtonDisabled(true);
updateHost(
{
ioc_id: ioc.id
......@@ -103,7 +107,7 @@ export default function ChangeHostAdmin({
}
}
);
}, [updateHost, ioc, host?.csEntryHost?.id]);
}, [updateHost, ioc, host?.csEntryHost?.id, setButtonDisabled]);
let disabledButtonTitle = "";
if (buttonDisabled || ioc.operationInProgress) {
......
......@@ -12,7 +12,9 @@ export function DeployIOC({
submitCallback,
iocId,
hasActiveDeployment,
init = {}
init = {},
buttonDisabled,
setButtonDisabled
}) {
const [error, setError] = useState();
const client = useContext(apiContext);
......@@ -27,9 +29,10 @@ export function DeployIOC({
useEffect(() => {
if (deployError) {
setButtonDisabled(false);
setError(deployError?.message);
}
}, [deployError]);
}, [deployError, setButtonDisabled]);
const { watchDeployment } = useContext(notificationContext);
......@@ -44,6 +47,8 @@ export function DeployIOC({
hasActiveDeployment={hasActiveDeployment}
error={error}
resetError={() => setError(null)}
buttonDisabled={buttonDisabled}
setButtonDisabled={setButtonDisabled}
/>
);
} else {
......
......@@ -3,9 +3,9 @@
* Component providing link (and tag) to gitlab
*/
import React from "react";
import { Typography, Link as MuiLink, Stack } from "@mui/material";
import { Typography } from "@mui/material";
import { string } from "prop-types";
import LaunchIcon from "@mui/icons-material/Launch";
import { ExternalLink, ExternalLinkContents } from "../../common/Link";
const propTypes = {
/** String containing url to gitlab template */
......@@ -15,16 +15,7 @@ const propTypes = {
};
const defaultRenderLinkContents = (revision) => {
return (
<Stack
flexDirection="row"
alignItems="center"
gap={0.5}
>
{revision}
<LaunchIcon fontSize="small" />
</Stack>
);
return <ExternalLinkContents>{revision}</ExternalLinkContents>;
};
export default function GitRefLink({
......@@ -53,16 +44,13 @@ export default function GitRefLink({
return (
<Typography display="inline">
<MuiLink
<ExternalLink
href={url}
target="_blank"
rel="noreferrer"
underline="hover"
aria-label={`External Git Link, revision ${revision}`}
{...LinkProps}
>
{renderLinkContents(revision)}
</MuiLink>
</ExternalLink>
</Typography>
);
}
......
......@@ -4,7 +4,13 @@ import IOCDelete from "../IOCDelete";
import IOCDetailAdmin from "../IOCDetailAdmin";
import ChangeHostAdmin from "../ChangeHostAdmin";
export default function IOCAdmin({ ioc, getIOC, resetTab, buttonDisabled }) {
export default function IOCAdmin({
ioc,
getIOC,
resetTab,
buttonDisabled,
setButtonDisabled
}) {
return (
<>
<IOCDetailAdmin
......@@ -12,6 +18,7 @@ export default function IOCAdmin({ ioc, getIOC, resetTab, buttonDisabled }) {
getIOC={getIOC}
resetTab={resetTab}
buttonDisabled={buttonDisabled}
setButtonDisabled={setButtonDisabled}
/>
{ioc.activeDeployment && (
<ChangeHostAdmin
......@@ -19,6 +26,7 @@ export default function IOCAdmin({ ioc, getIOC, resetTab, buttonDisabled }) {
getIOC={getIOC}
resetTab={resetTab}
buttonDisabled={buttonDisabled}
setButtonDisabled={setButtonDisabled}
/>
)}
<AdministerUndeployment
......@@ -28,6 +36,7 @@ export default function IOCAdmin({ ioc, getIOC, resetTab, buttonDisabled }) {
<IOCDelete
ioc={ioc}
buttonDisabled={buttonDisabled}
setButtonDisabled={setButtonDisabled}
/>
</>
);
......
......@@ -13,7 +13,7 @@ import AccessControl from "../../auth/AccessControl";
import { apiContext } from "../../../api/DeployApi";
import { useAPIMethod } from "@ess-ics/ce-ui-common";
export default function IOCDelete({ ioc, buttonDisabled }) {
export default function IOCDelete({ ioc, buttonDisabled, setButtonDisabled }) {
const navigate = useNavigate();
// for the dialog
......@@ -41,15 +41,17 @@ export default function IOCDelete({ ioc, buttonDisabled }) {
useEffect(() => {
if (errorResponse) {
setButtonDisabled(false);
setError(errorResponse?.message);
}
}, [errorResponse, setError]);
}, [errorResponse, setError, setButtonDisabled]);
useEffect(() => {
if (dataready && !error) {
setButtonDisabled(false);
navigate(-1);
}
}, [dataready, navigate, error]);
}, [dataready, navigate, error, setButtonDisabled]);
let disabledButtonTitle = "";
......@@ -63,8 +65,9 @@ export default function IOCDelete({ ioc, buttonDisabled }) {
}, [setOpen]);
const onConfirm = useCallback(() => {
setButtonDisabled(true);
deleteIOC();
}, [deleteIOC]);
}, [deleteIOC, setButtonDisabled]);
return (
<>
......
......@@ -25,7 +25,9 @@ export function IOCDeployDialog({
hasActiveDeployment,
init = {},
error,
resetError
resetError,
buttonDisabled,
setButtonDisabled
}) {
const client = useContext(apiContext);
const {
......@@ -82,6 +84,7 @@ export function IOCDeployDialog({
const onSubmit = (event) => {
event.preventDefault();
setButtonDisabled(true);
const { git: gitText } = event.currentTarget.elements;
const git = gitText.value;
......@@ -304,7 +307,7 @@ export function IOCDeployDialog({
color="primary"
variant="contained"
type="submit"
disabled={!host || !gitVersion}
disabled={!host || !gitVersion || buttonDisabled}
>
Deploy
</Button>
......
......@@ -18,7 +18,8 @@ export default function IOCDetailAdmin({
ioc,
getIOC,
resetTab,
buttonDisabled
buttonDisabled,
setButtonDisabled
}) {
const [gitId, setGitId] = useState(ioc.gitProjectId);
......@@ -65,9 +66,10 @@ export default function IOCDetailAdmin({
useEffect(() => {
if (updateError) {
setButtonDisabled(false);
setError(updateError?.message);
}
}, [updateError, setError]);
}, [updateError, setError, setButtonDisabled]);
const requiredDataMissing = useCallback(() => !gitId || !name, [gitId, name]);
......@@ -104,6 +106,7 @@ export default function IOCDetailAdmin({
}, [setOpen]);
const onConfirm = useCallback(() => {
setButtonDisabled(true);
actionUpdateIoc(
{ ioc_id: ioc?.id },
{
......@@ -113,14 +116,15 @@ export default function IOCDetailAdmin({
}
}
);
}, [actionUpdateIoc, ioc, name, gitId]);
}, [actionUpdateIoc, ioc, name, gitId, setButtonDisabled]);
useEffect(() => {
if (uioc) {
getIOC();
resetTab();
setButtonDisabled(false);
}
}, [uioc, getIOC, resetTab]);
}, [uioc, getIOC, resetTab, setButtonDisabled]);
const iocIsDeployed = Boolean(ioc.activeDeployment);
......
......@@ -4,7 +4,7 @@ import {
Brightness1,
Cancel,
Error,
StopCircle,
RadioButtonUnchecked,
ErrorOutline,
HelpOutline,
PlayCircleFilled,
......@@ -61,7 +61,7 @@ export function IOCStatusIcon({ ioc }) {
},
inactive: {
title: "Inactive",
icon: StopCircle
icon: RadioButtonUnchecked
},
"inactive alert": {
title: "Alert",
......@@ -195,7 +195,7 @@ export function CommandTypeIcon({
return (
<LabeledIcon
label={iconTitle}
LabelProps={{ nowrap: true }}
LabelProps={{ noWrap: true }}
labelPosition={labelPosition}
Icon={StatusIcon}
IconProps={{ style: { iconStyle } }}
......
import { Button, Link as MuiLink, Tooltip, Typography } from "@mui/material";
import { Button, Stack, Tooltip, Typography } from "@mui/material";
import React, {
useState,
useEffect,
......@@ -19,6 +19,7 @@ import { DeploymentStatus } from "../../../api/DataTypes";
import { IOCService } from "../IOCService";
import { JobTable } from "../../Job";
import { apiContext } from "../../../api/DeployApi";
import { ExternalLink, ExternalLinkContents } from "../../common/Link";
export function IOCManage({
ioc,
......@@ -58,7 +59,6 @@ export function IOCManage({
const closeDeployModal = () => {
setDeployDialogOpen(false);
getIOC();
// history.push(window.location.pathname);
};
const closeUndeployModal = () => {
......@@ -79,28 +79,20 @@ export function IOCManage({
let subset = {
"Naming service record": (
<Typography>
<MuiLink
href={`${window.NAMING_ADDRESS}/devices.xhtml?i=2&deviceName=${ioc.namingName}`}
target="_blank"
rel="noreferrer"
underline="hover"
>
{ioc.namingName}
</MuiLink>
</Typography>
<ExternalLink
href={`${window.NAMING_ADDRESS}/devices.xhtml?i=2&deviceName=${ioc.namingName}`}
aria-label="Naming Service Record, External Link"
>
<ExternalLinkContents>{ioc.namingName}</ExternalLinkContents>
</ExternalLink>
),
Repository: (
<Typography>
<MuiLink
href={git}
target="_blank"
rel="noreferrer"
underline="hover"
>
{git}
</MuiLink>
</Typography>
<ExternalLink
href={git}
aria-label="Git Repository"
>
<ExternalLinkContents>{git}</ExternalLinkContents>
</ExternalLink>
)
};
......@@ -134,7 +126,6 @@ export function IOCManage({
]
);
let content = <></>;
if (ioc) {
const managedIOC = { ...ioc };
......@@ -166,8 +157,8 @@ export function IOCManage({
"There is an ongoing operation, you can not undeploy the IOC right now";
}
content = (
<>
return (
<Stack gap={2}>
<IOCDetails
ioc={managedIOC}
getSubset={getSubset}
......@@ -214,12 +205,15 @@ export function IOCManage({
allowedRoles={["DeploymentToolAdmin", "DeploymentToolIntegrator"]}
renderNoAccess={() => <></>}
>
<JobTable
jobs={operations}
pagination={pagination}
onPage={onPage}
loading={operationsLoading}
/>
<Stack gap={2}>
<Typography variant="h3">Operations</Typography>
<JobTable
jobs={operations}
pagination={pagination}
onPage={onPage}
loading={operationsLoading}
/>
</Stack>
<DeployIOC
open={deployDialogOpen}
setOpen={setDeployDialogOpen}
......@@ -227,17 +221,20 @@ export function IOCManage({
init={formInit}
iocId={ioc.id}
hasActiveDeployment={Boolean(ioc.activeDeployment)}
buttonDisabled={buttonDisabled}
setButtonDisabled={setButtonDisabled}
/>
<UndeployIOC
open={undeployDialogOpen}
setOpen={setUndeployDialogOpen}
submitCallback={closeUndeployModal}
buttonDisabled={buttonDisabled}
setButtonDisabled={setButtonDisabled}
ioc={ioc}
/>
</AccessControl>
</>
</Stack>
);
}
return <>{content}</>;
return null;
}
......@@ -5,30 +5,6 @@ import { IOCDescription } from "./IOCDescription";
import { IOCStatus } from "./IOCStatus";
import { noWrapText } from "../../common/Helper";
const ownIocsColumns = [
{
field: "status",
headerName: "Status",
flex: 0,
headerAlign: "center",
align: "center"
},
{
field: "namingName",
headerName: "IOC name",
width: "15ch",
sortable: false
},
{
field: "description",
headerName: "Description",
width: "20ch",
sortable: false
},
{ field: "host", headerName: "Host", width: "10ch", sortable: false },
{ field: "network", headerName: "Network", width: "10ch", sortable: false }
];
const exploreIocsColumns = [
{
field: "status",
......@@ -40,13 +16,11 @@ const exploreIocsColumns = [
{
field: "namingName",
headerName: "IOC name",
width: "15ch",
sortable: false
},
{
field: "description",
headerName: "Description",
width: "20ch",
sortable: false
},
{ field: "host", headerName: "Host", width: "10ch", sortable: false },
......@@ -64,14 +38,13 @@ const hostDetailsColumns = [
{
field: "namingName",
headerName: "IOC name",
width: "8ch",
sortable: false
},
{
field: "description",
headerName: "Description",
width: "20ch",
sortable: false
sortable: false,
flex: 3
}
];
......@@ -105,26 +78,6 @@ function createTableRowForHostDetails(ioc) {
};
}
function createTableRowForOwnIocs(ioc) {
const { id, activeDeployment, namingName } = ioc;
return {
id: id,
status: (
<IOCStatus
id={ioc.id}
activeDeployment={ioc.activeDeployment}
/>
),
namingName: (
<Link href={`/iocs/${id}`}>{noWrapText(namingName ?? "---")}</Link>
),
description: <IOCDescription id={ioc.id} />,
host: createHostLink(activeDeployment?.host),
network: noWrapText(activeDeployment?.host.network || "---")
};
}
function createTableRowForExploreIocs(ioc) {
const { id, namingName, activeDeployment } = ioc;
......@@ -147,13 +100,12 @@ function createTableRowForExploreIocs(ioc) {
export function IOCTable({
iocs = [],
rowType = "own",
rowType = "explore",
loading,
pagination,
onPage
}) {
const tableTypeSpecifics = {
own: [ownIocsColumns, createTableRowForOwnIocs],
host: [hostDetailsColumns, createTableRowForHostDetails],
explore: [exploreIocsColumns, createTableRowForExploreIocs]
};
......
......@@ -14,7 +14,9 @@ export function IOCUndeployDialog({
setOpen,
submitCallback,
ioc,
error
error,
buttonDisabled,
setButtonDisabled
}) {
const handleClose = () => {
setOpen(false);
......@@ -22,6 +24,7 @@ export function IOCUndeployDialog({
const onSubmit = (event) => {
event.preventDefault();
setButtonDisabled(true);
submitCallback({
ioc_id: ioc.id
......@@ -60,6 +63,7 @@ export function IOCUndeployDialog({
color="primary"
variant="contained"
type="submit"
disabled={buttonDisabled}
>
Undeploy
</Button>
......
......@@ -6,7 +6,14 @@ import { apiContext } from "../../../api/DeployApi";
import { useAPIMethod } from "@ess-ics/ce-ui-common";
// Process component
export function UndeployIOC({ open, setOpen, submitCallback, ioc }) {
export function UndeployIOC({
open,
setOpen,
submitCallback,
ioc,
buttonDisabled,
setButtonDisabled
}) {
const [error, setError] = useState();
const client = useContext(apiContext);
const {
......@@ -20,9 +27,10 @@ export function UndeployIOC({ open, setOpen, submitCallback, ioc }) {
useEffect(() => {
if (deploymentError) {
setButtonDisabled(false);
setError(deploymentError?.message);
}
}, [deploymentError]);
}, [deploymentError, setButtonDisabled]);
const { watchDeployment } = useContext(notificationContext);
......@@ -34,6 +42,8 @@ export function UndeployIOC({ open, setOpen, submitCallback, ioc }) {
submitCallback={action}
ioc={ioc}
error={error}
buttonDisabled={buttonDisabled}
setButtonDisabled={setButtonDisabled}
/>
);
} else {
......
import React, { useEffect, useState, useMemo } from "react";
import {
Grid,
Typography,
Card,
CardContent,
Container,
Link as MuiLink
Link as MuiLink,
Stack
} from "@mui/material";
import {
KeyValueTable,
......@@ -20,6 +19,8 @@ import GitRefLink from "../IOC/GitRefLink";
import AccessControl from "../auth/AccessControl";
import { AWXJobDetails } from "../../api/DataTypes";
import { JobStatusStepper } from "./JobStatus";
import { ExternalLink, ExternalLinkContents } from "../common/Link";
import { JobDuration } from "./JobDuration";
function createAlert(operation, job) {
const jobDetails = new AWXJobDetails(operation.type, job);
......@@ -83,98 +84,68 @@ export function JobDetails({ operation, job }) {
</MuiLink>
),
"AWX job link": (
<Typography>
<MuiLink
href={operation.awxJobUrl}
target="_blank"
rel="noreferrer"
underline="hover"
>
{operation.awxJobUrl}
</MuiLink>
</Typography>
<ExternalLink
href={operation.awxJobUrl}
aria-label="AWX job"
>
<ExternalLinkContents>{operation.awxJobUrl}</ExternalLinkContents>
</ExternalLink>
),
"created time": operation?.createdAt
? formatDate(operation.createdAt)
: "-",
"AWX job start time": operation?.startTime
? formatDate(operation.startTime)
: "-"
: "-",
duration: operation?.finishedAt ? (
<JobDuration
job={operation}
renderContents={(duration) => duration}
/>
) : (
"-"
)
};
const finishedJobAlerts = finishedJob ? (
<AlertBannerList alerts={[alert].concat(jobAlert)} />
) : null;
const unFinishedJobsWithAlerts =
!finishedJob && alert ? <AlertBannerList alerts={[alert]} /> : null;
return (
<Grid
container
spacing={1}
>
{finishedJob && (
<Grid
item
xs={12}
>
<AlertBannerList alerts={[alert].concat(jobAlert)} />
</Grid>
)}
<Grid
item
xs={12}
md={12}
>
<SimpleAccordion
defaultExpanded
summary={<JobBadge operation={operation} />}
>
<KeyValueTable
obj={deploymentDetails}
variant="overline"
sx={{ border: 0 }}
valueOptions={{ headerName: "" }}
/>
</SimpleAccordion>
</Grid>
<Grid
item
xs={12}
md={12}
>
<Stack spacing={2}>
<Stack>
{finishedJobAlerts}
{unFinishedJobsWithAlerts}
</Stack>
<JobBadge operation={operation} />
<KeyValueTable
obj={deploymentDetails}
variant="overline"
sx={{ border: 0 }}
valueOptions={{ headerName: "" }}
/>
{job ? (
<Card>
<CardContent>
<div>{job && <JobStatusStepper {...{ job, operation }} />}</div>
<JobStatusStepper {...{ job, operation }} />
</CardContent>
</Card>
</Grid>
{!finishedJob && alert && (
<Grid
item
xs={12}
>
<AlertBannerList alerts={[alert]} />
</Grid>
)}
) : null}
<AccessControl
allowedRoles={["DeploymentToolAdmin", "DeploymentToolIntegrator"]}
renderNoAccess={() => <></>}
>
<Grid
item
xs={12}
md={12}
style={{ paddingBottom: 0 }}
>
{job ? (
<SimpleAccordion
summary="Ansible Job Output"
defaultExpanded
>
<Container>
<DeploymentJobOutput deploymentJob={job} />
</Container>
</SimpleAccordion>
) : (
<></>
)}
</Grid>
{job ? (
<SimpleAccordion summary="Ansible Job Output">
<DeploymentJobOutput deploymentJob={job} />
</SimpleAccordion>
) : (
<></>
)}
</AccessControl>
</Grid>
</Stack>
);
}
import React from "react";
import AccessTimeIcon from "@mui/icons-material/AccessTime";
import { Tooltip } from "@mui/material";
import LabeledIcon from "../common/LabeledIcon";
import moment from "moment";
const formattedDuration = ({ startDate, finishDate }) => {
if (startDate && finishDate) {
return moment
.utc(finishDate.getTime() - startDate.getTime())
.format("HH:mm:ss");
} else {
return null;
}
};
export const JobDuration = ({ job, renderContents }) => {
const createOrStartDate = new Date(job?.startTime ?? job.createdAt);
const duration = formattedDuration({
finishDate: new Date(job.finishedAt),
startDate: new Date(createOrStartDate)
});
const contents = renderContents ? (
renderContents(duration)
) : (
<LabeledIcon
label={`${duration}`}
LabelProps={{ variant: "body2" }}
labelPosition="right"
Icon={AccessTimeIcon}
IconProps={{ fontSize: "small" }}
/>
);
return (
<Tooltip
title={`Finished ${job.finishedAt}`}
aria-label={`Finshed ${job.finishedAt}, after ${duration}`}
>
{contents}
</Tooltip>
);
};