From 2da24013666f31624be7cedce2498d4ea930ab98 Mon Sep 17 00:00:00 2001
From: Chris Brown <1731074+ccbrown@users.noreply.github.com>
Date: Mon, 12 May 2025 10:35:22 -0400
Subject: [PATCH] add basic admin control panel
---
frontend/src/app/(user-area)/Header.tsx | 11 +++
.../(user-area)/control-panel/TabLayout.tsx | 23 ++++++
.../app/(user-area)/control-panel/layout.tsx | 21 +++++
.../app/(user-area)/control-panel/page.tsx | 7 ++
.../control-panel/teams/[teamId]/page.tsx | 82 +++++++++++++++++++
.../(user-area)/control-panel/teams/page.tsx | 50 +++++++++++
frontend/src/models/teams.ts | 7 ++
7 files changed, 201 insertions(+)
create mode 100644 frontend/src/app/(user-area)/control-panel/TabLayout.tsx
create mode 100644 frontend/src/app/(user-area)/control-panel/layout.tsx
create mode 100644 frontend/src/app/(user-area)/control-panel/page.tsx
create mode 100644 frontend/src/app/(user-area)/control-panel/teams/[teamId]/page.tsx
create mode 100644 frontend/src/app/(user-area)/control-panel/teams/page.tsx
diff --git a/frontend/src/app/(user-area)/Header.tsx b/frontend/src/app/(user-area)/Header.tsx
index 90ac4ec0..1d67f5ac 100644
--- a/frontend/src/app/(user-area)/Header.tsx
+++ b/frontend/src/app/(user-area)/Header.tsx
@@ -5,12 +5,14 @@ import {
ArrowRightStartOnRectangleIcon,
ChevronDownIcon,
CogIcon,
+ ComputerDesktopIcon,
UserCircleIcon,
UserGroupIcon,
} from '@heroicons/react/24/outline';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
+import { UserRole } from '@/generated/api';
import { Logo } from '@/components';
import { useCurrentUser } from '@/hooks';
import { useDispatch } from '@/store';
@@ -68,6 +70,15 @@ export const Header = (props: Props) => {
Teams
+ {currentUser?.role === UserRole.Administrator && (
+
+
+ Control Panel
+
+ )}
signOut()}
diff --git a/frontend/src/app/(user-area)/control-panel/TabLayout.tsx b/frontend/src/app/(user-area)/control-panel/TabLayout.tsx
new file mode 100644
index 00000000..0d882481
--- /dev/null
+++ b/frontend/src/app/(user-area)/control-panel/TabLayout.tsx
@@ -0,0 +1,23 @@
+'use client';
+
+import { UserGroupIcon } from '@heroicons/react/24/outline';
+
+import { TabLayout as TabLayoutImpl } from '@/components';
+
+const TABS = [
+ {
+ title: 'Teams',
+ path: '/control-panel/teams',
+ icon: UserGroupIcon,
+ },
+];
+
+interface Props {
+ children?: React.ReactNode;
+}
+
+export const TabLayout = (props: Props) => (
+
Control Panel} tabs={TABS}>
+ {props.children}
+
+);
diff --git a/frontend/src/app/(user-area)/control-panel/layout.tsx b/frontend/src/app/(user-area)/control-panel/layout.tsx
new file mode 100644
index 00000000..43ac6ab2
--- /dev/null
+++ b/frontend/src/app/(user-area)/control-panel/layout.tsx
@@ -0,0 +1,21 @@
+import type { Metadata } from 'next';
+
+import { Header } from '../Header';
+import { TabLayout } from './TabLayout';
+
+export const metadata: Metadata = {
+ title: 'Control Panel',
+};
+
+export default function Layout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return (
+
+
+ {children}
+
+ );
+}
diff --git a/frontend/src/app/(user-area)/control-panel/page.tsx b/frontend/src/app/(user-area)/control-panel/page.tsx
new file mode 100644
index 00000000..eebfada6
--- /dev/null
+++ b/frontend/src/app/(user-area)/control-panel/page.tsx
@@ -0,0 +1,7 @@
+import { redirect } from 'next/navigation';
+
+const Page = () => {
+ redirect('control-panel/teams');
+};
+
+export default Page;
diff --git a/frontend/src/app/(user-area)/control-panel/teams/[teamId]/page.tsx b/frontend/src/app/(user-area)/control-panel/teams/[teamId]/page.tsx
new file mode 100644
index 00000000..c4ba7ef1
--- /dev/null
+++ b/frontend/src/app/(user-area)/control-panel/teams/[teamId]/page.tsx
@@ -0,0 +1,82 @@
+'use client';
+
+import { useParams } from 'next/navigation';
+import { useEffect } from 'react';
+
+import { Tooltip } from '@/components';
+import { useDispatch, useSelector } from '@/store';
+
+const Page = () => {
+ const { teamId } = useParams<{ teamId: string }>();
+
+ const dispatch = useDispatch();
+
+ useEffect(() => {
+ dispatch.teams.fetch(teamId);
+ }, [dispatch, teamId]);
+
+ const team = useSelector((state) => state.teams.teams[teamId]);
+
+ useEffect(() => {
+ if (team) {
+ dispatch.aws.fetchIntegrationsByTeamId(team.id);
+ }
+ }, [dispatch, team]);
+
+ const awsIntegrationIds = useSelector((state) => state.aws.teamIntegrationIds[teamId]);
+ const allAwsIntegrations = useSelector((state) => state.aws.integrations);
+
+ if (!team) {
+ return
Loading...
;
+ }
+
+ return (
+
+
{team.name}
+
+ Entitlements:{' '}
+ {team.entitlements.teamFeatures
+ ? 'Team Features'
+ : team.entitlements.individualFeatures
+ ? 'Individual Features'
+ : 'None'}
+
+
AWS Integrations
+
+
+
+ | Name |
+ Orgs |
+ SCPs |
+ CloudTrail Trail |
+
+
+
+ {awsIntegrationIds
+ ?.map((integrationId) => allAwsIntegrations[integrationId])
+ .filter((integration) => integration)
+ .map((integration) => (
+
+ | {integration.name} |
+ {integration.getAccountNamesFromOrganizations ? 'Yes' : 'No'} |
+ {integration.manageScps ? 'Yes' : 'No'} |
+
+ {integration.cloudtrailTrail ? (
+
+ Yes
+
+ ) : (
+ 'No'
+ )}
+ |
+
+ ))}
+
+
+
+ );
+};
+
+export default Page;
diff --git a/frontend/src/app/(user-area)/control-panel/teams/page.tsx b/frontend/src/app/(user-area)/control-panel/teams/page.tsx
new file mode 100644
index 00000000..84b3def0
--- /dev/null
+++ b/frontend/src/app/(user-area)/control-panel/teams/page.tsx
@@ -0,0 +1,50 @@
+'use client';
+
+import Link from 'next/link';
+import { useEffect } from 'react';
+
+import { useDispatch, useSelector } from '@/store';
+
+const Page = () => {
+ const dispatch = useDispatch();
+
+ useEffect(() => {
+ dispatch.teams.fetchAll();
+ }, [dispatch]);
+
+ const teams = useSelector((state) => state.teams.teams);
+
+ return (
+ <>
+
Teams
+
+
+
+ | Name |
+ Entitlements |
+
+
+
+ {Object.values(teams).map((team) => (
+
+ |
+
+ {team.name}
+
+ |
+
+ {team.entitlements.teamFeatures
+ ? 'Team Features'
+ : team.entitlements.individualFeatures
+ ? 'Individual Features'
+ : 'None'}
+ |
+
+ ))}
+
+
+ >
+ );
+};
+
+export default Page;
diff --git a/frontend/src/models/teams.ts b/frontend/src/models/teams.ts
index 0242cc8e..aee88b06 100644
--- a/frontend/src/models/teams.ts
+++ b/frontend/src/models/teams.ts
@@ -139,6 +139,13 @@ export const teams = createModel
()({
});
dispatch.teams.put(resp);
},
+ async fetchAll(_payload: void, state) {
+ const api = new TeamApi(apiConfiguration(state.api));
+ const resp = await api.getTeams();
+ resp.forEach((team) => {
+ dispatch.teams.put(team);
+ });
+ },
async fetchBillingProfile(id: string, state) {
const api = new TeamApi(apiConfiguration(state.api));
try {