Skip to content

Commit 2be0614

Browse files
committed
Add frontend component to view plugin configuration and status.
1 parent d067fbb commit 2be0614

File tree

3 files changed

+268
-0
lines changed

3 files changed

+268
-0
lines changed
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
/**
2+
* Read-only component for displaying App Group Lifecycle Plugin configuration and status.
3+
* Used in Read pages (view mode).
4+
*/
5+
6+
import * as React from 'react';
7+
import Accordion from '@mui/material/Accordion';
8+
import AccordionDetails from '@mui/material/AccordionDetails';
9+
import AccordionSummary from '@mui/material/AccordionSummary';
10+
import Box from '@mui/material/Box';
11+
import CircularProgress from '@mui/material/CircularProgress';
12+
import Paper from '@mui/material/Paper';
13+
import Stack from '@mui/material/Stack';
14+
import Table from '@mui/material/Table';
15+
import TableBody from '@mui/material/TableBody';
16+
import TableCell from '@mui/material/TableCell';
17+
import TableContainer from '@mui/material/TableContainer';
18+
import TableRow from '@mui/material/TableRow';
19+
import Tooltip from '@mui/material/Tooltip';
20+
import Typography from '@mui/material/Typography';
21+
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
22+
23+
import {
24+
useGetAppGroupLifecyclePlugins,
25+
useGetAppGroupLifecyclePluginAppConfigProperties,
26+
useGetAppGroupLifecyclePluginGroupConfigProperties,
27+
useGetAppGroupLifecyclePluginAppStatusProperties,
28+
useGetAppGroupLifecyclePluginGroupStatusProperties,
29+
PluginConfigProperties,
30+
PluginStatusProperties,
31+
} from '../api/apiComponents';
32+
33+
type PluginData = {
34+
[propertyId: string]: any;
35+
};
36+
37+
interface AppGroupLifecyclePluginDataProps {
38+
/**
39+
* Entity type at which the plugin is configured ('app' or 'group')
40+
*/
41+
entityType: 'app' | 'group';
42+
43+
/**
44+
* Plugin ID to display
45+
*/
46+
pluginId: string;
47+
48+
/**
49+
* Current configuration values
50+
*/
51+
currentConfig?: PluginData;
52+
53+
/**
54+
* Current status values (read-only)
55+
*/
56+
currentStatus?: PluginData;
57+
}
58+
59+
function PluginDataPropertiesTable({
60+
type,
61+
properties,
62+
data,
63+
}: {
64+
type: 'Configuration' | 'Status';
65+
properties: PluginConfigProperties | PluginStatusProperties;
66+
data: PluginData;
67+
}) {
68+
return (
69+
<Stack direction="column" spacing={1} sx={{minWidth: '300px', flexGrow: 1}}>
70+
<Typography variant="body1" component={'div'}>
71+
{type}
72+
</Typography>
73+
<TableContainer component={Paper}>
74+
<Table sx={{minWidth: 325}} size="small" aria-label={`plugin ${type.toLowerCase()}`}>
75+
<TableBody>
76+
{Object.entries(properties).map(([propertyId, property]) => {
77+
const value = data[propertyId];
78+
let displayValue = '—';
79+
if (value != null) {
80+
switch (property.type) {
81+
case 'date':
82+
try {
83+
displayValue = new Date(value).toLocaleString();
84+
} catch {
85+
displayValue = String(value);
86+
}
87+
break;
88+
case 'boolean':
89+
displayValue = value ? 'Yes' : 'No';
90+
break;
91+
default:
92+
displayValue = String(value);
93+
}
94+
}
95+
96+
const row = (
97+
<TableRow key={propertyId}>
98+
<TableCell>
99+
<Typography variant="body2">{property.display_name}</Typography>
100+
</TableCell>
101+
<TableCell>
102+
<Typography variant="body2">{displayValue}</Typography>
103+
</TableCell>
104+
</TableRow>
105+
);
106+
107+
return property.help_text ? (
108+
<Tooltip key={propertyId} title={property.help_text} placement="top" arrow>
109+
{row}
110+
</Tooltip>
111+
) : (
112+
row
113+
);
114+
})}
115+
</TableBody>
116+
</Table>
117+
</TableContainer>
118+
</Stack>
119+
);
120+
}
121+
122+
export default function AppGroupLifecyclePluginData({
123+
entityType,
124+
pluginId,
125+
currentConfig = {},
126+
currentStatus = {},
127+
}: AppGroupLifecyclePluginDataProps) {
128+
const [expanded, setExpanded] = React.useState(false);
129+
const {data: plugins, isLoading: pluginsLoading} = useGetAppGroupLifecyclePlugins();
130+
131+
const useConfigPropertiesHook =
132+
entityType === 'app'
133+
? useGetAppGroupLifecyclePluginAppConfigProperties
134+
: useGetAppGroupLifecyclePluginGroupConfigProperties;
135+
const useStatusPropertiesHook =
136+
entityType === 'app'
137+
? useGetAppGroupLifecyclePluginAppStatusProperties
138+
: useGetAppGroupLifecyclePluginGroupStatusProperties;
139+
140+
const {data: configProperties, isLoading: configLoading} = useConfigPropertiesHook(
141+
{pathParams: {pluginId: pluginId}},
142+
{enabled: !!pluginId},
143+
);
144+
145+
const {data: statusProperties, isLoading: statusLoading} = useStatusPropertiesHook(
146+
{pathParams: {pluginId: pluginId}},
147+
{enabled: !!pluginId},
148+
);
149+
150+
const selectedPlugin = React.useMemo(() => {
151+
if (!plugins || !pluginId) return null;
152+
return plugins.find((p) => p.id === pluginId) || null;
153+
}, [plugins, pluginId]);
154+
155+
if (pluginsLoading) {
156+
return (
157+
<Box sx={{display: 'flex', justifyContent: 'center', p: 2}}>
158+
<CircularProgress size={24} />
159+
</Box>
160+
);
161+
}
162+
163+
if (!selectedPlugin) {
164+
return null;
165+
}
166+
167+
const hasConfig = configProperties && Object.keys(configProperties).length > 0;
168+
const hasStatus = statusProperties && Object.keys(statusProperties).length > 0;
169+
170+
return (
171+
<TableContainer component={Paper}>
172+
<Accordion expanded={expanded} onChange={(_e, newExpanded) => setExpanded(newExpanded)}>
173+
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
174+
<Box
175+
sx={{
176+
display: 'inline-flex',
177+
flexGrow: 1,
178+
}}>
179+
<Stack
180+
direction="column"
181+
spacing={1}
182+
sx={{
183+
flexGrow: 0.95,
184+
}}>
185+
<Typography variant="h6" color="text.accent">
186+
Associated App Group Lifecycle Plugin: {selectedPlugin.display_name}
187+
</Typography>
188+
<Typography variant="body1" color="grey">
189+
{selectedPlugin.description}
190+
</Typography>
191+
</Stack>
192+
</Box>
193+
</AccordionSummary>
194+
<AccordionDetails>
195+
<Table aria-label="plugin details">
196+
<TableBody className="accordion-body">
197+
<TableRow>
198+
<TableCell colSpan={2}>
199+
{configLoading || statusLoading ? (
200+
<Box sx={{display: 'flex', justifyContent: 'center', p: 2}}>
201+
<CircularProgress size={24} />
202+
</Box>
203+
) : hasConfig || hasStatus ? (
204+
<Stack direction="row" useFlexGap flexWrap={'wrap'} justifyContent={'space-between'} gap={'2rem'}>
205+
{hasConfig && (
206+
<PluginDataPropertiesTable
207+
type="Configuration"
208+
properties={configProperties}
209+
data={currentConfig}
210+
/>
211+
)}
212+
{hasStatus && (
213+
<PluginDataPropertiesTable type="Status" properties={statusProperties} data={currentStatus} />
214+
)}
215+
</Stack>
216+
) : (
217+
<Typography variant="body2" color="text.secondary">
218+
No configuration or status information available for this plugin.
219+
</Typography>
220+
)}
221+
</TableCell>
222+
</TableRow>
223+
</TableBody>
224+
</Table>
225+
</AccordionDetails>
226+
</Accordion>
227+
</TableContainer>
228+
);
229+
}

src/pages/apps/Read.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {useParams} from 'react-router-dom';
33

44
import Container from '@mui/material/Container';
55
import Grid from '@mui/material/Grid';
6+
import Paper from '@mui/material/Paper';
67

78
import {useGetAppById} from '../../api/apiComponents';
89
import {App, AppGroup} from '../../api/apiSchemas';
@@ -12,6 +13,7 @@ import NotFound from '../NotFound';
1213
import Loading from '../../components/Loading';
1314
import {AppsAccordionListGroup, AppsAdminActionGroup, AppsHeader} from './components/';
1415
import ChangeTitle from '../../tab-title';
16+
import AppGroupLifecyclePluginData from '../../components/AppGroupLifecyclePluginData';
1517

1618
export default function ReadApp() {
1719
const currentUser = useCurrentUser();
@@ -67,6 +69,24 @@ export default function ReadApp() {
6769
<Container maxWidth="lg" sx={{my: 4}}>
6870
<Grid container spacing={3}>
6971
<AppsHeader app={app} currentUser={currentUser} />
72+
{(app as any)?.app_group_lifecycle_plugin && (
73+
<Grid item xs={12}>
74+
<AppGroupLifecyclePluginData
75+
entityType="app"
76+
pluginId={(app as any).app_group_lifecycle_plugin}
77+
currentConfig={
78+
(app as any)?.plugin_data
79+
? (app as any).plugin_data[(app as any).app_group_lifecycle_plugin]?.configuration || {}
80+
: {}
81+
}
82+
currentStatus={
83+
(app as any)?.plugin_data
84+
? (app as any).plugin_data[(app as any).app_group_lifecycle_plugin]?.status || {}
85+
: {}
86+
}
87+
/>
88+
</Grid>
89+
)}
7090
{app.active_owner_app_groups && (
7191
<AppsAccordionListGroup
7292
app_group={app.active_owner_app_groups}

src/pages/groups/Read.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import {Diversity3 as RoleIcon} from '@mui/icons-material';
5757
import AppLinkButton from './AppLinkButton';
5858
import AvatarButton from '../../components/AvatarButton';
5959
import MembershipChip from '../../components/MembershipChip';
60+
import AppGroupLifecyclePluginData from '../../components/AppGroupLifecyclePluginData';
6061

6162
function sortGroupMembers(
6263
[aUserId, aUsers]: [string, Array<OktaUserGroupMember>],
@@ -294,6 +295,24 @@ export default function ReadGroup() {
294295
</Stack>
295296
</Paper>
296297
</Grid>
298+
{group.type === 'app_group' && (app as any)?.app_group_lifecycle_plugin && (
299+
<Grid item xs={12}>
300+
<AppGroupLifecyclePluginData
301+
entityType="group"
302+
pluginId={(app as any).app_group_lifecycle_plugin}
303+
currentConfig={
304+
(group as any)?.plugin_data
305+
? (group as any).plugin_data[(app as any).app_group_lifecycle_plugin]?.configuration || {}
306+
: {}
307+
}
308+
currentStatus={
309+
(group as any)?.plugin_data
310+
? (group as any).plugin_data[(app as any).app_group_lifecycle_plugin]?.status || {}
311+
: {}
312+
}
313+
/>
314+
</Grid>
315+
)}
297316
{group.type == 'role_group' ? (
298317
<>
299318
<Grid item xs={6}>

0 commit comments

Comments
 (0)