diff --git a/src/App.tsx b/src/App.tsx
index 3acd3833e9..4e2a003aa7 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -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";
function App() {
const dispatch = useAppDispatch();
@@ -41,6 +42,8 @@ function App() {
} />
+ } />
+
} />
} />
diff --git a/src/components/events/LifeCyclePolicies.tsx b/src/components/events/LifeCyclePolicies.tsx
new file mode 100644
index 0000000000..f61c9e1d7d
--- /dev/null
+++ b/src/components/events/LifeCyclePolicies.tsx
@@ -0,0 +1,150 @@
+import React, { useEffect, useState } from "react";
+import MainNav from "../shared/MainNav";
+import { useTranslation } from "react-i18next";
+import { Link } from "react-router";
+import cn from "classnames";
+import TableFilters from "../shared/TableFilters";
+import Table from "../shared/Table";
+import Notifications from "../shared/Notifications";
+import { loadEventsIntoTable, loadLifeCyclePoliciesIntoTable, loadSeriesIntoTable } from "../../thunks/tableThunks";
+import { fetchFilters, editTextFilter, fetchStats } from "../../slices/tableFilterSlice";
+import Header from "../Header";
+import NavBar from "../NavBar";
+import MainView from "../MainView";
+import Footer from "../Footer";
+import { getUserInformation } from "../../selectors/userInfoSelectors";
+import { hasAccess } from "../../utils/utils";
+import { getCurrentFilterResource } from "../../selectors/tableFilterSelectors";
+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 { fetchEvents } from "../../slices/eventSlice";
+import { setOffset } from "../../slices/tableSlice";
+import { fetchSeries } from "../../slices/seriesSlice";
+
+/**
+ * This component renders the table view of policies
+ */
+const LifeCyclePolicies = () => {
+ const { t } = useTranslation();
+ const dispatch = useAppDispatch();
+ const [displayNavigation, setNavigation] = useState(false);
+
+ const user = useAppSelector(state => getUserInformation(state));
+ const policiesTotal = useAppSelector(state => getTotalLifeCyclePolicies(state));
+ const currentFilterType = useAppSelector(state => getCurrentFilterResource(state));
+
+ const loadEvents = async () => {
+ // Fetching stats from server
+ dispatch(fetchStats());
+
+ // Fetching events from server
+ await dispatch(fetchEvents());
+
+ // Load events into table
+ dispatch(loadEventsIntoTable());
+ };
+
+ const loadSeries = () => {
+ // Reset the current page to first page
+ dispatch(setOffset(0));
+
+ //fetching series from server
+ dispatch(fetchSeries());
+
+ //load series into table
+ dispatch(loadSeriesIntoTable());
+ };
+
+ const loadLifeCyclePolicies = async () => {
+ // Fetching policies from server
+ await dispatch(fetchLifeCyclePolicies());
+
+ // Load policies into table
+ dispatch(loadLifeCyclePoliciesIntoTable());
+ };
+
+ useEffect(() => {
+ if ("lifeCyclePolicies" !== currentFilterType) {
+ dispatch(fetchFilters("lifeCyclePolicies"));
+ }
+
+ // Reset text filter
+ dispatch(editTextFilter(""));
+
+ // Load policies on mount
+ loadLifeCyclePolicies().then((r) => console.info(r));
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const toggleNavigation = () => {
+ setNavigation(!displayNavigation);
+ };
+
+ return (
+ <>
+
+
+ {/* Include Burger-button menu*/}
+
+
+
+ {hasAccess("ROLE_UI_EVENTS_VIEW", user) && (
+ loadEvents()}
+ >
+ {t("EVENTS.EVENTS.NAVIGATION.EVENTS")}
+
+ )}
+ {hasAccess("ROLE_UI_SERIES_VIEW", user) && (
+ loadSeries()}
+ >
+ {t("EVENTS.EVENTS.NAVIGATION.SERIES")}
+
+ )}
+ {hasAccess("ROLE_UI_LIFECYCLEPOLICIES_VIEW", user) && (
+ loadLifeCyclePolicies()}
+ >
+ {t("LIFECYCLE.NAVIGATION.POLICIES")}
+
+ )}
+
+
+
+
+ {/* Include notifications component */}
+
+
+
+ {/* 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 >.> */}
+
}
+ loadResourceIntoTable={loadLifeCyclePoliciesIntoTable}
+ resource={"lifeCyclePolicies"}
+ />
+
+ {t("LIFECYCLE.POLICIES.TABLE.CAPTION")}
+ {t("TABLE_SUMMARY", { numberOfRows: policiesTotal })}
+
+ {/* Include table component */}
+
+
+
+ >
+ );
+};
+
+export default LifeCyclePolicies;
diff --git a/src/components/events/partials/EventsNavigation.tsx b/src/components/events/partials/EventsNavigation.tsx
index 5185035005..97577e53a8 100644
--- a/src/components/events/partials/EventsNavigation.tsx
+++ b/src/components/events/partials/EventsNavigation.tsx
@@ -1,8 +1,9 @@
import { fetchEvents } from "../../../slices/eventSlice";
+import { fetchLifeCyclePolicies } from "../../../slices/lifeCycleSlice";
import { fetchSeries } from "../../../slices/seriesSlice";
import { fetchStats } from "../../../slices/tableFilterSlice";
import { AppDispatch } from "../../../store";
-import { loadEventsIntoTable, loadSeriesIntoTable } from "../../../thunks/tableThunks";
+import { loadEventsIntoTable, loadLifeCyclePoliciesIntoTable, loadSeriesIntoTable } from "../../../thunks/tableThunks";
/**
* Utility file for the navigation bar
@@ -27,6 +28,14 @@ export const loadSeries = (dispatch: AppDispatch) => {
dispatch(loadSeriesIntoTable());
};
+export const loadLifeCyclePolicies = (dispatch: AppDispatch) => {
+ // Fetching policies from server
+ dispatch(fetchLifeCyclePolicies());
+
+ // Load policies into table
+ dispatch(loadLifeCyclePoliciesIntoTable());
+};
+
export const eventsLinks = [
{
path: "/events/events",
@@ -39,5 +48,11 @@ export const eventsLinks = [
accessRole: "ROLE_UI_SERIES_VIEW",
loadFn: loadSeries,
text: "EVENTS.EVENTS.NAVIGATION.SERIES"
+ },
+ {
+ path: "/events/lifeCyclePolicies",
+ accessRole: "ROLE_UI_LIFECYCLEPOLICIES_VIEW",
+ loadFn: loadLifeCyclePolicies,
+ text: "LIFECYCLE.NAVIGATION.POLICIES"
}
];
diff --git a/src/components/events/partials/LifeCyclePolicyActionCell.tsx b/src/components/events/partials/LifeCyclePolicyActionCell.tsx
new file mode 100644
index 0000000000..ce5df1e317
--- /dev/null
+++ b/src/components/events/partials/LifeCyclePolicyActionCell.tsx
@@ -0,0 +1,62 @@
+import React, { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { useAppDispatch, useAppSelector } from "../../../store";
+import { Tooltip } from "../../shared/Tooltip";
+import { 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";
+
+/**
+ * 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 user = useAppSelector(state => getUserInformation(state));
+
+ const showLifeCyclePolicyDetails = async () => {
+ await dispatch(fetchLifeCyclePolicyDetails(row.id));
+
+ setLifeCyclePolicyDetails(true);
+ };
+
+ const hideLifeCyclePolicyDetails = () => {
+ setLifeCyclePolicyDetails(false);
+ };
+
+ return (
+ <>
+ {/* view details location/recording */}
+ {hasAccess("ROLE_UI_LIFECYCLEPOLICY_DETAILS_VIEW", user) && (
+
+ showLifeCyclePolicyDetails()}
+ />
+
+ )}
+
+ {displayLifeCyclePolicyDetails && (
+
+
+
+ )}
+ >
+ );
+};
+
+export default LifeCyclePolicyActionCell;
diff --git a/src/components/events/partials/LifeCyclePolicyIsActiveCell.tsx b/src/components/events/partials/LifeCyclePolicyIsActiveCell.tsx
new file mode 100644
index 0000000000..b4272ec085
--- /dev/null
+++ b/src/components/events/partials/LifeCyclePolicyIsActiveCell.tsx
@@ -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 (
+
+ );
+};
+
+export default LifeCyclePolicyIsActiveCell;
diff --git a/src/components/events/partials/ModalTabsAndPages/LifeCyclePolicyAccessTab.tsx b/src/components/events/partials/ModalTabsAndPages/LifeCyclePolicyAccessTab.tsx
new file mode 100644
index 0000000000..855f8bdc39
--- /dev/null
+++ b/src/components/events/partials/ModalTabsAndPages/LifeCyclePolicyAccessTab.tsx
@@ -0,0 +1,50 @@
+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";
+
+/**
+ * This component manages the access policy tab of the series details modal
+ */
+const LifeCyclePolicyDetailsAccessTab = ({
+ seriesId,
+ header,
+ policyChanged,
+ setPolicyChanged,
+}: {
+ seriesId: string,
+ header: string,
+ 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 (
+
+ );
+};
+
+export default LifeCyclePolicyDetailsAccessTab;
diff --git a/src/components/events/partials/ModalTabsAndPages/LifeCyclePolicyGeneralTab.tsx b/src/components/events/partials/ModalTabsAndPages/LifeCyclePolicyGeneralTab.tsx
new file mode 100644
index 0000000000..86184665ad
--- /dev/null
+++ b/src/components/events/partials/ModalTabsAndPages/LifeCyclePolicyGeneralTab.tsx
@@ -0,0 +1,120 @@
+import React from "react";
+import { useTranslation } from "react-i18next";
+import { LifeCyclePolicy } from "../../../../slices/lifeCycleSlice";
+import { renderValidDate } from "../../../../utils/dateUtils";
+
+/**
+ * This component renders details about a recording/capture agent
+ */
+const LifeCyclePolicyGeneralTab = ({
+ policy,
+}: {
+ policy: LifeCyclePolicy
+}) => {
+ const { t } = useTranslation();
+
+ return (
+
+
+
+
+
+ {t("LIFECYCLE.POLICIES.DETAILS.GENERAL.CAPTION")}
+
+
+ {/* Render table containing general information */}
+
+
+
+
+
+
+ );
+};
+
+export default LifeCyclePolicyGeneralTab;
diff --git a/src/components/events/partials/modals/LifeCyclePolicyDetails.tsx b/src/components/events/partials/modals/LifeCyclePolicyDetails.tsx
new file mode 100644
index 0000000000..e818dd57c2
--- /dev/null
+++ b/src/components/events/partials/modals/LifeCyclePolicyDetails.tsx
@@ -0,0 +1,54 @@
+import React, { useState } from "react";
+import ModalNavigation from "../../../shared/modals/ModalNavigation";
+import { getLifeCyclePolicyDetails } from "../../../../selectors/lifeCycleDetailsSelectors";
+import LifeCyclePolicyGeneralTab from "../ModalTabsAndPages/LifeCyclePolicyGeneralTab";
+import LifeCyclePolicyDetailsAccessTab from "../ModalTabsAndPages/LifeCyclePolicyAccessTab";
+import { useAppSelector } from "../../../../store";
+
+/**
+ * This component manages the tabs of the series details modal
+ */
+const LifeCyclePolicyDetails = () => {
+ const [page, setPage] = useState(0);
+
+ const policy = useAppSelector(state => getLifeCyclePolicyDetails(state));
+
+ // information about tabs
+ const tabs = [
+ {
+ tabTranslation: "LIFECYCLE.POLICIES.DETAILS.TAB.GENERAL",
+ accessRole: "ROLE_UI_LIFECYCLEPOLICIES_DETAILS_GENERAL_VIEW",
+ name: "general",
+ },
+ {
+ tabTranslation: "LIFECYCLE.POLICIES.DETAILS.TAB.ACCESSPOLICIES",
+ accessRole: "ROLE_UI_LIFECYCLEPOLICIES_DETAILS_ACCESSPOLICIES_VIEW",
+ name: "WARNING: None of the changes you make here can be saved!",
+ },
+ ];
+
+ const openTab = (tabNr: number) => {
+ setPage(tabNr);
+ };
+
+ return (
+ <>
+ {/* Navigation */}
+
+
+
+ {page === 0 && }
+ {page === 1 &&
+ false}
+ />
+ }
+
+ >
+ );
+};
+
+export default LifeCyclePolicyDetails;
diff --git a/src/components/shared/MainNav.tsx b/src/components/shared/MainNav.tsx
index 2207141cd8..88110f03a7 100644
--- a/src/components/shared/MainNav.tsx
+++ b/src/components/shared/MainNav.tsx
@@ -6,6 +6,7 @@ import {
loadEventsIntoTable,
loadGroupsIntoTable,
loadJobsIntoTable,
+ loadLifeCyclePoliciesIntoTable,
loadRecordingsIntoTable,
loadSeriesIntoTable,
loadServersIntoTable,
@@ -34,6 +35,7 @@ import { fetchSeries } from "../../slices/seriesSlice";
import { fetchJobs } from "../../slices/jobSlice";
import { fetchEvents } from "../../slices/eventSlice";
import { Tooltip } from "./Tooltip";
+import { fetchLifeCyclePolicies } from "../../slices/lifeCycleSlice";
/**
* This component renders the main navigation that opens when the burger button is clicked
@@ -84,6 +86,19 @@ const MainNav = ({
dispatch(loadSeriesIntoTable());
};
+ const loadLifeCyclePolicies = () => {
+ dispatch(fetchFilters("lifeCylePolicies"));
+
+ // Reset the current page to first page
+ dispatch(setOffset(0));
+
+ // Fetching lifeCycle policies from server
+ dispatch(fetchLifeCyclePolicies());
+
+ // Load lifeCycle policies into table
+ dispatch(loadLifeCyclePoliciesIntoTable());
+ };
+
const loadRecordings = () => {
dispatch(fetchFilters("recordings"));
@@ -228,13 +243,19 @@ const MainNav = ({
- ) : (
- hasAccess("ROLE_UI_SERIES_VIEW", user) && (
+ ) : hasAccess("ROLE_UI_SERIES_VIEW", user) ? (
loadSeries()}>
+ ) : (
+ hasAccess("ROLE_UI_LIFECYCLEPOLICIES_VIEW", user) && (
+ loadLifeCyclePolicies()}>
+
+
+
+
)
))}
{hasAccess("ROLE_UI_NAV_CAPTURE_VIEW", user) &&
diff --git a/src/components/shared/modals/DetailsModal.tsx b/src/components/shared/modals/DetailsModal.tsx
index 6e13ff755d..5ebd58ec5a 100644
--- a/src/components/shared/modals/DetailsModal.tsx
+++ b/src/components/shared/modals/DetailsModal.tsx
@@ -45,4 +45,4 @@ const DetailsModal = ({
);
};
-export default DetailsModal;
\ No newline at end of file
+export default DetailsModal;
diff --git a/src/configs/tableConfigs/lifeCyclePoliciesTableConfig.ts b/src/configs/tableConfigs/lifeCyclePoliciesTableConfig.ts
new file mode 100644
index 0000000000..71a47c8a8e
--- /dev/null
+++ b/src/configs/tableConfigs/lifeCyclePoliciesTableConfig.ts
@@ -0,0 +1,30 @@
+import { TableConfig } from "./aclsTableConfig";
+
+export const lifeCyclePolicyTableConfig: TableConfig = {
+ columns: [
+ {
+ name: "title",
+ label: "LIFECYCLE.POLICIES.TABLE.TITLE",
+ sortable: true,
+ },
+ {
+ template: "LifeCyclePolicyIsActiveCell",
+ name: "isActive",
+ label: "LIFECYCLE.POLICIES.TABLE.ISACTIVE",
+ },
+ {
+ name: "timing",
+ label: "LIFECYCLE.POLICIES.TABLE.TIMING",
+ sortable: true,
+ },
+ {
+ template: "LifeCyclePolicyActionCell",
+ name: "actions",
+ label: "LIFECYCLE.POLICIES.TABLE.ACTION",
+ },
+ ],
+ caption: "TABLE.CAPTION",
+ resource: "lifeCyclePolicies",
+ category: "events",
+ multiSelect: false,
+};
diff --git a/src/configs/tableConfigs/lifeCyclePoliciesTableMap.ts b/src/configs/tableConfigs/lifeCyclePoliciesTableMap.ts
new file mode 100644
index 0000000000..7a460a6363
--- /dev/null
+++ b/src/configs/tableConfigs/lifeCyclePoliciesTableMap.ts
@@ -0,0 +1,11 @@
+import LifeCyclePolicyActionCell from "../../components/events/partials/LifeCyclePolicyActionCell";
+import LifeCyclePolicyIsActiveCell from "../../components/events/partials/LifeCyclePolicyIsActiveCell";
+
+/**
+ * This map contains the mapping between the template strings above and the corresponding react component.
+ * This helps to render different templates of cells more dynamically
+ */
+export const lifeCyclePoliciesTemplateMap = {
+ LifeCyclePolicyIsActiveCell: LifeCyclePolicyIsActiveCell,
+ LifeCyclePolicyActionCell: LifeCyclePolicyActionCell,
+};
diff --git a/src/i18n/org/opencastproject/adminui/languages/lang-en_US.json b/src/i18n/org/opencastproject/adminui/languages/lang-en_US.json
index 046922106e..6a70a57394 100644
--- a/src/i18n/org/opencastproject/adminui/languages/lang-en_US.json
+++ b/src/i18n/org/opencastproject/adminui/languages/lang-en_US.json
@@ -1255,6 +1255,48 @@
}
}
},
+ "LIFECYCLE": {
+ "NAVIGATION": {
+ "POLICIES": "LifeCycle Policies"
+ },
+ "POLICIES": {
+ "TABLE": {
+ "ACTION": "Actions",
+ "CAPTION": "LifeCycle Policies",
+ "ISACTIVE": "Active",
+ "TIMING": "Timing",
+ "TITLE": "Title",
+ "TOOLTIP": {
+ "DETAILS": "LifeCycle Policy Details"
+ }
+ },
+ "DETAILS": {
+ "HEADER": "LifeCycle Policy Details",
+ "GENERAL": {
+ "ACTION": "Action",
+ "ACTIONDATE": "Action Date",
+ "ACTIONPARAMETERS": "Action Parameters",
+ "CAPTION": "LifeCycle Policy Details",
+ "CRONTRIGGER": "Cron Trigger",
+ "ID": "Identifier",
+ "ISACTIVE": "Active",
+ "ISCREATEDFROMCONFIG": "Created from Config",
+ "TARGETTYPE": "Target Type",
+ "TARGETFILTERS": "Target Filters",
+ "TIMING": "Timing",
+ "TITLE": "Title"
+ },
+ "ACCESS": {
+ "LABEL": "Select a template",
+ "DESCRIPTION": "At least one role with Read and Write permissions is required."
+ },
+ "TAB": {
+ "GENERAL": "General",
+ "ACCESSPOLICIES": "Access Policies"
+ }
+ }
+ }
+ },
"RECORDINGS": {
"NAVIGATION": {
"LOCATIONS": "Locations"
diff --git a/src/selectors/lifeCycleDetailsSelectors.ts b/src/selectors/lifeCycleDetailsSelectors.ts
new file mode 100644
index 0000000000..364368819a
--- /dev/null
+++ b/src/selectors/lifeCycleDetailsSelectors.ts
@@ -0,0 +1,7 @@
+import { RootState } from "../store";
+
+/**
+ * This file contains selectors regarding details of a certain lifeCyclePolicy/capture agent
+ */
+export const getLifeCyclePolicyDetails = (state: RootState) => state.lifeCyclePolicyDetails;
+export const getLifeCyclePolicyDetailsAcl = (state: RootState) => state.lifeCyclePolicyDetails.accessControlEntries;
diff --git a/src/selectors/lifeCycleSelectors.ts b/src/selectors/lifeCycleSelectors.ts
new file mode 100644
index 0000000000..fadb641e3d
--- /dev/null
+++ b/src/selectors/lifeCycleSelectors.ts
@@ -0,0 +1,7 @@
+import { RootState } from "../store";
+
+/**
+ * This file contains selectors regarding acls
+ */
+export const getLifeCyclePolicies = (state: RootState) => state.lifeCycle.results;
+export const getTotalLifeCyclePolicies = (state: RootState) => state.lifeCycle.total;
diff --git a/src/slices/lifeCycleDetailsSlice.ts b/src/slices/lifeCycleDetailsSlice.ts
new file mode 100644
index 0000000000..2bba3cda56
--- /dev/null
+++ b/src/slices/lifeCycleDetailsSlice.ts
@@ -0,0 +1,136 @@
+import { PayloadAction, SerializedError, createSlice } from '@reduxjs/toolkit'
+import axios from 'axios';
+import { createAppAsyncThunk } from '../createAsyncThunkWithTypes';
+import { LifeCyclePolicy } from './lifeCycleSlice';
+import { TransformedAcl } from './aclDetailsSlice';
+import { createPolicy } from '../utils/resourceUtils';
+import { Ace } from './aclSlice';
+
+/**
+ * This file contains redux reducer for actions affecting the state of a lifeCyclePolicy/capture agent
+ */
+interface LifeCyclePolicyDetailsState extends LifeCyclePolicy {
+ statusLifeCyclePolicyDetails: 'uninitialized' | 'loading' | 'succeeded' | 'failed',
+ errorLifeCyclePolicyDetails: SerializedError | null,
+}
+
+// Initial state of lifeCyclePolicy details in redux store
+const initialState: LifeCyclePolicyDetailsState = {
+ statusLifeCyclePolicyDetails: 'uninitialized',
+ errorLifeCyclePolicyDetails: null,
+ actionParameters: {},
+ timing: "SPECIFIC_DATE",
+ action: "START_WORKFLOW",
+ targetType: "EVENT",
+ id: "",
+ title: "",
+ isActive: false,
+ isCreatedFromConfig: false,
+ actionDate: "",
+ cronTrigger: "",
+ targetFilters: {},
+ accessControlEntries: []
+};
+
+// fetch details of certain lifeCyclePolicy from server
+export const fetchLifeCyclePolicyDetails = createAppAsyncThunk('lifeCyclePolicyDetails/fetchLifeCyclePolicyDetails', async (id: string) => {
+ const res = await axios.get(`/api/lifecyclemanagement/policies/${id}`);
+ const data = res.data;
+
+ data.actionParameters = JSON.parse(data.actionParameters)
+ data.targetFilters = JSON.parse(data.targetFilters)
+
+ let accessPolicies : {
+ id: number,
+ allow: boolean,
+ role: string,
+ action: string,
+ }[] = data.accessControlEntries;
+ let acls: TransformedAcl[] = [];
+
+ const json = accessPolicies;
+ let newPolicies: { [key: string]: TransformedAcl } = {};
+ let policyRoles: string[] = [];
+ for (let i = 0; i < json.length; i++) {
+ const policy: Ace = json[i];
+ if (!newPolicies[policy.role]) {
+ newPolicies[policy.role] = createPolicy(policy.role);
+ policyRoles.push(policy.role);
+ }
+ if (policy.action === "read" || policy.action === "write") {
+ newPolicies[policy.role][policy.action] = policy.allow;
+ } else if (policy.allow === true) { //|| policy.allow === "true") {
+ newPolicies[policy.role].actions.push(policy.action);
+ }
+ }
+ acls = policyRoles.map((role) => newPolicies[role]);
+
+ data.accessControlEntries = acls;
+
+ return data;
+});
+
+// Dummy function for compatability
+export const fetchLifeCyclePolicyDetailsAcls = createAppAsyncThunk('lifeCyclePolicyDetails/fetchLifeCyclePolicyDetailsAcls', async (id: string, {getState}) => {
+ const state = getState();
+ return state.lifeCyclePolicyDetails.accessControlEntries;
+});
+
+// Dummy function for compatability
+export const updateLifeCyclePolicyAccess = createAppAsyncThunk('lifeCyclePolicyDetails/fetchLifeCyclePolicyDetailsAcls', async (params: {
+ id: string,
+ policies: { acl: { ace: Ace[] } }
+}, {dispatch}) => {
+ return false;
+});
+
+const lifeCyclePolicyDetailsSlice = createSlice({
+ name: 'lifeCyclePolicyDetails',
+ initialState,
+ reducers: {},
+ // These are used for thunks
+ extraReducers: builder => {
+ builder
+ .addCase(fetchLifeCyclePolicyDetails.pending, (state) => {
+ state.statusLifeCyclePolicyDetails = 'loading';
+ })
+ .addCase(fetchLifeCyclePolicyDetails.fulfilled, (state, action: PayloadAction<{
+ actionParameters: LifeCyclePolicyDetailsState["actionParameters"],
+ timing: LifeCyclePolicyDetailsState["timing"],
+ action: LifeCyclePolicyDetailsState["action"],
+ targetType: LifeCyclePolicyDetailsState["targetType"],
+ id: LifeCyclePolicyDetailsState["id"],
+ title: LifeCyclePolicyDetailsState["title"],
+ isActive: LifeCyclePolicyDetailsState["isActive"],
+ isCreatedFromConfig: LifeCyclePolicyDetailsState["isCreatedFromConfig"],
+ actionDate: LifeCyclePolicyDetailsState["actionDate"],
+ cronTrigger: LifeCyclePolicyDetailsState["cronTrigger"],
+ targetFilters: LifeCyclePolicyDetailsState["targetFilters"],
+ accessControlEntries: LifeCyclePolicyDetailsState["accessControlEntries"],
+ }>) => {
+ state.statusLifeCyclePolicyDetails = 'succeeded';
+ const lifeCyclePolicyDetails = action.payload;
+ state.actionParameters = lifeCyclePolicyDetails.actionParameters;
+ state.timing = lifeCyclePolicyDetails.timing;
+ state.action = lifeCyclePolicyDetails.action;
+ state.targetType = lifeCyclePolicyDetails.targetType;
+ state.id = lifeCyclePolicyDetails.id;
+ state.title = lifeCyclePolicyDetails.title;
+ state.isActive = lifeCyclePolicyDetails.isActive;
+ state.isCreatedFromConfig = lifeCyclePolicyDetails.isCreatedFromConfig;
+ state.actionDate = lifeCyclePolicyDetails.actionDate;
+ state.cronTrigger = lifeCyclePolicyDetails.cronTrigger;
+ state.targetFilters = lifeCyclePolicyDetails.targetFilters;
+ state.accessControlEntries = lifeCyclePolicyDetails.accessControlEntries;
+ })
+ .addCase(fetchLifeCyclePolicyDetails.rejected, (state, action) => {
+ state.statusLifeCyclePolicyDetails = 'failed';
+ state.errorLifeCyclePolicyDetails = action.error;
+ });
+ }
+});
+
+// export const {} = lifeCyclePolicyDetailsSlice.actions;
+
+// Export the slice reducer as the default export
+export default lifeCyclePolicyDetailsSlice.reducer;
diff --git a/src/slices/lifeCycleSlice.ts b/src/slices/lifeCycleSlice.ts
new file mode 100644
index 0000000000..cfa4e78192
--- /dev/null
+++ b/src/slices/lifeCycleSlice.ts
@@ -0,0 +1,112 @@
+import { PayloadAction, SerializedError, createSlice } from '@reduxjs/toolkit'
+import { TableConfig } from "../configs/tableConfigs/aclsTableConfig";
+import { lifeCyclePolicyTableConfig } from "../configs/tableConfigs/lifeCyclePoliciesTableConfig";
+import axios from 'axios';
+import { getURLParams } from '../utils/resourceUtils';
+import { createAppAsyncThunk } from '../createAsyncThunkWithTypes';
+import { TransformedAcl } from './aclDetailsSlice';
+
+type LifeCyclePolicyTiming = "SPECIFIC_DATE" | "REPEATING" | "ALWAYS";
+type LifeCyclePolicyAction = "START_WORKFLOW"
+type LifeCyclePolicyTargetType = "EVENT"
+
+export type LifeCyclePolicy = {
+ actionParameters: { [key: string]: unknown }, // JSON. Variable, depends on action
+ timing: LifeCyclePolicyTiming,
+ action: LifeCyclePolicyAction,
+ targetType: LifeCyclePolicyTargetType,
+ id: string,
+ title: string,
+ isActive: boolean,
+ isCreatedFromConfig: boolean,
+ actionDate: string, // Date
+ cronTrigger: string,
+ targetFilters: { [key: string]: {
+ value: string,
+ type: "SEARCH" | "WILDCARD" | "GREATER_THAN" | "LESS_THAN",
+ must: boolean
+ }},
+ accessControlEntries: TransformedAcl[]
+}
+
+type LifeCycleState = {
+ status: 'uninitialized' | 'loading' | 'succeeded' | 'failed',
+ error: SerializedError | null,
+ results: LifeCyclePolicy[],
+ columns: TableConfig["columns"],
+ total: number,
+ count: number,
+ offset: number,
+ limit: number,
+};
+
+// Fill columns initially with columns defined in aclsTableConfig
+const initialColumns = lifeCyclePolicyTableConfig.columns.map((column) => ({
+ ...column,
+ deactivated: false,
+}));
+
+// Initial state of acls in redux store
+const initialState: LifeCycleState = {
+ status: 'uninitialized',
+ error: null,
+ results: [],
+ columns: initialColumns,
+ total: 0,
+ count: 0,
+ offset: 0,
+ limit: 0,
+};
+
+export const fetchLifeCyclePolicies = createAppAsyncThunk('lifeCycle/fetchLifeCyclePolicies', async (_, { getState }) => {
+ const state = getState();
+ let params = getURLParams(state);
+ const res = await axios.get("/api/lifecyclemanagement/policies", { params: params });
+ return res.data;
+});
+
+const lifeCycleSlice = createSlice({
+ name: 'lifeCycle',
+ initialState,
+ reducers: {
+ setLifeCycleColumns(state, action: PayloadAction<
+ LifeCycleState["columns"]
+ >) {
+ state.columns = action.payload;
+ },
+ },
+ // These are used for thunks
+ extraReducers: builder => {
+ builder
+ .addCase(fetchLifeCyclePolicies.pending, (state) => {
+ state.status = 'loading';
+ })
+ // Pass the generated action creators to `.addCase()`
+ .addCase(fetchLifeCyclePolicies.fulfilled, (state, action: PayloadAction<{
+ total: LifeCycleState["total"],
+ count: LifeCycleState["count"],
+ limit: LifeCycleState["limit"],
+ offset: LifeCycleState["offset"],
+ results: LifeCycleState["results"],
+ }>) => {
+ // Same "mutating" update syntax thanks to Immer
+ state.status = 'succeeded';
+ const policies = action.payload;
+ state.total = policies.total;
+ state.count = policies.count;
+ state.limit = policies.limit;
+ state.offset = policies.offset;
+ state.results = policies.results;
+ })
+ .addCase(fetchLifeCyclePolicies.rejected, (state, action) => {
+ state.status = 'failed';
+ state.results = [];
+ state.error = action.error;
+ });
+ }
+});
+
+export const { setLifeCycleColumns } = lifeCycleSlice.actions;
+
+// Export the slice reducer as the default export
+export default lifeCycleSlice.reducer;
diff --git a/src/slices/tableSlice.ts b/src/slices/tableSlice.ts
index 1e2595bfbe..5d052940b3 100644
--- a/src/slices/tableSlice.ts
+++ b/src/slices/tableSlice.ts
@@ -10,6 +10,7 @@ import { AclResult } from './aclSlice';
import { ThemeDetailsType } from './themeSlice';
import { Series } from './seriesSlice';
import { Event } from './eventSlice';
+import { LifeCyclePolicy } from './lifeCycleSlice';
/*
Overview of the structure of the data in arrays in state
@@ -59,16 +60,16 @@ export function isRowSelectable(row: Row) {
return false;
}
-export function isEvent(row: Event | Series | Recording | Server | Job | Service | User | Group | AclResult | ThemeDetailsType): row is Event {
+export function isEvent(row: Event | Series | Recording | Server | Job | Service | User | Group | AclResult | ThemeDetailsType | LifeCyclePolicy): row is Event {
return (row as Event).event_status !== undefined;
}
-export function isSeries(row: Row | Event | Series | Recording | Server | Job | Service | User | Group | AclResult | ThemeDetailsType): row is Series {
+export function isSeries(row: Row | Event | Series | Recording | Server | Job | Service | User | Group | AclResult | ThemeDetailsType | LifeCyclePolicy): row is Series {
return (row as Series).organizers !== undefined;
}
// TODO: Improve row typing. While this somewhat correctly reflects the current state of our code, it is rather annoying to work with.
-export type Row = { selected: boolean } & ( Event | Series | Recording | Server | Job | Service | User | Group | AclResult | ThemeDetailsType )
+export type Row = { selected: boolean } & ( Event | Series | Recording | Server | Job | Service | User | Group | AclResult | ThemeDetailsType | LifeCyclePolicy)
type TableState = {
status: 'uninitialized' | 'loading' | 'succeeded' | 'failed',
diff --git a/src/store.ts b/src/store.ts
index d6973a7233..1a6175c879 100644
--- a/src/store.ts
+++ b/src/store.ts
@@ -6,6 +6,7 @@ import tableFilterProfiles from "./slices/tableFilterProfilesSlice";
import events from "./slices/eventSlice";
import table from "./slices/tableSlice";
import series from "./slices/seriesSlice";
+import lifeCycle from "./slices/lifeCycleSlice";
import recordings from "./slices/recordingSlice";
import jobs from "./slices/jobSlice";
import servers from "./slices/serverSlice";
@@ -19,6 +20,7 @@ import notifications from "./slices/notificationSlice";
import workflows from "./slices/workflowSlice";
import eventDetails from "./slices/eventDetailsSlice";
import seriesDetails from "./slices/seriesDetailsSlice";
+import lifeCyclePolicyDetails from "./slices/lifeCycleDetailsSlice";
import userDetails from "./slices/userDetailsSlice";
import recordingDetails from "./slices/recordingDetailsSlice";
import groupDetails from "./slices/groupDetailsSlice";
@@ -38,6 +40,7 @@ import autoMergeLevel2 from "redux-persist/lib/stateReconciler/autoMergeLevel2";
const tableFilterProfilesPersistConfig = { key: "tableFilterProfiles", storage, whitelist: ["profiles"] }
const eventsPersistConfig = { key: "events", storage, whitelist: ["columns"] }
const seriesPersistConfig = { key: "series", storage, whitelist: ["columns"] }
+const lifeCyclePersistConfig = { key: "lifeCycle", storage, whitelist: ["columns"] }
const tablePersistConfig = { key: "table", storage, whitelist: ["pagination"] }
const recordingsPersistConfig = { key: "recordings", storage, whitelist: ["columns"] }
const jobsPersistConfig = { key: "jobs", storage, whitelist: ["columns"] }
@@ -54,6 +57,7 @@ const reducers = combineReducers({
tableFilterProfiles: persistReducer(tableFilterProfilesPersistConfig, tableFilterProfiles),
events: persistReducer(eventsPersistConfig, events),
series: persistReducer(seriesPersistConfig, series),
+ lifeCycle: persistReducer(lifeCyclePersistConfig, lifeCycle),
table: persistReducer(tablePersistConfig, table),
recordings: persistReducer(recordingsPersistConfig, recordings),
jobs: persistReducer(jobsPersistConfig, jobs),
@@ -69,6 +73,7 @@ const reducers = combineReducers({
eventDetails,
themeDetails,
seriesDetails,
+ lifeCyclePolicyDetails,
recordingDetails,
userDetails,
groupDetails,
diff --git a/src/thunks/tableThunks.ts b/src/thunks/tableThunks.ts
index 875e774f0a..723386b5b9 100644
--- a/src/thunks/tableThunks.ts
+++ b/src/thunks/tableThunks.ts
@@ -44,6 +44,8 @@ import { fetchRecordings, setRecordingsColumns } from "../slices/recordingSlice"
import { setGroupColumns } from "../slices/groupSlice";
import { fetchAcls, setAclColumns } from "../slices/aclSlice";
import { AppDispatch, AppThunk, RootState } from "../store";
+import { lifeCyclePolicyTableConfig } from "../configs/tableConfigs/lifeCyclePoliciesTableConfig";
+import { fetchLifeCyclePolicies, setLifeCycleColumns } from "../slices/lifeCycleSlice";
/**
* This file contains methods/thunks used to manage the table in the main view and its state changes
@@ -147,6 +149,40 @@ export const loadSeriesIntoTable = (): AppThunk => (dispatch, getState) => {
dispatch(loadResourceIntoTable(tableData));
};
+export const loadLifeCyclePoliciesIntoTable = (): AppThunk => (dispatch, getState) => {
+ const { lifeCycle, table } = getState() as RootState;
+ const pagination = table.pagination;
+ const resource = lifeCycle.results;
+ const total = lifeCycle.total;
+
+ const pages = calculatePages(total / pagination.limit, pagination.offset);
+
+ let tableData = {
+ resource: "lifeCyclePolicies",
+ rows: resource.map((obj) => {
+ return { ...obj, selected: false }
+ }),
+ columns: lifeCycle.columns,
+ multiSelect: table.multiSelect,
+ pages: pages,
+ sortBy: table.sortBy,
+ reverse: table.reverse,
+ totalItems: total,
+ };
+
+ if (table.resource !== "lifeCyclePolicies") {
+ const multiSelect = lifeCyclePolicyTableConfig.multiSelect;
+
+ tableData = {
+ ...tableData,
+ sortBy: "title",
+ reverse: "ASC",
+ multiSelect: multiSelect,
+ };
+ }
+ dispatch(loadResourceIntoTable(tableData));
+}
+
export const loadRecordingsIntoTable = (): AppThunk => (dispatch, getState) => {
const { recordings, table } = getState() as RootState;
const pagination = table.pagination;
@@ -446,6 +482,11 @@ export const goToPage = (pageNumber: number) => async (dispatch: AppDispatch, ge
dispatch(loadSeriesIntoTable());
break;
}
+ case "lifeCyclePolicies": {
+ await dispatch(fetchLifeCyclePolicies());
+ dispatch(loadLifeCyclePoliciesIntoTable());
+ break;
+ }
case "recordings": {
await dispatch(fetchRecordings());
dispatch(loadRecordingsIntoTable());
@@ -515,6 +556,11 @@ export const updatePages = () => async (dispatch: AppDispatch, getState: () => R
dispatch(loadSeriesIntoTable());
break;
}
+ case "lifeCyclePolicies": {
+ await dispatch(fetchLifeCyclePolicies());
+ dispatch(loadLifeCyclePoliciesIntoTable());
+ break;
+ }
case "recordings": {
await dispatch(fetchRecordings());
dispatch(loadRecordingsIntoTable());
@@ -627,6 +673,11 @@ export const changeColumnSelection = (updatedColumns: TableConfig["columns"]) =>
dispatch(loadSeriesIntoTable());
break;
}
+ case "lifeCyclePolicies": {
+ await dispatch(setLifeCycleColumns(updatedColumns));
+ dispatch(loadLifeCyclePoliciesIntoTable());
+ break;
+ }
case "recordings": {
await dispatch(setRecordingsColumns(updatedColumns));
dispatch(loadRecordingsIntoTable());