diff --git a/src/core/theme/Theme.tsx b/src/core/theme/Theme.tsx index fd73eafe..eb6d619a 100644 --- a/src/core/theme/Theme.tsx +++ b/src/core/theme/Theme.tsx @@ -105,6 +105,20 @@ export function Theme({ children }: ThemeProps) { }, }, components: { + MuiTooltip: { + styleOverrides: { + tooltip: { + background: palette.background?.default, + color: palette.common?.black, + fontSize: 14, + boxShadow: + 'rgba(0, 0, 0, 0.2) 0px 11px 15px -7px, rgba(0, 0, 0, 0.14) 0px 24px 38px 3px, rgba(0, 0, 0, 0.12) 0px 9px 46px 8px', + }, + arrow: { + color: palette.background?.default, + }, + }, + }, MuiCssBaseline: { styleOverrides: { 'body > #root': { diff --git a/src/locales/de-DE/messages.po b/src/locales/de-DE/messages.po index ef579b37..faf1d7b8 100644 --- a/src/locales/de-DE/messages.po +++ b/src/locales/de-DE/messages.po @@ -13,35 +13,35 @@ msgstr "" "Language-Team: \n" "Plural-Forms: \n" -#: src/shared/utils/parseISO8601Duration.ts:110 +#: src/shared/utils/parseDuration.ts:139 msgid "{0, plural, one {# Day} other {# Days}}" msgstr "{0, plural, one {# Tag} other {# Tage}}" -#: src/shared/utils/parseISO8601Duration.ts:118 +#: src/shared/utils/parseDuration.ts:147 msgid "{0, plural, one {# Hour} other {# Hours}}" msgstr "{0, plural, one {# Stunde} other {# Stunden}}" -#: src/shared/utils/parseISO8601Duration.ts:126 +#: src/shared/utils/parseDuration.ts:155 msgid "{0, plural, one {# Minute} other {# Minutes}}" msgstr "{0, plural, one {# Minute} other {# Minuten}}" -#: src/shared/utils/parseISO8601Duration.ts:94 +#: src/shared/utils/parseDuration.ts:123 msgid "{0, plural, one {# Month} other {# Months}}" msgstr "{0, plural, one {# Monat} other {# Monate}}" -#: src/shared/utils/parseISO8601Duration.ts:134 +#: src/shared/utils/parseDuration.ts:163 msgid "{0, plural, one {# Second} other {# Seconds}}" msgstr "{0, plural, one {# Sekunde} other {# Sekunden}}" -#: src/shared/utils/parseISO8601Duration.ts:102 +#: src/shared/utils/parseDuration.ts:131 msgid "{0, plural, one {# Week} other {# Weeks}}" msgstr "{0, plural, one {# Woche} other {# Wochen}}" -#: src/shared/utils/parseISO8601Duration.ts:86 +#: src/shared/utils/parseDuration.ts:115 msgid "{0, plural, one {# Year} other {# Years}}" msgstr "{0, plural, one {# Jahr} other {# Jahre}}" -#: src/shared/utils/parseISO8601Duration.ts:141 +#: src/shared/utils/parseDuration.ts:170 msgid "{0} Milliseconds" msgstr "{0} Millisekunden" @@ -54,7 +54,7 @@ msgid "© 2023 Some Engineering Inc. All rights reserved." msgstr "© 2023 Some Engineering Services GMBH. Alle Rechte vorbehalten." #: src/pages/panel/accounts/AccountsTable.tsx:31 -#: src/pages/panel/inventory/InventoryTable.tsx:55 +#: src/pages/panel/inventory/InventoryTable.tsx:60 #: src/pages/panel/inventory/utils/getAutoCompleteFromKey.tsx:66 #: src/shared/layouts/panel-layout/UserProfileButton.tsx:103 msgid "Accounts" @@ -72,6 +72,10 @@ msgstr "Aktionen" msgid "Add an account" msgstr "Füge ein Konto hinzu" +#: src/pages/panel/inventory/ResourceDetail.tsx:178 +msgid "Age" +msgstr "Alter" + #: src/pages/auth/register/RegisterPage.tsx:132 msgid "Already have an account? Click here to Log in." msgstr "Sie haben bereits ein Konto? Klicken Sie hier, um sich einzuloggen." @@ -84,11 +88,11 @@ msgstr "Wenn Sie den Stack alternativ manuell bereitstellen möchten, können Si msgid "Alternatively: Manual Cloud Setup" msgstr "Alternativ: Manuelles Cloud Setup" -#: src/shared/utils/parseISO8601Duration.ts:143 +#: src/shared/utils/parseDuration.ts:172 msgid "and" msgstr "und" -#: src/pages/panel/accounts/AccountRow.tsx:241 +#: src/pages/panel/accounts/AccountRow.tsx:242 msgid "Are you sure?" msgstr "Bist du sicher?" @@ -100,24 +104,32 @@ msgstr "Automatisches Cloud Setup" msgid "AWS Marketplace has been successfully subscribed" msgstr "AWS Marketplace wurde erfolgreich abonniert" +#: src/pages/panel/inventory/ResourceDetail.tsx:165 +msgid "Basic Information" +msgstr "Grundinformation" + #: src/pages/panel/shared/utils/chartToShow.tsx:39 msgid "Benchmarks" msgstr "Benchmarks" -#: src/pages/panel/accounts/AccountRow.tsx:185 -#: src/pages/panel/accounts/AccountRow.tsx:189 +#: src/pages/panel/accounts/AccountRow.tsx:186 #: src/pages/panel/accounts/AccountRow.tsx:190 -#: src/pages/panel/accounts/AccountRow.tsx:260 +#: src/pages/panel/accounts/AccountRow.tsx:191 +#: src/pages/panel/accounts/AccountRow.tsx:261 msgid "Cancel" msgstr "Stornieren" -#: src/pages/panel/accounts/AccountRow.tsx:280 +#: src/pages/panel/inventory/ResourceDetail.tsx:245 +msgid "Check" +msgstr "Überprüfen" + +#: src/pages/panel/accounts/AccountRow.tsx:281 #: src/pages/panel/accounts/AccountsTable.tsx:35 #: src/pages/panel/home/AccountCard.tsx:33 msgid "Cloud" msgstr "Cloud" -#: src/shared/event-button/EventButton.tsx:47 +#: src/shared/event-button/EventButton.tsx:56 msgid "Cloud account added, id: {0}" msgstr "Cloud-Konto hinzugefügt, ID: {0}" @@ -143,6 +155,10 @@ msgstr "In die Zwischenablage kopiert!" msgid "Copy" msgstr "Kopieren" +#: src/pages/panel/inventory/ResourceDetail.tsx:174 +msgid "Created Time" +msgstr "Erstellte Zeit" + #: src/shared/defined-messages/getMessage.ts:5 msgid "Critical" msgstr "Kritisch" @@ -151,10 +167,10 @@ msgstr "Kritisch" msgid "Dashboard" msgstr "Dashboard" -#: src/pages/panel/accounts/AccountRow.tsx:229 -#: src/pages/panel/accounts/AccountRow.tsx:233 +#: src/pages/panel/accounts/AccountRow.tsx:230 #: src/pages/panel/accounts/AccountRow.tsx:234 -#: src/pages/panel/accounts/AccountRow.tsx:271 +#: src/pages/panel/accounts/AccountRow.tsx:235 +#: src/pages/panel/accounts/AccountRow.tsx:272 msgid "Delete" msgstr "Löschen" @@ -163,7 +179,11 @@ msgstr "Löschen" msgid "Deploy Stack" msgstr "Deploy Stack" -#: src/pages/panel/accounts/AccountRow.tsx:244 +#: src/pages/panel/inventory/ResourceDetail.tsx:205 +msgid "Details" +msgstr "Einzelheiten" + +#: src/pages/panel/accounts/AccountRow.tsx:245 msgid "Do you want to delete this account?" msgstr "Möchten Sie dieses Konto löschen?" @@ -171,8 +191,8 @@ msgstr "Möchten Sie dieses Konto löschen?" msgid "Don't have an account? Click here to Sign up." msgstr "Sie haben noch kein Konto? Klicken Sie hier, um sich anzumelden." -#: src/pages/panel/accounts/AccountRow.tsx:201 #: src/pages/panel/accounts/AccountRow.tsx:202 +#: src/pages/panel/accounts/AccountRow.tsx:203 msgid "Edit" msgstr "Edit" @@ -185,7 +205,7 @@ msgstr "Email" msgid "Enabled" msgstr "Ermöglicht" -#: src/shared/event-button/EventButton.tsx:78 +#: src/shared/event-button/EventButton.tsx:90 msgid "Events" msgstr "Veranstaltungen" @@ -206,6 +226,11 @@ msgstr "FALSCH" msgid "Fix is an open-source Wiz alternative for cloud infrastructure security. Take control of cloud risks with an asset inventory, compliance scans, and remediation workflows." msgstr "Fix ist eine Open-Source Wiz Alternative für die Sicherheit Ihrer Cloud Infrastruktur. Übernehmen Sie die Kontrolle über Cloud-Risiken mit einem Cloud-Asset-Inventar, Compliance-Scans und Workflows." +#: src/pages/panel/inventory/ResourceDetail.tsx:229 +#: src/pages/panel/inventory/ResourceDetail.tsx:247 +msgid "Found at" +msgstr "Gefunden am" + #: src/shared/defined-messages/getMessage.ts:8 msgid "High" msgstr "Hoch" @@ -214,9 +239,10 @@ msgstr "Hoch" msgid "How to fix" msgstr "Wie repariert man" -#: src/pages/panel/accounts/AccountRow.tsx:277 +#: src/pages/panel/accounts/AccountRow.tsx:278 #: src/pages/panel/accounts/AccountsTable.tsx:38 #: src/pages/panel/home/AccountCard.tsx:30 +#: src/pages/panel/inventory/ResourceDetail.tsx:171 msgid "ID" msgstr "ID" @@ -232,7 +258,15 @@ msgstr "Im nächsten Schritt richten wir einen Trust zwischen FIX und Ihrem AWS msgid "Inventory" msgstr "Inventar" -#: src/pages/panel/inventory/InventoryForm.tsx:205 +#: src/pages/panel/inventory/ResourceDetail.tsx:239 +msgid "Issues" +msgstr "Probleme" + +#: src/pages/panel/inventory/ResourceDetail.tsx:170 +msgid "Kind" +msgstr "Art" + +#: src/pages/panel/inventory/InventoryForm.tsx:211 msgid "Kinds" msgstr "Arten" @@ -273,8 +307,9 @@ msgstr "Am meisten verbesserte Konten" msgid "Most Improved Resources" msgstr "Am meisten verbesserte Ressourcen" -#: src/pages/panel/accounts/AccountRow.tsx:283 +#: src/pages/panel/accounts/AccountRow.tsx:284 #: src/pages/panel/accounts/AccountsTable.tsx:41 +#: src/pages/panel/inventory/ResourceDetail.tsx:172 msgid "Name" msgstr "Name" @@ -286,7 +321,7 @@ msgstr "Nächster Scan" msgid "No changes" msgstr "Keine Änderungen" -#: src/pages/panel/accounts/AccountRow.tsx:247 +#: src/pages/panel/accounts/AccountRow.tsx:248 msgid "Note: You are about to delete a management or delegated admin account. Please be aware that once deleted, we will no longer have the capability to retrieve any account names, requiring you to edit them manually." msgstr "Sie löschen gerade ein Management- oder delegiertes Admin-Konto. Beachten Sie bitte, dass wir nach der Löschung nicht mehr in der Lage sein werden, Kontonamen abzurufen, wodurch Sie diese manuell bearbeiten müssen." @@ -341,7 +376,7 @@ msgstr "Privilegiert" msgid "Profile" msgstr "Profil" -#: src/pages/panel/inventory/InventoryFormFilterRowProperty.tsx:195 +#: src/pages/panel/inventory/InventoryFormFilterRowProperty.tsx:194 msgid "Property" msgstr "Eigentum" @@ -361,6 +396,10 @@ msgstr "Ressourcen" msgid "Risk" msgstr "Risiko" +#: src/pages/panel/inventory/ResourceDetail.tsx:224 +msgid "Security" +msgstr "Sicherheit" + #: src/pages/panel/home/Overview.tsx:37 msgid "Security Scan in Progress" msgstr "Sicherheitsscan läuft" @@ -373,6 +412,8 @@ msgstr "Sicherheitsbewertung" msgid "Select Language" msgstr "Sprache auswählen" +#: src/pages/panel/inventory/ResourceDetail.tsx:235 +#: src/pages/panel/inventory/ResourceDetail.tsx:251 #: src/pages/panel/inventory/utils/getAutoCompleteFromKey.tsx:87 msgid "Severity" msgstr "Schwere" @@ -396,13 +437,14 @@ msgstr "Einfacher und erschwinglicher Einblick in die Sicherheitslage Ihrer Clou msgid "Since {since}" msgstr "Seit {since}" -#: src/pages/panel/accounts/AccountRow.tsx:174 #: src/pages/panel/accounts/AccountRow.tsx:175 -#: src/pages/panel/accounts/AccountRow.tsx:180 +#: src/pages/panel/accounts/AccountRow.tsx:176 +#: src/pages/panel/accounts/AccountRow.tsx:181 msgid "Submit" msgstr "Einreichen" #: src/pages/panel/inventory/InventoryTagAutoComplete.tsx:69 +#: src/pages/panel/inventory/ResourceDetail.tsx:192 msgid "Tags" msgstr "Stichworte" diff --git a/src/locales/en-US/messages.po b/src/locales/en-US/messages.po index d920629e..05a4da31 100644 --- a/src/locales/en-US/messages.po +++ b/src/locales/en-US/messages.po @@ -13,35 +13,35 @@ msgstr "" "Language-Team: \n" "Plural-Forms: \n" -#: src/shared/utils/parseISO8601Duration.ts:110 +#: src/shared/utils/parseDuration.ts:139 msgid "{0, plural, one {# Day} other {# Days}}" msgstr "{0, plural, one {# Day} other {# Days}}" -#: src/shared/utils/parseISO8601Duration.ts:118 +#: src/shared/utils/parseDuration.ts:147 msgid "{0, plural, one {# Hour} other {# Hours}}" msgstr "{0, plural, one {# Hour} other {# Hours}}" -#: src/shared/utils/parseISO8601Duration.ts:126 +#: src/shared/utils/parseDuration.ts:155 msgid "{0, plural, one {# Minute} other {# Minutes}}" msgstr "{0, plural, one {# Minute} other {# Minutes}}" -#: src/shared/utils/parseISO8601Duration.ts:94 +#: src/shared/utils/parseDuration.ts:123 msgid "{0, plural, one {# Month} other {# Months}}" msgstr "{0, plural, one {# Month} other {# Months}}" -#: src/shared/utils/parseISO8601Duration.ts:134 +#: src/shared/utils/parseDuration.ts:163 msgid "{0, plural, one {# Second} other {# Seconds}}" msgstr "{0, plural, one {# Second} other {# Seconds}}" -#: src/shared/utils/parseISO8601Duration.ts:102 +#: src/shared/utils/parseDuration.ts:131 msgid "{0, plural, one {# Week} other {# Weeks}}" msgstr "{0, plural, one {# Week} other {# Weeks}}" -#: src/shared/utils/parseISO8601Duration.ts:86 +#: src/shared/utils/parseDuration.ts:115 msgid "{0, plural, one {# Year} other {# Years}}" msgstr "{0, plural, one {# Year} other {# Years}}" -#: src/shared/utils/parseISO8601Duration.ts:141 +#: src/shared/utils/parseDuration.ts:170 msgid "{0} Milliseconds" msgstr "{0} Milliseconds" @@ -54,7 +54,7 @@ msgid "© 2023 Some Engineering Inc. All rights reserved." msgstr "© 2023 Some Engineering Inc. All rights reserved." #: src/pages/panel/accounts/AccountsTable.tsx:31 -#: src/pages/panel/inventory/InventoryTable.tsx:55 +#: src/pages/panel/inventory/InventoryTable.tsx:60 #: src/pages/panel/inventory/utils/getAutoCompleteFromKey.tsx:66 #: src/shared/layouts/panel-layout/UserProfileButton.tsx:103 msgid "Accounts" @@ -72,6 +72,10 @@ msgstr "Actions" msgid "Add an account" msgstr "Add an account" +#: src/pages/panel/inventory/ResourceDetail.tsx:178 +msgid "Age" +msgstr "Age" + #: src/pages/auth/register/RegisterPage.tsx:132 msgid "Already have an account? Click here to Log in." msgstr "Already have an account? Click here to Log in." @@ -84,11 +88,11 @@ msgstr "Alternatively if you would like to deploy the stack manually, you can us msgid "Alternatively: Manual Cloud Setup" msgstr "Alternatively: Manual Cloud Setup" -#: src/shared/utils/parseISO8601Duration.ts:143 +#: src/shared/utils/parseDuration.ts:172 msgid "and" msgstr "and" -#: src/pages/panel/accounts/AccountRow.tsx:241 +#: src/pages/panel/accounts/AccountRow.tsx:242 msgid "Are you sure?" msgstr "Are you sure?" @@ -100,24 +104,32 @@ msgstr "Automatic Cloud Setup" msgid "AWS Marketplace has been successfully subscribed" msgstr "AWS Marketplace has been successfully subscribed" +#: src/pages/panel/inventory/ResourceDetail.tsx:165 +msgid "Basic Information" +msgstr "Basic Information" + #: src/pages/panel/shared/utils/chartToShow.tsx:39 msgid "Benchmarks" msgstr "Benchmarks" -#: src/pages/panel/accounts/AccountRow.tsx:185 -#: src/pages/panel/accounts/AccountRow.tsx:189 +#: src/pages/panel/accounts/AccountRow.tsx:186 #: src/pages/panel/accounts/AccountRow.tsx:190 -#: src/pages/panel/accounts/AccountRow.tsx:260 +#: src/pages/panel/accounts/AccountRow.tsx:191 +#: src/pages/panel/accounts/AccountRow.tsx:261 msgid "Cancel" msgstr "Cancel" -#: src/pages/panel/accounts/AccountRow.tsx:280 +#: src/pages/panel/inventory/ResourceDetail.tsx:245 +msgid "Check" +msgstr "Check" + +#: src/pages/panel/accounts/AccountRow.tsx:281 #: src/pages/panel/accounts/AccountsTable.tsx:35 #: src/pages/panel/home/AccountCard.tsx:33 msgid "Cloud" msgstr "Cloud" -#: src/shared/event-button/EventButton.tsx:47 +#: src/shared/event-button/EventButton.tsx:56 msgid "Cloud account added, id: {0}" msgstr "Cloud account added, id: {0}" @@ -143,6 +155,10 @@ msgstr "Copied to Clipboard!" msgid "Copy" msgstr "Copy" +#: src/pages/panel/inventory/ResourceDetail.tsx:174 +msgid "Created Time" +msgstr "Created Time" + #: src/shared/defined-messages/getMessage.ts:5 msgid "Critical" msgstr "Critical" @@ -151,10 +167,10 @@ msgstr "Critical" msgid "Dashboard" msgstr "Dashboard" -#: src/pages/panel/accounts/AccountRow.tsx:229 -#: src/pages/panel/accounts/AccountRow.tsx:233 +#: src/pages/panel/accounts/AccountRow.tsx:230 #: src/pages/panel/accounts/AccountRow.tsx:234 -#: src/pages/panel/accounts/AccountRow.tsx:271 +#: src/pages/panel/accounts/AccountRow.tsx:235 +#: src/pages/panel/accounts/AccountRow.tsx:272 msgid "Delete" msgstr "Delete" @@ -163,7 +179,11 @@ msgstr "Delete" msgid "Deploy Stack" msgstr "Deploy Stack" -#: src/pages/panel/accounts/AccountRow.tsx:244 +#: src/pages/panel/inventory/ResourceDetail.tsx:205 +msgid "Details" +msgstr "Details" + +#: src/pages/panel/accounts/AccountRow.tsx:245 msgid "Do you want to delete this account?" msgstr "Do you want to delete this account?" @@ -171,8 +191,8 @@ msgstr "Do you want to delete this account?" msgid "Don't have an account? Click here to Sign up." msgstr "Don't have an account? Click here to Sign up." -#: src/pages/panel/accounts/AccountRow.tsx:201 #: src/pages/panel/accounts/AccountRow.tsx:202 +#: src/pages/panel/accounts/AccountRow.tsx:203 msgid "Edit" msgstr "Edit" @@ -185,7 +205,7 @@ msgstr "Email" msgid "Enabled" msgstr "Enabled" -#: src/shared/event-button/EventButton.tsx:78 +#: src/shared/event-button/EventButton.tsx:90 msgid "Events" msgstr "Events" @@ -206,6 +226,11 @@ msgstr "False" msgid "Fix is an open-source Wiz alternative for cloud infrastructure security. Take control of cloud risks with an asset inventory, compliance scans, and remediation workflows." msgstr "Fix is an open-source Wiz alternative for cloud infrastructure security. Take control of cloud risks with an asset inventory, compliance scans, and remediation workflows." +#: src/pages/panel/inventory/ResourceDetail.tsx:229 +#: src/pages/panel/inventory/ResourceDetail.tsx:247 +msgid "Found at" +msgstr "Found at" + #: src/shared/defined-messages/getMessage.ts:8 msgid "High" msgstr "High" @@ -214,9 +239,10 @@ msgstr "High" msgid "How to fix" msgstr "How to fix" -#: src/pages/panel/accounts/AccountRow.tsx:277 +#: src/pages/panel/accounts/AccountRow.tsx:278 #: src/pages/panel/accounts/AccountsTable.tsx:38 #: src/pages/panel/home/AccountCard.tsx:30 +#: src/pages/panel/inventory/ResourceDetail.tsx:171 msgid "ID" msgstr "ID" @@ -232,7 +258,15 @@ msgstr "In the next step we are going to set up the trust between FIX and your A msgid "Inventory" msgstr "Inventory" -#: src/pages/panel/inventory/InventoryForm.tsx:205 +#: src/pages/panel/inventory/ResourceDetail.tsx:239 +msgid "Issues" +msgstr "Issues" + +#: src/pages/panel/inventory/ResourceDetail.tsx:170 +msgid "Kind" +msgstr "Kind" + +#: src/pages/panel/inventory/InventoryForm.tsx:211 msgid "Kinds" msgstr "Kinds" @@ -273,8 +307,9 @@ msgstr "Most Improved Accounts" msgid "Most Improved Resources" msgstr "Most Improved Resources" -#: src/pages/panel/accounts/AccountRow.tsx:283 +#: src/pages/panel/accounts/AccountRow.tsx:284 #: src/pages/panel/accounts/AccountsTable.tsx:41 +#: src/pages/panel/inventory/ResourceDetail.tsx:172 msgid "Name" msgstr "Name" @@ -286,7 +321,7 @@ msgstr "Next scan" msgid "No changes" msgstr "No changes" -#: src/pages/panel/accounts/AccountRow.tsx:247 +#: src/pages/panel/accounts/AccountRow.tsx:248 msgid "Note: You are about to delete a management or delegated admin account. Please be aware that once deleted, we will no longer have the capability to retrieve any account names, requiring you to edit them manually." msgstr "Note: You are about to delete a management or delegated admin account. Please be aware that once deleted, we will no longer have the capability to retrieve any account names, requiring you to edit them manually." @@ -341,7 +376,7 @@ msgstr "Privileged" msgid "Profile" msgstr "Profile" -#: src/pages/panel/inventory/InventoryFormFilterRowProperty.tsx:195 +#: src/pages/panel/inventory/InventoryFormFilterRowProperty.tsx:194 msgid "Property" msgstr "Property" @@ -361,6 +396,10 @@ msgstr "Resources" msgid "Risk" msgstr "Risk" +#: src/pages/panel/inventory/ResourceDetail.tsx:224 +msgid "Security" +msgstr "Security" + #: src/pages/panel/home/Overview.tsx:37 msgid "Security Scan in Progress" msgstr "Security Scan in Progress" @@ -373,6 +412,8 @@ msgstr "Security Score" msgid "Select Language" msgstr "Select Language" +#: src/pages/panel/inventory/ResourceDetail.tsx:235 +#: src/pages/panel/inventory/ResourceDetail.tsx:251 #: src/pages/panel/inventory/utils/getAutoCompleteFromKey.tsx:87 msgid "Severity" msgstr "Severity" @@ -396,13 +437,14 @@ msgstr "Simple and affordable visibility into your cloud security posture." msgid "Since {since}" msgstr "Since {since}" -#: src/pages/panel/accounts/AccountRow.tsx:174 #: src/pages/panel/accounts/AccountRow.tsx:175 -#: src/pages/panel/accounts/AccountRow.tsx:180 +#: src/pages/panel/accounts/AccountRow.tsx:176 +#: src/pages/panel/accounts/AccountRow.tsx:181 msgid "Submit" msgstr "Submit" #: src/pages/panel/inventory/InventoryTagAutoComplete.tsx:69 +#: src/pages/panel/inventory/ResourceDetail.tsx:192 msgid "Tags" msgstr "Tags" diff --git a/src/pages/panel/accounts/AccountRow.tsx b/src/pages/panel/accounts/AccountRow.tsx index f1bfa85c..c2892176 100644 --- a/src/pages/panel/accounts/AccountRow.tsx +++ b/src/pages/panel/accounts/AccountRow.tsx @@ -132,7 +132,8 @@ export const AccountRow = ({ account }: { account: Account }) => { }, ) void queryClient.invalidateQueries({ - queryKey: ['workspace-inventory-report-summary', selectedWorkspace?.id], + predicate: (query) => + (typeof query.queryKey[0] === 'string' && query.queryKey[0].startsWith('workspace-cloud-accounts')) ?? false, }) }, }, diff --git a/src/pages/panel/home/OverallCard.tsx b/src/pages/panel/home/OverallCard.tsx index a43b800d..0b3df0ef 100644 --- a/src/pages/panel/home/OverallCard.tsx +++ b/src/pages/panel/home/OverallCard.tsx @@ -10,7 +10,7 @@ import { CircularScore } from 'src/shared/circular-score' import { colorFromRedToGreen } from 'src/shared/constants' import { OverviewCard } from 'src/shared/overview-card' import { GetWorkspaceInventoryReportSummaryResponse } from 'src/shared/types/server' -import { iso8601DurationToString, parseISO8601Duration } from 'src/shared/utils/parseISO8601Duration' +import { iso8601DurationToString, parseISO8601Duration } from 'src/shared/utils/parseDuration' interface OverallCardProps { data?: GetWorkspaceInventoryReportSummaryResponse diff --git a/src/pages/panel/inventory/InventoryAdvanceSearch.tsx b/src/pages/panel/inventory/InventoryAdvanceSearch.tsx index 226165c3..581bddc7 100644 --- a/src/pages/panel/inventory/InventoryAdvanceSearch.tsx +++ b/src/pages/panel/inventory/InventoryAdvanceSearch.tsx @@ -75,7 +75,7 @@ export const InventoryAdvanceSearch = ({ value: searchCrit, onChange }: Inventor } if (item.property && item.op && item.value && item.fqn) { const value = - item.fqn === 'string' ? (item.op === 'in' ? JSON.stringify(getArrayFromInOP(item.value)) : `'${item.value}'`) : item.value + item.fqn === 'string' ? (item.op === 'in' ? JSON.stringify(getArrayFromInOP(item.value)) : `"${item.value}"`) : item.value return `${item.property} ${item.op} ${value}` } return null diff --git a/src/pages/panel/inventory/InventoryForm.tsx b/src/pages/panel/inventory/InventoryForm.tsx index 5bca69cb..c4f55335 100644 --- a/src/pages/panel/inventory/InventoryForm.tsx +++ b/src/pages/panel/inventory/InventoryForm.tsx @@ -104,8 +104,12 @@ export const InventoryForm = ({ searchCrit, kind, setKind, config, setConfig }: } } } + const selectedKindCloud = processedStartData.kinds.find((i) => i.id === kind)?.cloud + if (selectedKindCloud) { + result.push(selectedKindCloud) + } return [...new Set(result.filter((cloud) => cloud))] - }, [config, processedStartData]) + }, [config, processedStartData, kind]) const filteredStartData = useMemo( () => ({ accounts: (selectedClouds.length @@ -132,7 +136,9 @@ export const InventoryForm = ({ searchCrit, kind, setKind, config, setConfig }: label: regions.find(({ name, id }) => name === region.name && id !== region.id) ? `${region.name} (${region.id})` : region.name, })), severities: processedStartData.severity.map((severity) => ({ label: severity, value: severity })), - clouds: processedStartData.clouds.map((cloud) => ({ label: cloud.toUpperCase(), value: cloud })), + clouds: selectedClouds.length + ? selectedClouds.map((cloud) => ({ label: cloud.toUpperCase(), value: cloud })) + : processedStartData.clouds.map((cloud) => ({ label: cloud.toUpperCase(), value: cloud })), }), [processedStartData, selectedClouds], ) diff --git a/src/pages/panel/inventory/InventoryFormFilterRowProperty.tsx b/src/pages/panel/inventory/InventoryFormFilterRowProperty.tsx index f16295ea..2805c3ec 100644 --- a/src/pages/panel/inventory/InventoryFormFilterRowProperty.tsx +++ b/src/pages/panel/inventory/InventoryFormFilterRowProperty.tsx @@ -80,10 +80,9 @@ export const InventoryFormFilterRowProperty = ({ selectedKind, defaultValue, kin } } const handleInputKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Tab' || e.key === 'Enter' || e.key === 'ArrowRight' || e.key === '.') { + if (e.key === 'Tab' || e.key === 'Enter' || e.key === '.') { handleChange(undefined, highlightedOptionRef.current) - e.preventDefault() - e.stopPropagation() + e.currentTarget.selectionStart = e.currentTarget.selectionEnd = e.currentTarget.value.length } } const handleInputChange = (e: ChangeEvent) => { diff --git a/src/pages/panel/inventory/InventoryRow.tsx b/src/pages/panel/inventory/InventoryRow.tsx index 7979f485..d4d6dc51 100644 --- a/src/pages/panel/inventory/InventoryRow.tsx +++ b/src/pages/panel/inventory/InventoryRow.tsx @@ -1,4 +1,4 @@ -import { TableCell, TableRow } from '@mui/material' +import { ButtonBase, ButtonBaseProps, TableCell, TableRow } from '@mui/material' import { panelUI } from 'src/shared/constants' import { GetWorkspaceInventorySearchTableColumn, GetWorkspaceInventorySearchTableRow } from 'src/shared/types/server' import { rowStrFromColumnKind } from './utils' @@ -6,17 +6,27 @@ import { rowStrFromColumnKind } from './utils' interface InventoryRowProps { row: GetWorkspaceInventorySearchTableRow columns: GetWorkspaceInventorySearchTableColumn[] + onClick: (params: GetWorkspaceInventorySearchTableRow) => void } -export const InventoryRow = ({ row, columns }: InventoryRowProps) => { +export const InventoryRow = ({ row, columns, onClick }: InventoryRowProps) => { + const handleClick = () => { + onClick(row) + } + return ( - + {columns.map((column, i) => ( {rowStrFromColumnKind(row.row[column.name], column.kind) ?? '-'} ))} - + ) } diff --git a/src/pages/panel/inventory/InventoryTable.tsx b/src/pages/panel/inventory/InventoryTable.tsx index a9958afa..85517133 100644 --- a/src/pages/panel/inventory/InventoryTable.tsx +++ b/src/pages/panel/inventory/InventoryTable.tsx @@ -8,6 +8,7 @@ import { TablePagination, TableViewPage } from 'src/shared/layouts/panel-layout' import { LoadingSuspenseFallback } from 'src/shared/loading' import { GetWorkspaceInventorySearchTableColumn, GetWorkspaceInventorySearchTableRow } from 'src/shared/types/server' import { InventoryRow } from './InventoryRow' +import { ResourceDetail } from './ResourceDetail' interface InventoryTableProps { searchCrit: string @@ -25,6 +26,7 @@ export const InventoryTable = ({ searchCrit }: InventoryTableProps) => { queryFn: getWorkspaceInventorySearchTableQuery, enabled: !!selectedWorkspace?.id, }) + const [selectedRow, setSelectedRow] = useState() useEffect(() => { setDataCount(-1) @@ -49,36 +51,39 @@ export const InventoryTable = ({ searchCrit }: InventoryTableProps) => { }, [isLoading, rowsPerPage, page, data?.length]) return columns.length ? ( - - } - > - - - - {columns.map((column, i) => ( - {column.display} - ))} - - - - {isLoading - ? rows.length && columns.length - ? rows.map((row, i) => ( - - {columns.map((column, j) => ( - - - - ))} - - )) - : null - : rows?.map((row, i) => )} - -
- {isLoading && (!rows.length || !columns.length) ? : null} -
+ <> + + } + > + + + + {columns.map((column, i) => ( + {column.display} + ))} + + + + {isLoading + ? rows.length && columns.length + ? rows.map((row, i) => ( + + {columns.map((column, j) => ( + + + + ))} + + )) + : null + : rows?.map((row, i) => )} + +
+ {isLoading && (!rows.length || !columns.length) ? : null} +
+ setSelectedRow(undefined)} /> + ) : null } diff --git a/src/pages/panel/inventory/ResourceDetail.tsx b/src/pages/panel/inventory/ResourceDetail.tsx new file mode 100644 index 00000000..504c73fb --- /dev/null +++ b/src/pages/panel/inventory/ResourceDetail.tsx @@ -0,0 +1,265 @@ +import { Trans } from '@lingui/macro' +import CloseIcon from '@mui/icons-material/Close' +import { + Accordion, + AccordionDetails, + AccordionSummary, + Box, + Divider, + Grid, + IconButton, + Modal as MuiModal, + Skeleton, + Slide, + Stack, + Tooltip, + Typography, + styled, +} from '@mui/material' +import { useQuery } from '@tanstack/react-query' +import { Fragment, ReactNode, useEffect, useState } from 'react' +import { useUserProfile } from 'src/core/auth' +import { getWorkspaceInventoryNodeQuery } from 'src/pages/panel/shared/queries' +import { getColorBySeverity } from 'src/pages/panel/shared/utils' +import { panelUI } from 'src/shared/constants' +import { GetWorkspaceInventorySearchTableRow } from 'src/shared/types/server' +import { diffDateTimeToDuration, iso8601DurationToString } from 'src/shared/utils/parseDuration' +import { snakeCaseWordsToUFStr } from 'src/shared/utils/snakeCaseToUFStr' + +interface ResourceDetailProps { + detail: GetWorkspaceInventorySearchTableRow | undefined + onClose: () => void +} + +const Modal = styled(MuiModal)(({ theme }) => ({ + position: 'fixed', + top: 0, + right: 0, + left: 'auto', + height: '100%', + width: '100%', + + [theme.breakpoints.up('md')]: { + top: panelUI.headerHeight, + width: '50%', + height: `calc(100vh - ${panelUI.headerHeight}px)`, + }, + + [theme.breakpoints.up('xl')]: { + width: '33%', + maxWidth: 700, + }, +})) + +const GridItem = ({ + property, + value, + color, + isReactNode, +}: { + property: ReactNode + value: unknown + color?: string + isReactNode?: boolean +}) => { + const isSimpleValue = isReactNode ? true : ['string', 'boolean', 'number'].includes(typeof value) + const stringValue = isReactNode ? '' : isSimpleValue ? (value as string | boolean | number).toString() : JSON.stringify(value) + return ( + <> + + + + {property}: + + + + + {isReactNode ? ( + (value as ReactNode) + ) : ( + {stringValue} + ) : ( + {JSON.stringify(value, null, ' ')} + ) + } + placement="left" + > + + {stringValue} + + + )} + + + ) +} + +export const ResourceDetail = ({ detail, onClose }: ResourceDetailProps) => { + const { selectedWorkspace } = useUserProfile() + const { data, isLoading } = useQuery({ + queryKey: ['workspace-inventory-node', selectedWorkspace?.id, detail?.id], + queryFn: getWorkspaceInventoryNodeQuery, + }) + const [selectedRow, setSelectedRow] = useState(detail) + + useEffect(() => { + if (detail) { + setSelectedRow(detail) + } + }, [detail]) + + const { id, name, kind, ctime, age: _age, tags, ...reported } = data?.resource.reported ?? {} + + return selectedRow ? ( + + + + + {selectedRow.row['name']} + + + + + {/* + {data ? ( + + ) : isLoading ? ( + + ) : null} + */} + + + Basic Information + + + {data ? ( + + Kind} value={kind} /> + ID} value={id} /> + Name} value={name} /> + Created Time} + value={`${new Date(ctime as string).toLocaleDateString()} ${new Date(ctime as string).toLocaleTimeString()}`} + /> + Age} + value={iso8601DurationToString(diffDateTimeToDuration(new Date(ctime as string), new Date()), 2)} + /> + + ) : isLoading ? ( + <> + + + ) : null} + + + {data && Object.entries(tags ?? {}).length ? ( + + + Tags + + + + {Object.entries(tags as Record).map(([property, value], key) => ( + + ))} + + + + ) : null} + + + Details + + + {data ? ( + + {Object.entries(reported).map(([property, value], key) => ( + + ))} + + ) : isLoading ? ( + <> + + + ) : null} + + + {data?.resource.security?.has_issues ? ( + + + Security + + + + Found at} + value={`${new Date(data.resource.security.opened_at).toLocaleDateString()} ${new Date( + data.resource.security.opened_at, + ).toLocaleTimeString()}`} + /> + Severity} + value={snakeCaseWordsToUFStr(data.resource.security.severity)} + color={getColorBySeverity(data.resource.security.severity)} + /> + Issues} value={null} isReactNode /> + + {data.resource.security.issues.map((issue, i) => ( + + + + Check} value={issue.check} /> + Found at} + value={`${new Date(issue.opened_at).toLocaleDateString()} ${new Date(issue.opened_at).toLocaleTimeString()}`} + /> + Severity} + color={getColorBySeverity(issue.severity)} + value={snakeCaseWordsToUFStr(issue.severity)} + /> + + ))} + + + + ) : null} + + + + ) : undefined +} diff --git a/src/pages/panel/inventory/utils/rowStrFromColumnKind.ts b/src/pages/panel/inventory/utils/rowStrFromColumnKind.ts index ca9f7a8a..baea23f4 100644 --- a/src/pages/panel/inventory/utils/rowStrFromColumnKind.ts +++ b/src/pages/panel/inventory/utils/rowStrFromColumnKind.ts @@ -1,5 +1,5 @@ import { ResourceComplexKindSimpleTypeDefinitions } from 'src/shared/types/server' -import { iso8601DurationToString, parseCustomDuration } from 'src/shared/utils/parseISO8601Duration' +import { iso8601DurationToString, parseCustomDuration } from 'src/shared/utils/parseDuration' export const rowStrFromColumnKind = (value: string | number | null, kind: ResourceComplexKindSimpleTypeDefinitions) => { switch (kind) { diff --git a/src/pages/panel/setup-cloud/SetupCloudPage.tsx b/src/pages/panel/setup-cloud/SetupCloudPage.tsx index 0a1e7825..45fa2a54 100644 --- a/src/pages/panel/setup-cloud/SetupCloudPage.tsx +++ b/src/pages/panel/setup-cloud/SetupCloudPage.tsx @@ -1,9 +1,7 @@ import { Trans } from '@lingui/macro' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import { Accordion, AccordionDetails, AccordionSummary, Box, Button, Divider, Skeleton, Typography } from '@mui/material' -import { useQueryClient } from '@tanstack/react-query' import { Suspense, useEffect } from 'react' -import { useUserProfile } from 'src/core/auth' import { useEvents } from 'src/core/events' import { useAbsoluteNavigate } from 'src/shared/absolute-navigate' import { ErrorBoundaryFallback, NetworkErrorBoundary } from 'src/shared/error-boundary-fallback' @@ -16,9 +14,6 @@ import { SetupTemplateButtonSkeleton } from './SetupTemplateButton.skeleton' import { TenantId } from './TenantId' export default function SetupCloud() { - const queryClient = useQueryClient() - const { selectedWorkspace } = useUserProfile() - const { addListener } = useEvents() const navigate = useAbsoluteNavigate() @@ -29,13 +24,10 @@ export default function SetupCloud() { useEffect(() => { return addListener('event-button', (ev) => { if (ev.kind === 'cloud_account_created') { - void queryClient.invalidateQueries({ - queryKey: ['workspace-cloud-accounts', selectedWorkspace?.id], - }) navigate('/accounts') } }) - }, [addListener, navigate, queryClient, selectedWorkspace?.id]) + }, [addListener, navigate]) return ( <> diff --git a/src/pages/panel/shared/queries/getWorkspaceInventoryNode.query.ts b/src/pages/panel/shared/queries/getWorkspaceInventoryNode.query.ts new file mode 100644 index 00000000..62a66c67 --- /dev/null +++ b/src/pages/panel/shared/queries/getWorkspaceInventoryNode.query.ts @@ -0,0 +1,15 @@ +import { QueryFunctionContext } from '@tanstack/react-query' +import { endPoints } from 'src/shared/constants' +import { GetWorkspaceInventoryNodeResponse } from 'src/shared/types/server' +import { axiosWithAuth } from 'src/shared/utils/axios' + +export const getWorkspaceInventoryNodeQuery = ({ + signal, + queryKey: [_, workspaceId, nodeId], +}: QueryFunctionContext<['workspace-inventory-node', string | undefined, string | undefined]>) => { + return workspaceId && nodeId + ? axiosWithAuth + .post(endPoints.workspaces.workspace(workspaceId).inventory.node(nodeId), { signal }) + .then((res) => res.data) + : null +} diff --git a/src/pages/panel/shared/queries/index.ts b/src/pages/panel/shared/queries/index.ts index f7909aee..1389075a 100644 --- a/src/pages/panel/shared/queries/index.ts +++ b/src/pages/panel/shared/queries/index.ts @@ -3,6 +3,7 @@ export { getWorkspaceCfTemplateQuery } from './getWorkspaceCfTemplate.query' export { getWorkspaceCfUrlQuery } from './getWorkspaceCfUrl.query' export { getWorkspaceCloudAccountsQuery } from './getWorkspaceCloudAccounts.query' export { getWorkspaceInventoryModelQuery } from './getWorkspaceInventoryModel.query' +export { getWorkspaceInventoryNodeQuery } from './getWorkspaceInventoryNode.query' export { getWorkspaceInventoryPropertyAttributesQuery } from './getWorkspaceInventoryPropertyAttributes.query' export { getWorkspaceInventoryPropertyPathCompleteQuery } from './getWorkspaceInventoryPropertyPathComplete.query' export { getWorkspaceInventoryPropertyValuesQuery } from './getWorkspaceInventoryPropertyValues.query' diff --git a/src/shared/charts/NetworkDiagram.tsx b/src/shared/charts/NetworkDiagram.tsx new file mode 100644 index 00000000..8f32055b --- /dev/null +++ b/src/shared/charts/NetworkDiagram.tsx @@ -0,0 +1,185 @@ +import { Box, tooltipClasses, useTheme } from '@mui/material' +import { + BaseType, + Selection, + SimulationLinkDatum, + SimulationNodeDatum, + drag, + forceCenter, + forceLink, + forceManyBody, + forceSimulation, + select, + zoom, +} from 'd3' +import { useCallback, useEffect, useRef } from 'react' +import { WorkspaceInventoryNodeNeighborhood, WorkspaceInventoryNodeNeighborhoodNodeType } from 'src/shared/types/server' + +type NodesType = WorkspaceInventoryNodeNeighborhoodNodeType & SimulationNodeDatum +type LinksType = SimulationLinkDatum + +interface NetworkDiagramProps { + data: WorkspaceInventoryNodeNeighborhood[] +} + +export const NetworkDiagram = ({ data }: NetworkDiagramProps) => { + const { + palette: { + primary: { main: primaryColor, dark: primaryDarkColor, light: primaryLightColor }, + background: { paper: paperBgColor }, + }, + zIndex: { tooltip: tooltipZIndex }, + shadows: { '24': shadow24 }, + } = useTheme() + const containerRef = useRef() + + const getSimulation = useCallback(() => { + if (containerRef.current) { + const { offsetWidth: width, offsetHeight: height } = containerRef.current + const svg = select(containerRef.current) + .append('svg') + .attr('width', width) + .attr('height', height) + .attr('viewBox', [0, 0, width, height]) + .attr('style', 'max-width: 100%; height: auto;') + + const svgG = svg.append('g') + const nodes: NodesType[] = [] + const links: LinksType[] = [] + for (let i = 0; i < data.length; i++) { + const item = data[i] + if (item.type === 'node') { + nodes.push({ ...item, index: i }) + } else { + links.push({ source: item.from, target: item.to, index: i }) + } + } + + const tooltip = select(containerRef.current) + .append('div') + .style('opacity', 0) + .attr('class', tooltipClasses.tooltip) + .style('position', 'fixed') + .style('box-shadow', shadow24) + .style('background-color', paperBgColor) + .style('border-width', '2px') + .style('border-radius', '5px') + .style('padding', '5px') + .style('z-index', tooltipZIndex) + + const link = svgG + .append('g') + .attr('stroke', primaryDarkColor) + .attr('stroke-opacity', 0.6) + .selectAll() + .data(links) + .join('line') + .attr('stroke-width', 1.5) + + const node = svgG + .append('g') + .attr('stroke', primaryLightColor) + .attr('stroke-width', 1.5) + .selectAll() + .data(nodes) + .join('circle') + .attr('r', 8) + .attr('fill', primaryColor) as Selection + + node.append('title').text((d) => d.reported.name) + const simulation = forceSimulation(nodes) + .force( + 'link', + forceLink(links).id((d) => d.id), + ) + .force('charge', forceManyBody()) + .force('center', forceCenter(width / 2, height / 2)) + return { tooltip, svgG, svg, link, node, simulation } + } + }, [data, paperBgColor, primaryColor, primaryDarkColor, primaryLightColor, shadow24, tooltipZIndex]) + + useEffect(() => { + const result = getSimulation() + if (result) { + const { tooltip, svgG, svg, link, node, simulation } = result + const handleTick = () => { + link + .attr('x1', (d) => (d.source as SimulationNodeDatum).x ?? 0) + .attr('y1', (d) => (d.source as SimulationNodeDatum).y ?? 0) + .attr('x2', (d) => (d.target as SimulationNodeDatum).x ?? 0) + .attr('y2', (d) => (d.target as SimulationNodeDatum).y ?? 0) + + node.attr('cx', (d) => d.x ?? 0).attr('cy', (d) => d.y ?? 0) + } + + // Reheat the simulation when drag starts, and fix the subject position. + const handleDragStart = (event: DragEvent & { active?: boolean; subject: NodesType }) => { + if (!event.active) simulation.alphaTarget(0.3).restart() + event.subject.fx = event.subject.x + event.subject.fy = event.subject.y + } + + // Update the subject (dragged node) position during drag. + const handleDrag = (event: DragEvent & { subject: NodesType }) => { + event.subject.fx = event.x + event.subject.fy = event.y + } + + // Restore the target alpha so the simulation cools after dragging ends. + // Unfix the subject position now that it’s no longer being dragged. + const handleDragEnd = (event: DragEvent & { active?: boolean; subject: NodesType }) => { + if (!event.active) simulation.alphaTarget(0) + event.subject.fx = null + event.subject.fy = null + } + + const handleMouseOver = function (this: BaseType) { + tooltip.style('opacity', 1) + select(this).style('opacity', 1) + } + + const handleMouseMove = function (this: BaseType, event: MouseEvent, d: NodesType) { + tooltip + .html(`
${JSON.stringify(d, null, '  ')}
`) + .style('left', event.clientX + 20 + 'px') + .style('top', event.clientY + 20 + 'px') + } + + const handleMouseLeave = function (this: BaseType) { + tooltip.style('opacity', 0) + select(this).style('opacity', 0.8) + } + + const handleZoom = (event: { transform: string; sourceEvent: MouseEvent }) => { + event.sourceEvent.preventDefault() + event.sourceEvent.stopPropagation() + svgG.attr('transform', event.transform) + } + + simulation.on('tick', handleTick) + node + .call(drag().on('start', handleDragStart).on('drag', handleDrag).on('end', handleDragEnd)) + .on('mouseover', handleMouseOver) + .on('mousemove', handleMouseMove) + .on('mouseleave', handleMouseLeave) + + svg.call(zoom().on('zoom', handleZoom)) + + return () => { + simulation.on('tick', null) + simulation.stop() + node + .call(drag().on('start', null).on('drag', null).on('end', null)) + .on('mouseover', null) + .on('mousemove', null) + .on('mouseleave', null) + .remove() + link.remove() + svg.remove() + tooltip.remove() + } + } + }, [data, getSimulation]) + + return +} diff --git a/src/shared/charts/index.ts b/src/shared/charts/index.ts index 3cf360c1..a3fe58fb 100644 --- a/src/shared/charts/index.ts +++ b/src/shared/charts/index.ts @@ -1,3 +1,4 @@ export { Heatmap } from './Heatmap' +export { NetworkDiagram } from './NetworkDiagram' export { PieChart } from './PieChart' export { StackbarChart } from './StackbarChart' diff --git a/src/shared/constants/endPoints.ts b/src/shared/constants/endPoints.ts index 6f5d085f..c1f31b71 100644 --- a/src/shared/constants/endPoints.ts +++ b/src/shared/constants/endPoints.ts @@ -41,6 +41,7 @@ export const endPoints = { complete: `/api/workspaces/${workspaceId}/inventory/property/path/complete`, }, }, + node: (nodeId: string) => `api/workspaces/${workspaceId}/inventory/node/${nodeId}`, }, events: `api/workspaces/${workspaceId}/events`, }), diff --git a/src/shared/event-button/EventButton.tsx b/src/shared/event-button/EventButton.tsx index a7eb666d..c7377085 100644 --- a/src/shared/event-button/EventButton.tsx +++ b/src/shared/event-button/EventButton.tsx @@ -1,6 +1,7 @@ import { t } from '@lingui/macro' import EventIcon from '@mui/icons-material/Event' import { Badge, Box, Fade, IconButton, Paper, Popper, Tooltip, styled } from '@mui/material' +import { useQueryClient } from '@tanstack/react-query' import { useEffect, useRef, useState } from 'react' import { useEvents } from 'src/core/events' import { useSnackbar } from 'src/core/snackbar' @@ -14,6 +15,7 @@ const PopperContainer = styled(Popper)(({ theme }) => ({ export const EventButton = () => { const [openEvents, setOpenEvents] = useState(false) + const queryClient = useQueryClient() const anchorEl = useRef(null) const { showSnackbar } = useSnackbar() const { addListener, send } = useEvents() @@ -34,6 +36,9 @@ export const EventButton = () => { if (hasProgress) { newEvents[foundIndex] = ev } else { + void queryClient.invalidateQueries({ + predicate: (query) => (typeof query.queryKey[0] === 'string' && query.queryKey[0].startsWith('workspace')) ?? false, + }) newEvents.splice(foundIndex, 1) } } else if (hasProgress) { @@ -44,18 +49,25 @@ export const EventButton = () => { break } case 'cloud_account_created': + void queryClient.invalidateQueries({ + predicate: (query) => + (typeof query.queryKey[0] === 'string' && query.queryKey[0].startsWith('workspace-cloud-accounts')) ?? false, + }) void showSnackbar(t`Cloud account added, id: ${ev.data.aws_account_id}`, { severity: 'success', autoHideDuration: null }) break - // case 'collect-error': - // showSnackbar(t`Task "${ev.data.task}" with workflow "${ev.data.workflow}" failed: ${ev.data.message}`, { - // severity: 'error', - // autoHideDuration: null, - // }) - // break + case 'collect-error': + void queryClient.invalidateQueries({ + predicate: (query) => (typeof query.queryKey[0] === 'string' && query.queryKey[0].startsWith('workspace')) ?? false, + }) + // showSnackbar(t`Task "${ev.data.task}" with workflow "${ev.data.workflow}" failed: ${ev.data.message}`, { + // severity: 'error', + // autoHideDuration: null, + // }) + break } } return addListener('event-button', eventListener) - }, [addListener, send, showSnackbar]) + }, [addListener, queryClient, send, showSnackbar]) const hasNoEvents = !events.length diff --git a/src/shared/layouts/panel-layout/PanelContent.tsx b/src/shared/layouts/panel-layout/PanelContent.tsx index e0df2eca..a28d4652 100644 --- a/src/shared/layouts/panel-layout/PanelContent.tsx +++ b/src/shared/layouts/panel-layout/PanelContent.tsx @@ -10,7 +10,16 @@ interface PanelContent extends PropsWithChildren { export const PanelContent = ({ children, bottom }: PanelContent) => { return ( - + {children} {bottom} diff --git a/src/shared/layouts/panel-layout/table-view-page/TableViewPage.tsx b/src/shared/layouts/panel-layout/table-view-page/TableViewPage.tsx index 88d65d22..cadc9497 100644 --- a/src/shared/layouts/panel-layout/table-view-page/TableViewPage.tsx +++ b/src/shared/layouts/panel-layout/table-view-page/TableViewPage.tsx @@ -22,7 +22,7 @@ export const TableViewPage = ({ children, pagination }: TableViewPageProps) => { mb={-4} minHeight={panelUI.tableViewMinHeight} > - + {children} diff --git a/src/shared/types/server/responses/GetWorkspaceInventoryNode.ts b/src/shared/types/server/responses/GetWorkspaceInventoryNode.ts new file mode 100644 index 00000000..efebbbf4 --- /dev/null +++ b/src/shared/types/server/responses/GetWorkspaceInventoryNode.ts @@ -0,0 +1,81 @@ +import { SeverityType } from './shared' + +export type WorkspaceInventoryNodeType = 'node' | 'edge' + +export interface WorkspaceInventoryNodeNeighborhoodNodeType { + id: string + type: 'node' + metadata: { + icon?: string + group?: string + name?: string + } + age?: string + tags: Record + reported: { + id: string + kind: string + name: string + } +} + +export interface WorkspaceInventoryNodeNeighborhoodEdgeType { + type: 'edge' + from: string + to: string +} + +export type WorkspaceInventoryNodeNeighborhood = WorkspaceInventoryNodeNeighborhoodNodeType | WorkspaceInventoryNodeNeighborhoodEdgeType + +export interface GetWorkspaceInventoryNode { + resource: { + id: string + type: WorkspaceInventoryNodeType + revision: string + reported: Record | Record> & { + ctime: string + name: string + age: string + id: string + kind: string + tags: Record + } + security?: { + issues: { + benchmark: string + check: string + severity: SeverityType + opened_at: string + run_id: string + }[] + opened_at: string + reopen_counter: number + run_id: string + has_issues: boolean + severity: SeverityType + } + metadata: { + python_type: string + cleaned?: boolean + phantom?: boolean + protected?: boolean + replace?: boolean + exported_at?: string + descendant_summary?: Record + descendant_count?: number + exported_age?: string + } + ancestors: Record< + string, + { + reported: { + name: string + id: string + } + } + > + } + neighborhood: WorkspaceInventoryNodeNeighborhood[] +} + +export type GetWorkspaceInventoryNodeResponse = GetWorkspaceInventoryNode diff --git a/src/shared/types/server/responses/GetWorkspaceInventoryReportSummary.ts b/src/shared/types/server/responses/GetWorkspaceInventoryReportSummary.ts index a382ba55..4a11900b 100644 --- a/src/shared/types/server/responses/GetWorkspaceInventoryReportSummary.ts +++ b/src/shared/types/server/responses/GetWorkspaceInventoryReportSummary.ts @@ -1,6 +1,6 @@ -export type FailedChecksTypeKeys = 'critical' | 'high' | 'medium' | 'low' +import { SeverityType } from './shared' -export type FailedChecksType = Record +export type FailedChecksType = Record export interface Benchmark { id: string @@ -47,7 +47,7 @@ export interface TopChecks { result_kind: string risk: string service: string - severity: FailedChecksTypeKeys + severity: SeverityType title: string url: null } diff --git a/src/shared/types/server/responses/index.ts b/src/shared/types/server/responses/index.ts index 15ab7964..483458b8 100644 --- a/src/shared/types/server/responses/index.ts +++ b/src/shared/types/server/responses/index.ts @@ -5,6 +5,14 @@ export type { GetWorkspaceCfUrlResponse } from './GetWorkspaceCfUrl' export type { GetWorkspaceCloudAccountsResponse } from './GetWorkspaceCloudAccounts' export type { GetWorkspaceCloudAccountsLastScanResponse, WorkspaceCloudAccountsLastScanAccount } from './GetWorkspaceCloudAccountsLastScan' export type { GetWorkspaceInventoryModelResponse } from './GetWorkspaceInventoryModel' +export type { + GetWorkspaceInventoryNode, + GetWorkspaceInventoryNodeResponse, + WorkspaceInventoryNodeNeighborhood, + WorkspaceInventoryNodeNeighborhoodEdgeType, + WorkspaceInventoryNodeNeighborhoodNodeType, + WorkspaceInventoryNodeType, +} from './GetWorkspaceInventoryNode' export type { GetWorkspaceInventoryPropertyPathComplete } from './GetWorkspaceInventoryPropertyPathComplete' export type { FailedChecksType, @@ -12,7 +20,6 @@ export type { WorkspaceAccountReportSummary, Benchmark as WorkspaceBenchmark, ChangedSitatuation as WorkspaceChangedSitatuation, - FailedChecksTypeKeys as WorkspaceFailedChecksTypeKeys, } from './GetWorkspaceInventoryReportSummary' export type { GetWorkspaceInventorySearchStart, diff --git a/src/shared/types/server/responses/shared/SeverityType.ts b/src/shared/types/server/responses/shared/SeverityType.ts new file mode 100644 index 00000000..ac2196f8 --- /dev/null +++ b/src/shared/types/server/responses/shared/SeverityType.ts @@ -0,0 +1 @@ +export type SeverityType = 'critical' | 'high' | 'medium' | 'low' diff --git a/src/shared/types/server/responses/shared/index.ts b/src/shared/types/server/responses/shared/index.ts index 2d4156f3..1275e9c6 100644 --- a/src/shared/types/server/responses/shared/index.ts +++ b/src/shared/types/server/responses/shared/index.ts @@ -11,3 +11,4 @@ export type { ResourceComplexKindSimpleTypeDefinitions, } from './ResourceComplexKindProperty' export type { ResourceComplexKind, ResourceKind, ResourceKindGeneric, ResourceSimpleKind } from './ResourceKind' +export type { SeverityType } from './SeverityType' diff --git a/src/shared/utils/parseISO8601Duration.ts b/src/shared/utils/parseDuration.ts similarity index 67% rename from src/shared/utils/parseISO8601Duration.ts rename to src/shared/utils/parseDuration.ts index 782f6688..514164b0 100644 --- a/src/shared/utils/parseISO8601Duration.ts +++ b/src/shared/utils/parseDuration.ts @@ -17,6 +17,36 @@ interface ISO8601DurationType { duration: number } +const SECONDS = 1000 +const MINUTES = SECONDS * 60 +const HOURS = MINUTES * 60 +const DAYS = HOURS * 24 +const WEEKS = DAYS * 7 +const MONTHS = DAYS * 30 +const YEARS = DAYS * 365 + +export const diffDateTimeToDuration = (dateTimeFrom: Date, dateTimeTo: Date) => { + const negative = dateTimeFrom > dateTimeTo + const start = negative ? dateTimeTo : dateTimeFrom + const end = negative ? dateTimeFrom : dateTimeTo + const duration = Math.abs(start.valueOf() - end.valueOf()) + const years = Math.floor(duration / YEARS) + let soFar = years * YEARS + const months = Math.floor((duration - soFar) / MONTHS) + soFar += months * MONTHS + const weeks = Math.floor((duration - soFar) / WEEKS) + soFar += weeks * WEEKS + const days = Math.floor((duration - soFar) / DAYS) + soFar += days * DAYS + const hours = Math.floor((duration - soFar) / HOURS) + soFar += hours * HOURS + const minutes = Math.floor((duration - soFar) / MINUTES) + soFar += minutes * MINUTES + const seconds = Math.floor((duration - soFar) / SECONDS) + soFar += seconds * SECONDS + return { negative, years, months, weeks, days, hours, minutes, seconds, duration } as ISO8601DurationType +} + export const parseCustomDuration = (customDuration: string) => { const matches = customDuration.match(customDurationRegex) ?? [] @@ -36,14 +66,13 @@ export const parseCustomDuration = (customDuration: string) => { parsedDuration.duration = (parsedDuration.negative ? -1 : 1) * - (parsedDuration.seconds + - parsedDuration.minutes * 60 + - parsedDuration.hours * 60 * 60 + - parsedDuration.days * 60 * 60 * 24 + - parsedDuration.weeks * 60 * 60 * 24 * 7 + - parsedDuration.months * 60 * 60 * 24 * 30 + - parsedDuration.years * 60 * 60 * 24 * 30 * 365) * - 1000 + (parsedDuration.seconds * SECONDS + + parsedDuration.minutes * MINUTES + + parsedDuration.hours * HOURS + + parsedDuration.days * DAYS + + parsedDuration.weeks * WEEKS + + parsedDuration.months * MONTHS + + parsedDuration.years * YEARS) return parsedDuration } @@ -79,7 +108,7 @@ export const parseISO8601Duration = (iso8601Duration: string) => { return parsedDuration } -export const iso8601DurationToString = (duration: ISO8601DurationType) => { +export const iso8601DurationToString = (duration: ISO8601DurationType, maxStr: 1 | 2 | 3 | 4 | 5 | 6 | 7 = 7, showTime: boolean = true) => { const str: string[] = [] if (duration.years) { str.push( @@ -113,7 +142,7 @@ export const iso8601DurationToString = (duration: ISO8601DurationType) => { }), ) } - if (duration.hours) { + if (duration.hours && showTime) { str.push( plural(duration.hours, { one: '# Hour', @@ -121,7 +150,7 @@ export const iso8601DurationToString = (duration: ISO8601DurationType) => { }), ) } - if (duration.minutes) { + if (duration.minutes && showTime) { str.push( plural(duration.minutes, { one: '# Minute', @@ -129,7 +158,7 @@ export const iso8601DurationToString = (duration: ISO8601DurationType) => { }), ) } - if (duration.seconds) { + if (duration.seconds && showTime) { str.push( plural(duration.seconds, { one: '# Second', @@ -137,8 +166,8 @@ export const iso8601DurationToString = (duration: ISO8601DurationType) => { }), ) } - if (!str.length && duration.duration) { + if (!str.length && duration.duration && showTime) { str.push(t`${duration.duration.toString()} Milliseconds`) } - return str.join(` ${t`and`} `) + return str.slice(0, maxStr).join(` ${t`and`} `) }