Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Frontend - HealthCheck and Pull buttons #2025

Merged
merged 5 commits into from
Dec 14, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 47 additions & 16 deletions frontend/src/components/plugins/types/pluginActionsButtons.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from "react";
import PropTypes from "prop-types";
import { Spinner, Button, Modal, ModalHeader, ModalBody } from "reactstrap";
import { RiHeartPulseLine } from "react-icons/ri";
import { MdDelete } from "react-icons/md";
import { MdDelete, MdFileDownload } from "react-icons/md";
import { BsPeopleFill } from "react-icons/bs";

import { IconButton } from "@certego/certego-ui";
Expand All @@ -12,8 +12,8 @@ import { useOrganizationStore } from "../../../stores/useOrganizationStore";
import { usePluginConfigurationStore } from "../../../stores/usePluginConfigurationStore";

// we can't delete this function because IconButton expects Icon as a function
function PluginHealthSpinner() {
return <Spinner type="ripple" size="sm" className="text-darker" />;
function PluginSpinner() {
return <Spinner type="border" size="sm" className="text-darker" />;
}

export function PluginHealthCheckButton({ pluginName, pluginType_ }) {
Expand All @@ -26,39 +26,32 @@ export function PluginHealthCheckButton({ pluginName, pluginType_ }) {
),
);
const [isLoading, setIsLoading] = React.useState(false);
const [isHealthy, setIsHealthy] = React.useState(undefined);

const onClick = async () => {
setIsLoading(true);
const status = await checkPluginHealth(pluginType_, pluginName);
setIsHealthy(status);
await checkPluginHealth(pluginType_, pluginName);
setIsLoading(false);
};

return (
<div className="d-flex flex-column align-items-center">
<div className="d-flex flex-column align-items-center px-2">
<IconButton
id={`table-pluginhealthcheckbtn__${pluginName}`}
color="info"
size="sm"
Icon={!isLoading ? RiHeartPulseLine : PluginHealthSpinner}
title={!isLoading ? "perform health check" : "please wait..."}
Icon={!isLoading ? RiHeartPulseLine : PluginSpinner}
title={!isLoading ? "Perform health check" : "Please wait..."}
onClick={onClick}
titlePlacement="top"
/>
{isHealthy !== undefined &&
(isHealthy ? (
<span className="mt-2 text-success">Up and running!</span>
) : (
<span className="mt-2 text-warning">Failing!</span>
))}
</div>
);
}

PluginHealthCheckButton.propTypes = {
pluginName: PropTypes.string.isRequired,
pluginType_: PropTypes.oneOf(["analyzer", "connector"]).isRequired,
pluginType_: PropTypes.oneOf(["analyzer", "connector", "ingestor", "pivot"])
.isRequired,
};

export function OrganizationPluginStateToggle({
Expand Down Expand Up @@ -212,3 +205,41 @@ export function PlaybooksDeletionButton({ playbookName }) {
PlaybooksDeletionButton.propTypes = {
playbookName: PropTypes.string.isRequired,
};

export function PluginPullButton({ pluginName, pluginType_ }) {
const { pluginPull } = usePluginConfigurationStore(
React.useCallback(
(state) => ({
pluginPull: state.pluginPull,
}),
[],
),
);
const [isLoading, setIsLoading] = React.useState(false);

const onClick = async () => {
setIsLoading(true);
await pluginPull(pluginType_, pluginName);
setIsLoading(false);
};

return (
<div className="d-flex flex-column align-items-center px-2">
<IconButton
id={`table-pluginpullbtn__${pluginName}`}
color="info"
size="sm"
Icon={!isLoading ? MdFileDownload : PluginSpinner}
title={!isLoading ? "Pull" : "Please wait..."}
onClick={onClick}
titlePlacement="top"
/>
</div>
);
}

PluginPullButton.propTypes = {
pluginName: PropTypes.string.isRequired,
pluginType_: PropTypes.oneOf(["analyzer", "connector", "ingestor", "pivot"])
.isRequired,
};
48 changes: 41 additions & 7 deletions frontend/src/components/plugins/types/pluginTableColumns.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
OrganizationPluginStateToggle,
PluginHealthCheckButton,
PlaybooksDeletionButton,
PluginPullButton,
} from "./pluginActionsButtons";
import { JobTypes } from "../../../constants/jobConst";

Expand Down Expand Up @@ -206,12 +207,14 @@ export const analyzersTableColumns = [
refetch={value?.refetch}
type={PluginsTypes.ANALYZER}
/>
{value?.docker_based && (
<PluginHealthCheckButton
pluginName={value.name}
pluginType_={PluginsTypes.ANALYZER}
/>
)}
<PluginHealthCheckButton
pluginName={value.name}
pluginType_={PluginsTypes.ANALYZER}
/>
<PluginPullButton
pluginName={value.name}
pluginType_={PluginsTypes.ANALYZER}
/>
</div>
),
maxWidth: 100,
Expand Down Expand Up @@ -271,6 +274,10 @@ export const connectorTableColumns = [
pluginName={value?.name}
pluginType_={PluginsTypes.CONNECTOR}
/>
<PluginPullButton
pluginName={value.name}
pluginType_={PluginsTypes.CONNECTOR}
/>
</div>
),
maxWidth: 125,
Expand Down Expand Up @@ -324,6 +331,14 @@ export const pivotTableColumns = [
refetch={value?.refetch}
type={PluginsTypes.PIVOT}
/>
<PluginHealthCheckButton
pluginName={value.name}
pluginType_={PluginsTypes.PIVOT}
/>
<PluginPullButton
pluginName={value.name}
pluginType_={PluginsTypes.PIVOT}
/>
</div>
),
maxWidth: 125,
Expand Down Expand Up @@ -494,7 +509,7 @@ export const visualizerTableColumns = [
maxWidth: 90,
},
];
// Visualizers columns: these columns are shown for the visualizers
// Ingestors columns: these columns are shown for the ingestors
export const ingestorTableColumns = [
...pluginTableColumns,
{
Expand Down Expand Up @@ -542,4 +557,23 @@ export const ingestorTableColumns = [
disableSortBy: true,
maxWidth: 145,
},
{
Header: "Actions",
id: "actions",
accessor: (r) => r,
disableSortBy: true,
Cell: ({ value }) => (
<div className="d-flex justify-content-center mx-2">
<PluginHealthCheckButton
pluginName={value.name}
pluginType_={PluginsTypes.INGESTOR}
/>
<PluginPullButton
pluginName={value.name}
pluginType_={PluginsTypes.INGESTOR}
/>
</div>
),
maxWidth: 90,
},
];
64 changes: 56 additions & 8 deletions frontend/src/stores/usePluginConfigurationStore.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
PLAYBOOKS_CONFIG_URI,
INGESTORS_CONFIG_URI,
} from "../constants/apiURLs";
import { prettifyErrors } from "../utils/api";

async function downloadAllPlugin(pluginUrl) {
const pageSize = 70;
Expand Down Expand Up @@ -195,10 +196,57 @@ export const usePluginConfigurationStore = create((set, get) => ({
);
console.debug("usePluginConfigurationStore - checkPluginHealth: ");
console.debug(resp);
return Promise.resolve(resp.data?.status); // status is of type boolean
if (resp.data?.status)
addToast(
"Health check status: success",
`${PluginName} is up and running`,
"success",
true,
);
else
addToast(
"Health check status: warning",
`${PluginName} is NOT up`,
"warning",
true,
);
return Promise.resolve(resp.status);
} catch (e) {
addToast("Failed!", e.parsedMsg.toString(), "danger");
return null;
console.error(e);
addToast(
"Health check status: failed",
prettifyErrors(e),
"danger",
true,
);
return e.response.status;
}
},
pluginPull: async (type, PluginName) => {
try {
const resp = await axios.post(
`${API_BASE_URI}/${type}/${PluginName}/pull`,
);
console.debug("usePluginConfigurationStore - pluginPull: ");
console.debug(resp);
if (resp.data?.status)
addToast(
"Plugin pull: success",
`${PluginName} updated`,
"success",
true,
);
else
addToast(
"Plugin pull: warning",
`${PluginName} pull failed`,
"warning",
true,
);
return Promise.resolve(resp.status);
} catch (e) {
addToast("Plugin pull: failed", prettifyErrors(e), "danger", true);
return e.response.status;
}
},
deletePlaybook: async (playbook) => {
Expand All @@ -209,7 +257,7 @@ export const usePluginConfigurationStore = create((set, get) => ({
addToast(`${playbook} deleted`, null, "info");
return Promise.resolve(response);
} catch (e) {
addToast("Failed!", e.parsedMsg, "danger");
addToast("Failed!", prettifyErrors(e), "danger");
return null;
}
},
Expand All @@ -226,7 +274,7 @@ export const usePluginConfigurationStore = create((set, get) => ({
} catch (error) {
addToast(
`Failed to enabled ${pluginName} for the organization`,
error.parsedMsg,
prettifyErrors(error),
"danger",
true,
);
Expand All @@ -242,7 +290,7 @@ export const usePluginConfigurationStore = create((set, get) => ({
} catch (error) {
addToast(
`Failed to enabled ${pluginName} for the organization`,
error.parsedMsg,
prettifyErrors(error),
"danger",
true,
);
Expand All @@ -262,7 +310,7 @@ export const usePluginConfigurationStore = create((set, get) => ({
} catch (error) {
addToast(
`Failed to disabled ${pluginName} for the organization`,
error.parsedMsg,
prettifyErrors(error),
"danger",
true,
);
Expand All @@ -278,7 +326,7 @@ export const usePluginConfigurationStore = create((set, get) => ({
} catch (error) {
addToast(
`Failed to disabled ${pluginName} for the organization`,
error.parsedMsg,
prettifyErrors(error),
"danger",
true,
);
Expand Down
19 changes: 19 additions & 0 deletions frontend/src/utils/api.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react";

export function prettifyErrors(errorResponse) {
// multiple validation errors
// only validation errors returns an array of errors
/**
"errors":{
Expand All @@ -24,6 +25,24 @@ export function prettifyErrors(errorResponse) {
prettyHTMLList = prettyHTMLList.map((e) => <li>{e}</li>);
return <ul>{prettyHTMLList}</ul>;
}
// single validation error
/**
"errors":{
"detail": "Not implemented",
}
*/
if (errorResponse.response.data?.errors?.detail) {
return errorResponse.response.data.errors.detail;
}
// error directly in response data
/**
"data":{
"detail": "Method "POST" not allowed.",
}
*/
if (errorResponse.response.data?.detail) {
return errorResponse.response.data.detail;
}

return JSON.stringify(errorResponse.response.data);
}
Loading
Loading