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
6 changes: 5 additions & 1 deletion backend/app/aws_integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,11 @@ func (s *Session) GetAWSAccessReportByTeamAndAccountId(ctx context.Context, team
time.Sleep(time.Second)
continue
} else if output.JobStatus != iamtypes.JobStatusTypeCompleted {
return nil, s.SanitizedError(fmt.Errorf("failed to generate access report"))
var message string
if output.ErrorDetails != nil && output.ErrorDetails.Message != nil {
message = *output.ErrorDetails.Message
Copy link

Copilot AI Apr 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider providing a default fallback value for 'message' when 'output.ErrorDetails.Message' is nil to ensure that error messages remain informative.

Suggested change
message = *output.ErrorDetails.Message
message = *output.ErrorDetails.Message
} else {
message = "No error details provided."

Copilot uses AI. Check for mistakes.
}
return nil, s.SanitizedError(fmt.Errorf("unexpected job status for access report: %s, error message: %s", string(output.JobStatus), message))
}

for _, detail := range output.AccessDetails {
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/app/(user-area)/teams/[teamId]/Rules.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,11 @@ const AccountPage = ({ account, onBack }: AccountPageProps) => {
<div>
<span className="label">Region allowlist: </span>
<ChipEditor
options={awsRegions.map((region) => ({ label: region.id, value: region.id }))}
options={awsRegions.map((region) => ({
label: region.id,
value: region.id,
altLabel: region.name,
}))}
before={scpRuleSet?.regionAllowlist?.regions || new Set()}
after={ruleSet.regionAllowlist?.regions || new Set()}
onAdd={(region) => {
Expand Down Expand Up @@ -150,6 +154,7 @@ const AccountPage = ({ account, onBack }: AccountPageProps) => {
options={awsServices.map((service) => ({
label: service.namespace,
value: service.namespace,
altLabel: service.name,
}))}
before={scpRuleSet?.serviceAllowlist?.services || new Set()}
after={ruleSet.serviceAllowlist?.services || new Set()}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/(user-area)/teams/[teamId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ const Page = () => {
availableEndTime={maxUnfilteredReportEndTime}
onChange={setDurationSeconds}
/>
<Dialog isOpen={isRulesOpen} onClose={() => setIsRulesOpen(false)} title="Rules">
<Dialog isOpen={isRulesOpen} onClose={() => setIsRulesOpen(false)} title="Rules" size="xl">
<Rules />
</Dialog>
{canManageScps && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ const Page = () => {
);

return (
<div>
<div className="flex flex-col gap-4">
<Dialog isOpen={isInviting} onClose={() => setIsInviting(false)} title="Invite Team Member">
<InviteMemberForm
onSuccess={() => {
Expand All @@ -192,7 +192,7 @@ const Page = () => {
/>
)}
</Dialog>
<h2 className="mb-4 flex items-center gap-2">
<h2 className="flex items-center gap-2">
Members {memberships && `(${memberships.length})`}{' '}
{hasTeamSubscription && (
<PlusCircleIcon
Expand All @@ -213,7 +213,7 @@ const Page = () => {
{!memberships ? (
<p>Loading...</p>
) : (
<table className="w-full text-left mt-4">
<table className="w-full text-left">
<thead className="uppercase text-english-violet">
<tr>
<th>Email Address</th>
Expand Down
90 changes: 55 additions & 35 deletions frontend/src/components/ChipEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
'use client';

import { PlusCircleIcon, PlusIcon, XMarkIcon } from '@heroicons/react/24/outline';
import { useState } from 'react';
import { useMemo, useState } from 'react';
import { Combobox, ComboboxInput, ComboboxOption, ComboboxOptions } from '@headlessui/react';

import { Tooltip } from '@/components';

interface Option {
label: string;
altLabel?: string;
value: string;
}

Expand Down Expand Up @@ -52,23 +55,32 @@ export const ChipEditor = (props: Props) => {
const isRemoved = props.before.has(option.value) && !props.after.has(option.value);

return (
<span
<Tooltip
key={option.value}
className={`${isAdded ? addedChip : isRemoved ? removedChip : neutralChip}`}
disabled={!option.altLabel && !isAdded && !isRemoved}
content={
<div className="text-xs">
{option.altLabel && <div>{option.altLabel}</div>}
{isAdded && <div className="text-mint">Added</div>}
{isRemoved && <div className="text-indian-red">Removed</div>}
</div>
}
>
{option.label}
{props.after.has(option.value) ? (
<XMarkIcon
className="h-[0.8rem] inline-block ml-1 cursor-pointer"
onClick={() => props.onRemove(option.value)}
/>
) : (
<PlusIcon
className="h-[0.8rem] inline-block ml-1 cursor-pointer"
onClick={() => props.onAdd(option.value)}
/>
)}
</span>
<span className={`${isAdded ? addedChip : isRemoved ? removedChip : neutralChip}`}>
{option.label}
{props.after.has(option.value) ? (
<XMarkIcon
className="h-[0.8rem] inline-block ml-1 cursor-pointer"
onClick={() => props.onRemove(option.value)}
/>
) : (
<PlusIcon
className="h-[0.8rem] inline-block ml-1 cursor-pointer"
onClick={() => props.onAdd(option.value)}
/>
)}
</span>
</Tooltip>
);
})}
{isAdding ? (
Expand All @@ -77,8 +89,10 @@ export const ChipEditor = (props: Props) => {
(option) => !props.before.has(option.value) && !props.after.has(option.value),
)}
onChange={(value) => {
props.onAdd(value);
setIsAdding(false);
if (value) {
props.onAdd(value);
setIsAdding(false);
}
}}
onClose={() => setIsAdding(false)}
/>
Expand All @@ -94,52 +108,58 @@ export const ChipEditor = (props: Props) => {

interface InlineComboboxProps {
options: Option[];
value?: string;
onChange?: (value: string) => void;
onClose?: () => void;
}

const InlineCombobox = (props: InlineComboboxProps) => {
const InlineCombobox = ({ options, onChange, onClose }: InlineComboboxProps) => {
const [query, setQuery] = useState('');

const filteredOptions = props.options
.filter((option) => option.label.toLowerCase().includes(query.toLowerCase()))
.sort((a, b) => a.label.localeCompare(b.label));
const filteredOptions = useMemo(
() =>
options
.filter(
(option) =>
option.label.toLowerCase().includes(query.toLowerCase()) ||
option.altLabel?.toLowerCase().includes(query.toLowerCase()),
)
.sort((a, b) => a.label.localeCompare(b.label)),
[options, query],
);

return (
<Combobox
value={props.value}
onChange={props.onChange}
virtual={{ options: filteredOptions }}
onChange={(option: Option) => onChange?.(option.value)}
onClose={() => {
setQuery('');
props.onClose?.();
onClose?.();
}}
immediate
>
<ComboboxInput
autoFocus
displayValue={(value) => props.options.find((option) => option.value === value)?.label || ''}
onChange={(event) => setQuery(event.target.value)}
onBlur={() => {
setQuery('');
props.onClose?.();
onClose?.();
}}
className="bg-english-violet px-2 py-0.5 mx-0.5 leading-none rounded-md text-xs font-semibold text-snow focus:outline-none"
/>
<ComboboxOptions
anchor="bottom"
className="empty:invisible rounded-md text-snow bg-english-violet/80 backdrop-blur-md"
className="empty:invisible rounded-md text-snow bg-english-violet/80 backdrop-blur-md p-1 w-3xs"
static
>
{filteredOptions.map((option) => (
{({ option }) => (
<ComboboxOption
key={option.value}
value={option.value}
className="cursor-pointer text-xs px-2 py-0.5 hover:bg-white/20"
value={option}
className="cursor-pointer text-xs px-2 py-1 hover:bg-white/20 rounded-md w-full flex flex-col"
>
{option.label}
<div className="font-semibold">{option.label}</div>
{option.altLabel && <div>{option.altLabel}</div>}
</ComboboxOption>
))}
)}
</ComboboxOptions>
</Combobox>
);
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/components/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,18 @@ interface Props {
isOpen?: boolean;
onClose?: () => void;
title?: string;
size?: 'xl';
}

export const Dialog = (props: Props) => {
const width = props.size === 'xl' ? 'w-xl' : 'max-w-xl';

return (
<DialogImpl open={!!props.isOpen} onClose={() => props.onClose && props.onClose()} className="relative z-50">
<div className="fixed inset-0 flex w-screen items-center justify-center p-4 bg-radial from-black/10 to-black/40">
<DialogPanel className="min-w-lg max-w-xl space-y-4 translucent-snow border border-platinum rounded-xl p-8">
<DialogPanel
className={`min-w-lg ${width} space-y-4 translucent-snow border border-platinum rounded-xl p-8`}
>
{props.title && <DialogTitle className="font-bold">{props.title}</DialogTitle>}
{props.children}
</DialogPanel>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/ErrorMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ExclamationCircleIcon } from '@heroicons/react/24/outline';

export const ErrorMessage = ({ children }: { children: React.ReactNode }) => (
<p className="w-full my-2 rounded-md p-3 text-dark-purple bg-red-100 sm:text-sm sm:leading-6 [&_a]:underline">
<p className="w-full rounded-md p-3 text-dark-purple bg-light-coral/50 sm:text-sm sm:leading-6 [&_a]:underline">
<span className="inline-flex items-baseline">
<ExclamationCircleIcon className="h-6 w-6 shrink-0 self-center mr-2" />
<span>{children}</span>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/SuccessMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { CheckCircleIcon } from '@heroicons/react/24/outline';

export const SuccessMessage = ({ children }: { children: React.ReactNode }) => (
<p className="w-full my-2 rounded-md p-3 text-dark-purple bg-emerald sm:text-sm sm:leading-6 [&_a]:underline">
<p className="w-full rounded-md p-3 text-dark-purple bg-emerald/50 sm:text-sm sm:leading-6 [&_a]:underline">
<span className="inline-flex items-baseline">
<CheckCircleIcon className="h-6 w-6 shrink-0 self-center mr-2" />
<span>{children}</span>
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/models/aws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,5 +156,10 @@ export const aws = createModel<RootModel>()({
dispatch.reports.removeReportsByAwsIntegrationId(payload.id);
}
},
async fetchAccessReportByTeamAndAccountId(payload: { teamId: string; accountId: string }, state) {
const api = new AwsApi(apiConfiguration(state.api));
const resp = await api.getAWSAccessReport(payload);
return resp;
},
}),
});
1 change: 1 addition & 0 deletions frontend/src/models/reports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export const reports = createModel<RootModel>()({
teamId,
resp.map((r) => r.id),
);
return resp;
},
async queueTeamReportGeneration(input: { teamId: string; input: QueueTeamReportGenerationInput }, state) {
const api = new TeamApi(apiConfiguration(state.api));
Expand Down
Loading