Skip to content

Add Create, Edit, Delete for LifeCycle Policies #1004

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

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
98f596b
Display LifeCyclePolicies in UI (read-only)
Arnei Oct 10, 2024
5e5a850
Fix typescript complaint
Arnei Oct 14, 2024
c2d1c40
Add Create, Edit, Delete for LifeCycle Policies
Arnei Nov 4, 2024
2dd5a74
Merge branch 'main' into lifecycle-write
Arnei Dec 11, 2024
aae8eda
Merge branch 'main' into lifecycle-write
Arnei Dec 12, 2024
d6ae3d8
Merge branch 'main' into lifecycle-write
Arnei Jan 3, 2025
9fff939
Merge branch 'main' into lifecycle-write
Arnei Jan 15, 2025
f40dc0f
Merge branch 'main' into lifecycle-write
Arnei Jan 17, 2025
9caae0f
Merge branch 'main' into lifecycle-write
Arnei Jan 31, 2025
865e34c
Fix modal usage errors
Arnei Jan 31, 2025
691ffac
Merge branch 'main' into lifecycle-write
Arnei Feb 11, 2025
cd4becf
Merge branch 'main' into lifecycle-write
Arnei Feb 26, 2025
e876307
Merge branch 'main' into lifecycle-write
Arnei Feb 27, 2025
5e15fb4
Merge branch 'main' into lifecycle-write
Arnei Mar 10, 2025
05854ff
Merge branch 'main' into lifecycle-write
Arnei Mar 20, 2025
ef31bea
Merge branch 'main' into lifecycle-write
Arnei Apr 2, 2025
c31bda6
Merge branch 'main' into lifecycle-write
Arnei Apr 2, 2025
c3c8672
Fix various eslint complaints
Arnei Apr 2, 2025
565ded6
Merge branch 'main' into lifecycle-write
Arnei Apr 2, 2025
326bb92
Merge branch 'main' into lifecycle-write
Arnei Apr 2, 2025
8815f37
Fix undefined string
Arnei Apr 9, 2025
659fbc8
Merge branch 'main' into lifecycle-write
Arnei May 15, 2025
d293d54
Merge branch 'main' into lifecycle-write
Arnei May 15, 2025
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
8,072 changes: 3,565 additions & 4,507 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"react-hotkeys-hook": "^5.0.1",
"react-i18next": "^15.5.1",
"react-icons": "^5.5.0",
"react-js-cron": "^5.0.1",
"react-redux": "^9.2.0",
"react-router": "^7.6.0",
"react-select": "^5.10.1",
Expand Down
3 changes: 3 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import Acls from "./components/users/Acls";
import About from "./components/About";
import { useAppDispatch } from "./store";
import { fetchOcVersion, fetchUserInfo } from "./slices/userInfoSlice";
import LifeCyclePolicies from "./components/events/LifeCyclePolicies";
import { subscribeToAuthEvents } from "./utils/broadcastSync";

function App() {
Expand Down Expand Up @@ -45,6 +46,8 @@ function App() {

<Route path={"/events/series"} element={<Series />} />

<Route path={"/events/lifeCyclePolicies"} element={<LifeCyclePolicies />} />

<Route path={"/recordings/recordings"} element={<Recordings />} />

<Route path={"/systems/jobs"} element={<Jobs />} />
Expand Down
121 changes: 121 additions & 0 deletions src/components/events/LifeCyclePolicies.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import React, { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import TableFilters from "../shared/TableFilters";
import Table from "../shared/Table";
import Notifications from "../shared/Notifications";
import { loadLifeCyclePoliciesIntoTable } from "../../thunks/tableThunks";
import { fetchFilters, editTextFilter } from "../../slices/tableFilterSlice";
import Header from "../Header";
import NavBar from "../NavBar";
import MainView from "../MainView";
import Footer from "../Footer";
import { useAppDispatch, useAppSelector } from "../../store";
import { AsyncThunk } from "@reduxjs/toolkit";
import { getTotalLifeCyclePolicies } from "../../selectors/lifeCycleSelectors";
import { fetchLifeCyclePolicies } from "../../slices/lifeCycleSlice";
import { lifeCyclePoliciesTemplateMap } from "../../configs/tableConfigs/lifeCyclePoliciesTableMap";
import { fetchLifeCyclePolicyActions, fetchLifeCyclePolicyTargetTypes, fetchLifeCyclePolicyTimings } from "../../slices/lifeCycleDetailsSlice";
import { ModalHandle } from "../shared/modals/Modal";
import { availableHotkeys } from "../../configs/hotkeysConfig";
import { eventsLinks } from "./partials/EventsNavigation";
import { resetTableProperties } from "../../slices/tableSlice";

/**
* This component renders the table view of policies
*/
const LifeCyclePolicies = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const [displayNavigation, setNavigation] = useState(false);
const newPolicyModalRef = useRef<ModalHandle>(null);

const policiesTotal = useAppSelector(state => getTotalLifeCyclePolicies(state));

useEffect(() => {
// State variable for interrupting the load function
let allowLoadIntoTable = true;

// Clear table of previous data
dispatch(resetTableProperties());

dispatch(fetchFilters("lifeCyclePolicies"));

// Reset text filter
dispatch(editTextFilter(""));

// Load policies on mount
const loadLifeCyclePolicies = async () => {
// Fetching policies from server
await dispatch(fetchLifeCyclePolicies());

// Load policies into table
if (allowLoadIntoTable) {
dispatch(loadLifeCyclePoliciesIntoTable());
}
};
loadLifeCyclePolicies();

// Fetch policies repeatedly
let fetchInterval = setInterval(loadLifeCyclePolicies, 5000);

return () => {
allowLoadIntoTable = false;
clearInterval(fetchInterval);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const showNewPolicyModal = async () => {
await dispatch(fetchLifeCyclePolicyActions());
await dispatch(fetchLifeCyclePolicyTargetTypes());
await dispatch(fetchLifeCyclePolicyTimings());

newPolicyModalRef.current?.open()
};

return (
<>
<Header />
<NavBar
displayNavigation={displayNavigation}
setNavigation={setNavigation}
navAriaLabel={"EVENTS.EVENTS.NAVIGATION.LABEL"}
links={
eventsLinks
}
create={{
accessRole: "ROLE_UI_EVENTS_CREATE",
onShowModal: showNewPolicyModal,
text: "LIFECYCLE.POLICIES.TABLE.ADD_POLICY",
resource: "lifecyclepolicy",
}}
>
</NavBar>

<MainView open={displayNavigation}>
{/* Include notifications component */}
<Notifications context={"other"}/>

<div className="controls-container">
{/* Include filters component */}
{/* LifeCycle policies are not indexed, can't search or filter them */}
{/* But if we don't include this component, the policies won't load on page load, because the first
fetch request we send to the backend contains invalid params >.> */}
<TableFilters
loadResource={fetchLifeCyclePolicies as AsyncThunk<any, void, any>}
loadResourceIntoTable={loadLifeCyclePoliciesIntoTable}
resource={"lifeCyclePolicies"}
/>

<h1>{t("LIFECYCLE.POLICIES.TABLE.CAPTION")}</h1>
<h4>{t("TABLE_SUMMARY", { numberOfRows: policiesTotal })}</h4>
</div>
{/* Include table component */}
<Table templateMap={lifeCyclePoliciesTemplateMap} />
</MainView>
<Footer />
</>
);
};

export default LifeCyclePolicies;
5 changes: 5 additions & 0 deletions src/components/events/partials/EventsNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,10 @@ export const eventsLinks: {
path: "/events/series",
accessRole: "ROLE_UI_SERIES_VIEW",
text: "EVENTS.EVENTS.NAVIGATION.SERIES"
},
{
path: "/events/lifeCyclePolicies",
accessRole: "ROLE_UI_LIFECYCLEPOLICIES_VIEW",
text: "LIFECYCLE.NAVIGATION.POLICIES"
}
];
97 changes: 97 additions & 0 deletions src/components/events/partials/LifeCyclePolicyActionCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import React, { useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useAppDispatch, useAppSelector } from "../../../store";
import { Tooltip } from "../../shared/Tooltip";
import { deleteLifeCyclePolicy, LifeCyclePolicy } from "../../../slices/lifeCycleSlice";
import DetailsModal from "../../shared/modals/DetailsModal";
import LifeCyclePolicyDetails from "./modals/LifeCyclePolicyDetails";
import { hasAccess } from "../../../utils/utils";
import { getUserInformation } from "../../../selectors/userInfoSelectors";
import { fetchLifeCyclePolicyDetails } from "../../../slices/lifeCycleDetailsSlice";
import ConfirmModal from "../../shared/ConfirmModal";
import { ModalHandle } from "../../shared/modals/Modal";

/**
* This component renders the title cells of series in the table view
*/
const LifeCyclePolicyActionCell = ({
row,
}: {
row: LifeCyclePolicy
}) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();

const [displayLifeCyclePolicyDetails, setLifeCyclePolicyDetails] = useState(false);
const deleteConfirmationModalRef = useRef<ModalHandle>(null);

const user = useAppSelector(state => getUserInformation(state));

const showLifeCyclePolicyDetails = async () => {
await dispatch(fetchLifeCyclePolicyDetails(row.id));

setLifeCyclePolicyDetails(true);
};

const hideLifeCyclePolicyDetails = () => {
setLifeCyclePolicyDetails(false);
};

const hideDeleteConfirmation = () => {
deleteConfirmationModalRef.current?.close?.()
};

const showDeleteConfirmation = async () => {
deleteConfirmationModalRef.current?.open()
};

const deletingPolicy = (id: string) => {
dispatch(deleteLifeCyclePolicy(id));
};

return (
<>
{/* view details location/recording */}
{hasAccess("ROLE_UI_LIFECYCLEPOLICY_DETAILS_VIEW", user) && (
<Tooltip title={t("LIFECYCLE.POLICIES.TABLE.TOOLTIP.DETAILS")}>
<button
className="button-like-anchor more"
onClick={() => showLifeCyclePolicyDetails()}
/>
</Tooltip>
)}

{displayLifeCyclePolicyDetails && (
<DetailsModal
handleClose={hideLifeCyclePolicyDetails}
title={row.title}
prefix={"LIFECYCLE.POLICIES.DETAILS.HEADER"}
>
<LifeCyclePolicyDetails />
</DetailsModal>
)}

{/* delete policy */}
{hasAccess("ROLE_UI_LIFECYCLEPOLICY_DELETE", user) && (
<Tooltip title={t("LIFECYCLE.POLICIES.TABLE.TOOLTIP.DELETE")}>
<button
onClick={() => showDeleteConfirmation()}
className="button-like-anchor remove"

/>
</Tooltip>
)}

<ConfirmModal
close={hideDeleteConfirmation}
resourceName={row.title}
resourceType="LIFECYCLE_POLICY"
resourceId={row.id}
deleteMethod={deletingPolicy}
modalRef={deleteConfirmationModalRef}
/>
</>
);
};

export default LifeCyclePolicyActionCell;
22 changes: 22 additions & 0 deletions src/components/events/partials/LifeCyclePolicyIsActiveCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from "react";
import { LifeCyclePolicy } from "../../../slices/lifeCycleSlice";

/**
* This component renders the maintenance cells of servers in the table view
*/
const LifeCyclePolicyIsActiveCell = ({
row,
}: {
row: LifeCyclePolicy
}) => {

return (
<input
type="checkbox"
checked={row.isActive}
disabled={true}
/>
);
};

export default LifeCyclePolicyIsActiveCell;
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import ResourceDetailsAccessPolicyTab from "../../../shared/modals/ResourceDetailsAccessPolicyTab";
import { removeNotificationWizardForm } from "../../../../slices/notificationSlice";
import { useAppDispatch, useAppSelector } from "../../../../store";
import { getLifeCyclePolicyDetailsAcl } from "../../../../selectors/lifeCycleDetailsSelectors";
import { fetchLifeCyclePolicyDetailsAcls, updateLifeCyclePolicyAccess } from "../../../../slices/lifeCycleDetailsSlice";
import { ParseKeys } from "i18next";

/**
* This component manages the access policy tab of the series details modal
*/
const LifeCyclePolicyDetailsAccessTab = ({
seriesId,
header,
policyChanged,
setPolicyChanged,
}: {
seriesId: string,
header: ParseKeys,
policyChanged: boolean,
setPolicyChanged: (value: boolean) => void,
}) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();

const acl = useAppSelector(state => getLifeCyclePolicyDetailsAcl(state));

useEffect(() => {
dispatch(removeNotificationWizardForm());
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<ResourceDetailsAccessPolicyTab
resourceId={seriesId}
header={header}
buttonText={"LIFECYCLE.POLICIES.DETAILS.ACCESS.LABEL"}
saveButtonText={"SAVE"}
descriptionText={t("LIFECYCLE.POLICIES.DETAILS.ACCESS.DESCRIPTION")}
policies={acl}
fetchAccessPolicies={fetchLifeCyclePolicyDetailsAcls}
// @ts-expect-error: TODO: Figure out why typescript is being funky here
saveNewAccessPolicies={updateLifeCyclePolicyAccess}
editAccessRole={"ROLE_UI_LIFECYCLEPOLICY_DETAILS_ACL_EDIT"}
policyChanged={policyChanged}
setPolicyChanged={setPolicyChanged}
/>
);
};

export default LifeCyclePolicyDetailsAccessTab;
Loading
Loading