diff --git a/CHANGELOG.md b/CHANGELOG.md index e73b0ee328..5b90d4f241 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [20.04] - unreleased ### Added +- Added new BPM feature [#1931](https://github.com/greenbone/gsa/pull/1931) - Added clean-up-translations script [#1948](https://github.com/greenbone/gsa/pull/1948) - Added handling possible undefined trash in case of an error on the trashcanpage [#1908](https://github.com/greenbone/gsa/pull/1908) - Added translation using babel-plugin-i18next-extract [#1808](https://github.com/greenbone/gsa/pull/1808) diff --git a/gsa/CMakeLists.txt b/gsa/CMakeLists.txt index d21d17857e..d9b34e32e0 100644 --- a/gsa/CMakeLists.txt +++ b/gsa/CMakeLists.txt @@ -299,9 +299,11 @@ set (GSA_JS_SRC_FILES ${GSA_SRC_DIR}/src/web/components/icon/alterableicon.js ${GSA_SRC_DIR}/src/web/components/icon/arrowicon.js ${GSA_SRC_DIR}/src/web/components/icon/auditicon.js + ${GSA_SRC_DIR}/src/web/components/icon/bpmicon.js ${GSA_SRC_DIR}/src/web/components/icon/calendaricon.js ${GSA_SRC_DIR}/src/web/components/icon/certbundadvicon.js ${GSA_SRC_DIR}/src/web/components/icon/cloneicon.js + ${GSA_SRC_DIR}/src/web/components/icon/condcoloricon.js ${GSA_SRC_DIR}/src/web/components/icon/cpeicon.js ${GSA_SRC_DIR}/src/web/components/icon/cpelogoicon.js ${GSA_SRC_DIR}/src/web/components/icon/credentialicon.js @@ -321,6 +323,7 @@ set (GSA_JS_SRC_FILES ${GSA_SRC_DIR}/src/web/components/icon/downloadkeyicon.js ${GSA_SRC_DIR}/src/web/components/icon/downloadrpmicon.js ${GSA_SRC_DIR}/src/web/components/icon/downloadsvgicon.js + ${GSA_SRC_DIR}/src/web/components/icon/edgeicon.js ${GSA_SRC_DIR}/src/web/components/icon/editicon.js ${GSA_SRC_DIR}/src/web/components/icon/enableicon.js ${GSA_SRC_DIR}/src/web/components/icon/exporticon.js @@ -342,10 +345,13 @@ set (GSA_JS_SRC_FILES ${GSA_SRC_DIR}/src/web/components/icon/listsvgicon.js ${GSA_SRC_DIR}/src/web/components/icon/logouticon.js ${GSA_SRC_DIR}/src/web/components/icon/manualicon.js + ${GSA_SRC_DIR}/src/web/components/icon/magnifiericon.js + ${GSA_SRC_DIR}/src/web/components/icon/minusicon.js ${GSA_SRC_DIR}/src/web/components/icon/mysettingsicon.js ${GSA_SRC_DIR}/src/web/components/icon/newicon.js ${GSA_SRC_DIR}/src/web/components/icon/newnoteicon.js ${GSA_SRC_DIR}/src/web/components/icon/newoverrideicon.js + ${GSA_SRC_DIR}/src/web/components/icon/newprocessicon.js ${GSA_SRC_DIR}/src/web/components/icon/newticketicon.js ${GSA_SRC_DIR}/src/web/components/icon/nexticon.js ${GSA_SRC_DIR}/src/web/components/icon/noteicon.js @@ -356,6 +362,7 @@ set (GSA_JS_SRC_FILES ${GSA_SRC_DIR}/src/web/components/icon/overrideicon.js ${GSA_SRC_DIR}/src/web/components/icon/performanceicon.js ${GSA_SRC_DIR}/src/web/components/icon/permissionicon.js + ${GSA_SRC_DIR}/src/web/components/icon/plusicon.js ${GSA_SRC_DIR}/src/web/components/icon/policyicon.js ${GSA_SRC_DIR}/src/web/components/icon/portlisticon.js ${GSA_SRC_DIR}/src/web/components/icon/previousicon.js @@ -416,8 +423,10 @@ set (GSA_JS_SRC_FILES ${GSA_SRC_DIR}/src/web/components/icon/svg/alert.svg ${GSA_SRC_DIR}/src/web/components/icon/svg/alterable.svg ${GSA_SRC_DIR}/src/web/components/icon/svg/audit.svg + ${GSA_SRC_DIR}/src/web/components/icon/svg/bpm.svg ${GSA_SRC_DIR}/src/web/components/icon/svg/calendar.svg ${GSA_SRC_DIR}/src/web/components/icon/svg/cert_bund_adv.svg + ${GSA_SRC_DIR}/src/web/components/icon/svg/cond_color.svg ${GSA_SRC_DIR}/src/web/components/icon/svg/clone.svg ${GSA_SRC_DIR}/src/web/components/icon/svg/config.svg ${GSA_SRC_DIR}/src/web/components/icon/svg/cpe.svg @@ -438,6 +447,7 @@ set (GSA_JS_SRC_FILES ${GSA_SRC_DIR}/src/web/components/icon/svg/dl_rpm.svg ${GSA_SRC_DIR}/src/web/components/icon/svg/dl_svg.svg ${GSA_SRC_DIR}/src/web/components/icon/svg/download.svg + ${GSA_SRC_DIR}/src/web/components/icon/svg/edge.svg ${GSA_SRC_DIR}/src/web/components/icon/svg/edit.svg ${GSA_SRC_DIR}/src/web/components/icon/svg/enable.svg ${GSA_SRC_DIR}/src/web/components/icon/svg/export.svg @@ -461,19 +471,23 @@ set (GSA_JS_SRC_FILES ${GSA_SRC_DIR}/src/web/components/icon/svg/st_vendorfix.svg ${GSA_SRC_DIR}/src/web/components/icon/svg/st_willnotfix.svg ${GSA_SRC_DIR}/src/web/components/icon/svg/st_workaround.svg + ${GSA_SRC_DIR}/src/web/components/icon/svg/magnifier.svg + ${GSA_SRC_DIR}/src/web/components/icon/svg/minus.svg ${GSA_SRC_DIR}/src/web/components/icon/svg/my_setting.svg ${GSA_SRC_DIR}/src/web/components/icon/svg/new.svg - ${GSA_SRC_DIR}/src/web/components/icon/svg/note.svg + ${GSA_SRC_DIR}/src/web/components/icon/svg/new_process.svg ${GSA_SRC_DIR}/src/web/components/icon/svg/new_note.svg ${GSA_SRC_DIR}/src/web/components/icon/svg/new_override.svg ${GSA_SRC_DIR}/src/web/components/icon/svg/new_ticket.svg ${GSA_SRC_DIR}/src/web/components/icon/svg/next.svg + ${GSA_SRC_DIR}/src/web/components/icon/svg/note.svg ${GSA_SRC_DIR}/src/web/components/icon/svg/nvt.svg ${GSA_SRC_DIR}/src/web/components/icon/svg/os.svg ${GSA_SRC_DIR}/src/web/components/icon/svg/ovaldef.svg ${GSA_SRC_DIR}/src/web/components/icon/svg/override.svg ${GSA_SRC_DIR}/src/web/components/icon/svg/performance.svg ${GSA_SRC_DIR}/src/web/components/icon/svg/permission.svg + ${GSA_SRC_DIR}/src/web/components/icon/svg/plus.svg ${GSA_SRC_DIR}/src/web/components/icon/svg/policy.svg ${GSA_SRC_DIR}/src/web/components/icon/svg/port_list.svg ${GSA_SRC_DIR}/src/web/components/icon/svg/previous.svg @@ -563,6 +577,17 @@ set (GSA_JS_SRC_FILES ${GSA_SRC_DIR}/src/web/components/powerfilter/solutiontypegroup.js ${GSA_SRC_DIR}/src/web/components/powerfilter/sortbygroup.js ${GSA_SRC_DIR}/src/web/components/powerfilter/withFilterDialog.js + ${GSA_SRC_DIR}/src/web/components/processmap/background.js + ${GSA_SRC_DIR}/src/web/components/processmap/createprocessdialog.js + ${GSA_SRC_DIR}/src/web/components/processmap/edge.js + ${GSA_SRC_DIR}/src/web/components/processmap/hosttable.js + ${GSA_SRC_DIR}/src/web/components/processmap/processmap.js + ${GSA_SRC_DIR}/src/web/components/processmap/processmaploader.js + ${GSA_SRC_DIR}/src/web/components/processmap/processnode.js + ${GSA_SRC_DIR}/src/web/components/processmap/processpanel.js + ${GSA_SRC_DIR}/src/web/components/processmap/tools.js + ${GSA_SRC_DIR}/src/web/components/processmap/usecolorize.js + ${GSA_SRC_DIR}/src/web/components/processmap/utils.js ${GSA_SRC_DIR}/src/web/components/provider/capabilitiesprovider.js ${GSA_SRC_DIR}/src/web/components/provider/gmpprovider.js ${GSA_SRC_DIR}/src/web/components/provider/iconsizeprovider.js @@ -859,6 +884,7 @@ set (GSA_JS_SRC_FILES ${GSA_SRC_DIR}/src/web/pages/portlists/portrangestable.js ${GSA_SRC_DIR}/src/web/pages/portlists/row.js ${GSA_SRC_DIR}/src/web/pages/portlists/table.js + ${GSA_SRC_DIR}/src/web/pages/processmaps/processmapspage.js ${GSA_SRC_DIR}/src/web/pages/radius/dialog.js ${GSA_SRC_DIR}/src/web/pages/radius/radiuspage.js ${GSA_SRC_DIR}/src/web/pages/reportformats/component.js @@ -1059,6 +1085,9 @@ set (GSA_JS_SRC_FILES ${GSA_SRC_DIR}/src/web/pages/vulns/table.js ${GSA_SRC_DIR}/src/web/routes.js ${GSA_SRC_DIR}/src/web/store/actions.js + ${GSA_SRC_DIR}/src/web/store/businessprocessmaps/actions.js + ${GSA_SRC_DIR}/src/web/store/businessprocessmaps/reducers.js + ${GSA_SRC_DIR}/src/web/store/businessprocessmaps/selectors.js ${GSA_SRC_DIR}/src/web/store/dashboard/data/actions.js ${GSA_SRC_DIR}/src/web/store/dashboard/data/loader.js ${GSA_SRC_DIR}/src/web/store/dashboard/data/reducers.js diff --git a/gsa/public/locales/gsa-de.json b/gsa/public/locales/gsa-de.json index 8e9eb0c62c..240d0b5f37 100644 --- a/gsa/public/locales/gsa-de.json +++ b/gsa/public/locales/gsa-de.json @@ -57,6 +57,7 @@ "Active until": "Aktiv bis", "Add": "Hinzufügen", "Add Port Range": "Neuen Portbereich hinzufügen", + "Add Selected Hosts": "Ausgewählte Hosts hinzufügen", "Add Tag": "Tag hinzufügen", "Add Tag to All Filtered": "Tag zur Filterauswahl hinzufügen", "Add Tag to Page Contents": "Tag zum Seiteninhalt hinzufügen", @@ -80,6 +81,7 @@ "Advisory Link": "Advisory-Link", "Affected": "Betroffen", "Affected Software/OS": "Betroffene Software/OS", + "Affected processes:": "Betroffene Prozesse:", "Agent": "Agent", "Aggregates": "Aggregate", "Alemba Client ID": "Alemba-Client-ID", @@ -121,6 +123,7 @@ "Applications": "Anwendungen", "Applied filter: ": "Angewandter Filter: ", "Apply Overrides": "Übersteuerungen anwenden", + "Apply a minimum severity of 7.0.": "Minimalen Schweregrad von 7.0 anwenden", "Apply default timeout": "Standard-Timeout anwenden", "Apply to all filtered": "Auf gesamte Filterauswahl anwenden", "Apply to page contents": "Auf Seiteninhalt anwenden", @@ -176,6 +179,7 @@ "Base URL": "Basis URL", "Base Vector": "Basisvektor", "Browser Language": "Browser-Sprache", + "Business Process Map": "Geschäftsprozessanalyse", "By clicking the New Task icon": "Durch Anklicken des \"Neue Aufgabe\"-Symbols", "CA Certificate": "CA-Zertifikat", "CERT": "CERT", @@ -288,6 +292,7 @@ "Choose Display": "Anzeige auswählen", "Choose Tag": "Tag auswählen", "Class": "Klasse", + "Click here to create a process.": "Hier klicken, um einen Prozess zu erstellen.", "Client Certificate": "Benutzerzertifikat", "Client Certificate (from Credential)": "Benutzerzertifikat (aus Anmeldedaten)", "Clone Alert": "Benachrichtigung klonen", @@ -350,6 +355,7 @@ "Create Audit from Policy": "Audit für Richtlinie erstellen", "Create Multiple Permissions": "Mehrere Berechtigungen erstellen", "Create Permission": "Berechtigung erstellen", + "Create Process": "Prozess erstellen", "Create Schedule": "Zeitplan erstellen", "Create Schedule:": "Zeitplan erstellen:", "Create Target form all filtered": "Ziel aus gesamter Filterauswahl erstellen", @@ -374,6 +380,8 @@ "Create new Ticket": "Neues Ticket erstellen", "Create new Ticket for Result": "Neues Ticket für Ergebnis erstellen", "Create new Ticket for Result {{- name}}": "Neues Ticket für Ergebnis ({{- name}}) erstellen", + "Create new connection": "Neue Verbindung erstellen", + "Create new process": "Neuen Prozess erstellen", "Create new {{entity}}": "{{entity}} erstellen", "Create permission to grant full read and write access among all group members and across any resources": "Berechtigung erzeugen, um vollen Lese- und Schreibzugriff für alle Gruppenmitglieder und auf alle Ressourcen zu gewähren", "Created": "Erstellt", @@ -444,11 +452,13 @@ "Delete": "Löschen", "Delete Identifier": "Identifikator löschen", "Delete Port Range": "Portbereich löschen", + "Delete Process {{name}}": "Prozess {{name}} löschen", "Delete Report": "Bericht löschen", "Delete Requested": "Löschen Angefragt", "Delete TLS Certificate": "TLS-Zertifikat löschen", "Delete all filtered": "Gesamte Filterauswahl löschen", "Delete page contents": "Seiteninhalt löschen", + "Delete selected element": "Ausgewähltes Element löschen", "Delete selection": "Auswahl löschen", "Delete {{entity}}": "{{entity}} löschen", "Delta": "Delta", @@ -520,6 +530,7 @@ "Edit Policy NVT {{nvtOid}}": "Richtlinien-NVT {{nvtOid}} bearbeiten", "Edit Policy {{name}}": "Richtlinie {{name}} bearbeiten", "Edit Port List {{name}}": "Portliste {{name}} bearbeiten", + "Edit Process": "Prozess bearbeiten", "Edit RADIUS Authentication": "RADIUS-Authentifizierung bearbeiten", "Edit Report Format {{name}}": "Berichtformat {{name}} bearbeiten", "Edit Role {{name}}": "Rolle {{name}} bearbeiten", @@ -541,6 +552,7 @@ "Edit User Settings": "Benutzereinstellungen bearbeiten", "Edit User {{name}}": "Benutzer {{name}} bearbeiten", "Edit permission {{name}}": "Berechtigung {{name}} bearbeiten", + "Edit process": "Prozess bearbeiten", "Edit {{entity}}": "{{entity}} bearbeiten", "Effect": "Auswirkung", "Email": "E-Mail", @@ -560,8 +572,11 @@ "Entire Operation": "Gesamte Ausführung", "Entity": "Objekt", "Error": "Fehler", + "Error Loading Processes": "Fehler beim Laden der Prozesse", "Error Message": "Fehlermeldung", "Error Messages": "Fehlermeldungen", + "Error while loading Report {{reportId}}": "Fehler beim Laden des Berichts {{reportId}}", + "Error while loading Results for Report {{reportId}}": "Fehler beim Laden von Ergebnissen des Berichts {{reportId}}", "Event": "Ereignis", "Every": "Alle", "Every day": "Jeden Tag", @@ -661,6 +676,8 @@ "Filter Settings": "Filter-Einstellungen", "Filter matches at least {{count}} more {{result}} than the previous scan": "Filter stimmt überein mit mindestens {{count}} {{result}} mehr als der vorhergehende Scan", "Filter matches at least {{count}} {{type}}": "Filter stimmt überein mit mindestens {{count}} {{type}}", + "Filter out log message results.": "Log-Nachrichten herausfiltern", + "Filter out results with low severity.": "Ergebnisse mit niedrigem Schweregrad herausfiltern", "Filter: {{name}}": "Filter: {{name}}", "Filters": "Filter", "Filters Filter": "Filter-Filter", @@ -721,6 +738,7 @@ "Help: CERT-Bund Advisories": "Hilfe: CERT-Bund Advisories", "Help: Alerts": "Hilfe: Benachrichtigungen", "Help: Audits": "Hilfe: Audits", + "Help: Business Process Map": "Hilfe: Geschäftsprozessanalyse", "Help: CERT-Bund Advisories": "Hilfe: CERT-Bund Advisories", "Help: CPEs": "Hilfe: CPEs", "Help: CVEs": "Hilfe: CVEs", @@ -861,7 +879,7 @@ "Location (eg. port/protocol)": "Ort (z.B. Port/Protokoll)", "Log": "Log", "Log Out": "Abmelden", - "Log messages are currently excluded.": "Log Nachrichten sind derzeit ausgeschlossen.", + "Log messages are currently excluded.": "Log-Nachrichten sind derzeit ausgeschlossen.", "Logged in as: {{userName}}": "Angemeldet als: {{userName}}", "Login": "Login", "Login Failed. Invalid password or username.": "Anmeldung fehlgeschlagen. Ungültiger Benutzername oder Passwort.", @@ -1025,12 +1043,15 @@ "No credentials available": "Keine Anmeldedaten vorhanden", "No details available for this method.": "Keine Details für diese Methode vorhanden.", "No filters available": "Keine Filter vorhanden", + "No hosts associated with this process.": "Diesem Prozess sind keine Hosts zugewiesen.", "No hosts available": "Keine Hosts vorhanden", "No information about the Operation System": "Keine Information über das Betriebssystem", + "No original severity value available": "Kein ursprünglicher Schweregrad vorhanden", "No parameters available": "Keine Parameter vorhanden", "No permissions available": "Keine Berechtigungen vorhanden", "No port lists available": "Keine Portliste vorhanden", "No port ranges available": "Keine Portbereiche vorhanden", + "No process selected": "Kein Prozess ausgewählt", "No report formats available": "Keine Berichtformate vorhanden", "No reports available": "Keine Berichte vorhanden", "No results available": "Keine Ergebnisse vorhanden", @@ -1110,6 +1131,7 @@ "Operating Systems by Severity Class (Total: {{count}})": "Betriebssysteme nach Schweregradklasse (Gesamt: {{count}})", "Order for target hosts": "Reihenfolge der Ziel-Hosts", "Origin": "Ursprung", + "Original severity: ": "Ursprünglicher Schweregrad", "Orphan": "Verwaist", "Other": "Andere", "Other Links": "Andere Links", @@ -1174,6 +1196,7 @@ "Please enter your old password!": "Bitte geben Sie Ihr altes Passwort ein!", "Please insert a name for the new filter": "Bitte geben Sie einen Filternamen für den neuen Filter an", "Please note that assigning a tag to {{count}} items may take several minutes.": "Bitte beachten Sie, dass es einige Minuten dauern kann, einen Tag {{count}} Objekten zuzuordnen.", + "Please note: Deleting the process will also delete the tag that is associated with it. If this tag is used for any other purpose besides business process mapping, this functionality will be lost.": "Bitte beachten Sie: Das Entfernen des Prozesses löscht auch den Tag, der damit verbunden ist. Wird der Tag außer für die Geschäftsprozessanalyse noch für weitere Zwecke verwendet, wird diese Funktion verloren gehen.", "Please note: You are about to create a user without a role. This user will not have any permissions and as a result will not be able to login.": "Bitte beachten Sie: Sie sind dabei einen Benutzer ohne Rolle zu erstellen. Dieser Benutzer wird keine Berechtigungen haben und deshalb nicht in der Lage sein, sich einzuloggen.", "Please try again.": "Bitte versuchen Sie es erneut.", "Policies": "Richtlinien", @@ -1204,6 +1227,7 @@ "Privacy Algorithm": "Privacy-Algorithmus", "Privacy Password": "Privacy-Passwort", "Private Key": "Privater Schlüssel", + "Process: \"{{name}}\", tag ID: {{tagId}}": "Prozess: \"{{name}}\", Tag ID: {{tagId}}", "Product": "Produkt", "Product Detection Result": "Ergebnis zur Produkterkennung", "Protocol": "Protokoll", @@ -1244,6 +1268,7 @@ "Remove Tag from {{type}}": "Tag von {{type}} entfernen", "Remove all filter settings.": "Alle Filtereinstellungen entfernen.", "Remove from Assets": "Aus Assets entfernen", + "Remove host from process": "Host aus Prozess entfernen", "Remove the severity limit from your filter settings.": "Schweregrad-Grenze aus den Filtereinstellungen entfernen.", "Repeat": "Wiederholung", "Repeat at": "Wiederholen", @@ -1292,6 +1317,7 @@ "Requested": "Angefragt", "Reset to Default Filter": "Zum Standardfilter zurücksetzen", "Reset to Defaults": "Auf Standard zurücksetzen", + "Reset zoom": "Zoom zurücksetzen", "Resilience": "Resilience", "Resource": "Ressource", "Resource ID": "Ressourcen-ID", @@ -1624,6 +1650,7 @@ "The last": "Jeden letzten", "The last {{weekday}} every month": "Jeden letzten {{weekday}} jeden Monat", "The last {{weekday}} every {{interval}} months": "Jeden letzten {{weekday}} jeden {{interval}} Monat", + "The maximum of {{num}} hosts was exceeded. If there are more hosts associated with this process, they will not be taken into account.": "Das Maximum von {{num}} Hosts wurde überschritten. Wenn weitere Hosts mit diesem Prozess verbunden sind, werden sie hier nicht berücksichtigt.", "The port range needs numerical values for start and end!": "Der Portbereich benötigt numerische Werte für Start und Ende!", "The scan did not collect any results": "Der Scan hat keine Ergebnisse gesammelt", "The scan is still running and no results have arrived yet": "Der Scan läuft noch und es sind noch keine Ergebnisse eingetroffen", @@ -1663,6 +1690,7 @@ "To address": "Empfängeradresse", "To change the filter, please filter your results on the report page. This filter will not be stored as default.": "Um den Filter zu ändern, filtern Sie bitte die Ergebnisse in der Berichtansicht. Dieser Filter wird nicht als Standard gespeichert.", "To change the filter, please select a filter from the dropdown menu.": "Um den Filter zu ändern, wählen Sie bitte einen Filter aus dem Drop-Down-menu.", + "To connect processes, click here, select the source process first and then the target.": "Um Prozesse zu verbinden, klicken Sie bitte hier, wählen den Ausgangsprozess und danach den Zielprozess aus.", "To see all assigned resources click here:": "Um alle zugewiesenen Ressourcen zu sehen, klicken Sie bitte hier:", "Today": "Heute", "Toggle 2D/3D view": "Zwischen 2D/3D-Ansicht umschalten", @@ -1696,6 +1724,8 @@ "Trust vendor security updates": "Sicherheitsupdates des Anbieters vertrauen", "Tu.": "Di.", "Tuesday": "Dienstag", + "Turn off conditional colorization": "Bedingte Färbung ausschalten", + "Turn on conditional colorization": "Bedingte Färbung einschalten", "Type": "Typ", "UDP": "UDP", "UDP Port Count": "UDP-Portanzahl", @@ -1777,6 +1807,7 @@ "Wednesday": "Mittwoch", "Week": "Woche", "Weekly": "Wöchentlich", + "While loading the processes one or more corresponding tag(s) could not be found. Try reloading the map. If that does not help check the trashcan for those tags. If the tags are not there, affected processes need to be re-created by hand.": "Während des Ladens der Prozesse konnten ein oder mehrere Tag(s) nicht gefunden werden. Bitte laden Sie die Seite neu. Besteht das Problem weiterhin, schauen Sie bitte im Papierkorb nach den entsprechenden Tags. Sind sie auch dort nicht zu finden, müssen betroffene Prozesse manuell neu erstellt werden.", "Will not fix": "Wird nicht gelöst", "With Note": "Mit Notiz", "With Report": "Mit Bericht", @@ -1793,6 +1824,8 @@ "You should change the Alive Test Method of the target for the next scan. However, if the target hosts are indeed dead, the scan duration might increase significantly.": "Sie sollten die Methode des Erreichbarkeitstests des Ziels für den nächsten Scan ändern. Sollten die Ziel-Hosts tatsächlich unerreichbar sein, könnte sich die Scan-Dauer erheblich verlängern.", "Your filter settings may be too refined.": "Ihre Filtereinstellungen könnten zu präzise sein.", "Your last filter change may be too restrictive.": "Ihre letzte Filteränderung könnte zu restriktiv sein.", + "Zoom in": "Einzoomen", + "Zoom out": "Auszoomen", "at": "um", "changed": "verändert", "day(s)": "Tag(e)", diff --git a/gsa/src/gmp/commands/users.js b/gsa/src/gmp/commands/users.js index c287156eb8..ad06868aa1 100644 --- a/gsa/src/gmp/commands/users.js +++ b/gsa/src/gmp/commands/users.js @@ -43,6 +43,8 @@ import EntityCommand from './entity'; const log = logger.getLogger('gmp.commands.users'); +const BUSINESS_PROCESS_MAPS_SETTING_ID = '3ce2d136-bb52-448a-93f0-20069566f877'; + const REPORT_COMPOSER_DEFAULTS_SETTING_ID = 'b6b449ee-5d90-4ff0-af20-7e838c389d39'; @@ -378,6 +380,39 @@ export class UserCommand extends EntityCommand { }); } + getBusinessProcessMaps() { + return this.httpGet({ + cmd: 'get_setting', + setting_id: BUSINESS_PROCESS_MAPS_SETTING_ID, + }).then(response => { + const {data} = response; + const {setting = {}} = data.get_settings.get_settings_response; + const {value} = setting; + let processMaps; + + try { + processMaps = JSON.parse(value); + } catch (e) { + log.warn( + 'Could not parse saved business process map. Returning empty object.', + ); + processMaps = {}; + } + + return response.setData(processMaps); + }); + } + + saveBusinessProcessMaps(processMaps = {}) { + log.debug('Saving business process maps', processMaps); + + return this.action({ + cmd: 'save_setting', + setting_id: BUSINESS_PROCESS_MAPS_SETTING_ID, + setting_value: JSON.stringify(processMaps), + }); + } + renewSession() { return this.httpPost({ cmd: 'renew_session', diff --git a/gsa/src/stories/confirmationdialog.js b/gsa/src/stories/confirmationdialog.js index db836cd458..9b545344f9 100644 --- a/gsa/src/stories/confirmationdialog.js +++ b/gsa/src/stories/confirmationdialog.js @@ -77,22 +77,19 @@ class TestButton extends React.Component { if (this.state.dialog && this.state.notification === 'Light on') { dialog = ( ); - } else if ( - this.state.dialog && - this.state.notification === 'Light off' - ) { + } else if (this.state.dialog && this.state.notification === 'Light off') { dialog = ( ); } else { diff --git a/gsa/src/web/components/bar/menubar.js b/gsa/src/web/components/bar/menubar.js index 6b7cf3ea1b..f40c0d17f2 100644 --- a/gsa/src/web/components/bar/menubar.js +++ b/gsa/src/web/components/bar/menubar.js @@ -120,6 +120,11 @@ const MenuBar = ({isLoggedIn, capabilities}) => { false, ); + const mayOpBpm = ['hosts', 'tags'].reduce( + (sum, cur) => sum || capabilities.mayAccess(cur), + false, + ); + return ( @@ -181,6 +186,14 @@ const MenuBar = ({isLoggedIn, capabilities}) => { )} + {mayOpBpm && ( + + + + )} )} {capabilities.mayAccess('info') && ( diff --git a/gsa/src/web/components/dialog/__tests__/confirmationdialog.js b/gsa/src/web/components/dialog/__tests__/confirmationdialog.js index 919ec70131..cd9a87f585 100644 --- a/gsa/src/web/components/dialog/__tests__/confirmationdialog.js +++ b/gsa/src/web/components/dialog/__tests__/confirmationdialog.js @@ -30,7 +30,7 @@ describe('ConfirmationDialog component tests', () => { const {baseElement, getByTestId} = render( { expect(titleElement).toHaveTextContent('bar'); }); + test('should render ConfirmationDialog with element content and title', () => { + const handleClose = jest.fn(); + const handleResumeClick = jest.fn(); + + const {getByTestId} = render( + foo} + title="bar" + onClose={handleClose} + onResumeClick={handleResumeClick} + />, + ); + + const contentElement = getByTestId('confirmationdialog-content'); + const titleElement = getByTestId('dialog-title-bar'); + expect(contentElement).toHaveTextContent('foo'); + expect(titleElement).toHaveTextContent('bar'); + }); + test('should close ConfirmationDialog with close button', () => { const handleClose = jest.fn(); const handleResumeClick = jest.fn(); diff --git a/gsa/src/web/components/dialog/confirmationdialog.js b/gsa/src/web/components/dialog/confirmationdialog.js index 7e00308044..6cb125a3ca 100644 --- a/gsa/src/web/components/dialog/confirmationdialog.js +++ b/gsa/src/web/components/dialog/confirmationdialog.js @@ -39,13 +39,13 @@ const ConfirmationDialogContent = props => { } }; - const {moveprops, text, title, rightButtonTitle} = props; + const {content, moveprops, title, rightButtonTitle} = props; return ( - {text} + {content} { ConfirmationDialogContent.propTypes = { close: PropTypes.func.isRequired, + content: PropTypes.elementOrString, moveprops: PropTypes.object, rightButtonTitle: PropTypes.string, - text: PropTypes.string, title: PropTypes.string.isRequired, onResumeClick: PropTypes.func.isRequired, }; const ConfirmationDialog = ({ width = DEFAULT_DIALOG_WIDTH, - text, + content, title, rightButtonTitle = _('OK'), onClose, @@ -79,7 +79,7 @@ const ConfirmationDialog = ({ { - test('should render global styles', () => { - render(); - expect(document.documentElement).toMatchSnapshot(); - }); +describe('BpmIcon component tests', () => { + testIcon(BpmIcon); }); + +// vim: set ts=2 sw=2 tw=80: diff --git a/gsa/src/web/components/icon/__tests__/condcoloricon.js b/gsa/src/web/components/icon/__tests__/condcoloricon.js new file mode 100644 index 0000000000..4bb93ee6bb --- /dev/null +++ b/gsa/src/web/components/icon/__tests__/condcoloricon.js @@ -0,0 +1,28 @@ +/* Copyright (C) 2019 Greenbone Networks GmbH + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import {testIcon} from 'web/components/icon/testing'; + +import ConditionalColorizationIcon from '../condcoloricon'; + +describe('ConditionalColorizationIcon component tests', () => { + testIcon(ConditionalColorizationIcon); +}); + +// vim: set ts=2 sw=2 tw=80: diff --git a/gsa/src/web/components/icon/__tests__/edgeicon.js b/gsa/src/web/components/icon/__tests__/edgeicon.js new file mode 100644 index 0000000000..49bb507c23 --- /dev/null +++ b/gsa/src/web/components/icon/__tests__/edgeicon.js @@ -0,0 +1,28 @@ +/* Copyright (C) 2019 Greenbone Networks GmbH + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import {testIcon} from 'web/components/icon/testing'; + +import EdgeIcon from '../edgeicon'; + +describe('EdgeIcon component tests', () => { + testIcon(EdgeIcon); +}); + +// vim: set ts=2 sw=2 tw=80: diff --git a/gsa/src/web/components/icon/__tests__/magnifiericon.js b/gsa/src/web/components/icon/__tests__/magnifiericon.js new file mode 100644 index 0000000000..f93dc74806 --- /dev/null +++ b/gsa/src/web/components/icon/__tests__/magnifiericon.js @@ -0,0 +1,28 @@ +/* Copyright (C) 2019 Greenbone Networks GmbH + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import {testIcon} from 'web/components/icon/testing'; + +import MagnifierIcon from '../magnifiericon'; + +describe('MagnifierIcon component tests', () => { + testIcon(MagnifierIcon); +}); + +// vim: set ts=2 sw=2 tw=80: diff --git a/gsa/src/web/components/icon/__tests__/minusicon.js b/gsa/src/web/components/icon/__tests__/minusicon.js new file mode 100644 index 0000000000..4d7de159d8 --- /dev/null +++ b/gsa/src/web/components/icon/__tests__/minusicon.js @@ -0,0 +1,28 @@ +/* Copyright (C) 2019 Greenbone Networks GmbH + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import {testIcon} from 'web/components/icon/testing'; + +import MinusIcon from '../minusicon'; + +describe('MinusIcon component tests', () => { + testIcon(MinusIcon); +}); + +// vim: set ts=2 sw=2 tw=80: diff --git a/gsa/src/web/components/icon/__tests__/newprocessicon.js b/gsa/src/web/components/icon/__tests__/newprocessicon.js new file mode 100644 index 0000000000..8c38efff89 --- /dev/null +++ b/gsa/src/web/components/icon/__tests__/newprocessicon.js @@ -0,0 +1,28 @@ +/* Copyright (C) 2020 Greenbone Networks GmbH + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import {testIcon} from 'web/components/icon/testing'; + +import NewProcessIcon from '../newprocessicon'; + +describe('NewProcessIcon component tests', () => { + testIcon(NewProcessIcon); +}); + +// vim: set ts=2 sw=2 tw=80: diff --git a/gsa/src/web/components/icon/__tests__/plusicon.js b/gsa/src/web/components/icon/__tests__/plusicon.js new file mode 100644 index 0000000000..6cc47ff539 --- /dev/null +++ b/gsa/src/web/components/icon/__tests__/plusicon.js @@ -0,0 +1,28 @@ +/* Copyright (C) 2019 Greenbone Networks GmbH + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import {testIcon} from 'web/components/icon/testing'; + +import PlusIcon from '../plusicon'; + +describe('PlusIcon component tests', () => { + testIcon(PlusIcon); +}); + +// vim: set ts=2 sw=2 tw=80: diff --git a/gsa/src/web/components/icon/bpmicon.js b/gsa/src/web/components/icon/bpmicon.js new file mode 100644 index 0000000000..e6d7195c2c --- /dev/null +++ b/gsa/src/web/components/icon/bpmicon.js @@ -0,0 +1,27 @@ +/* Copyright (C) 2020 Greenbone Networks GmbH + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + */ +import withSvgIcon from './withSvgIcon'; + +import {ReactComponent as Icon} from './svg/bpm.svg'; + +const BpmIcon = withSvgIcon()(Icon); + +export default BpmIcon; + +// vim: set ts=2 sw=2 tw=80: diff --git a/gsa/src/web/components/icon/condcoloricon.js b/gsa/src/web/components/icon/condcoloricon.js new file mode 100644 index 0000000000..7904204e3c --- /dev/null +++ b/gsa/src/web/components/icon/condcoloricon.js @@ -0,0 +1,27 @@ +/* Copyright (C) 2020 Greenbone Networks GmbH + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + */ +import withSvgIcon from './withSvgIcon'; + +import {ReactComponent as Icon} from './svg/cond_color.svg'; + +const ConditionalColorizationIcon = withSvgIcon()(Icon); + +export default ConditionalColorizationIcon; + +// vim: set ts=2 sw=2 tw=80: diff --git a/gsa/src/web/components/icon/edgeicon.js b/gsa/src/web/components/icon/edgeicon.js new file mode 100644 index 0000000000..75350b1eb1 --- /dev/null +++ b/gsa/src/web/components/icon/edgeicon.js @@ -0,0 +1,27 @@ +/* Copyright (C) 2020 Greenbone Networks GmbH + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + */ +import withSvgIcon from './withSvgIcon'; + +import {ReactComponent as Icon} from './svg/edge.svg'; + +const EdgeIcon = withSvgIcon()(Icon); + +export default EdgeIcon; + +// vim: set ts=2 sw=2 tw=80: diff --git a/gsa/src/web/components/icon/magnifiericon.js b/gsa/src/web/components/icon/magnifiericon.js new file mode 100644 index 0000000000..1ce6c209e8 --- /dev/null +++ b/gsa/src/web/components/icon/magnifiericon.js @@ -0,0 +1,27 @@ +/* Copyright (C) 2020 Greenbone Networks GmbH + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + */ +import withSvgIcon from './withSvgIcon'; + +import {ReactComponent as Icon} from './svg/magnifier.svg'; + +const MagnifierIcon = withSvgIcon()(Icon); + +export default MagnifierIcon; + +// vim: set ts=2 sw=2 tw=80: diff --git a/gsa/src/web/components/icon/minusicon.js b/gsa/src/web/components/icon/minusicon.js new file mode 100644 index 0000000000..d0eacdd845 --- /dev/null +++ b/gsa/src/web/components/icon/minusicon.js @@ -0,0 +1,27 @@ +/* Copyright (C) 2020 Greenbone Networks GmbH + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + */ +import withSvgIcon from './withSvgIcon'; + +import {ReactComponent as Icon} from './svg/minus.svg'; + +const MinusIcon = withSvgIcon()(Icon); + +export default MinusIcon; + +// vim: set ts=2 sw=2 tw=80: diff --git a/gsa/src/web/components/icon/newprocessicon.js b/gsa/src/web/components/icon/newprocessicon.js new file mode 100644 index 0000000000..29ef7e6cc9 --- /dev/null +++ b/gsa/src/web/components/icon/newprocessicon.js @@ -0,0 +1,27 @@ +/* Copyright (C) 2020 Greenbone Networks GmbH + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + */ +import withSvgIcon from './withSvgIcon'; + +import {ReactComponent as Icon} from './svg/new_process.svg'; + +const NewProcessIcon = withSvgIcon()(Icon); + +export default NewProcessIcon; + +// vim: set ts=2 sw=2 tw=80: diff --git a/gsa/src/web/components/icon/plusicon.js b/gsa/src/web/components/icon/plusicon.js new file mode 100644 index 0000000000..b12467e37c --- /dev/null +++ b/gsa/src/web/components/icon/plusicon.js @@ -0,0 +1,27 @@ +/* Copyright (C) 2020 Greenbone Networks GmbH + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + */ +import withSvgIcon from './withSvgIcon'; + +import {ReactComponent as Icon} from './svg/plus.svg'; + +const PlusIcon = withSvgIcon()(Icon); + +export default PlusIcon; + +// vim: set ts=2 sw=2 tw=80: diff --git a/gsa/src/web/components/icon/svg/bpm.svg b/gsa/src/web/components/icon/svg/bpm.svg new file mode 100644 index 0000000000..909ebce6db --- /dev/null +++ b/gsa/src/web/components/icon/svg/bpm.svg @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/gsa/src/web/components/icon/svg/cond_color.svg b/gsa/src/web/components/icon/svg/cond_color.svg new file mode 100644 index 0000000000..9aba02b744 --- /dev/null +++ b/gsa/src/web/components/icon/svg/cond_color.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + diff --git a/gsa/src/web/components/icon/svg/edge.svg b/gsa/src/web/components/icon/svg/edge.svg new file mode 100644 index 0000000000..04cb93e884 --- /dev/null +++ b/gsa/src/web/components/icon/svg/edge.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + diff --git a/gsa/src/web/components/icon/svg/magnifier.svg b/gsa/src/web/components/icon/svg/magnifier.svg new file mode 100644 index 0000000000..91711b3a6b --- /dev/null +++ b/gsa/src/web/components/icon/svg/magnifier.svg @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/gsa/src/web/components/icon/svg/minus.svg b/gsa/src/web/components/icon/svg/minus.svg new file mode 100644 index 0000000000..fe9791e4a9 --- /dev/null +++ b/gsa/src/web/components/icon/svg/minus.svg @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/gsa/src/web/components/icon/svg/new_process.svg b/gsa/src/web/components/icon/svg/new_process.svg new file mode 100644 index 0000000000..c8485badb3 --- /dev/null +++ b/gsa/src/web/components/icon/svg/new_process.svg @@ -0,0 +1,19 @@ + + + + + + + diff --git a/gsa/src/web/components/icon/svg/plus.svg b/gsa/src/web/components/icon/svg/plus.svg new file mode 100644 index 0000000000..79687e0411 --- /dev/null +++ b/gsa/src/web/components/icon/svg/plus.svg @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/gsa/src/web/components/layout/__tests__/__snapshots__/globalstyles.js.snap b/gsa/src/web/components/layout/__tests__/__snapshots__/globalstyles.js.snap deleted file mode 100644 index e223862a8d..0000000000 --- a/gsa/src/web/components/layout/__tests__/__snapshots__/globalstyles.js.snap +++ /dev/null @@ -1,836 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`GlobalStyles tests should render global styles 1`] = ` - - - - - - - - - - - - -
-
- - -`; diff --git a/gsa/src/web/components/link/externallink.js b/gsa/src/web/components/link/externallink.js index 7a9d4150fd..6729a8e4e9 100644 --- a/gsa/src/web/components/link/externallink.js +++ b/gsa/src/web/components/link/externallink.js @@ -79,7 +79,7 @@ class ExternalLink extends React.Component { { + test('should render dialog for creating a process', () => { + const handleClose = jest.fn(); + const handleCreate = jest.fn(); + const handleEdit = jest.fn(); + + const {getByName, getByTestId, getAllByTestId} = render( + , + ); + + const titleBar = getByTestId('dialog-title-bar'); + const cancelButton = getByTestId('dialog-close-button'); + const saveButton = getByTestId('dialog-save-button'); + const formGroups = getAllByTestId('formgroup-title'); + + const nameInput = getByName('name'); + const commentInput = getByName('comment'); + + expect(titleBar).toHaveTextContent('Create Process'); + + expect(formGroups[0]).toHaveTextContent('Name'); + expect(nameInput).toHaveAttribute('value', 'Unnamed'); + + expect(formGroups[1]).toHaveTextContent('Comment'); + expect(commentInput).toHaveAttribute('value', ''); + + expect(cancelButton).toHaveTextContent('Cancel'); + expect(saveButton).toHaveTextContent('Create'); + }); + + test('should render dialog for editing a process', () => { + const handleClose = jest.fn(); + const handleCreate = jest.fn(); + const handleEdit = jest.fn(); + + const {getByName, getByTestId, getAllByTestId} = render( + , + ); + + const titleBar = getByTestId('dialog-title-bar'); + const cancelButton = getByTestId('dialog-close-button'); + const saveButton = getByTestId('dialog-save-button'); + const formGroups = getAllByTestId('formgroup-title'); + + const nameInput = getByName('name'); + const commentInput = getByName('comment'); + + expect(titleBar).toHaveTextContent('Edit Process'); + + expect(formGroups[0]).toHaveTextContent('Name'); + expect(nameInput).toHaveAttribute('value', 'foo'); + + expect(formGroups[1]).toHaveTextContent('Comment'); + expect(commentInput).toHaveAttribute('value', 'bar'); + + expect(cancelButton).toHaveTextContent('Cancel'); + expect(saveButton).toHaveTextContent('Save'); + }); + + test('should allow to close the dialog', () => { + const handleClose = jest.fn(); + const handleCreate = jest.fn(); + const handleEdit = jest.fn(); + + const {getByTestId} = render( + , + ); + + const closeButton = getByTestId('dialog-title-close-button'); + + fireEvent.click(closeButton); + + expect(handleEdit).not.toHaveBeenCalled(); + expect(handleCreate).not.toHaveBeenCalled(); + expect(handleClose).toHaveBeenCalled(); + }); + + test('should allow to cancel the dialog', () => { + const handleClose = jest.fn(); + const handleCreate = jest.fn(); + const handleEdit = jest.fn(); + + const {getByTestId} = render( + , + ); + + const cancelButton = getByTestId('dialog-close-button'); + + fireEvent.click(cancelButton); + + expect(handleEdit).not.toHaveBeenCalled(); + expect(handleCreate).not.toHaveBeenCalled(); + expect(handleClose).toHaveBeenCalled(); + }); + + test('should allow to create a process', () => { + const handleClose = jest.fn(); + const handleCreate = jest.fn(); + const handleEdit = jest.fn(); + + const {getByName, getByTestId} = render( + , + ); + + const nameInput = getByName('name'); + fireEvent.change(nameInput, {target: {value: 'foo'}}); + + const commentInput = getByName('comment'); + fireEvent.change(commentInput, {target: {value: 'bar'}}); + + const saveButton = getByTestId('dialog-save-button'); + fireEvent.click(saveButton); + + expect(handleEdit).not.toHaveBeenCalled(); + expect(handleCreate).toHaveBeenCalledWith({ + comment: 'bar', + id: undefined, + name: 'foo', + }); + }); + + test('should allow to edit a process', () => { + const handleClose = jest.fn(); + const handleCreate = jest.fn(); + const handleEdit = jest.fn(); + + const {getByName, getByTestId} = render( + , + ); + + const nameInput = getByName('name'); + fireEvent.change(nameInput, {target: {value: 'lorem'}}); + + const commentInput = getByName('comment'); + fireEvent.change(commentInput, {target: {value: 'ipsum'}}); + + const saveButton = getByTestId('dialog-save-button'); + fireEvent.click(saveButton); + + expect(handleCreate).not.toHaveBeenCalled(); + expect(handleEdit).toHaveBeenCalledWith({ + comment: 'ipsum', + id: '123', + name: 'lorem', + }); + }); +}); diff --git a/gsa/src/web/components/processmap/__tests__/hosttable.js b/gsa/src/web/components/processmap/__tests__/hosttable.js new file mode 100644 index 0000000000..f6419c137d --- /dev/null +++ b/gsa/src/web/components/processmap/__tests__/hosttable.js @@ -0,0 +1,132 @@ +/* Copyright (C) 2020 Greenbone Networks GmbH + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + */ +import React from 'react'; + +import {setLocale} from 'gmp/locale/lang'; + +import {rendererWith, fireEvent} from 'web/utils/testing'; + +import HostTable from '../hosttable'; + +setLocale('en'); + +const hosts = [ + {name: '123.456.78.910', id: '1234', severity: 5}, + {name: '109.876.54.321', id: '5678', severity: undefined}, +]; + +describe('HostTable tests', () => { + test('should render HostTable', () => { + const handleDeleteHost = jest.fn(); + + const {render} = rendererWith({ + capabilities: true, + router: true, + }); + + const {element} = render(); + + const header = element.querySelectorAll('th'); + + expect(header[0]).toHaveTextContent('Host'); + expect(header[1]).toHaveTextContent('Severity'); + expect(header[2]).toHaveTextContent('Actions'); + + expect(element).not.toHaveTextContent( + 'No hosts associated with this process.', + ); + }); + + test('should render empty HostTable', () => { + const handleDeleteHost = jest.fn(); + + const {render} = rendererWith({ + capabilities: true, + router: true, + }); + + const {element} = render( + , + ); + + const header = element.querySelectorAll('th'); + + expect(header[0]).toHaveTextContent('Host'); + expect(header[1]).toHaveTextContent('Severity'); + expect(header[2]).toHaveTextContent('Actions'); + + expect(element).toHaveTextContent('No hosts associated with this process.'); + }); + + test('should render HostTable with hosts', () => { + const handleDeleteHost = jest.fn(); + + const {render} = rendererWith({ + capabilities: true, + router: true, + }); + + const {element, getAllByTestId} = render( + , + ); + + const header = element.querySelectorAll('th'); + const detailsLinks = getAllByTestId('details-link'); + const icons = getAllByTestId('svg-icon'); + const progressBars = getAllByTestId('progressbar-box'); + + // Headings + expect(header[0]).toHaveTextContent('Host'); + expect(header[1]).toHaveTextContent('Severity'); + expect(header[2]).toHaveTextContent('Actions'); + + // Row 1 + expect(detailsLinks[0]).toHaveAttribute('href', '/host/1234'); + expect(detailsLinks[0]).toHaveTextContent('123.456.78.910'); + expect(progressBars[0]).toHaveAttribute('title', 'Medium'); + expect(progressBars[0]).toHaveTextContent('5.0 (Medium)'); + expect(icons[0]).toHaveAttribute('title', 'Remove host from process'); + + // Row 2 + expect(detailsLinks[1]).toHaveAttribute('href', '/host/5678'); + expect(detailsLinks[1]).toHaveTextContent('109.876.54.321'); + expect(progressBars[1]).toHaveAttribute('title', 'N/A'); + expect(progressBars[1]).toHaveTextContent('N/A'); + expect(icons[1]).toHaveAttribute('title', 'Remove host from process'); + }); + + test('should call click handler', () => { + const handleDeleteHost = jest.fn(); + + const {render} = rendererWith({ + capabilities: true, + router: true, + }); + + const {getAllByTestId} = render( + , + ); + + const icons = getAllByTestId('svg-icon'); + + expect(icons[0]).toHaveAttribute('title', 'Remove host from process'); + fireEvent.click(icons[0]); + expect(handleDeleteHost).toHaveBeenCalledWith(hosts[0].id); + }); +}); diff --git a/gsa/src/web/components/processmap/__tests__/processmap.js b/gsa/src/web/components/processmap/__tests__/processmap.js new file mode 100644 index 0000000000..d8cf3cf865 --- /dev/null +++ b/gsa/src/web/components/processmap/__tests__/processmap.js @@ -0,0 +1,914 @@ +/* Copyright (C) 2020 Greenbone Networks GmbH + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + */ +import React from 'react'; +import {act} from 'react-dom/test-utils'; + +import {setLocale} from 'gmp/locale/lang'; + +import CollectionCounts from 'gmp/collection/collectioncounts'; + +import Filter from 'gmp/models/filter'; + +import {entitiesLoadingActions} from 'web/store/entities/hosts'; + +import {KeyCode} from 'gmp/utils/event'; + +import {rendererWith, fireEvent} from 'web/utils/testing'; + +import {hostsFilter} from 'web/components/processmap/processmaploader'; + +import ProcessMap from '../processmap'; + +setLocale('en'); + +export const getMockProcessMap = () => { + const mockProcessMap = { + edges: {'11': {id: '11', source: '21', target: '22', type: 'edge'}}, + processes: { + '21': { + color: '#f0a519', + comment: 'bar', + derivedSeverity: 5, + id: '21', + name: 'foo', + severity: 5, + tagId: '31', + type: 'process', + x: 600, + y: 300, + }, + '22': { + color: '#f0a519', + comment: 'ipsum', + derivedSeverity: 5, + id: '22', + name: 'lorem', + severity: undefined, + tagId: '32', + type: 'process', + x: 300, + y: 200, + }, + '23': { + color: '#c83814', + comment: 'world', + derivedSeverity: 10, + id: '23', + name: 'hello', + severity: 10, + tagId: 33, + type: 'process', + x: 600, + y: 200, + }, + }, + }; + return { + mockProcessMap, + processes: mockProcessMap.processes, + edges: mockProcessMap.edges, + }; +}; + +const hostFilter = hostsFilter('31'); + +const hosts = [ + {name: '123.456.78.910', id: '1234', severity: 5}, + {name: '109.876.54.321', id: '5678', severity: undefined}, +]; + +const renewSession = jest.fn().mockResolvedValue({data: {}}); + +const getAllHosts = jest.fn().mockResolvedValue({ + data: hosts, + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, +}); + +describe('ProcessMap tests', () => { + test('should render ProcessMap', () => { + const {mockProcessMap} = getMockProcessMap(); + + const handleForceUpdate = jest.fn(); + const handleSelectElement = jest.fn(); + const handleToggleConditionalColorization = jest.fn(); + const gmp = { + hosts: { + getAll: getAllHosts, + }, + user: {renewSession}, + }; + + const {render} = rendererWith({ + gmp, + store: true, + }); + + const {element, getByTestId, getAllByTestId} = render( + , + ); + + const processes = getAllByTestId('process-node-group'); + const circles = getAllByTestId('process-node-circle'); + const edges = getAllByTestId('bpm-edge-line'); + const icons = getAllByTestId('svg-icon'); + + const newIcon = getByTestId('bpm-tool-icon-new'); + const edgeIcon = getByTestId('bpm-tool-icon-edge'); + const deleteIcon = getByTestId('bpm-tool-icon-delete'); + const colorIcon = getByTestId('bpm-tool-icon-color'); + const zoomInIcon = getByTestId('bpm-tool-icon-zoomin'); + const zoomResetIcon = getByTestId('bpm-tool-icon-zoomreset'); + const zoomOutIcon = getByTestId('bpm-tool-icon-zoomout'); + + const buttons = element.querySelectorAll('button'); + const header = element.querySelectorAll('th'); + + // process map + + // process 1 + expect(processes[0]).toHaveAttribute('cursor', 'grab'); + expect(processes[0]).toHaveTextContent('foo'); + expect(processes[0]).toHaveTextContent('bar'); + + expect(circles[0]).toHaveAttribute('fill', '#f0a519'); + expect(circles[0]).toHaveAttribute('cx', '600'); + expect(circles[0]).toHaveAttribute('cy', '300'); + + // process 2 + expect(processes[1]).toHaveAttribute('cursor', 'grab'); + expect(processes[1]).toHaveTextContent('lorem'); + expect(processes[1]).toHaveTextContent('ipsum'); + + expect(circles[1]).toHaveAttribute('fill', '#f0a519'); + expect(circles[1]).toHaveAttribute('cx', '300'); + expect(circles[1]).toHaveAttribute('cy', '200'); + + // edge + expect(edges[0]).toHaveAttribute('x1', '600'); + expect(edges[0]).toHaveAttribute('x2', '300'); + expect(edges[0]).toHaveAttribute('y1', '300'); + expect(edges[0]).toHaveAttribute('y2', '200'); + expect(edges[0]).toHaveAttribute('fill', '#393637'); + + // tools + + expect(newIcon).toHaveAttribute('title', 'Create new process'); + expect(edgeIcon).toHaveAttribute('title', 'Create new connection'); + expect(deleteIcon).toHaveAttribute('title', 'Delete selected element'); + expect(colorIcon).toHaveAttribute( + 'title', + 'Turn on conditional colorization', + ); + expect(zoomInIcon).toHaveAttribute('title', 'Zoom in'); + expect(zoomResetIcon).toHaveAttribute('title', 'Reset zoom'); + expect(zoomOutIcon).toHaveAttribute('title', 'Zoom out'); + + // process panel + + expect(element).toHaveTextContent('No process selected'); + expect(icons[7]).toHaveAttribute('title', 'Edit process'); + + expect(buttons[0]).toHaveAttribute('title', 'Add Selected Hosts'); + expect(buttons[0]).toHaveTextContent('Add Selected Hosts'); + + expect(header[0]).toHaveTextContent('Host'); + expect(header[1]).toHaveTextContent('Severity'); + expect(header[2]).toHaveTextContent('Actions'); + + expect(element).not.toHaveTextContent( + 'No hosts associated with this process.', + ); + }); + + test('should render ProcessMap without map', () => { + const handleForceUpdate = jest.fn(); + const handleSelectElement = jest.fn(); + const handleToggleConditionalColorization = jest.fn(); + const gmp = { + hosts: { + getAll: getAllHosts, + }, + user: {renewSession}, + }; + + const {render} = rendererWith({ + gmp, + store: true, + }); + + const {element, getByTestId, getAllByTestId} = render( + , + ); + + const icons = getAllByTestId('svg-icon'); + const newIcon = getByTestId('bpm-tool-icon-new'); + const edgeIcon = getByTestId('bpm-tool-icon-edge'); + const deleteIcon = getByTestId('bpm-tool-icon-delete'); + const colorIcon = getByTestId('bpm-tool-icon-color'); + const zoomInIcon = getByTestId('bpm-tool-icon-zoomin'); + const zoomResetIcon = getByTestId('bpm-tool-icon-zoomreset'); + const zoomOutIcon = getByTestId('bpm-tool-icon-zoomout'); + + const buttons = element.querySelectorAll('button'); + const header = element.querySelectorAll('th'); + + // tools + expect(newIcon).toHaveAttribute('title', 'Create new process'); + expect(edgeIcon).toHaveAttribute('title', 'Create new connection'); + expect(deleteIcon).toHaveAttribute('title', 'Delete selected element'); + expect(colorIcon).toHaveAttribute( + 'title', + 'Turn on conditional colorization', + ); + expect(zoomInIcon).toHaveAttribute('title', 'Zoom in'); + expect(zoomResetIcon).toHaveAttribute('title', 'Reset zoom'); + expect(zoomOutIcon).toHaveAttribute('title', 'Zoom out'); + + // process panel + expect(element).toHaveTextContent('No process selected'); + expect(icons[7]).toHaveAttribute('title', 'Edit process'); + + expect(buttons[0]).toHaveAttribute('title', 'Add Selected Hosts'); + expect(buttons[0]).toHaveTextContent('Add Selected Hosts'); + + expect(header[0]).toHaveTextContent('Host'); + expect(header[1]).toHaveTextContent('Severity'); + expect(header[2]).toHaveTextContent('Actions'); + + expect(element).not.toHaveTextContent( + 'No hosts associated with this process.', + ); + }); + + test('should render with selected Element without hosts', () => { + const {mockProcessMap} = getMockProcessMap(); + + const handleForceUpdate = jest.fn(); + const handleSelectElement = jest.fn(); + const handleToggleConditionalColorization = jest.fn(); + + const getEmptyHostsList = jest.fn().mockResolvedValue({ + data: [], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, + }); + + const gmp = { + hosts: { + getAll: getEmptyHostsList, + }, + user: {renewSession}, + }; + + const {render, store} = rendererWith({ + capabilities: true, + gmp, + router: true, + store: true, + }); + + const {element, getAllByTestId} = render( + , + ); + + store.dispatch(entitiesLoadingActions.success([], hostFilter, hostFilter)); + + const circles = getAllByTestId('process-node-circle'); + + // select process + fireEvent.mouseDown(circles[0]); + fireEvent.mouseUp(circles[0]); + + const icons = getAllByTestId('svg-icon'); + const buttons = element.querySelectorAll('button'); + const header = element.querySelectorAll('th'); + + // panel title + expect(element).toHaveTextContent('foo'); + expect(icons[7]).toHaveAttribute('title', 'Edit process'); + + // add hosts + expect(buttons[0]).toHaveAttribute('title', 'Add Selected Hosts'); + expect(buttons[0]).toHaveTextContent('Add Selected Hosts'); + + // host table + + // headings + expect(header[0]).toHaveTextContent('Host'); + expect(header[1]).toHaveTextContent('Severity'); + expect(header[2]).toHaveTextContent('Actions'); + + expect(element).toHaveTextContent('No hosts associated with this process.'); + }); + + test('should render with selected Element with hosts', () => { + const {mockProcessMap} = getMockProcessMap(); + + const handleForceUpdate = jest.fn(); + const handleSelectElement = jest.fn(); + const handleToggleConditionalColorization = jest.fn(); + const gmp = { + hosts: { + getAll: getAllHosts, + }, + user: {renewSession}, + }; + + const {render, store} = rendererWith({ + capabilities: true, + gmp, + router: true, + store: true, + }); + + const {element, getAllByTestId} = render( + , + ); + + store.dispatch( + entitiesLoadingActions.success(hosts, hostFilter, hostFilter), + ); + + const circles = getAllByTestId('process-node-circle'); + + // select process + fireEvent.mouseDown(circles[0]); + fireEvent.mouseUp(circles[0]); + + const detailsLinks = getAllByTestId('details-link'); + const icons = getAllByTestId('svg-icon'); + const progressBars = getAllByTestId('progressbar-box'); + const buttons = element.querySelectorAll('button'); + const header = element.querySelectorAll('th'); + + // panel title + expect(element).toHaveTextContent('foo'); + expect(icons[7]).toHaveAttribute('title', 'Edit process'); + + // add hosts + expect(buttons[0]).toHaveAttribute('title', 'Add Selected Hosts'); + expect(buttons[0]).toHaveTextContent('Add Selected Hosts'); + + // host table + + // Headings + expect(header[0]).toHaveTextContent('Host'); + expect(header[1]).toHaveTextContent('Severity'); + expect(header[2]).toHaveTextContent('Actions'); + + // Row 1 + expect(detailsLinks[0]).toHaveAttribute('href', '/host/1234'); + expect(detailsLinks[0]).toHaveTextContent('123.456.78.910'); + expect(progressBars[0]).toHaveAttribute('title', 'Medium'); + expect(progressBars[0]).toHaveTextContent('5.0 (Medium)'); + expect(icons[8]).toHaveAttribute('title', 'Remove host from process'); + + // Row 2 + expect(detailsLinks[1]).toHaveAttribute('href', '/host/5678'); + expect(detailsLinks[1]).toHaveTextContent('109.876.54.321'); + expect(progressBars[1]).toHaveAttribute('title', 'N/A'); + expect(progressBars[1]).toHaveTextContent('N/A'); + expect(icons[9]).toHaveAttribute('title', 'Remove host from process'); + }); + + test('should call click handler for colorization', () => { + const {mockProcessMap} = getMockProcessMap(); + + const handleForceUpdate = jest.fn(); + const handleSelectElement = jest.fn(); + const handleToggleConditionalColorization = jest.fn(); + + const gmp = { + hosts: { + getAll: getAllHosts, + }, + user: {renewSession}, + }; + + const {render} = rendererWith({ + gmp, + store: true, + }); + + const {getByTestId} = render( + , + ); + + const colorIcon = getByTestId('bpm-tool-icon-color'); + + fireEvent.click(colorIcon); + expect(handleToggleConditionalColorization).toHaveBeenCalled(); + }); + + test('should select elements', () => { + const {mockProcessMap, processes, edges} = getMockProcessMap(); + const {21: process1} = processes; + const {11: edge1} = edges; + + const handleForceUpdate = jest.fn(); + const handleSelectElement = jest.fn(); + const handleToggleConditionalColorization = jest.fn(); + + const saveBusinessProcessMaps = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const gmp = { + hosts: { + getAll: getAllHosts, + }, + user: { + saveBusinessProcessMaps, + renewSession, + }, + }; + + const {render} = rendererWith({ + gmp, + store: true, + }); + + const {element, getAllByTestId} = render( + , + ); + + const circles = getAllByTestId('process-node-circle'); + const bpmEdges = getAllByTestId('bpm-edge-line'); + const background = element.querySelectorAll('rect'); + + fireEvent.mouseDown(background[0]); + fireEvent.mouseUp(background[0]); + + expect(handleSelectElement).not.toHaveBeenCalled(); + + fireEvent.mouseDown(circles[0]); + fireEvent.mouseUp(circles[0]); + + expect(handleSelectElement).toHaveBeenCalledWith(process1); + + fireEvent.mouseDown(bpmEdges[0]); + fireEvent.mouseUp(bpmEdges[0]); + + expect(handleSelectElement).toHaveBeenCalledWith(edge1); + }); + + test('should end drawing mode', () => { + const {mockProcessMap} = getMockProcessMap(); + + const handleForceUpdate = jest.fn(); + const handleSelectElement = jest.fn(); + const handleToggleConditionalColorization = jest.fn(); + + const saveBusinessProcessMaps = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const gmp = { + hosts: { + getAll: getAllHosts, + }, + user: { + saveBusinessProcessMaps, + renewSession, + }, + }; + + const {render} = rendererWith({ + gmp, + store: true, + }); + + const {element, getByTestId, getAllByTestId} = render( + , + ); + + const circles = getAllByTestId('process-node-circle'); + const edgeIcon = getByTestId('bpm-tool-icon-edge'); + + expect(edgeIcon).not.toHaveStyleRule('background-color', '#66c430'); + fireEvent.click(edgeIcon); + expect(edgeIcon).toHaveStyleRule('background-color', '#66c430'); + + fireEvent.mouseDown(circles[2]); + fireEvent.mouseUp(circles[2]); + + expect(saveBusinessProcessMaps).not.toHaveBeenCalled(); + + fireEvent.keyDown(element, {key: 'Escape', keyCode: KeyCode.Escape}); + + expect(edgeIcon).not.toHaveStyleRule('background-color', '#66c430'); + + expect(saveBusinessProcessMaps).not.toHaveBeenCalled(); + }); + + test('should save map when drawing new edge', () => { + const {mockProcessMap} = getMockProcessMap(); + + const handleForceUpdate = jest.fn(); + const handleSelectElement = jest.fn(); + const handleToggleConditionalColorization = jest.fn(); + + const saveBusinessProcessMaps = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const gmp = { + hosts: { + getAll: getAllHosts, + }, + user: { + saveBusinessProcessMaps, + renewSession, + }, + }; + + const {render} = rendererWith({ + gmp, + store: true, + }); + + const {getByTestId, getAllByTestId} = render( + , + ); + + const circles = getAllByTestId('process-node-circle'); + const edgeIcon = getByTestId('bpm-tool-icon-edge'); + + expect(edgeIcon).not.toHaveStyleRule('background-color', '#66c430'); + fireEvent.click(edgeIcon); + expect(edgeIcon).toHaveStyleRule('background-color', '#66c430'); + + fireEvent.mouseDown(circles[2]); + fireEvent.mouseUp(circles[2]); + + expect(saveBusinessProcessMaps).not.toHaveBeenCalled(); + + fireEvent.mouseDown(circles[1]); + fireEvent.mouseUp(circles[1]); + + expect(edgeIcon).not.toHaveStyleRule('background-color', '#66c430'); + + expect(saveBusinessProcessMaps).toHaveBeenCalled(); + }); + + test('should save map when deleting an edge', () => { + const {mockProcessMap} = getMockProcessMap(); + + const handleForceUpdate = jest.fn(); + const handleSelectElement = jest.fn(); + const handleToggleConditionalColorization = jest.fn(); + + const saveBusinessProcessMaps = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const gmp = { + hosts: { + getAll: getAllHosts, + }, + user: { + saveBusinessProcessMaps, + renewSession, + }, + }; + + const {render} = rendererWith({ + gmp, + store: true, + }); + + const {getByTestId, getAllByTestId} = render( + , + ); + + const edges = getAllByTestId('bpm-edge-line'); + const deleteIcon = getByTestId('bpm-tool-icon-delete'); + + fireEvent.mouseDown(edges[0]); + fireEvent.mouseUp(edges[0]); + + expect(saveBusinessProcessMaps).not.toHaveBeenCalled(); + + fireEvent.click(deleteIcon); + + expect(saveBusinessProcessMaps).toHaveBeenCalled(); + }); + + test('should save map when deleting an edge with delete key', () => { + const {mockProcessMap} = getMockProcessMap(); + + const handleForceUpdate = jest.fn(); + const handleSelectElement = jest.fn(); + const handleToggleConditionalColorization = jest.fn(); + + const saveBusinessProcessMaps = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const gmp = { + hosts: { + getAll: getAllHosts, + }, + user: { + saveBusinessProcessMaps, + renewSession, + }, + }; + + const {render} = rendererWith({ + gmp, + store: true, + }); + + const {element, getAllByTestId} = render( + , + ); + + const edges = getAllByTestId('bpm-edge-line'); + + fireEvent.mouseDown(edges[0]); + fireEvent.mouseUp(edges[0]); + + expect(saveBusinessProcessMaps).not.toHaveBeenCalled(); + + fireEvent.keyDown(element, {key: 'Delete', keyCode: KeyCode.Delete}); + + expect(saveBusinessProcessMaps).toHaveBeenCalled(); + }); + + test('should save map after a process was moved', () => { + const {mockProcessMap} = getMockProcessMap(); + + const handleForceUpdate = jest.fn(); + const handleSelectElement = jest.fn(); + const handleToggleConditionalColorization = jest.fn(); + + const saveBusinessProcessMaps = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const gmp = { + hosts: { + getAll: getAllHosts, + }, + user: { + saveBusinessProcessMaps, + renewSession, + }, + }; + + const {render} = rendererWith({ + gmp, + store: true, + }); + + const {getAllByTestId} = render( + , + ); + + const circles = getAllByTestId('process-node-circle'); + + fireEvent.mouseDown(circles[0]); + + expect(saveBusinessProcessMaps).not.toHaveBeenCalled(); + + fireEvent.mouseMove(circles[0]); + + expect(saveBusinessProcessMaps).not.toHaveBeenCalled(); + + fireEvent.mouseUp(circles[0]); + + expect(saveBusinessProcessMaps).toHaveBeenCalled(); + }); + + test('should force update for deleting a host', async () => { + const {mockProcessMap} = getMockProcessMap(); + + const handleForceUpdate = jest.fn(); + const handleSelectElement = jest.fn(); + const handleToggleConditionalColorization = jest.fn(); + + const saveTag = jest.fn().mockResolvedValue({ + data: [], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, + }); + + const gmp = { + hosts: { + getAll: getAllHosts, + }, + tag: { + save: saveTag, + }, + user: {renewSession}, + }; + + const {render, store} = rendererWith({ + capabilities: true, + gmp, + router: true, + store: true, + }); + + const {getAllByTestId} = render( + , + ); + + store.dispatch( + entitiesLoadingActions.success(hosts, hostFilter, hostFilter), + ); + + const circles = getAllByTestId('process-node-circle'); + + // select process + fireEvent.mouseDown(circles[0]); + fireEvent.mouseUp(circles[0]); + + const icons = getAllByTestId('svg-icon'); + + await act(async () => { + expect(icons[8]).toHaveAttribute('title', 'Remove host from process'); + fireEvent.click(icons[8]); + }); + + expect(handleForceUpdate).toHaveBeenCalled(); + }); + + test('should zoom', () => { + const {mockProcessMap} = getMockProcessMap(); + + const handleForceUpdate = jest.fn(); + const handleSelectElement = jest.fn(); + const handleToggleConditionalColorization = jest.fn(); + const gmp = { + hosts: { + getAll: getAllHosts, + }, + user: {renewSession}, + }; + + const {render} = rendererWith({ + gmp, + store: true, + }); + + const {element, getByTestId, getAllByTestId} = render( + , + ); + + const zoomInIcon = getByTestId('bpm-tool-icon-zoomin'); + const zoomResetIcon = getByTestId('bpm-tool-icon-zoomreset'); + const zoomOutIcon = getByTestId('bpm-tool-icon-zoomout'); + const svgs = element.querySelectorAll('svg'); + let processes = getAllByTestId('process-node-group'); + + expect(processes[0]).toHaveAttribute('scale', '1'); + expect(processes[1]).toHaveAttribute('scale', '1'); + + fireEvent.click(zoomInIcon); + + processes = getAllByTestId('process-node-group'); + + expect(processes[0]).toHaveAttribute('scale', '1.1'); + expect(processes[1]).toHaveAttribute('scale', '1.1'); + + fireEvent.click(zoomOutIcon); + fireEvent.click(zoomOutIcon); + fireEvent.click(zoomOutIcon); + + expect(processes[0]).toHaveAttribute('scale', '0.8'); + expect(processes[1]).toHaveAttribute('scale', '0.8'); + + fireEvent.click(zoomResetIcon); + + expect(processes[0]).toHaveAttribute('scale', '1'); + expect(processes[1]).toHaveAttribute('scale', '1'); + + fireEvent.wheel(zoomInIcon); + + expect(processes[0]).toHaveAttribute('scale', '1'); + expect(processes[1]).toHaveAttribute('scale', '1'); + + fireEvent.wheel(svgs[0]); + + expect(processes[0]).toHaveAttribute('scale', '1.1'); + expect(processes[1]).toHaveAttribute('scale', '1.1'); + }); +}); diff --git a/gsa/src/web/components/processmap/__tests__/processpanel.js b/gsa/src/web/components/processmap/__tests__/processpanel.js new file mode 100644 index 0000000000..30c827aa56 --- /dev/null +++ b/gsa/src/web/components/processmap/__tests__/processpanel.js @@ -0,0 +1,447 @@ +/* Copyright (C) 2020 Greenbone Networks GmbH + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + */ +import React from 'react'; + +import {setLocale} from 'gmp/locale/lang'; + +import CollectionCounts from 'gmp/collection/collectioncounts'; + +import Filter from 'gmp/models/filter'; + +import {rendererWith, fireEvent} from 'web/utils/testing'; + +import {getMockProcessMap} from './processmap'; + +import ProcessPanel from '../processpanel'; + +setLocale('en'); +const {processes} = getMockProcessMap(); +const {'21': process1, '22': process2} = processes; + +const hosts = [ + {name: '123.456.78.910', id: '1234', severity: 5}, + {name: '109.876.54.321', id: '5678', severity: undefined}, +]; + +const getAllHosts = jest.fn().mockResolvedValue({ + data: [hosts], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, +}); + +describe('ProcessPanel tests', () => { + test('should render ProcessPanel', () => { + const handleAddHosts = jest.fn(); + const handleDeleteHost = jest.fn(); + const handleEditProcessClick = jest.fn(); + + const gmp = { + hosts: { + getAll: getAllHosts, + }, + }; + + const {render} = rendererWith({ + capabilities: true, + gmp, + router: true, + }); + + const {element, getAllByTestId} = render( + , + ); + + const header = element.querySelectorAll('th'); + const buttons = element.querySelectorAll('button'); + const icons = getAllByTestId('svg-icon'); + + // Title + expect(element).toHaveTextContent('No process selected'); + expect(icons[0]).toHaveAttribute('title', 'Edit process'); + + // Add Hosts + expect(buttons[0]).toHaveAttribute('title', 'Add Selected Hosts'); + expect(buttons[0]).toHaveTextContent('Add Selected Hosts'); + + // Host Table + expect(header[0]).toHaveTextContent('Host'); + expect(header[1]).toHaveTextContent('Severity'); + expect(header[2]).toHaveTextContent('Actions'); + + expect(element).not.toHaveTextContent( + 'No hosts associated with this process.', + ); + }); + + test('should render ProcessPanel with selected element without hosts', () => { + const handleAddHosts = jest.fn(); + const handleDeleteHost = jest.fn(); + const handleEditProcessClick = jest.fn(); + + const gmp = { + hosts: { + getAll: getAllHosts, + }, + }; + + const {render} = rendererWith({ + capabilities: true, + gmp, + router: true, + }); + + const {element, getAllByTestId} = render( + , + ); + + const header = element.querySelectorAll('th'); + const buttons = element.querySelectorAll('button'); + const icons = getAllByTestId('svg-icon'); + + // Title + expect(element).toHaveTextContent('lorem'); + expect(icons[0]).toHaveAttribute('title', 'Edit process'); + + // Add Hosts + expect(buttons[0]).toHaveAttribute('title', 'Add Selected Hosts'); + expect(buttons[0]).toHaveTextContent('Add Selected Hosts'); + + // Host Table + expect(header[0]).toHaveTextContent('Host'); + expect(header[1]).toHaveTextContent('Severity'); + expect(header[2]).toHaveTextContent('Actions'); + + expect(element).toHaveTextContent('No hosts associated with this process.'); + }); + + test('should render ProcessPanel with selected element with hosts', () => { + const handleAddHosts = jest.fn(); + const handleDeleteHost = jest.fn(); + const handleEditProcessClick = jest.fn(); + + const gmp = { + hosts: { + getAll: getAllHosts, + }, + }; + + const {render} = rendererWith({ + capabilities: true, + gmp, + router: true, + }); + + const {element, getAllByTestId} = render( + , + ); + + const header = element.querySelectorAll('th'); + const buttons = element.querySelectorAll('button'); + const detailsLinks = getAllByTestId('details-link'); + const icons = getAllByTestId('svg-icon'); + const progressBars = getAllByTestId('progressbar-box'); + + // Title + expect(element).toHaveTextContent('foo'); + expect(icons[0]).toHaveAttribute('title', 'Edit process'); + + // Add Hosts + expect(buttons[0]).toHaveAttribute('title', 'Add Selected Hosts'); + expect(buttons[0]).toHaveTextContent('Add Selected Hosts'); + + // Host Table + + // Headings + expect(header[0]).toHaveTextContent('Host'); + expect(header[1]).toHaveTextContent('Severity'); + expect(header[2]).toHaveTextContent('Actions'); + + // Row 1 + expect(detailsLinks[0]).toHaveAttribute('href', '/host/1234'); + expect(detailsLinks[0]).toHaveTextContent('123.456.78.910'); + expect(progressBars[0]).toHaveAttribute('title', 'Medium'); + expect(progressBars[0]).toHaveTextContent('5.0 (Medium)'); + expect(icons[1]).toHaveAttribute('title', 'Remove host from process'); + + // Row 2 + expect(detailsLinks[1]).toHaveAttribute('href', '/host/5678'); + expect(detailsLinks[1]).toHaveTextContent('109.876.54.321'); + expect(progressBars[1]).toHaveAttribute('title', 'N/A'); + expect(progressBars[1]).toHaveTextContent('N/A'); + expect(icons[2]).toHaveAttribute('title', 'Remove host from process'); + }); + + test('should call click handler', () => { + const handleAddHosts = jest.fn(); + const handleDeleteHost = jest.fn(); + const handleEditProcessClick = jest.fn(); + + const gmp = { + hosts: { + getAll: getAllHosts, + }, + }; + + const {render} = rendererWith({ + capabilities: true, + gmp, + router: true, + }); + + const {element, getAllByTestId} = render( + , + ); + + const buttons = element.querySelectorAll('button'); + const icons = getAllByTestId('svg-icon'); + + expect(icons[0]).toHaveAttribute('title', 'Edit process'); + fireEvent.click(icons[0]); + expect(handleEditProcessClick).toHaveBeenCalled(); + + expect(buttons[0]).toHaveAttribute('title', 'Add Selected Hosts'); + fireEvent.click(buttons[0]); + expect(handleAddHosts).toHaveBeenCalledWith([]); + + expect(icons[1]).toHaveAttribute('title', 'Remove host from process'); + fireEvent.click(icons[1]); + expect(handleDeleteHost).toHaveBeenCalledWith(hosts[0].id); + }); + + test('should not call click handler if no process is selected', () => { + const handleAddHosts = jest.fn(); + const handleDeleteHost = jest.fn(); + const handleEditProcessClick = jest.fn(); + + const gmp = { + hosts: { + getAll: getAllHosts, + }, + }; + + const {render} = rendererWith({ + capabilities: true, + gmp, + router: true, + }); + + const {element, getAllByTestId} = render( + , + ); + + const buttons = element.querySelectorAll('button'); + const icons = getAllByTestId('svg-icon'); + + expect(icons.length).toBe(1); + + expect(icons[0]).toHaveAttribute('title', 'Edit process'); + fireEvent.click(icons[0]); + expect(handleEditProcessClick).not.toHaveBeenCalled(); + + expect(buttons[0]).toHaveAttribute('title', 'Add Selected Hosts'); + fireEvent.click(buttons[0]); + expect(handleAddHosts).not.toHaveBeenCalled(); + }); + + test('should allow pagination', () => { + const handleAddHosts = jest.fn(); + const handleDeleteHost = jest.fn(); + const handleEditProcessClick = jest.fn(); + + const hosts2 = [ + {id: '01', severity: '10'}, + {id: '02', severity: '9'}, + {id: '03', severity: '9'}, + {id: '04', severity: '9'}, + {id: '05', severity: '9'}, + {id: '06', severity: '9'}, + {id: '07', severity: '9'}, + {id: '08', severity: '9'}, + {id: '09', severity: '9'}, + {id: '10', severity: '9'}, + {id: '11', severity: '9'}, + {id: '12', severity: '9'}, + {id: '13', severity: '9'}, + {id: '14', severity: '9'}, + {id: '15', severity: '9'}, + {id: '16', severity: '9'}, + {id: '17', severity: '9'}, + {id: '18', severity: '9'}, + {id: '19', severity: '9'}, + {id: '20', severity: '9'}, + {id: '21', severity: '9'}, + {id: '22', severity: '9'}, + {id: '23', severity: '9'}, + {id: '24', severity: '9'}, + {id: '25', severity: '9'}, + {id: '26', severity: '9'}, + {id: '27', severity: '9'}, + {id: '28', severity: '9'}, + {id: '29', severity: '9'}, + {id: '30', severity: '9'}, + {id: '31', severity: '5'}, + {id: '32', severity: '4'}, + {id: '33', severity: '4'}, + {id: '34', severity: '4'}, + {id: '35', severity: '4'}, + {id: '36', severity: '4'}, + {id: '37', severity: '4'}, + {id: '38', severity: '4'}, + {id: '39', severity: '4'}, + {id: '40', severity: '4'}, + {id: '41', severity: '4'}, + {id: '42', severity: '4'}, + {id: '43', severity: '4'}, + {id: '44', severity: '4'}, + {id: '45', severity: '4'}, + {id: '46', severity: '4'}, + {id: '47', severity: '4'}, + {id: '48', severity: '4'}, + {id: '49', severity: '4'}, + {id: '50', severity: '4'}, + {id: '51', severity: '4'}, + {id: '52', severity: '4'}, + {id: '53', severity: '4'}, + {id: '54', severity: '4'}, + {id: '55', severity: '4'}, + {id: '56', severity: '4'}, + {id: '57', severity: '4'}, + {id: '58', severity: '4'}, + {id: '59', severity: '4'}, + {id: '60', severity: '4'}, + {id: '61', severity: '0'}, + ]; + + const getAllHosts2 = jest.fn().mockResolvedValue({ + data: [hosts2], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, + }); + + const gmp = { + hosts: { + getAll: getAllHosts2, + }, + }; + + const {render} = rendererWith({ + capabilities: true, + gmp, + router: true, + }); + + const {element, getAllByTestId} = render( + , + ); + + const detailsLinks = getAllByTestId('details-link'); + let icons = getAllByTestId('svg-icon'); + + // 1. Page + expect(detailsLinks[0]).toHaveAttribute('href', '/host/01'); + expect(element).toHaveTextContent('1 - 30 of 61'); + + expect(icons[33]).toHaveAttribute('title', 'Next'); + fireEvent.click(icons[33]); + + // 2. Page + icons = getAllByTestId('svg-icon'); + + expect(detailsLinks[0]).toHaveAttribute('href', '/host/31'); + expect(element).toHaveTextContent('31 - 60 of 61'); + + expect(icons[33]).toHaveAttribute('title', 'Next'); + fireEvent.click(icons[33]); + + // 3. Page + icons = getAllByTestId('svg-icon'); + + expect(detailsLinks[0]).toHaveAttribute('href', '/host/61'); + expect(element).toHaveTextContent('61 - 61 of 61'); + + expect(icons[2]).toHaveAttribute('title', 'First'); + fireEvent.click(icons[2]); + + // 1. Page + icons = getAllByTestId('svg-icon'); + + expect(detailsLinks[0]).toHaveAttribute('href', '/host/01'); + expect(element).toHaveTextContent('1 - 30 of 61'); + + expect(icons[34]).toHaveAttribute('title', 'Last'); + fireEvent.click(icons[34]); + + // 3. Page + icons = getAllByTestId('svg-icon'); + + expect(detailsLinks[0]).toHaveAttribute('href', '/host/61'); + expect(element).toHaveTextContent('61 - 61 of 61'); + + expect(icons[3]).toHaveAttribute('title', 'Previous'); + fireEvent.click(icons[3]); + + // 2. Page + icons = getAllByTestId('svg-icon'); + + expect(detailsLinks[0]).toHaveAttribute('href', '/host/31'); + expect(element).toHaveTextContent('31 - 60 of 61'); + }); +}); diff --git a/gsa/src/web/components/processmap/__tests__/tools.js b/gsa/src/web/components/processmap/__tests__/tools.js new file mode 100644 index 0000000000..3bb0c59103 --- /dev/null +++ b/gsa/src/web/components/processmap/__tests__/tools.js @@ -0,0 +1,174 @@ +/* Copyright (C) 2020 Greenbone Networks GmbH + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + */ +import React from 'react'; + +import {setLocale} from 'gmp/locale/lang'; + +import {render, fireEvent} from 'web/utils/testing'; + +import Tools from '../tools'; + +setLocale('en'); + +describe('Tools tests', () => { + test('should render Tools', () => { + const handleCreateProcessClick = jest.fn(); + const handleDrawEdgeClick = jest.fn(); + const handleDeleteClick = jest.fn(); + const handleToggleConditionalColorization = jest.fn(); + + const {getByTestId} = render( + , + ); + + const newIcon = getByTestId('bpm-tool-icon-new'); + const edgeIcon = getByTestId('bpm-tool-icon-edge'); + const deleteIcon = getByTestId('bpm-tool-icon-delete'); + const colorIcon = getByTestId('bpm-tool-icon-color'); + const zoomInIcon = getByTestId('bpm-tool-icon-zoomin'); + const zoomResetIcon = getByTestId('bpm-tool-icon-zoomreset'); + const zoomOutIcon = getByTestId('bpm-tool-icon-zoomout'); + + expect(newIcon).toHaveAttribute('title', 'Create new process'); + + expect(edgeIcon).toHaveAttribute('title', 'Create new connection'); + expect(edgeIcon).not.toHaveStyleRule('background-color', '#66c430'); + + expect(deleteIcon).toHaveAttribute('title', 'Delete selected element'); + + expect(colorIcon).toHaveAttribute( + 'title', + 'Turn off conditional colorization', + ); + expect(colorIcon).not.toHaveStyleRule('background-color', '#66c430'); + + expect(zoomInIcon).toHaveAttribute('title', 'Zoom in'); + expect(zoomResetIcon).toHaveAttribute('title', 'Reset zoom'); + expect(zoomOutIcon).toHaveAttribute('title', 'Zoom out'); + }); + + test('should render active icons', () => { + const handleCreateProcessClick = jest.fn(); + const handleDrawEdgeClick = jest.fn(); + const handleDeleteClick = jest.fn(); + const handleToggleConditionalColorization = jest.fn(); + + const {getByTestId} = render( + , + ); + + const newIcon = getByTestId('bpm-tool-icon-new'); + const edgeIcon = getByTestId('bpm-tool-icon-edge'); + const deleteIcon = getByTestId('bpm-tool-icon-delete'); + const colorIcon = getByTestId('bpm-tool-icon-color'); + const zoomInIcon = getByTestId('bpm-tool-icon-zoomin'); + const zoomResetIcon = getByTestId('bpm-tool-icon-zoomreset'); + const zoomOutIcon = getByTestId('bpm-tool-icon-zoomout'); + + expect(newIcon).toHaveAttribute('title', 'Create new process'); + + expect(edgeIcon).toHaveAttribute('title', 'Create new connection'); + expect(edgeIcon).toHaveStyleRule('background-color', '#66c430'); + + expect(deleteIcon).toHaveAttribute('title', 'Delete selected element'); + + expect(colorIcon).toHaveAttribute( + 'title', + 'Turn on conditional colorization', + ); + expect(colorIcon).toHaveStyleRule('background-color', '#66c430'); + + expect(zoomInIcon).not.toHaveStyleRule('background-color', '#66c430'); + expect(zoomResetIcon).not.toHaveStyleRule('background-color', '#66c430'); + expect(zoomOutIcon).not.toHaveStyleRule('background-color', '#66c430'); + }); + + test('should call click handler', () => { + const handleCreateProcessClick = jest.fn(); + const handleDrawEdgeClick = jest.fn(); + const handleDeleteClick = jest.fn(); + const handleToggleConditionalColorization = jest.fn(); + const handleZoomChangeClick = jest.fn(); + + const {getByTestId} = render( + , + ); + + const newIcon = getByTestId('bpm-tool-icon-new'); + const edgeIcon = getByTestId('bpm-tool-icon-edge'); + const deleteIcon = getByTestId('bpm-tool-icon-delete'); + const colorIcon = getByTestId('bpm-tool-icon-color'); + const zoomInIcon = getByTestId('bpm-tool-icon-zoomin'); + const zoomResetIcon = getByTestId('bpm-tool-icon-zoomreset'); + const zoomOutIcon = getByTestId('bpm-tool-icon-zoomout'); + + expect(newIcon).toHaveAttribute('title', 'Create new process'); + fireEvent.click(newIcon); + expect(handleCreateProcessClick).toHaveBeenCalled(); + + expect(edgeIcon).toHaveAttribute('title', 'Create new connection'); + fireEvent.click(edgeIcon); + expect(handleDrawEdgeClick).toHaveBeenCalled(); + + expect(deleteIcon).toHaveAttribute('title', 'Delete selected element'); + fireEvent.click(deleteIcon); + expect(handleDeleteClick).toHaveBeenCalled(); + + expect(colorIcon).toHaveAttribute( + 'title', + 'Turn off conditional colorization', + ); + fireEvent.click(colorIcon); + expect(handleToggleConditionalColorization).toHaveBeenCalled(); + + expect(zoomInIcon).toHaveAttribute('title', 'Zoom in'); + fireEvent.click(zoomInIcon); + expect(handleZoomChangeClick).toHaveBeenCalledWith('+'); + + expect(zoomResetIcon).toHaveAttribute('title', 'Reset zoom'); + fireEvent.click(zoomResetIcon); + expect(handleZoomChangeClick).toHaveBeenCalledWith('0'); + + expect(zoomOutIcon).toHaveAttribute('title', 'Zoom out'); + fireEvent.click(zoomOutIcon); + expect(handleZoomChangeClick).toHaveBeenCalledWith('-'); + }); +}); diff --git a/gsa/src/web/components/processmap/__tests__/usecolorize.js b/gsa/src/web/components/processmap/__tests__/usecolorize.js new file mode 100644 index 0000000000..3eb8913891 --- /dev/null +++ b/gsa/src/web/components/processmap/__tests__/usecolorize.js @@ -0,0 +1,225 @@ +/* Copyright (C) 2020 Greenbone Networks GmbH + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + */ +import React from 'react'; + +import {setLocale} from 'gmp/locale/lang'; + +import {entitiesLoadingActions} from 'web/store/entities/hosts'; + +import { + LOG_VALUE, + LOW_VALUE, + MEDIUM_VALUE, + NA_VALUE, + HIGH_VALUE, +} from 'web/utils/severity'; + +import Theme from 'web/utils/theme'; + +import {rendererWith} from 'web/utils/testing'; + +import {hostsFilter} from '../processmaploader'; + +import useColorize from '../usecolorize'; + +setLocale('en'); + +const mockProcessMap2 = { + edges: { + 11: {id: 11, source: 21, target: 22, type: 'edge'}, + 12: {id: 12, source: 22, target: 23, type: 'edge'}, + 13: {id: 13, source: 21, target: 24, type: 'edge'}, + 14: {id: 14, source: 24, target: 25, type: 'edge'}, + 15: {id: 15, source: 21, target: 26, type: 'edge'}, + }, + processes: { + 21: {id: 21, tagId: 31}, + 22: {id: 22, tagId: 32}, + 23: {id: 23, tagId: 33}, + 24: {id: 24, tagId: 34}, + 25: {id: 25, tagId: 35}, + 26: {id: 26, tagId: 36}, + }, +}; + +const hostFilter1 = hostsFilter('31'); +const hostFilter2 = hostsFilter('32'); +const hostFilter3 = hostsFilter('33'); +const hostFilter4 = hostsFilter('34'); +const hostFilter5 = hostsFilter('35'); +const hostFilter6 = hostsFilter('36'); + +const hosts = [ + {name: '123.456.78.1', id: '41', severity: LOW_VALUE}, + {name: '123.456.78.2', id: '42', severity: MEDIUM_VALUE}, + {name: '123.456.78.3', id: '43', severity: HIGH_VALUE}, + {name: '123.456.78.4', id: '44', severity: LOG_VALUE}, + {name: '123.456.78.5', id: '45', severity: NA_VALUE}, +]; + +const TestHook = ({callback}) => { + callback(); + return null; +}; + +describe('UseColorize tests', () => { + test('should color with derived severity', () => { + const hosts1 = [hosts[1]]; + const hosts2 = [hosts[2]]; + const hosts3 = [hosts[0]]; + const hosts4 = [hosts[3]]; + const hosts5 = [hosts[4]]; + const hosts6 = []; + + const {render, store} = rendererWith({ + store: true, + }); + + const testHook = callback => { + render(); + }; + + store.dispatch( + entitiesLoadingActions.success(hosts1, hostFilter1, hostFilter1), + ); + store.dispatch( + entitiesLoadingActions.success(hosts2, hostFilter2, hostFilter2), + ); + store.dispatch( + entitiesLoadingActions.success(hosts3, hostFilter3, hostFilter3), + ); + store.dispatch( + entitiesLoadingActions.success(hosts4, hostFilter4, hostFilter5), + ); + store.dispatch( + entitiesLoadingActions.success(hosts5, hostFilter5, hostFilter5), + ); + store.dispatch( + entitiesLoadingActions.success(hosts6, hostFilter6, hostFilter6), + ); + + let coloredMap = {}; + testHook(() => (coloredMap = useColorize(mockProcessMap2, true))); + + expect(Object.entries(coloredMap).length).not.toBe(0); + + expect(coloredMap.processes[21].color).toBe(Theme.severityWarnYellow); + expect(coloredMap.processes[22].color).toBe(Theme.errorRed); + expect(coloredMap.processes[23].color).toBe(Theme.errorRed); + expect(coloredMap.processes[24].color).toBe(Theme.severityWarnYellow); + expect(coloredMap.processes[25].color).toBe(Theme.severityWarnYellow); + expect(coloredMap.processes[26].color).toBe(Theme.severityWarnYellow); + }); + + test('should color without conditional colorization', () => { + const hosts1 = [hosts[1]]; + const hosts2 = [hosts[2]]; + const hosts3 = [hosts[0]]; + const hosts4 = [hosts[3]]; + const hosts5 = [hosts[4]]; + const hosts6 = []; + + const {render, store} = rendererWith({ + store: true, + }); + + const testHook = callback => { + render(); + }; + + store.dispatch( + entitiesLoadingActions.success(hosts1, hostFilter1, hostFilter1), + ); + store.dispatch( + entitiesLoadingActions.success(hosts2, hostFilter2, hostFilter2), + ); + store.dispatch( + entitiesLoadingActions.success(hosts3, hostFilter3, hostFilter3), + ); + store.dispatch( + entitiesLoadingActions.success(hosts4, hostFilter4, hostFilter5), + ); + store.dispatch( + entitiesLoadingActions.success(hosts5, hostFilter5, hostFilter5), + ); + store.dispatch( + entitiesLoadingActions.success(hosts6, hostFilter6, hostFilter6), + ); + + let coloredMap = {}; + testHook(() => (coloredMap = useColorize(mockProcessMap2, false))); + + expect(Object.entries(coloredMap).length).not.toBe(0); + + expect(coloredMap.processes[21].color).toBe(Theme.severityWarnYellow); + expect(coloredMap.processes[22].color).toBe(Theme.errorRed); + expect(coloredMap.processes[23].color).toBe(Theme.severityLowBlue); + expect(coloredMap.processes[24].color).toBe(Theme.lightGray); + expect(coloredMap.processes[25].color).toBe(Theme.mediumGray); + expect(coloredMap.processes[26].color).toBe(Theme.white); + }); + + test('should not forward log severity', () => { + const hosts1 = [hosts[3]]; + const hosts2 = [hosts[2]]; + const hosts3 = [hosts[0]]; + const hosts4 = [hosts[1]]; + const hosts5 = [hosts[4]]; + const hosts6 = []; + + const {render, store} = rendererWith({ + store: true, + }); + + const testHook = callback => { + render(); + }; + + store.dispatch( + entitiesLoadingActions.success(hosts1, hostFilter1, hostFilter1), + ); + store.dispatch( + entitiesLoadingActions.success(hosts2, hostFilter2, hostFilter2), + ); + store.dispatch( + entitiesLoadingActions.success(hosts3, hostFilter3, hostFilter3), + ); + store.dispatch( + entitiesLoadingActions.success(hosts4, hostFilter4, hostFilter5), + ); + store.dispatch( + entitiesLoadingActions.success(hosts5, hostFilter5, hostFilter5), + ); + store.dispatch( + entitiesLoadingActions.success(hosts6, hostFilter6, hostFilter6), + ); + + let coloredMap = {}; + testHook(() => (coloredMap = useColorize(mockProcessMap2, true))); + + expect(Object.entries(coloredMap).length).not.toBe(0); + + expect(coloredMap.processes[21].color).toBe(Theme.lightGray); + expect(coloredMap.processes[22].color).toBe(Theme.errorRed); + expect(coloredMap.processes[23].color).toBe(Theme.errorRed); + expect(coloredMap.processes[24].color).toBe(Theme.severityWarnYellow); + expect(coloredMap.processes[25].color).toBe(Theme.severityWarnYellow); + expect(coloredMap.processes[26].color).toBe(Theme.white); + }); +}); diff --git a/gsa/src/web/components/processmap/__tests__/utils.js b/gsa/src/web/components/processmap/__tests__/utils.js new file mode 100644 index 0000000000..a2da070726 --- /dev/null +++ b/gsa/src/web/components/processmap/__tests__/utils.js @@ -0,0 +1,99 @@ +/* Copyright (C) 2020 Greenbone Networks GmbH + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + */ +import {createTag, editTag, deleteTag} from '../utils'; + +const create = jest.fn().mockResolvedValue({ + foo: 'bar', +}); + +const deleteFunc = jest.fn().mockResolvedValue({ + foo: 'bar', +}); + +const save = jest.fn().mockResolvedValue({ + foo: 'bar', +}); + +const gmp = { + tag: { + create, + delete: deleteFunc, + save, + }, +}; +const process = { + name: 'lorem', + tagId: 31, +}; + +describe('processmap utils tests', () => { + test('should call create command', () => { + const {name} = process; + const input = { + active: '1', + name: name, + resource_type: 'host', + }; + + createTag({name, gmp}); + expect(create).toHaveBeenCalledWith(input); + }); + + test('should call save command for adding hosts', () => { + const {name, tagId} = process; + const hostIds = ['1a', '2b']; + const input = { + active: '1', + name: name, + id: tagId, + resource_ids: hostIds, + resource_type: 'host', + resources_action: 'add', + }; + + editTag({hostIds, name, tagId, gmp}); + expect(save).toHaveBeenCalledWith(input); + }); + + test('should call save command for deleting hosts', () => { + const {name, tagId} = process; + const hostIds = ['1a']; + const input = { + active: '1', + name: name, + id: tagId, + resource_ids: hostIds, + resource_type: 'host', + resources_action: 'remove', + }; + + editTag({action: 'remove', hostIds, name, tagId, gmp}); + expect(save).toHaveBeenCalledWith(input); + }); + + test('should call delete command', () => { + const {tagId} = process; + const input = { + id: tagId, + }; + + deleteTag({tagId, gmp}); + expect(deleteFunc).toHaveBeenCalledWith(input); + }); +}); diff --git a/gsa/src/web/components/processmap/background.js b/gsa/src/web/components/processmap/background.js new file mode 100644 index 0000000000..ba1adb74e7 --- /dev/null +++ b/gsa/src/web/components/processmap/background.js @@ -0,0 +1,48 @@ +/* Copyright (C) 2020 Greenbone Networks GmbH + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import React from 'react'; +import PropTypes from 'web/utils/proptypes'; +import Theme from 'web/utils/theme'; + +const DEFAULT_SIZE = 8000; +const DEFAULT_COLOR = Theme.dialogGray; + +const Background = ({ + color = DEFAULT_COLOR, + height = DEFAULT_SIZE, + width = DEFAULT_SIZE, +}) => ( + +); + +Background.propTypes = { + color: PropTypes.string, + height: PropTypes.number, + width: PropTypes.number, +}; + +export default Background; + +// vim: set ts=2 sw=2 tw=80: diff --git a/gsa/src/web/components/processmap/createprocessdialog.js b/gsa/src/web/components/processmap/createprocessdialog.js new file mode 100644 index 0000000000..4f39f7b967 --- /dev/null +++ b/gsa/src/web/components/processmap/createprocessdialog.js @@ -0,0 +1,91 @@ +/* Copyright (C) 2020 Greenbone Networks GmbH + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import React from 'react'; + +import _ from 'gmp/locale'; + +import {isDefined} from 'gmp/utils/identity'; + +import PropTypes from 'web/utils/proptypes'; + +import SaveDialog from 'web/components/dialog/savedialog'; + +import FormGroup from 'web/components/form/formgroup'; +import TextField from 'web/components/form/textfield'; + +import Layout from 'web/components/layout/layout'; + +const CreateProcessDialog = ({ + comment = '', + id, + name = _('Unnamed'), + onClose, + onCreate, + onEdit, +}) => { + const isEdit = isDefined(id); + const title = isEdit ? _('Edit Process') : _('Create Process'); + const buttonTitle = isEdit ? _('Save') : _('Create'); + const onSave = isEdit ? onEdit : onCreate; + return ( + + {({values, onValueChange}) => ( + + + + + + + + + )} + + ); +}; + +CreateProcessDialog.propTypes = { + comment: PropTypes.string, + id: PropTypes.string, + name: PropTypes.string, + onClose: PropTypes.func.isRequired, + onCreate: PropTypes.func, + onEdit: PropTypes.func, +}; + +export default CreateProcessDialog; + +// vim: set ts=2 sw=2 tw=80: diff --git a/gsa/src/web/components/processmap/edge.js b/gsa/src/web/components/processmap/edge.js new file mode 100644 index 0000000000..ab0d98d668 --- /dev/null +++ b/gsa/src/web/components/processmap/edge.js @@ -0,0 +1,104 @@ +/* Copyright (C) 2020 Greenbone Networks GmbH + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import React from 'react'; +import styled from 'styled-components'; + +import PropTypes from 'web/utils/proptypes'; +import Theme from 'web/utils/theme'; + +const StyledG = styled.g` + & line { + stroke: ${props => (props.isSelected ? Theme.mediumBlue : Theme.darkGray)}; + } + &:hover line { + stroke: ${Theme.mediumBlue}; + } +`; + +const calculateLine = ({source, target}) => { + const middleX = (target.x + source.x) / 2; + const middleY = (target.y + source.y) / 2; + return ( + source.x + + ',' + + source.y + + ' ' + + middleX + + ',' + + middleY + + ' ' + + target.x + + ',' + + target.y + ); +}; + +const Edge = ({cursor, isSelected = false, source, target, onMouseDown}) => { + return ( + + + + + + + + {/* in order to be able to use midMarker this invisible polyline is + rendered to position the arrow head. The polyline can not be hovered + or selected, though, so for element selection the is used */} + + + ); +}; + +Edge.propTypes = { + cursor: PropTypes.string, + isSelected: PropTypes.bool, + name: PropTypes.string, + radius: PropTypes.number, + source: PropTypes.shape({x: PropTypes.number, y: PropTypes.number}) + .isRequired, + target: PropTypes.shape({x: PropTypes.number, y: PropTypes.number}) + .isRequired, + onMouseDown: PropTypes.func, +}; + +export default Edge; + +// vim: set ts=2 sw=2 tw=80: diff --git a/gsa/src/web/components/processmap/hosttable.js b/gsa/src/web/components/processmap/hosttable.js new file mode 100644 index 0000000000..9587cd3a1b --- /dev/null +++ b/gsa/src/web/components/processmap/hosttable.js @@ -0,0 +1,121 @@ +/* Copyright (C) 2020 Greenbone Networks GmbH + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import React from 'react'; + +import styled from 'styled-components'; + +import _ from 'gmp/locale'; + +import {isDefined} from 'gmp/utils/identity'; + +import SeverityBar from 'web/components/bar/severitybar'; + +import DeleteIcon from 'web/components/icon/deleteicon'; + +import Layout from 'web/components/layout/layout'; + +import DetailsLink from 'web/components/link/detailslink'; + +import Body from 'web/components/table/body'; +import Data from 'web/components/table/data'; +import Head from 'web/components/table/head'; +import Header from 'web/components/table/header'; +import Row from 'web/components/table/row'; +import StripedTable from 'web/components/table/stripedtable'; + +import {Col} from 'web/entity/page'; + +import PropTypes from 'web/utils/proptypes'; + +const StyledDiv = styled.div` + padding: 5px; +`; + +const HostRow = ({host, onDeleteHost}) => { + const {id, name, severity} = host; + return ( + + + + {name} + + + + + + + onDeleteHost(id)} + /> + + + ); +}; + +HostRow.propTypes = { + host: PropTypes.object, + onDeleteHost: PropTypes.func.isRequired, +}; + +const StyledLayout = styled(Layout)` + width: 100%; + overflow: auto; +`; + +const HostTable = ({hosts, onDeleteHost}) => { + return ( + + + + + + + +
+ + {_('Host')} + {_('Severity')} + {_('Actions')} + +
+ {isDefined(hosts) && hosts.length > 0 && ( + + {hosts.map((host, i) => ( + + ))} + + )} +
+ {isDefined(hosts) && hosts.length === 0 && ( + {_('No hosts associated with this process.')} + )} +
+ ); +}; + +HostTable.propTypes = { + hosts: PropTypes.array, + onDeleteHost: PropTypes.func.isRequired, +}; + +export default HostTable; + +// vim: set ts=2 sw=2 tw=80: diff --git a/gsa/src/web/components/processmap/processmap.js b/gsa/src/web/components/processmap/processmap.js new file mode 100644 index 0000000000..1e0871706d --- /dev/null +++ b/gsa/src/web/components/processmap/processmap.js @@ -0,0 +1,777 @@ +/* Copyright (C) 2020 Greenbone Networks GmbH + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import 'core-js/features/object/keys'; + +import React from 'react'; +import styled from 'styled-components'; + +import {connect} from 'react-redux'; + +import uuid from 'uuid/v4'; + +import {_} from 'gmp/locale/lang'; + +import {isDefined} from 'gmp/utils/identity'; +import {exclude} from 'gmp/utils/object'; + +import Group from 'web/components/chart/group'; +import ToolTip from 'web/components/chart/tooltip'; +import ConfirmationDialog from 'web/components/dialog/confirmationdialog'; +import ErrorBoundary from 'web/components/error/errorboundary'; + +import {selector as hostSelector} from 'web/store/entities/hosts'; + +import {saveBusinessProcessMap} from 'web/store/businessprocessmaps/actions'; +import {renewSessionTimeout} from 'web/store/usersettings/actions'; + +import compose from 'web/utils/compose'; +import PropTypes from 'web/utils/proptypes'; +import withGmp from 'web/utils/withGmp'; + +import Background from './background'; +import CreateProcessDialog from './createprocessdialog'; +import Edge from './edge'; +import ProcessNode from './processnode'; +import ProcessPanel from './processpanel'; +import Tools from './tools'; + +import {createTag, createToolTipContent, deleteTag, editTag} from './utils'; + +const DEFAULT_PROCESS_SIZE = 75; + +const BPM_TAG_PREFIX = 'myBP:'; + +const SCROLL_STEP = 0.1; + +const MAX_SCALE = 1.6; +const MIN_SCALE = 0.3; + +const Wrapper = styled.div` + display: flex; + position: relative; + height: 100%; + width: 100%; +`; + +const Map = styled.svg` + display: flex; + position: relative; + width: 100%; + align-content: stretch; + cursor: ${props => props.cursor}; +`; + +class ProcessMap extends React.Component { + constructor(...args) { + super(...args); + + this.state = { + confirmDeleteDialogVisible: false, + createProcessDialogVisible: false, + edgeDrawSource: undefined, + edgeDrawTarget: undefined, + scale: 1.0, + translateX: 0, + translateY: 0, + isDraggingBackground: false, + isDraggingProcess: false, + isDrawingEdge: false, + }; + + this.svg = React.createRef(); + + this.handleMouseDown = this.handleMouseDown.bind(this); + this.handleMouseUp = this.handleMouseUp.bind(this); + this.handleMouseMove = this.handleMouseMove.bind(this); + this.handleMouseWheel = this.handleMouseWheel.bind(this); + + this.handleCloseCreateProcessDialog = this.handleCloseCreateProcessDialog.bind( + this, + ); + this.handleAddHosts = this.handleAddHosts.bind(this); + this.handleDeleteHosts = this.handleDeleteHosts.bind(this); + this.handleDrawEdge = this.handleDrawEdge.bind(this); + this.handleCreateEdge = this.handleCreateEdge.bind(this); + this.handleCreateProcess = this.handleCreateProcess.bind(this); + this.handleInteraction = this.handleInteraction.bind(this); + this.handleSelectElement = this.handleSelectElement.bind(this); + this.handleValueChange = this.handleValueChange.bind(this); + this.handleProcessChange = this.handleProcessChange.bind(this); + this.handleDeleteElement = this.handleDeleteElement.bind(this); + + this.keyDownListener = this.keyDownListener.bind(this); + this.handleOpenCreateProcessDialog = this.handleOpenCreateProcessDialog.bind( + this, + ); + this.handleZoomChange = this.handleZoomChange.bind(this); + this.openCreateProcessDialog = this.openCreateProcessDialog.bind(this); + this.openConfirmDeleteDialog = this.openConfirmDeleteDialog.bind(this); + this.closeConfirmDeleteDialog = this.closeConfirmDeleteDialog.bind(this); + this.saveMaps = this.saveMaps.bind(this); + } + + static getDerivedStateFromProps(props, state) { + if (isDefined(props.processMaps)) { + return { + ...state, + edges: props.processMaps.edges, + processes: props.processMaps.processes, + }; + } + return { + ...state, + }; + } + + componentDidMount() { + document.addEventListener('keydown', this.keyDownListener); + } + + componentWillUnmount() { + document.removeEventListener('keydown', this.keyDownListener); + } + + componentDidUpdate() { + const {isDrawingEdge, edgeDrawSource, edgeDrawTarget} = this.state; + if ( + isDrawingEdge && + isDefined(edgeDrawSource) && + isDefined(edgeDrawTarget) && + edgeDrawSource !== edgeDrawTarget + ) { + this.handleCreateEdge(edgeDrawSource, edgeDrawTarget); + } + } + + zoom(px, py, scale) { + const {x, y} = this.toChartCoords(px, py); + + // calculate new pixel coords and afterwards diff to previous coords + const diffX = x * scale - px; + const diffY = y * scale - py; + + this.setState({ + scale, + translateX: -diffX, + translateY: -diffY, + }); + } + + zoomIn(px, py) { + let {scale} = this.state; + + if (scale === MAX_SCALE) { + // avoid setting state and rerendering + return; + } + + scale = scale + SCROLL_STEP; + + if (scale > MAX_SCALE) { + // limit min scale + scale = MAX_SCALE; + } + + this.zoom(px, py, scale); + } + + zoomOut(px, py) { + let {scale} = this.state; + + if (scale === MIN_SCALE) { + // avoid setting state and rerendering + return; + } + + scale = scale - SCROLL_STEP; + + if (scale < MIN_SCALE) { + // limit scale + scale = MIN_SCALE; + } + + this.zoom(px, py, scale); + } + + toChartCoords(x, y) { + const {scale, translateX, translateY} = this.state; + // transform pixel coords into chart coords + return { + x: (x - translateX) / scale, + y: (y - translateY) / scale, + }; + } + + handleMouseWheel(event) { + const {x, y} = this.getMousePosition(event); + + // event.deltaY returns nagative values for mouse wheel up and positive values for mouse wheel down + const isZoomOut = Math.sign(event.deltaY) === 1; + + isZoomOut ? this.zoomOut(x, y) : this.zoomIn(x, y); + } + + handleZoomChange(dir) { + const {scale} = this.state; + let zoomDir; + if (dir === '+') { + zoomDir = 1; + } else if (dir === '-') { + zoomDir = -1; + } + let newScale; + if (dir === '0') { + return this.setState({ + scale: 1, + translateX: 0, + translateY: 0, + }); + } + newScale = scale + zoomDir * SCROLL_STEP; + if (newScale > MAX_SCALE) { + newScale = MAX_SCALE; + } + if (newScale < MIN_SCALE) { + newScale = MIN_SCALE; + } + return this.setState({scale: newScale}); + } + + keyDownListener(event) { + const {createProcessDialogVisible} = this.state; + switch (event.key) { + case 'Delete': + if ( + !createProcessDialogVisible && + isDefined(this.selectedElement) && + this.selectedElement.type === 'process' + ) { + this.openConfirmDeleteDialog(); + } else if ( + isDefined(this.selectedElement) && + !createProcessDialogVisible + ) { + this.handleDeleteElement(); + } + break; + case 'Escape': + this.setState({isDrawingEdge: false}); + break; + default: { + break; + } + } + } + + saveMaps({processes = this.state.processes, edges = this.state.edges}) { + const {mapId, saveUpdatedMaps} = this.props; + + const updatedMaps = { + [mapId]: { + processes, + edges, + }, + // ...otherMaps; + }; + saveUpdatedMaps(updatedMaps); + } + + handleDeleteElement() { + if (isDefined(this.selectedElement)) { + const {id} = this.selectedElement; + const {processes, edges} = this.state; + if (isDefined(id)) { + if (this.selectedElement && this.selectedElement.type === 'edge') { + delete edges[id]; + + this.setState({ + edges, + }); + } else { + // get all the edges that don't have the process as source or target + // and exclude those from all edges in order to get a list of attached + // edges + const attachedEdges = exclude(edges, key => { + return id !== edges[key].target && id !== edges[key].source; + }); + for (const edge in attachedEdges) { + delete edges[edge]; + } + deleteTag({tagId: processes[id].tagId, gmp: this.props.gmp}); + delete processes[id]; + this.setState({ + edges, + processes, + }); + } + this.selectedElement = undefined; + this.saveMaps({processes, edges}); + } + } + this.handleInteraction(); + } + + getMousePosition(event) { + const {clientX, clientY} = event; + const {left, top} = this.svg.current.getBoundingClientRect(); + + return { + x: clientX - left, + y: clientY - top, + }; + } + + handleOpenCreateProcessDialog() { + this.selectedElement = undefined; // the dialog will otherwise become an edit dialog + this.setState({createProcessDialogVisible: true}); + } + + openCreateProcessDialog() { + this.setState({createProcessDialogVisible: true}); + } + + closeCreateProcessDialog() { + this.setState({createProcessDialogVisible: false}); + } + + openConfirmDeleteDialog() { + this.setState({confirmDeleteDialogVisible: true}); + } + + closeConfirmDeleteDialog() { + this.setState({confirmDeleteDialogVisible: false}); + } + + handleCloseCreateProcessDialog() { + this.closeCreateProcessDialog(); + } + + handleValueChange(value, name) { + this.setState({[name]: value}); + } + + handleCreateProcess(process) { + let {processes = {}, translateX, translateY} = this.state; + const {name, comment} = process; + const id = uuid(); + createTag({name: BPM_TAG_PREFIX + name, gmp: this.props.gmp}).then( + newTagId => { + processes = { + [id]: { + name, + comment, + id, + tagId: newTagId, + x: 150 - translateX, // create Node next to Toolbar + y: 100 - translateY, // + type: 'process', + }, + ...processes, + }; + + this.closeCreateProcessDialog(); + + this.setState(() => ({ + processes, + })); + this.saveMaps({processes}); + }, + ); + this.handleInteraction(); + } + + handleProcessChange(newProcess) { + const {processes} = this.state; + const {id} = newProcess; + const oldProcess = processes[id]; + + processes[id] = {...oldProcess, ...newProcess}; // merge new info into process + + this.closeCreateProcessDialog(); + + this.selectedElement = processes[id]; + + this.setState(() => ({ + processes, + })); + editTag({ + name: BPM_TAG_PREFIX + processes[id].name, + tagId: processes[id].tagId, + gmp: this.props.gmp, + }); + this.saveMaps({processes}); + this.handleInteraction(); + } + + handleAddHosts(hostIds) { + editTag({ + hostIds, + name: BPM_TAG_PREFIX + this.selectedElement.name, + tagId: this.selectedElement.tagId, + gmp: this.props.gmp, + }).then(() => { + this.props.forceUpdate(); + }); + this.handleInteraction(); + } + + handleDeleteHosts(hostId) { + editTag({ + action: 'remove', + hostIds: [hostId], + name: BPM_TAG_PREFIX + this.selectedElement.name, + tagId: this.selectedElement.tagId, + gmp: this.props.gmp, + }).then(() => { + this.props.forceUpdate(); + }); + this.handleInteraction(); + } + + handleDrawEdge() { + this.setState({isDrawingEdge: true}); + } + + handleCreateEdge(source, target) { + let {edges = {}} = this.state; + const id = uuid(); + edges = { + [id]: { + source: source.id, + target: target.id, + id, + type: 'edge', + }, + ...edges, + }; + this.selectedElement = edges[id]; + this.setState(() => ({ + edges, + isDrawingEdge: false, + edgeDrawSource: undefined, + edgeDrawTarget: undefined, + })); + this.saveMaps({edges}); + this.handleInteraction(); + } + + handleSelectElement(event, element) { + event.stopPropagation(); + const {isDrawingEdge, edgeDrawSource, edgeDrawTarget} = this.state; + + if (isDrawingEdge) { + if (element.type === 'process' && !isDefined(edgeDrawSource)) { + this.draggingElement = undefined; + this.selectedElement = element; + + this.setState({edgeDrawSource: element}); + } else if (element.type === 'process' && !isDefined(edgeDrawTarget)) { + this.setState({edgeDrawTarget: element}); + } + } + this.props.onSelectElement(element); + this.selectedElement = element; + this.draggingElement = element; + this.handleInteraction(); + } + + handleMouseDown(event) { + this.coords = { + x: event.pageX, + y: event.pageY, + }; + this.selectedElement = undefined; + this.setState({ + isDraggingBackground: true, + isDrawingEdge: false, + edgeDrawSource: undefined, + edgeDrawTarget: undefined, + }); + } + + handleMouseUp(event) { + if (isDefined(this.draggingElement)) { + this.draggingElement.dx = undefined; + this.draggingElement.dy = undefined; + this.draggingElement = undefined; + } + this.coords = {}; + if (this.state.isDraggingProcess) { + // if dragging just stopped + this.saveMaps({}); + } + this.setState({isDraggingBackground: false, isDraggingProcess: false}); + } + + handleMouseMove(event) { + if ( + isDefined(this.draggingElement) && + this.draggingElement.type !== 'edge' + ) { + // we are dragging an element + const {x: mx, y: my} = this.getMousePosition(event); + const {x, y} = this.toBackgroundCoords(mx, my); + this.draggingElement.x = x; + this.draggingElement.y = y; + this.setState({isDraggingProcess: true}); + return; + } + + if (!this.state.isDraggingBackground) { + // we aren't dragging anything + return; + } + + event.preventDefault(); + + const xDiff = this.coords.x - event.pageX; + const yDiff = this.coords.y - event.pageY; + + this.coords.x = event.pageX; + this.coords.y = event.pageY; + + this.setState(state => ({ + translateX: state.translateX - xDiff, + translateY: state.translateY - yDiff, + })); + } + + toBackgroundCoords = (x, y) => { + const {translateX, translateY} = this.state; + return { + x: x - translateX, + y: y - translateY, + }; + }; + + getCoordinates = processId => { + const {processes} = this.state; + const processNode = processes[processId]; + return {x: processNode.x, y: processNode.y}; + }; + + handleInteraction() { + const {onInteraction} = this.props; + if (isDefined(onInteraction)) { + onInteraction(); + } + } + + render() { + const { + confirmDeleteDialogVisible, + createProcessDialogVisible, + edges = {}, + isDraggingBackground, + isDraggingProcess, + isDrawingEdge, + processes = {}, + scale, + translateX, + translateY, + } = this.state; + const cursorType = isDraggingBackground ? 'move' : 'default'; + const processCursorType = isDraggingProcess ? 'move' : 'grab'; + + const deleteHandler = + isDefined(this.selectedElement) && this.selectedElement.type === 'process' + ? this.openConfirmDeleteDialog + : this.handleDeleteElement; + + return ( + + + + + + {Object.keys(edges).map(key => { + const isSelected = edges[key] === this.selectedElement; + const {id} = edges[key]; + const source = this.getCoordinates(edges[key].source); + const target = this.getCoordinates(edges[key].target); + return ( + + this.handleSelectElement(event, edges[key]) + } + /> + ); + })} + {Object.keys(processes).map(key => { + const { + color, + comment, + derivedSeverity, + id, + name, + severity, + x, + y, + } = processes[key]; + const isSelected = processes[key] === this.selectedElement; + return ( + + {({targetRef, hide, show}) => ( + + this.handleSelectElement(event, processes[key]) + } + /> + )} + + ); + })} + + + 1 + } + showNoProcessHelper={Object.entries(processes).length < 1} + onCreateProcessClick={this.handleOpenCreateProcessDialog} + onDrawEdgeClick={this.handleDrawEdge} + onDeleteClick={deleteHandler} + onToggleConditionalColorization={ + this.props.onToggleConditionalColorization + } + onZoomChangeClick={this.handleZoomChange} + /> + + + {createProcessDialogVisible && ( + + )} + {confirmDeleteDialogVisible && ( + this.closeConfirmDeleteDialog()} + onResumeClick={() => { + this.handleDeleteElement(); + this.closeConfirmDeleteDialog(); + }} + /> + )} + + ); + } +} + +const mapStateToProps = (rootState, {hostFilter}) => { + const hostSel = hostSelector(rootState); + return { + hostList: hostSel.getEntities(hostFilter), + }; +}; + +const mapDispatchToProps = (dispatch, {gmp}) => { + return { + saveUpdatedMaps: updatedMaps => + dispatch(saveBusinessProcessMap(gmp)(updatedMaps)), + onInteraction: () => dispatch(renewSessionTimeout(gmp)()), + }; +}; + +ProcessMap.propTypes = { + applyConditionalColorization: PropTypes.bool, + forceUpdate: PropTypes.func.isRequired, + gmp: PropTypes.gmp.isRequired, + hostList: PropTypes.array, + mapId: PropTypes.id, // isRequired + processMaps: PropTypes.object, + saveUpdatedMaps: PropTypes.func.isRequired, + onInteraction: PropTypes.func.isRequired, + onSelectElement: PropTypes.func.isRequired, + onToggleConditionalColorization: PropTypes.func.isRequired, +}; + +export default compose( + withGmp, + connect(mapStateToProps, mapDispatchToProps), +)(ProcessMap); + +// vim: set ts=2 sw=2 tw=80: diff --git a/gsa/src/web/components/processmap/processmaploader.js b/gsa/src/web/components/processmap/processmaploader.js new file mode 100644 index 0000000000..d404e64f37 --- /dev/null +++ b/gsa/src/web/components/processmap/processmaploader.js @@ -0,0 +1,216 @@ +/* Copyright (C) 2020 Greenbone Networks GmbH + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import React, {useEffect, useState} from 'react'; + +import {useSelector, useDispatch} from 'react-redux'; + +import {_} from 'gmp/locale/lang'; + +import Filter from 'gmp/models/filter'; + +import {isDefined} from 'gmp/utils/identity'; + +import Loading from 'web/components/loading/loading'; +import ConfirmationDialog from 'web/components/dialog/confirmationdialog'; + +import { + loadEntities as loadHosts, + selector as hostSelector, +} from 'web/store/entities/hosts'; + +import PropTypes from 'web/utils/proptypes'; + +import useGmp from 'web/utils/useGmp'; + +import {loadBusinessProcessMaps} from 'web/store/businessprocessmaps/actions'; + +import useColorize from './usecolorize'; + +export const MAX_HOSTS_PER_PROCESS = 100; + +export const hostsFilter = id => + Filter.fromString( + 'tag_id=' + id + ' first=1 rows=' + (MAX_HOSTS_PER_PROCESS + 1), + ); + +const createDialogContent = failedTags => { + return ( + +
+ {_( + 'While loading the processes one or more corresponding tag(s) ' + + 'could not be found. Try reloading the map. If that does not help ' + + 'check the trashcan for those tags. If the tags are not there, ' + + 'affected processes need to be re-created by hand.', + )} +
+
+
+ {_('Affected processes:')} +
+ {failedTags.map((tag, index) => { + return ( + + {_('Process: "{{name}}", tag ID: {{tagId}}', { + name: tag.processName, + tagId: tag.tagId, + })} +
+
+ ); + })} +
+
+ ); +}; + +const ProcessMapsLoader = ({children, mapId = '1'}) => { + // TODO '1' is an ID that needs to be dynamically changed when >1 maps are + // loaded and must be replaced by a uuid. The dashboard display needs to know + // via dashboard settings, which map it has to render + + const dispatch = useDispatch(); + const gmp = useGmp(); + + const [ + applyConditionalColorization, + setApplyConditionalColorization, + ] = useState(true); + const [confirmDialogVisible, setConfirmDialogVisible] = useState(false); + const [dialogShownOnce, setDialogShownOnce] = useState(false); + const [failedTags, setFailedTags] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [selectedElement, setSelectedElement] = useState({}); + const [update, setUpdate] = useState(false); + + // get map from store + const processMap = useSelector(state => { + return isDefined(state.businessProcessMaps[mapId]) + ? state.businessProcessMaps[mapId] + : {}; + }); + + // load map if processMap is empty + useEffect(() => { + if (Object.entries(processMap).length === 0) { + dispatch(loadBusinessProcessMaps(gmp)()); + } + }, []); // eslint-disable-line react-hooks/exhaustive-deps + // used to prevent infinite reloads and deal with an empty usersetting + // no dep-array or a non-empty one will not prevent reloads + + // currently used to force an update when new hosts are added + const forceUpdate = () => setUpdate(!update); + + // keep track of currently selected element + const handleSelectElement = selEl => { + return setSelectedElement(selEl); + }; + + // create a filter for each individual tagId used to load all hosts associated + // to a specific process + const hostFilter = hostsFilter(selectedElement.tagId); + + // loop through all processes and load their associated hosts via individual + // host filters + useEffect(() => { + const processMapsTemp = isDefined(processMap) ? processMap : {}; + let tempHostFilter; + const tmpFailedTags = []; + for (const proc in processMapsTemp.processes) { + const {tagId, name} = processMapsTemp.processes[proc]; + // check whether a tag with tagId exists prior to loading hosts + /* eslint-disable handle-callback-err */ + gmp.tag.get({id: tagId}).catch(err => { + /* eslint-enable */ + tmpFailedTags.push({processName: name, tagId}); + }); + tempHostFilter = hostsFilter(tagId); + dispatch(loadHosts(gmp)(tempHostFilter)); + setFailedTags(tmpFailedTags); + } + }, [processMap, update, dispatch, gmp]); + + // in combination with the next useEffect(), this will update the map once + // it is loaded to apply the correct colorization + const isLoadingHosts = useSelector(rootState => { + const hostSel = hostSelector(rootState); + return hostSel.isLoadingAnyEntities(); + }); + useEffect(() => { + setIsLoading(false); + }, [isLoadingHosts]); + + const handleToggleConditionalColorization = () => { + setApplyConditionalColorization(!applyConditionalColorization); + }; + + const coloredProcessMap = useColorize( + processMap, + applyConditionalColorization, + ); + + // use to show dialog, if there are processes without an existing tag + useEffect(() => { + if (failedTags.length > 0 && !isLoading && !dialogShownOnce) { + setConfirmDialogVisible(true); + } + }, [failedTags.length, isLoading, dialogShownOnce]); + + return ( + + {isLoading ? ( + + ) : ( + children({ + applyConditionalColorization, + hostFilter, + isLoading, + mapId, + processMaps: coloredProcessMap, + forceUpdate, + onSelectElement: handleSelectElement, + onToggleConditionalColorization: handleToggleConditionalColorization, + }) + )} + {confirmDialogVisible && ( + { + setConfirmDialogVisible(false); + setDialogShownOnce(true); + }} + onResumeClick={() => { + setConfirmDialogVisible(false); + setDialogShownOnce(true); + }} + /> + )} + + ); +}; + +ProcessMapsLoader.propTypes = { + mapId: PropTypes.string, // TODO change this to uuid +}; + +export default ProcessMapsLoader; diff --git a/gsa/src/web/components/processmap/processnode.js b/gsa/src/web/components/processmap/processnode.js new file mode 100644 index 0000000000..386b4fd928 --- /dev/null +++ b/gsa/src/web/components/processmap/processnode.js @@ -0,0 +1,149 @@ +/* Copyright (C) 2020 Greenbone Networks GmbH + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import React from 'react'; +import styled, {keyframes} from 'styled-components'; + +import PropTypes from 'web/utils/proptypes'; +import Theme from 'web/utils/theme'; + +const StyledText = styled.text` + font-family: ${Theme.Font.default}; + text-anchor: middle; + font-size: 1em; + user-select: none; + max-width: 70px; +`; + +const StyledCircle = styled.circle` + cursor: ${props => props.cursor}; + stroke: ${props => (props.isSelected ? Theme.mediumBlue : undefined)}; + stroke-width: ${props => (props.isSelected ? '5px' : undefined)}; + animation: ${props => + keyframes({ + '0%': { + fill: Theme.white, + stroke: Theme.white, + strokeWidth: '0px', + }, + '50%': { + stroke: Theme.mediumBlue, + strokeWidth: '7px', + }, + '100%': { + fill: props.fill, + stroke: Theme.white, + strokeWidth: '0px', + }, + })} + 3s ease; +`; + +const getTextScale = scale => { + scale = scale.toPrecision(2); + const scaleDiff = 1 - scale; + const factor = scale <= 0.5 ? 1.5 + scaleDiff : 1 + scaleDiff; + const preciseFactor = factor.toPrecision(2); + const percentage = scale >= 1 ? 100 : preciseFactor * 100 + 50; + return percentage + '%'; +}; + +const StyledG = styled.g` + ${StyledText} { + font-size: ${props => getTextScale(props.scale)}; + } + &:hover { + ${StyledCircle} { + stroke: ${props => + props.isSelected ? Theme.mediumBlue : Theme.severityLowBlue}; + stroke-width: 5px; + cursor: ${props => props.cursor}; + } + ${StyledText} { + cursor: ${props => props.cursor}; + } + } +`; + +const ProcessNode = ({ + color, + comment, + cursor, + isSelected = false, + name, + radius, + forwardedRef, + scale, + x, + y, + onMouseDown, + onMouseEnter, + onMouseLeave, + onMouseUp, +}) => { + return ( + + + + {name} + + + {comment} + + + ); +}; + +ProcessNode.propTypes = { + color: PropTypes.string, + comment: PropTypes.string, + cursor: PropTypes.string, + forwardedRef: PropTypes.ref, + isSelected: PropTypes.bool, + name: PropTypes.string, + radius: PropTypes.number.isRequired, + scale: PropTypes.number, + x: PropTypes.number.isRequired, + y: PropTypes.number.isRequired, + onMouseDown: PropTypes.func, + onMouseEnter: PropTypes.func, + onMouseLeave: PropTypes.func, + onMouseUp: PropTypes.func, +}; + +export default ProcessNode; + +// vim: set ts=2 sw=2 tw=80: diff --git a/gsa/src/web/components/processmap/processpanel.js b/gsa/src/web/components/processmap/processpanel.js new file mode 100644 index 0000000000..e3d104fd9c --- /dev/null +++ b/gsa/src/web/components/processmap/processpanel.js @@ -0,0 +1,390 @@ +/* Copyright (C) 2020 Greenbone Networks GmbH + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import React from 'react'; +import styled from 'styled-components'; + +import _ from 'gmp/locale'; + +import CollectionCounts from 'gmp/collection/collectioncounts'; + +import {parseSeverity} from 'gmp/parser'; + +import {isDefined} from 'gmp/utils/identity'; + +import Button from 'web/components/form/button'; +import MultiSelect from 'web/components/form/multiselect'; + +import EditIcon from 'web/components/icon/editicon'; + +import Layout from 'web/components/layout/layout'; + +import Loading from 'web/components/loading/loading'; + +import Pagination from 'web/components/pagination/pagination'; + +import PropTypes from 'web/utils/proptypes'; +import {renderSelectItems} from 'web/utils/render'; +import Theme from 'web/utils/theme'; +import withGmp from 'web/utils/withGmp'; + +import HostTable from './hosttable'; + +import {MAX_HOSTS_PER_PROCESS} from './processmaploader'; +const NUMBER_OF_LISTED_HOSTS = 30; + +const Container = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + position: absolute; + left: 70%; + top: 0; + width: 30%; + min-width: 250px; + height: 100%; + background-color: ${Theme.white}; + border-left: 2px solid ${Theme.lightGray}; +`; + +const TitleBox = styled.div` + display: flex; + font-size: 16px; + font-weight: bold; + justify-content: space-between; + padding: 20px 20px 10px 20px; + width: 100%; +`; + +const CommentBox = styled.div` + box-sizing: border-box; + display: flex; + align-items: flex-start; + padding: 0px 0px 10px 20px; + min-height: 21px; +`; + +const HostsList = styled.div` + display: flex; + height: 100%; + overflow: hidden; +`; + +const MaxHostsWarning = styled.div` + width: 100%; + background-color: ${Theme.lightRed}; + padding: 5px; +`; + +const compareSeverity = (host1, host2) => { + // sort by highest to lowest severity + const sev1 = parseSeverity(host1.severity); + const sev2 = parseSeverity(host2.severity); + if (sev1 > sev2) { + return -1; + } + if (sev1 < sev2) { + return 1; + } + return 0; +}; + +const getNumListedItems = (first, length) => { + const rest = length % NUMBER_OF_LISTED_HOSTS; + const allPages = Math.ceil(length / NUMBER_OF_LISTED_HOSTS); + const currentPage = Math.ceil(first / NUMBER_OF_LISTED_HOSTS); + const listedItems = currentPage === allPages ? rest : NUMBER_OF_LISTED_HOSTS; + + return listedItems; +}; + +class ProcessPanel extends React.Component { + constructor(...args) { + super(...args); + + const counts = new CollectionCounts({ + first: 1, + }); + this.state = { + processDialogVisible: false, + selectedHosts: [], + counts, + }; + + this.openProcessDialog = this.openProcessDialog.bind(this); + this.closeProcessDialog = this.closeProcessDialog.bind(this); + this.handleAddHosts = this.handleAddHosts.bind(this); + this.handleSelectedHostsChange = this.handleSelectedHostsChange.bind(this); + this.handleFirstClick = this.handleFirstClick.bind(this); + this.handleNextClick = this.handleNextClick.bind(this); + this.handleLastClick = this.handleLastClick.bind(this); + this.handlePreviousClick = this.handlePreviousClick.bind(this); + } + + // in order to correctly use the pagination in the panel, we need to compare + // the current element in the panel and the previous element that was selected + static getDerivedStateFromProps = (nextProps, prevState) => { + if (!isDefined(nextProps.element)) { + // if no element will be selected, set current element to undefined + // -> the panel will not show process information + return { + ...prevState, + element: undefined, + }; + } else if ( + // if the element does not change compared to the previous one (prevState) + // or if the prevElement (stored for comparison) is equal to the current + // element, set the counts according to the given hostList and don't reset + // pagination + nextProps.element === prevState.element || + nextProps.element === nextProps.prevElement + ) { + const {hostList = []} = nextProps; + let {length} = hostList; + + // account for the +1 hosts loaded with hostFilter() + length = + hostList.length > MAX_HOSTS_PER_PROCESS + ? hostList.length - 1 + : hostList.length; + + const newCounts = prevState.counts.clone({ + // use count first of prevElement to be able to remember the page + first: prevState.counts.first, + length: getNumListedItems(prevState.counts.first, length), + filtered: length, + }); + + return { + counts: newCounts, + }; + } else if (nextProps.element !== nextProps.prevElement) { + // if the selected element changes, the counts need to be reset so that + // the pagination starts at the first element. Otherwise, e.g., navigating + // to the third page of one element and then changing the element would + // leed to the new elements hosts being listed from the third page on + const {hostList = []} = nextProps; + + // account for the +1 hosts loaded with hostFilter() + const length = + hostList.length > MAX_HOSTS_PER_PROCESS + ? hostList.length - 1 + : hostList.length; + + const newCounts = prevState.counts.clone({ + first: 1, + all: length, + length: NUMBER_OF_LISTED_HOSTS, + filtered: length, + rows: NUMBER_OF_LISTED_HOSTS, + }); + + return { + ...prevState, + counts: newCounts, + element: nextProps.element, + }; + } + }; + + componentDidMount() { + const {gmp} = this.props; + let allHosts = []; + gmp.hosts.getAll().then(response => { + allHosts = response.data; + this.setState({allHosts}); + }); + this.handleFirstClick(); + } + + openProcessDialog() { + this.setState({processDialogVisible: true}); + } + + closeProcessDialog() { + this.setState({processDialogVisible: false}); + } + + handleSelectedHostsChange(value, name) { + this.setState({[name]: value}); + } + + handleAddHosts(hosts) { + this.props.onAddHosts(this.state.selectedHosts); + this.setState({selectedHosts: []}); + } + + handleFirstClick() { + const {counts} = this.state; + const {hostList = []} = this.props; + + counts.first = 1; + counts.length = getNumListedItems(counts.first, hostList.length); + + if (isDefined(this.props.onInteraction)) { + this.props.onInteraction(); + } + + this.setState({ + counts, + }); + } + + handleNextClick() { + const {counts} = this.state; + const {hostList = []} = this.props; + const newFirst = counts.first + NUMBER_OF_LISTED_HOSTS; + + counts.first = newFirst; + counts.length = getNumListedItems(newFirst, hostList.length); + + if (isDefined(this.props.onInteraction)) { + this.props.onInteraction(); + } + + this.setState({ + counts, + }); + } + + handleLastClick() { + const {counts} = this.state; + const {hostList = []} = this.props; + const remainingHosts = hostList.length % NUMBER_OF_LISTED_HOSTS; + + counts.first = hostList.length - remainingHosts + 1; + counts.length = remainingHosts; + + if (isDefined(this.props.onInteraction)) { + this.props.onInteraction(); + } + + this.setState({ + counts, + }); + } + + handlePreviousClick() { + const {counts} = this.state; + const newFirst = counts.first - NUMBER_OF_LISTED_HOSTS; + + counts.first = newFirst; + + if (isDefined(this.props.onInteraction)) { + this.props.onInteraction(); + } + + this.setState({ + counts, + }); + } + + render() { + const { + element = {}, + hostList = [], + isLoadingHosts, + onDeleteHost, + onEditProcessClick, + } = this.props; + + const {allHosts, counts} = this.state; + + const {name = _('No process selected'), comment} = element; + + const hostItems = renderSelectItems(allHosts); + + const sortedHostList = hostList.sort(compareSeverity); + + const paginatedHosts = sortedHostList.slice(counts.first - 1, counts.last); + + return ( + + + {name} + + + {comment} + +