Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
11 changes: 11 additions & 0 deletions frontend/src/app/(user-area)/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -68,6 +70,15 @@ export const Header = (props: Props) => {
<UserGroupIcon className="h-[1.5rem]" />
<span>Teams</span>
</Link>
{currentUser?.role === UserRole.Administrator && (
<Link
className="whitespace-nowrap flex items-center gap-2 cursor-pointer hover:bg-white/80 p-2 rounded-md"
href="/control-panel"
>
<ComputerDesktopIcon className="h-[1.5rem]" />
<span>Control Panel</span>
</Link>
)}
<div
className="whitespace-nowrap flex items-center gap-2 cursor-pointer hover:bg-white/80 p-2 rounded-md"
onClick={() => signOut()}
Expand Down
23 changes: 23 additions & 0 deletions frontend/src/app/(user-area)/control-panel/TabLayout.tsx
Original file line number Diff line number Diff line change
@@ -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) => (
<TabLayoutImpl header={<h1>Control Panel</h1>} tabs={TABS}>
{props.children}
</TabLayoutImpl>
);
21 changes: 21 additions & 0 deletions frontend/src/app/(user-area)/control-panel/layout.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="bg-platinum flex flex-col min-h-screen">
<Header />
<TabLayout>{children}</TabLayout>
</div>
);
}
7 changes: 7 additions & 0 deletions frontend/src/app/(user-area)/control-panel/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { redirect } from 'next/navigation';

const Page = () => {
redirect('control-panel/teams');
};

export default Page;
82 changes: 82 additions & 0 deletions frontend/src/app/(user-area)/control-panel/teams/[teamId]/page.tsx
Original file line number Diff line number Diff line change
@@ -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 <div>Loading...</div>;
}

return (
<div className="flex flex-col gap-2">
<h2>{team.name}</h2>
<div>
<span className="label">Entitlements:</span>{' '}
{team.entitlements.teamFeatures
? 'Team Features'
: team.entitlements.individualFeatures
? 'Individual Features'
: 'None'}
</div>
<h3>AWS Integrations</h3>
<table className="w-full text-left">
<thead className="uppercase text-sm text-english-violet">
<tr>
<th>Name</th>
<th>Orgs</th>
<th>SCPs</th>
<th>CloudTrail Trail</th>
</tr>
</thead>
<tbody>
{awsIntegrationIds
?.map((integrationId) => allAwsIntegrations[integrationId])
.filter((integration) => integration)
.map((integration) => (
<tr key={integration.id}>
<td>{integration.name}</td>
<td>{integration.getAccountNamesFromOrganizations ? 'Yes' : 'No'}</td>
<td>{integration.manageScps ? 'Yes' : 'No'}</td>
<td>
{integration.cloudtrailTrail ? (
<Tooltip
content={`s3://${integration.cloudtrailTrail.s3BucketName}${integration.cloudtrailTrail.s3KeyPrefix}`}
>
<span className="hoverable">Yes</span>
</Tooltip>
) : (
'No'
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};

export default Page;
50 changes: 50 additions & 0 deletions frontend/src/app/(user-area)/control-panel/teams/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<h2>Teams</h2>
<table className="w-full text-left">
<thead className="uppercase text-sm text-english-violet">
<tr>
<th>Name</th>
<th>Entitlements</th>
</tr>
</thead>
<tbody>
{Object.values(teams).map((team) => (
<tr key={team.id}>
<td>
<Link href={`/control-panel/teams/${team.id}`} className="link">
{team.name}
</Link>
</td>
<td>
{team.entitlements.teamFeatures
? 'Team Features'
: team.entitlements.individualFeatures
? 'Individual Features'
: 'None'}
</td>
</tr>
))}
</tbody>
</table>
</>
);
};

export default Page;
7 changes: 7 additions & 0 deletions frontend/src/models/teams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,13 @@ export const teams = createModel<RootModel>()({
});
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 {
Expand Down
Loading