diff --git a/backend/app/aws_integration.go b/backend/app/aws_integration.go index c72fd54d..75a9f04c 100644 --- a/backend/app/aws_integration.go +++ b/backend/app/aws_integration.go @@ -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 + } + 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 { diff --git a/frontend/src/app/(user-area)/teams/[teamId]/Rules.tsx b/frontend/src/app/(user-area)/teams/[teamId]/Rules.tsx index 578b939d..c4e739ec 100644 --- a/frontend/src/app/(user-area)/teams/[teamId]/Rules.tsx +++ b/frontend/src/app/(user-area)/teams/[teamId]/Rules.tsx @@ -107,7 +107,11 @@ const AccountPage = ({ account, onBack }: AccountPageProps) => {
Region allowlist: ({ 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) => { @@ -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()} diff --git a/frontend/src/app/(user-area)/teams/[teamId]/page.tsx b/frontend/src/app/(user-area)/teams/[teamId]/page.tsx index fb2ca318..8660365d 100644 --- a/frontend/src/app/(user-area)/teams/[teamId]/page.tsx +++ b/frontend/src/app/(user-area)/teams/[teamId]/page.tsx @@ -318,7 +318,7 @@ const Page = () => { availableEndTime={maxUnfilteredReportEndTime} onChange={setDurationSeconds} /> - setIsRulesOpen(false)} title="Rules"> + setIsRulesOpen(false)} title="Rules" size="xl"> {canManageScps && ( diff --git a/frontend/src/app/(user-area)/teams/[teamId]/settings/members/page.tsx b/frontend/src/app/(user-area)/teams/[teamId]/settings/members/page.tsx index ae47ecff..51d9e39d 100644 --- a/frontend/src/app/(user-area)/teams/[teamId]/settings/members/page.tsx +++ b/frontend/src/app/(user-area)/teams/[teamId]/settings/members/page.tsx @@ -169,7 +169,7 @@ const Page = () => { ); return ( -
+
setIsInviting(false)} title="Invite Team Member"> { @@ -192,7 +192,7 @@ const Page = () => { /> )} -

+

Members {memberships && `(${memberships.length})`}{' '} {hasTeamSubscription && ( { {!memberships ? (

Loading...

) : ( - +
diff --git a/frontend/src/components/ChipEditor.tsx b/frontend/src/components/ChipEditor.tsx index 3f49312a..2f026aaa 100644 --- a/frontend/src/components/ChipEditor.tsx +++ b/frontend/src/components/ChipEditor.tsx @@ -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; } @@ -52,23 +55,32 @@ export const ChipEditor = (props: Props) => { const isRemoved = props.before.has(option.value) && !props.after.has(option.value); return ( - + {option.altLabel &&
{option.altLabel}
} + {isAdded &&
Added
} + {isRemoved &&
Removed
} + + } > - {option.label} - {props.after.has(option.value) ? ( - props.onRemove(option.value)} - /> - ) : ( - props.onAdd(option.value)} - /> - )} -
+ + {option.label} + {props.after.has(option.value) ? ( + props.onRemove(option.value)} + /> + ) : ( + props.onAdd(option.value)} + /> + )} + + ); })} {isAdding ? ( @@ -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)} /> @@ -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 ( onChange?.(option.value)} onClose={() => { setQuery(''); - props.onClose?.(); + onClose?.(); }} immediate > 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" /> - {filteredOptions.map((option) => ( + {({ option }) => ( - {option.label} +
{option.label}
+ {option.altLabel &&
{option.altLabel}
}
- ))} + )}
); diff --git a/frontend/src/components/Dialog.tsx b/frontend/src/components/Dialog.tsx index 52b64612..56d36356 100644 --- a/frontend/src/components/Dialog.tsx +++ b/frontend/src/components/Dialog.tsx @@ -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 ( props.onClose && props.onClose()} className="relative z-50">
- + {props.title && {props.title}} {props.children} diff --git a/frontend/src/components/ErrorMessage.tsx b/frontend/src/components/ErrorMessage.tsx index 5433ad4c..063e996a 100644 --- a/frontend/src/components/ErrorMessage.tsx +++ b/frontend/src/components/ErrorMessage.tsx @@ -1,7 +1,7 @@ import { ExclamationCircleIcon } from '@heroicons/react/24/outline'; export const ErrorMessage = ({ children }: { children: React.ReactNode }) => ( -

+

{children} diff --git a/frontend/src/components/SuccessMessage.tsx b/frontend/src/components/SuccessMessage.tsx index 66728f75..5b6b7bba 100644 --- a/frontend/src/components/SuccessMessage.tsx +++ b/frontend/src/components/SuccessMessage.tsx @@ -1,7 +1,7 @@ import { CheckCircleIcon } from '@heroicons/react/24/outline'; export const SuccessMessage = ({ children }: { children: React.ReactNode }) => ( -

+

{children} diff --git a/frontend/src/models/aws.ts b/frontend/src/models/aws.ts index a4310efc..9a497e6f 100644 --- a/frontend/src/models/aws.ts +++ b/frontend/src/models/aws.ts @@ -156,5 +156,10 @@ export const aws = createModel()({ 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; + }, }), }); diff --git a/frontend/src/models/reports.ts b/frontend/src/models/reports.ts index 1b27e45e..9c521ee6 100644 --- a/frontend/src/models/reports.ts +++ b/frontend/src/models/reports.ts @@ -81,6 +81,7 @@ export const reports = createModel()({ teamId, resp.map((r) => r.id), ); + return resp; }, async queueTeamReportGeneration(input: { teamId: string; input: QueueTeamReportGenerationInput }, state) { const api = new TeamApi(apiConfiguration(state.api));

Email Address