diff --git a/.gitignore b/.gitignore index 729dd9e63..bf3be24d4 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,7 @@ coverage storybook-static .build_cache .eslintcache +.cache* + +# Old locale files +locales/**/*_old.json diff --git a/TESTING.md b/TESTING.md index 0974c868b..84e5f430b 100644 --- a/TESTING.md +++ b/TESTING.md @@ -39,6 +39,8 @@ In order to render the component under test into its HTML DOM representation and * To search for a localized text, for example, with `getByText`, use `testTranslate` function from `@test/Common.tsx` to return a translated text. Tests are configued to use `en` locale as default. +* If any of the test codes cause the changes in React states (e.g. open/close a action menu or modal), wrap them in [`act`](https://testing-library.com/docs/react-testing-library/api/#act) function, imported from `@testing-library/react`. The `act` function allows flushing all pending state changes. + ## MOCKING ### Overview @@ -83,6 +85,8 @@ Where the `-u` flag tells Jest to update the snapshot and the `-t` flag specifie * Some PatternFly components use random, dynamic strings as `ids` which will then be displayed as elements in the rendered React virtual DOM. These strings change upon every render, causing snapshots to fail even though the component under test is still functionally the same. This can be remedied by supplying [custom `ids` as props](https://github.com/patternfly/patternfly-react/issues/3518) to the culprit PatternFly child components inside the source file of the component under test. +* Since `react-test-renderer` does not support the use of `ref` (see [reference](https://github.com/facebook/react/issues/7740)), the tests might fail if any third-party codes use `ref` unsafely (without checking `null`). To workaround that, use [`createNodeMock`](https://legacy.reactjs.org/docs/test-renderer.html#ideas) to construct a mock `ref` when calling `renderSnapshot`. + ## INTEGRATION TESTING @@ -107,6 +111,7 @@ This will automatically start a Mirage dev server, run the integration tests on ### Tips * Running the integration tests will open a Firefox browser and simulate any actions that you instruct the browser to perform. That means we must first navigate to the local Cryostat Web page, before performing any useful testing. + * In our `beforeAll` jest declaration, we setup our web driver with the [default configurations](src/itest/util.ts)), and then use that driver to create our first **Page Object**. A Page Object is an abstraction that acts as an interface to your web pages. For more info on the **Page Object Model** in Selenium, see https://www.selenium.dev/documentation/test_practices/encouraged/page_object_models/. ```ts beforeAll(async function () { @@ -135,4 +140,4 @@ Add more methods to each PO, to test more actions. The point is, we want to abst In the code, we first tell the driver to wait, until an element is located by the css selector ('button[data-action="skip"]'), and assign it to a variable. If not found, we assign null. Then if the variable is non-null, we click it. To find a good query to use, it is recommended to use the [Selenium IDE](https://addons.mozilla.org/en-CA/firefox/addon/selenium-ide/) extension on your browser. The extension allows you to easily see queries that can be used to select an element you want. * Integration tests are found in [src/itest](src/itest). * All code is asynchronous which entails the use of the `async/await` pattern. -* Follow the Selenium testing practices when writing integration tests: https://www.selenium.dev/documentation/test_practices/. \ No newline at end of file +* Follow the Selenium testing practices when writing integration tests: https://www.selenium.dev/documentation/test_practices/. diff --git a/jest.config.js b/jest.config.js index b442111e1..611b7b559 100644 --- a/jest.config.js +++ b/jest.config.js @@ -58,7 +58,9 @@ module.exports = { // An array of regexp pattern strings that are matched against all source file paths before transformation. // If the file path matches any of the patterns, it will not be transformed. - transformIgnorePatterns: ["/node_modules/(?!@patternfly)"], + transformIgnorePatterns: [ + "/node_modules/(?!@patternfly|d3|d3-array|internmap|delaunator|robust-predicates)", + ], roots: ['/src'] }; diff --git a/locales/en/common.json b/locales/en/common.json index 3379ebef0..68b232d43 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -1,17 +1,24 @@ { + "ADD": "Add", + "ARCHIVAL_PERIOD": "Archival Period", "AriaLabels": { "MAXIMUM_AGE": "Maximum age value", "MAXIMUM_AGE_UNITS_INPUT": "Maximum age units input", "MAXIMUM_SIZE": "Maximum size value", "MAXIMUM_SIZE_UNITS_INPUT": "Maximum size units input" }, - "BETA": "BETA", + "AUTOMATED_RULES": "Automated Rules", + "BETA": "Beta", "CANCEL": "Cancel", "CARD_TYPE": "Card type", + "CAUTION": "Caution", + "CLEAN": "Clean", "CLEAR": "Clear", "CLEAR_FILTERS": "Clear all filters", "CLEAR_RECENT": "Clear recent", "CLOSE": "Close", + "COPIED": "Copied", + "COPY": "Copy", "CREATE": "Create", "CREATING": "Creating", "CRITICAL": "CRITICAL", @@ -19,46 +26,69 @@ "DATE": "Date", "DELETE": "Delete", "DESCRIPTION": "Description", - "DEVELOPMENT": "DEVELOPMENT", + "DEVELOPMENT": "Development", + "DISABLE": "Disable", + "DONOT_ASK_AGAIN": "Don't ask me again", "DOWNLOAD": "Download", + "DURATION": "Duration", "EDIT": "Edit", + "ENABLED": "Enabled", + "EVENT_SPECIFIER": "Event Specifier", "FILTER_NAME": "Name", "FILTER_TOPIC": "Topic", + "FROM": "From", "HELP": "Help", "HOUR": "Hour", "HOUR_one": "Hour", "HOUR_other": "Hours", - "MAXIMUM_AGE": "Maximum age", + "HOUR_compact": "h", + "INITIAL_DELAY": "Initial Delay", + "LABEL": "Label", + "MATCH_EXPRESSION": "Match Expression", + "MAXIMUM_AGE": "Maximum Age", "MAXIMUM_AGE_HELPER_TEXT": "The maximum age of Recording data stored to disk.", - "MAXIMUM_SIZE": "Maximum size", + "MAXIMUM_SIZE": "Maximum Size", "MAXIMUM_SIZE_HELPER_TEXT": "The maximum size of Recording data saved to disk.", "MERIDIEM_AM": "AM", "MERIDIEM_PM": "PM", + "MILLISECOND_other": "Milliseconds", "MINUTE": "Minute", "MINUTE_one": "Minute", "MINUTE_other": "Minutes", + "MINUTE_compact": "m", "N/A": "N/A", "NAME": "Name", + "NO_DESCRIPTION": "No description", "OK": "OK", - "PRODUCTION": "PRODUCTION", + "PRESERVED_ARCHIVES": "Preserved Archives", + "PRODUCTION": "Production", + "REFRESH": "Refresh", "REMOVE": "Remove", "RENAME": "Rename", "RESET": "Reset", "RETRY": "Retry", + "SAVE": "Save", "SCORE": "Score", "SECOND": "Second", "SECOND_one": "Second", "SECOND_other": "Seconds", + "SECOND_compact": "s", "SELECT": "Select", "SHOW_LESS": "Show less", "SHOW_MORE": "Show more", "SOMETHING_WENT_WRONG": "Something went wrong", + "STATE": "State", + "STATUS": "Status", "SUBMIT": "Submit", "SUBMITTING": "Submitting", "SUGGESTED": "Suggested", + "TARGET": "Target", "TEMPLATE": "Template", + "TEST": "Test", "TIME": "Time", - "UPLOAD": "Upload", + "TIMEZONE": "Timezone", + "TO": "To", + "UNKNOWN_ERROR": "Unknown error", "USER_SUBMITTED": "User-submitted", "VIEW": "View", "VIEW_MORE": "View more", diff --git a/locales/en/public.json b/locales/en/public.json index c28650831..13eb90a47 100644 --- a/locales/en/public.json +++ b/locales/en/public.json @@ -14,6 +14,29 @@ "OPEN_SOURCE_LICENSE": "Open source license", "VERSION": "Version" }, + "AgentLiveProbes": { + "SEARCH_PLACEHOLDER": "Find by name, description, class, or method..." + }, + "AgentLiveProves": { + "ARIA_LABELS": { + "SEARCH_INPUT": "active-probe-filter" + } + }, + "AgentProbeTemplates": { + "ARIA_LABELS": { + "ROW_ACTION": "agent-probe-template-action-menu", + "SEARCH_INPUT": "agent-probe-template-search" + }, + "SEARCH_PLACEHOLDER": "Find by name or XML content..." + }, + "AllArchivedRecordingsTable": { + "SEARCH_PLACEHOLDER": "Find by JVM ID..." + }, + "AllTargetsArchivedRecordingsTable": { + "HIDE_TARGET_WITH_ZERO_RECORDING": "Hide targets with zero Recordings", + "SEARCH_PLACEHOLDER": "Find by connection URL or alias...", + "TARGET_DISPLAY": "{{alias}} ({{connectUrl}})" + }, "AppLayout": { "APP_LAUNCHER": { "ABOUT": "About", @@ -21,6 +44,22 @@ "GUIDED_TOUR": "Guided tour", "HELP": "Help", "QUICKSTARTS": "Quick starts" + }, + "NOTIFICATIONS": { + "OVERFLOW_MESSAGE_one": "View one more", + "OVERFLOW_MESSAGE_other": "View {{count}} more" + }, + "TOOLBAR": { + "ARIA_LABELS": { + "GLOBAL_NAVIGATION": "Global navigation", + "NAVIGATION": "Navigation", + "NOTIFICATIONS": "Notifications", + "SETTINGS": "Settings" + } + }, + "USER_MENU": { + "LANGUAGE_PREFERENCE": "Language preference", + "LOGOUT": "Log out" } }, "AutomatedAnalysisCard": { @@ -42,6 +81,10 @@ "TOOLTIP": "Report data is stale. Click the Create Recording button and choose an option to start an active Recording to source automated reports from." }, "TOOLBAR": { + "ARIA_LABELS": { + "GRID_VIEW": "grid-view", + "LIST_VIEW": "list-view" + }, "CHECKBOX": { "SHOW_NA": { "LABEL": "Show N/A scores" @@ -53,15 +96,14 @@ "LABEL": "Automated analysis toolbar", "REFRESH": { "LABEL": "Refresh automated analysis" - }, - "SWITCH": { - "LIST_VIEW": { - "LABEL": "List view" - } } }, - "WARNING_RESULTS_one": "{{count}} Warning Result", - "WARNING_RESULTS_other": "{{count}} Warning Results" + "TOOLTIP": { + "CLEAR_ANALYSIS": "Clear analysis", + "REFRESH_ANALYSIS": "Refresh analysis" + }, + "WARNING_RESULTS_one": "{{count}} warning result", + "WARNING_RESULTS_other": "{{count}} warning results" }, "AutomatedAnalysisConfigDrawer": { "INPUT_GROUP": { @@ -77,9 +119,8 @@ "CURRENT_CONFIG": "Current configuration", "FORM_TITLE": "Profiling Recording configuration", "FORMATTED_TEMPLATE": "Name: {{template.name}}, Type: {{template.type}}", - "MAXIMUM_AGE": "Maximum age ({{unit}})", - "MAXIMUM_SIZE": "Maximum size ({{unit}})", - "SAVE_CHANGES": "Save changes", + "MAXIMUM_AGE": "Maximum age", + "MAXIMUM_SIZE": "Maximum size", "TEMPLATE_HELPER_TEXT": "The Event Template to be applied to automated analysis Recordings.", "TEMPLATE_INVALID_WARNING": "WARNING: Setting a target template as a default template type configuration may not apply to all target JVMs if the JVMs do not support them." }, @@ -105,6 +146,7 @@ "POPOUT": { "LABEL": "Pop out {{chartKind}} chart" }, + "REFRESH_TOOLTIP": "Refresh chart data", "SYNC": { "LABEL": "Synchronize {{chartKind}} chart" } @@ -148,9 +190,60 @@ "POPOVER": "automated-analysis-description-popover" } }, + "CreateCredentialModal": { + "ARIA_LABELS": { + "HELPER_ICON": "More info for Match Expression field" + }, + "EVALUATING_EXPRESSION": "Evaluating Match Expression...", + "FAILING_EVALUATION": "The expression matching failed.", + "MATCH_EXPRESSION_HELPER_TEXT": "Enter a Match Expression. This is a Java-like code snippet that is evaluated against each target application to determine whether the rule should be applied.", + "MATCH_EXPRESSION_HINT_BODY": "Try an expression like:", + "MATCH_EXPRESSION_HINT_MODAL_HEADER": "Match Expression hint", + "MODAL_DESCRIPTION": "Create Stored Credentials for target JVMs. Cryostat will use these credentials to connect to Cryostat agents or target JVMs over JMX (if required).", + "MODAL_TITLE": "Stored Credentials", + "VISUALIZER": "Visualizer", + "WARNING_NO_MATCH": "Warning: Match Expression matches no targets." + }, + "CreateRule": { + "ABOUT": "Automated Rules are configurations that instruct Cryostat to create JDK Flight Recordings on matching Target JVM applications. Each Automated Rule specifies parameters for which Event Template to use, how much data should be kept in the application Recording buffer, and how frequently Cryostat should copy the application Recording buffer into Cryostat's own archived storage.", + "ARCHIVAL_PERIOD_HELPER_TEXT": "Time between copies of active Recording data being pulled into Cryostat archive storage.", + "ARIA_LABELS": { + "HELPER_ICON": "More info for Match Expression field" + }, + "DESCRIPTION_HELPER_TEXT": "Enter a rule description. This is only used for display purposes to aid in identifying rules and their intentions.", + "ENABLE_SWITCH_HELPER_TEXT": "Rules take effect when created if enabled and will be matched against all discovered target applications immediately. When new target applications appear they are checked against all enabled rules. Disabled rules have no effect but are available to be enabled in the future.", + "EVALUATING_EXPRESSION": "Evaluating Match Expression...", + "FAILING_EVALUATION": "The expression matching failed.", + "INITIAL_DELAY_HELPER_TEXT": "Initial delay before archiving starts. The first archived copy will be made this long after the Recording is started. The second archived copy will occur one Archival period later.", + "MATCH_EXPRESSION_HELPER_TEXT": "Enter a Match Expression. This is a Java-like code snippet that is evaluated against each target application to determine whether the rule should be applied.", + "MATCH_EXPRESSION_HINT_BODY": "Try an expression like:", + "MATCH_EXPRESSION_HINT_MODAL_HEADER": "Match Expression hint", + "MAXAGE_HELPER_TEXT": "The maximum age of Recording data retained in the target application's Recording buffer.", + "MAXSIZE_HELPER_TEXT": "The maximum size of Recording data retained in the target application's Recording buffer.", + "NAME_HELPER_TEXT": "Enter a rule name.", + "NAME_HINT": "A rule name can contain only letters, numbers, and underscores.", + "PRESERVED_ARCHIVES_HELPER_TEXT": "The number of Archived Recording copies to preserve in archives for each target application affected by this rule.", + "TEMPLATE_HELPER_TEXT": "The Event Template to be applied by this Rule against matching target applications.", + "TEMPLATE_HINT": "A Template must be selected.", + "WARNING_NO_MATCH": "Warning: Match Expression matches no targets." + }, + "CredentialTestTable": { + "ARIA_LABELS": { + "STATUS_POPOVER": "Test Result Details ({{connectUrl}})", + "STATUS_SELECT": "test-status-select", + "TOOLBAR": "credential-test-table-toolbar" + }, + "CLEAR_AND_TRY_AGAIN": "Clear Match Expression and try again.", + "ENTER_ANOTHER": "Enter another Match Expression.", + "NO_TARGET_MATCHED": "No Targets Matched", + "SEARCH_PLACEHOLDER": "Find by connection URL or alias...", + "TEST_ALL": "Test All", + "TEST_ALL_TOOLTIP": "Test credentials against all matching targets.", + "TEST_FAILED": "Test failed" + }, "Dashboard": { "ADD_CARD_HELPER_TEXT": "Choose a card type to add to your Dashboard. Some cards require additional configuration.", - "CARD_CATALOG_DESCRIPTION": "Cards added to this Dashboard Layout present information at a glance about the selected target. The layout is preserved for all targets viewed on this client.", + "CARD_CATALOG_DESCRIPTION": "Cards added to this Dashboard Layout present information at a glance about the selected target. The layout is preserved locally for all targets viewed only on this client.", "CARD_CATALOG_TITLE": "Dashboard Card catalog", "INVALID_CARD_CONFIGURATIONS": "Invalid card configurations", "PAGE_TITLE": "Dashboard" @@ -224,14 +317,29 @@ "SEARCH_BUTTON": "Search for date", "TOGGLE_CALENDAR": "Toggle the calendar" }, - "INVALID_DATE_TEXT": "Invalid date time" + "HELPER_TEXT": { + "INVALID_UPPER_BOUND": "From Datetime must be equal or later than To Datetime" + }, + "INVALID_DATE_TEXT": "Invalid date time input. The input must have the ISO 8601 format." }, "DateTimePicker": { "ARIA_LABELS": { - "DISPLAY_SELECTED_DATETIME": "Displayed selected datetime", - "TABS": "Select a date or time tab" + "DISPLAY_SELECTED_DATETIME": "Displayed selected datetime" }, - "SELECTED_DATETIME": "Selected DateTime" + "DATETIME": "Date and Time" + }, + "DurationFilter": { + "ARIA_LABELS": { + "FROM_DURATION": "duration-from", + "SEARCH_BUTTON": "search-for-duration", + "TO_DURATION": "duration-to" + }, + "HELPER_TEXT": { + "INVALID_UPPER_BOUND": "Duration upper value must be higher than or equal to lower one" + }, + "TOOLTIP": { + "CHECKBOX_DISABLED_CONTENT": "A duration filter without upper limit (i.e. >=10s) selects Continuous Recordings" + } }, "ERROR_BOUNDARY": { "ERROR_MESSAGE": "Reason: {{message}}", @@ -240,6 +348,25 @@ "ErrorView": { "EVENT_TEMPLATES": "Error retrieving Event Templates" }, + "EventTemplates": { + "ARIA_LABELS": { + "SEARCH_INPUT": "event-template-search-input" + }, + "SEARCH_PLACEHOLDER": "Find by name, description, or provider..." + }, + "EventTypes": { + "ARIA_LABELS": { + "SEARCH_INPUT": "event-type-search-input" + }, + "SEARCH_PLACEHOLDER": "Find by name, description, typeId, or description..." + }, + "ImportCertificate": { + "CARD_DESCRIPTION": "The following certificates are present in Cryostat&apos's additional trust store. Contact your Cryostat administrator if your application requires a new trusted certificate. You must restart the Cryostat server to reload the certificate store after adding new certificates.", + "CARD_TITLE": "Imported SSL/TLS Certificates", + "CARD_TITLE_POPOVER_HEADER": "JMX over SSL/TLS", + "NO_CERTIFICATE_BODY": "No additional certificates are loaded.", + "NO_CERTIFICATE_TITLE": "No certificates" + }, "JvmDetailsCard": { "CARD_DESCRIPTION": "Display details about the selected Target JVM.", "CARD_DESCRIPTION_FULL": "View information such as the connection URL, Labels, and Annotations belonging to the selected Target JVM.", @@ -250,10 +377,11 @@ "ITEMS_other": "{{count}} items" }, "LayoutTemplatePicker": { + "CARD_COUNT": "Card count", + "SEARCH_PLACEHOLDER": "Find by name...", "SORT_BY": { "CARD_COUNT": "Sort by: Card count", - "NAME": "Sort by: Name", - "PLACEHOLDER": "Sort by..." + "NAME": "Sort by: Name" } }, "LayoutTemplateUploadModal": { @@ -273,21 +401,89 @@ }, "TITLE": "Upload Dashboard Layout Template" }, + "MATCH_EXPRESSION_VISUALIZER": { + "TITLE": "Match Expression visualizer" + }, + "MatchedTargetsTable": { + "ARIA_LABELS": { + "TABLE": "matched-targets-table" + }, + "NO_TARGET_MATCHED": "No Targets Matched" + }, "MeridiemPicker": { "ARIA_LABELS": { "LISTBOX": "Select AM or PM" } }, + "NotFound": { + "DESCRIPTION": "The page you're trying to find was either removed, moved or maybe the URL isn't quite right. Try returning home or visit one of the following pages below.", + "HOME_REDIRECT_BUTTON_CONTENT": "Return to home page", + "TITLE": "404: We couldn't find that page" + }, "QuickStarts": { "CATALOG_PAGE": { "HINT": "Quick start tutorials to get started with Cryostat.", "TITLE": "Quick starts" } }, + "RecordingActions": { + "ARIA_LABELS": { + "MENU_TOGGLE": "recording-action-toggle" + } + }, + "RecordingFilters": { + "ARIA_LABELS": { + "MENU_TOGGLE": "recording-filters-toggle" + }, + "FILTER_CHIP": { + "DURATION_CONTINUOUS": "Continuous", + "DURATION_EXACT": "{{value}}{{unit}}", + "DURATION_FROM": ">={{value}}{{unit}}", + "DURATION_FROM_TO": "{{fromValue}}{{fromUnit}}-{{toValue}}{{toUnit}}", + "DURATION_TO": "<={{value}}{{unit}}", + "START_TIME_EXACT": "On {{value}}", + "START_TIME_FROM": "After {{value}}", + "START_TIME_FROM_TO": "From: {{from}} To: {{to}}", + "START_TIME_TO": "Before {{value}}" + }, + "START_TIME": "Start time" + }, "RecordingLabelFields": { "INVALID_UPLOADS_one": "The file does not contain valid Recording metadata:", "INVALID_UPLOADS_other": "These files do not contain valid Recording metadata:" }, + "RecordingsTable": { + "NO_ARCHIVES": "No Archived Recordings" + }, + "RecordingStateFilter": { + "ARIA_LABELS": { + "MENU_TOGGLE": "recording-state-toggle", + "SELECT": "select-by-state" + }, + "FILTER_BY_STATE": "Filter by state" + }, + "RuleDeleteWarningModal": { + "CLEAN_DESCRIPTION": "Clean will stop any Active Recordings that {{ruleName}} created." + }, + "Rules": { + "ABOUT_BODY": "Automated Rules are configurations that instruct Cryostat to create JDK Flight Recordings on matching target JVM applications, using a specific <1>Event Template. If your Target JVM connections require JMX Credentials, you can configure these in <2>Security. Automated Rules can be configured to periodically copy the contents of the Active Recording to <3>Archives to ensure you always have up-to-date information about your JVMs.", + "ARCHIVAL_PERIOD_TOOLTIP": "Period in seconds. Cryostat will connect to matching targets at this interval and copy the relevant Recording data into its archives. Values less than 1 prevent data from being repeatedly copied into archives - Recordings will be started and remain only in Target JVM memory.", + "EVENT_SPECIFIER_TOOLTIP": "The name and location of the Event Template applied by this rule.", + "INITIAL_DELAY_TOOLTIP": "Initial delay in seconds. Cryostat will wait this amount of time before first copying Recording data into its archives. Values less than 0 default to equal to the Archival period. You can set a non-zero Initial delay with a zero Archival period, which will start a Recording and copy it into archives exactly once after a set delay.", + "MATCH_EXPRESSION_TOOLTIP": "A code-snippet expression which must evaluate to a boolean when applied to a given target. If the expression evaluates to true then the rule applies to that target.", + "MAX_AGE_TOOLTIP": "The maximum age in seconds for data kept in the JFR Recordings started by this rule. Values less than 1 indicate no limit.", + "MAX_SIZE_TOOLTIP": "The maximum size in bytes for JFR Recordings started by this rule. Values less than 1 indicate no limit.", + "NO_RULES": "No Automated Rules", + "PRESERVED_ARCHIVES_TOOLTIP": "The number of Recording copies to be maintained in the Cryostat archives. Cryostat will continue retrieving further archived copies and trimming the oldest copies from the archive to maintain this limit. Values less than 1 prevent data from being copied into archives - Recordings will be started and remain only in Target JVM memory.", + "SEARCH_PLACEHOLDER": "Find by name, or description..." + }, + "RulesUploadModal": { + "DESCRIPTION": "Select an Automated Rules definition file to upload. File must be in valid JSON format.", + "HEADER_CONTENT": "Whats this?", + "INVALID_RULE_CONTENT": "Automated Rule content is invalid.", + "JSON_FILE": "JSON File", + "TITLE": "Upload Automated Rules" + }, "SETTINGS": { "AUTO_REFRESH": { "CHECKBOX_LABEL": "Enabled", @@ -296,7 +492,7 @@ }, "AUTOMATED_ANALYSIS_CONFIG": { "DESCRIPTION": "Set the Recording configuration for automated analysis Recordings. You may want smaller or larger values for max-age and max-size depending on how recent you want events to be recorded from the analysis.", - "TITLE": "Automated analysis Recording configuration" + "TITLE": "Recording configuration for Automated analysis" }, "CATEGORIES": { "ADVANCED": "Advanced", @@ -324,10 +520,12 @@ }, "DATETIME_CONTROL": { "ARIA_LABELS": { - "LOCALE_SELECT": "Select a datetime locale" + "LOCALE_SELECT": "Select a datetime locale", + "MENU_TOGGLE": "datetime-locale-setting-toggle" }, "DESCRIPTION": "", "LOCALE_SELECT_DESCRIPTION": "Select current date locale.", + "SEARCH_PLACEHOLDER": "Find by locale...", "TIMEZONE_SELECT_DESCRIPTION": "Select current timezone.", "TITLE": "Date & Time" }, @@ -338,11 +536,16 @@ "TITLE": "Show deletion dialogs" }, "FEATURE_LEVEL": { + "BETA_DESCRIPTION": "Experimental features", "DESCRIPTION": "Control which graphical features appear in the application.", + "DEVELOPMENT_DESCRIPTION": "Under development features", + "MENU_TOGGLE": "feature-level-setting-toggle", + "PRODUCTION_DESCRIPTION": "Stable production-ready features", "TITLE": "Feature level" }, "LANGUAGE": { "ARIA_LABELS": { + "MENU_TOGGLE": "language-setting-toggle", "SELECT": "Select a language" }, "DESCRIPTION": "Set the current language for web console.", @@ -356,13 +559,14 @@ "TITLE": "Notifications" }, "THEME": { + "ARIA_LABELS": { + "MENU_TOGGLE": "theme-setting-toggle", + "SELECT": "Select a theme" + }, "AUTO": "Auto", "DARK": "Dark", "DESCRIPTION": "Set the current theme for web console.", "LIGHT": "Light", - "SELECT": { - "LABEL": "Select a theme" - }, "TITLE": "Theme" }, "WEBSOCKET_CONNECTION_DEBOUNCE": { @@ -370,9 +574,52 @@ "TITLE": "WebSocket retry interval" } }, + "ShortCuts": { + "ARIA_LABELS": { + "TABLE": "Shortcuts table" + } + }, + "StoredCredentials": { + "ARIA_LABELS": { + "Add": "add-credential", + "Delete": "delete-selected-credential", + "FILTER_CHECKBOX": "select-all", + "ROW_CHECKBOX": "credentials-table-row-{{index}}", + "TABLE": "stored-credentials" + }, + "AT_LEAST_ONE_MATCH": "At least 1 match", + "CARD_DESCRIPTION": "Credentials that Cryostat uses to connect to Cryostat agents or target JVMs over JMX are stored in encrypted storage managed by the Cryostat server.", + "CARD_TITLE": "Stored Credentials", + "CARD_TITLE_POPOVER_HEADER": "JMX Authentication", + "NO_CREDENTIAL_TITLE": "No Stored Credentials", + "NO_MATCH": "No Match", + "SEARCH_PLACEHOLDER": "Find by Match Expression..." + }, + "TargetContextSelector": { + "CLEAR_SELECTION": "Clear selection", + "CREATE_TARGET": "Create Target", + "NO_SEARCH_MATCHES": "No Target found", + "SEARCH_PLACEHOLDER": "Find by URL, alias, or discovery group...", + "TOGGLE_LABEL": "Select Target", + "TOGGLE_PLACEHOLDER": "Select a Target" + }, + "TargetSelect": { + "ARIA_LABELS": { + "CARD_DETAILS": "Details" + }, + "CARD": { + "TITLE": "Target JVM" + } + }, + "ThemeToggle": { + "ARIA_LABELS": { + "DARK_THEME": "dark-theme", + "LIGHT_THEME": "light-theme" + } + }, "TimePicker": { - "24HOUR": "24-hour", - "USE_24HR_TIME": "Use 24-hour time" + "12_H": "12Hr View", + "24_H": "24Hr View" }, "TimeSpinner": { "DECREMENT_HOUR12_VALUE": "Decrement hour12 value", @@ -390,9 +637,11 @@ }, "TimezonePicker": { "ARIA_LABELS": { + "MENU_TOGGLE": "timezone-select-toggle", "SELECT": "Select a timezone", "TYPE_AHEAD": "Search a timezone" - } + }, + "SEARCH_PLACEHOLDER": "Find by timezone..." }, "Topology": { "GRAPH_VIEW": "Graph view", diff --git a/package.json b/package.json index 5aa122009..26368766b 100644 --- a/package.json +++ b/package.json @@ -90,14 +90,14 @@ "webpack-merge": "^5.8.0" }, "dependencies": { - "@patternfly/quickstarts": "^2.3.3", - "@patternfly/react-catalog-view-extension": "4.93.15", - "@patternfly/react-charts": "^6.94.18", - "@patternfly/react-core": "4.267.6", - "@patternfly/react-icons": "^4.93.6", - "@patternfly/react-styles": "^4.92.6", - "@patternfly/react-table": "^4.112.39", - "@patternfly/react-topology": "4.91.27", + "@patternfly/quickstarts": "^5.3.0", + "@patternfly/react-catalog-view-extension": "^5.0.0", + "@patternfly/react-charts": "^7.3.0", + "@patternfly/react-core": "^5.3.4", + "@patternfly/react-icons": "^5.3.2", + "@patternfly/react-styles": "^5.3.1", + "@patternfly/react-table": "^5.3.4", + "@patternfly/react-topology": "^5.2.0", "@reduxjs/toolkit": "^1.9.3", "@types/lodash": "^4.14.202", "@types/react": "^17.0.69", diff --git a/src/app/About/AboutCryostatModal.tsx b/src/app/About/AboutCryostatModal.tsx index e7356080b..7df28ccf4 100644 --- a/src/app/About/AboutCryostatModal.tsx +++ b/src/app/About/AboutCryostatModal.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import bkgImg from '@app/assets/about_background.png'; +import bkgImg from '@app/assets/cryostat_icon_bg.svg'; import cryostatLogo from '@app/assets/cryostat_icon_rgb_reverse.svg'; import build from '@app/build.json'; import { portalRoot } from '@app/utils/utils'; diff --git a/src/app/Agent/AgentLiveProbes.tsx b/src/app/Agent/AgentLiveProbes.tsx index f74c6d63c..ce5c20be8 100644 --- a/src/app/Agent/AgentLiveProbes.tsx +++ b/src/app/Agent/AgentLiveProbes.tsx @@ -29,12 +29,12 @@ import { ToolbarContent, ToolbarGroup, ToolbarItem, - TextInput, Stack, StackItem, EmptyState, EmptyStateIcon, - Title, + EmptyStateHeader, + SearchInput, } from '@patternfly/react-core'; import { SearchIcon } from '@patternfly/react-icons'; import { @@ -42,14 +42,16 @@ import { ISortBy, SortByDirection, ThProps, - TableComposable, + Table, Tbody, Th, Thead, Tr, Td, } from '@patternfly/react-table'; +import _ from 'lodash'; import * as React from 'react'; +import { useTranslation } from 'react-i18next'; import { combineLatest } from 'rxjs'; import { AboutAgentCard } from './AboutAgentCard'; @@ -85,8 +87,9 @@ const tableColumns: TableColumn[] = [ export interface AgentLiveProbesProps {} -export const AgentLiveProbes: React.FC = (_) => { +export const AgentLiveProbes: React.FC = () => { const context = React.useContext(ServiceContext); + const { t } = useTranslation(); const addSubscription = useSubscriptions(); const [probes, setProbes] = React.useState([]); @@ -176,6 +179,8 @@ export const AgentLiveProbes: React.FC = (_) => { } }, [context.settings, setWarningModalOpen, handleDeleteAllProbes]); + const handleFilterTextChange = React.useCallback((_, value: string) => setFilterText(value), [setFilterText]); + React.useEffect(() => { addSubscription( context.target.target().subscribe(() => { @@ -243,14 +248,14 @@ export const AgentLiveProbes: React.FC = (_) => { if (!filterText) { filtered = probes; } else { - const ft = filterText.trim().toLowerCase(); + const reg = new RegExp(_.escapeRegExp(filterText), 'i'); filtered = probes.filter( (t: EventProbe) => - t.name.toLowerCase().includes(ft) || - t.description.toLowerCase().includes(ft) || - t.clazz.toLowerCase().includes(ft) || - t.methodDescriptor.toLowerCase().includes(ft) || - t.methodName.toLowerCase().includes(ft), + reg.test(t.name) || + reg.test(t.description) || + reg.test(t.clazz) || + reg.test(t.methodDescriptor) || + reg.test(t.methodName), ); } @@ -323,16 +328,18 @@ export const AgentLiveProbes: React.FC = (_) => { - + ) : ( <> @@ -476,10 +488,10 @@ export const AgentProbeTemplateUploadModal: React.FC - Submit + {t('SUBMIT', { ns: 'common' })} )} @@ -496,6 +508,7 @@ export interface AgentTemplateActionProps { } export const AgentTemplateAction: React.FC = ({ onInsert, onDelete, template }) => { + const { t } = useTranslation(); const [isOpen, setIsOpen] = React.useState(false); const actionItems = React.useMemo(() => { @@ -506,34 +519,64 @@ export const AgentTemplateAction: React.FC = ({ onInse onClick: () => onInsert && onInsert(template), isDisabled: !onInsert, }, + { + isSeparator: true, + }, { key: 'delete-template', title: 'Delete', + isDanger: true, onClick: () => onDelete(template), }, ]; }, [onInsert, onDelete, template]); + const handleToggle = React.useCallback((_, opened: boolean) => setIsOpen(opened), [setIsOpen]); + + const dropdownItems = React.useMemo( + () => + actionItems.map((action, idx) => + action.isSeparator ? ( + + ) : ( + { + setIsOpen(false); + action.onClick && action.onClick(); + }} + isAriaDisabled={action.isDisabled} + isDanger={action.isDanger} + > + {action.title} + + ), + ), + [actionItems, setIsOpen], + ); + return ( } - menuAppendTo={document.body} - position={DropdownPosition.right} - isFlipEnabled - dropdownItems={actionItems.map((action) => ( - { - setIsOpen(false); - action.onClick(); - }} - isDisabled={action.isDisabled} + toggle={(toggleRef: React.Ref) => ( + handleToggle(event, !isOpen)} > - {action.title} - - ))} - /> + + + )} + onOpenChange={setIsOpen} + onOpenChangeKeys={['Escape']} + isOpen={isOpen} + popperProps={{ + position: 'right', + enableFlip: true, + }} + > + {dropdownItems} + ); }; diff --git a/src/app/AppLayout/AppLayout.tsx b/src/app/AppLayout/AppLayout.tsx index cf7d5bbe3..a8d482ba8 100644 --- a/src/app/AppLayout/AppLayout.tsx +++ b/src/app/AppLayout/AppLayout.tsx @@ -24,7 +24,7 @@ import { useJoyride } from '@app/Joyride/JoyrideProvider'; import { GlobalQuickStartDrawer } from '@app/QuickStarts/QuickStartDrawer'; import { IAppRoute, navGroups, routes } from '@app/routes'; import { ThemeSetting, SettingTab } from '@app/Settings/types'; -import { selectTab, tabAsParam } from '@app/Settings/utils'; +import { DARK_THEME_CLASS, selectTab, tabAsParam } from '@app/Settings/utils'; import { DynamicFeatureFlag, FeatureFlag } from '@app/Shared/Components/FeatureFlag'; import { NotificationCategory, Notification } from '@app/Shared/Services/api.types'; import { NotificationsContext } from '@app/Shared/Services/Notifications.service'; @@ -39,14 +39,8 @@ import { AlertActionCloseButton, AlertGroup, AlertVariant, - ApplicationLauncher, - ApplicationLauncherItem, Brand, Button, - Dropdown, - DropdownGroup, - DropdownItem, - DropdownToggle, Icon, Label, Masthead, @@ -66,22 +60,30 @@ import { ToolbarContent, ToolbarGroup, ToolbarItem, + PageSidebarBody, + MenuToggleElement, + MenuToggle, + DropdownList, + DropdownItem, + Dropdown, } from '@patternfly/react-core'; import { BarsIcon, BellIcon, - CaretDownIcon, CogIcon, ExternalLinkAltIcon, + LanguageIcon, PlusCircleIcon, QuestionCircleIcon, UserIcon, } from '@patternfly/react-icons'; import _ from 'lodash'; import * as React from 'react'; -import { useTranslation } from 'react-i18next'; +import { Trans, useTranslation } from 'react-i18next'; import { Link, matchPath, NavLink, useLocation, useNavigate } from 'react-router-dom'; import { map } from 'rxjs/operators'; +import { LogoutIcon } from './LogoutIcon'; +import { ThemeToggle } from './ThemeToggle'; export interface AppLayoutProps { children?: React.ReactNode; @@ -119,9 +121,9 @@ export const AppLayout: React.FC = ({ children }) => { React.useEffect(() => { if (theme === ThemeSetting.DARK) { - document.documentElement.classList.add('pf-theme-dark'); + document.documentElement.classList.add(DARK_THEME_CLASS); } else { - document.documentElement.classList.remove('pf-theme-dark'); + document.documentElement.classList.remove(DARK_THEME_CLASS); } }, [theme]); @@ -166,10 +168,10 @@ export const AppLayout: React.FC = ({ children }) => { } const overflow = notificationsToDisplay.length - visibleNotificationsCount; if (overflow > 0) { - return `View ${overflow} more`; + return t('AppLayout.NOTIFICATIONS.OVERFLOW_MESSAGE', { count: overflow }); } return ''; - }, [isNotificationDrawerExpanded, notificationsToDisplay, visibleNotificationsCount]); + }, [isNotificationDrawerExpanded, notificationsToDisplay, visibleNotificationsCount, t]); React.useEffect(() => { addSubscription(notificationsContext.unreadNotifications().subscribe((s) => setUnreadNotificationsCount(s.length))); @@ -231,7 +233,7 @@ export const AppLayout: React.FC = ({ children }) => { // prevent page resize to close nav during tour const onPageResize = React.useCallback( - (props: { mobileView: boolean; windowSize: number }) => { + (_, props: { mobileView: boolean; windowSize: number }) => { if (joyState.run === false) { setIsMobileView(props.mobileView); setIsNavOpen(!props.mobileView); @@ -282,22 +284,26 @@ export const AppLayout: React.FC = ({ children }) => { const userInfoItems = React.useMemo( () => [ - - Language preference - + }> + + , - - Log out - , + }> + {t('AppLayout.USER_MENU.LOGOUT')} + , ], - [handleLogout, handleLanguagePref], + [t, handleLogout, handleLanguagePref], ); - const UserInfoToggle = React.useMemo( - () => ( - - {username || } - + const userInfoToggle = React.useCallback( + (toggleRef: React.Ref) => ( + + {username || ( + + + + )} + ), [username, handleUserInfoToggle], ); @@ -326,104 +332,138 @@ export const AppLayout: React.FC = ({ children }) => { const helpItems = React.useMemo(() => { return [ - {t('AppLayout.APP_LAUNCHER.QUICKSTARTS')}} - />, - + }> + {t('AppLayout.APP_LAUNCHER.QUICKSTARTS')} + , + {t('AppLayout.APP_LAUNCHER.DOCUMENTATION')} - , - + , + {t('AppLayout.APP_LAUNCHER.GUIDED_TOUR')} - , - + , + {t('AppLayout.APP_LAUNCHER.HELP')} - , - + , + {t('AppLayout.APP_LAUNCHER.ABOUT')} - , + , ]; }, [t, handleOpenDocumentation, handleOpenGuidedTour, handleOpenDiscussion, handleOpenAboutModal]); - const levelBadge = React.useCallback((level: FeatureLevel) => { - return ( - - ); - }, []); + const levelBadge = React.useCallback( + (level: FeatureLevel) => { + return ( + + ); + }, + [t], + ); - const HeaderToolbar = React.useMemo( + const headerToolbar = React.useMemo( () => ( <> - + - handleHelpToggle()} + className="application-launcher" + toggle={(toggleRef: React.Ref) => ( + handleHelpToggle()} + > + + + + + )} isOpen={showHelpDropdown} - items={helpItems} - position="right" - toggleIcon={} - data-tour-id="application-launcher" - data-quickstart-id="application-launcher" - /> + onOpenChange={setShowHelpDropdown} + onOpenChangeKeys={['Escape']} + popperProps={{ + position: 'right', + }} + > + {helpItems} + setShowUserInfoDropdown(false)} + toggle={userInfoToggle} isOpen={showUserInfoDropdown} - toggle={UserInfoToggle} - position="right" - dropdownItems={userInfoItems} - /> + onOpenChange={setShowUserInfoDropdown} + onOpenChangeKeys={['Escape']} + popperProps={{ + position: 'right', + }} + > + {userInfoItems} + @@ -439,26 +479,29 @@ export const AppLayout: React.FC = ({ children }) => { setShowUserInfoDropdown, showUserInfoDropdown, showHelpDropdown, - UserInfoToggle, + userInfoToggle, userInfoItems, helpItems, + t, ], ); - const Header = React.useMemo( + const header = React.useMemo( () => ( <> - + + + @@ -467,15 +510,14 @@ export const AppLayout: React.FC = ({ children }) => { - - {HeaderToolbar} + {headerToolbar} ), - [isNavOpen, aboutModalOpen, HeaderToolbar, handleCloseAboutModal, onNavToggle, levelBadge], + [isNavOpen, aboutModalOpen, headerToolbar, handleCloseAboutModal, onNavToggle, levelBadge, t], ); const isActiveRoute = React.useCallback( @@ -497,7 +539,13 @@ export const AppLayout: React.FC = ({ children }) => { const Navigation = React.useMemo( () => ( -