From 982c7d690d1b1d01e3b4edd131fb036e290a793d Mon Sep 17 00:00:00 2001 From: Asim Regmi <54924215+asimregmi@users.noreply.github.com> Date: Mon, 9 Oct 2023 14:31:40 -0500 Subject: [PATCH 01/23] WP-299: Add Data Files button dropdown needs minor adjustment in alignment (#878) * small UI change to align dropdown * linting issues fixed --------- Co-authored-by: Chandra Y --- .../DataFiles/DataFilesSidebar/DataFilesSidebar.scss | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/client/src/components/DataFiles/DataFilesSidebar/DataFilesSidebar.scss b/client/src/components/DataFiles/DataFilesSidebar/DataFilesSidebar.scss index 9f4fac877..3b5cd538b 100644 --- a/client/src/components/DataFiles/DataFilesSidebar/DataFilesSidebar.scss +++ b/client/src/components/DataFiles/DataFilesSidebar/DataFilesSidebar.scss @@ -24,15 +24,14 @@ .dropdown-menu { border-color: var(--global-color-accent--normal); border-radius: 0; - margin-top: 11px; - padding: 0; + margin: 11px 3px 0; width: 200px; vertical-align: top; } .dropdown-menu::before { position: absolute; top: -10px; - left: 65px; + left: 67px; border-right: 10px solid transparent; border-bottom: 10px solid var(--global-color-accent--normal); border-left: 10px solid transparent; @@ -42,7 +41,7 @@ .dropdown-menu::after { position: absolute; top: -9px; - left: 66px; + left: 68px; border-right: 9px solid transparent; border-bottom: 9px solid #ffffff; border-left: 9px solid transparent; From 4cacf3253c3415efdaf530ee73de993de7ae6535 Mon Sep 17 00:00:00 2001 From: Sal Tijerina Date: Mon, 9 Oct 2023 15:44:52 -0500 Subject: [PATCH 02/23] handle interactive session messages (#877) --- .../Jobs/JobsSessionModal/JobsSessionModal.jsx | 8 +++++++- client/src/components/Jobs/JobsStatus/JobsStatus.jsx | 3 +++ .../shared_workspace_migration.py | 12 ++++++++++-- server/portal/apps/webhooks/views.py | 4 ++++ 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/client/src/components/Jobs/JobsSessionModal/JobsSessionModal.jsx b/client/src/components/Jobs/JobsSessionModal/JobsSessionModal.jsx index bb0827877..9fbd2e06f 100644 --- a/client/src/components/Jobs/JobsSessionModal/JobsSessionModal.jsx +++ b/client/src/components/Jobs/JobsSessionModal/JobsSessionModal.jsx @@ -4,7 +4,12 @@ import { bool, func, string } from 'prop-types'; import './JobsSessionModal.global.scss'; import styles from './JobsSessionModal.module.scss'; -const JobsSessionModal = ({ isOpen, toggle, interactiveSessionLink }) => { +const JobsSessionModal = ({ + isOpen, + toggle, + interactiveSessionLink, + message, +}) => { return ( { Click the button below to connect to the interactive session. + {message && {message}} To end the job, quit the application within the session. Files may take some time to appear in the output location after the diff --git a/client/src/components/Jobs/JobsStatus/JobsStatus.jsx b/client/src/components/Jobs/JobsStatus/JobsStatus.jsx index e162a3a77..4f5ec2291 100644 --- a/client/src/components/Jobs/JobsStatus/JobsStatus.jsx +++ b/client/src/components/Jobs/JobsStatus/JobsStatus.jsx @@ -72,6 +72,7 @@ function JobsStatus({ status, fancy, jobUuid }) { const notifs = useSelector((state) => state.notifications.list.notifs); let interactiveSessionLink; + let message; const jobConcluded = isTerminalState(status) || status === 'ARCHIVING'; @@ -85,6 +86,7 @@ function JobsStatus({ status, fancy, jobUuid }) { ); const notif = interactiveNotifs.find((n) => n.extra.uuid === jobUuid); interactiveSessionLink = notif ? notif.action_link : null; + message = notif ? notif.message : null; } return ( @@ -109,6 +111,7 @@ function JobsStatus({ status, fancy, jobUuid }) { toggle={toggleModal} isOpen={modal} interactiveSessionLink={interactiveSessionLink} + message={message} /> )} diff --git a/server/portal/apps/projects/workspace_operations/shared_workspace_migration.py b/server/portal/apps/projects/workspace_operations/shared_workspace_migration.py index 6bf6d4ec8..58cd57e3f 100644 --- a/server/portal/apps/projects/workspace_operations/shared_workspace_migration.py +++ b/server/portal/apps/projects/workspace_operations/shared_workspace_migration.py @@ -62,7 +62,11 @@ def migrate_project(project_id): for co_pi in v2_project.co_pis.all(): v2_role = get_role(project_id, co_pi.username) - v3_role = ROLE_MAP[v2_role] + try: + v3_role = ROLE_MAP[v2_role] + except KeyError: + print(f'ERROR: No role found for: {v2_role}') + v3_role = "reader" try: add_user_to_workspace(client, project_id, co_pi.username, v3_role) except NotFoundError: @@ -71,7 +75,11 @@ def migrate_project(project_id): for team_member in v2_project.team_members.all(): v2_role = get_role(project_id, team_member.username) - v3_role = ROLE_MAP[v2_role] + try: + v3_role = ROLE_MAP[v2_role] + except KeyError: + print(f'ERROR: No role found for: {v2_role}') + v3_role = "reader" try: add_user_to_workspace(client, project_id, team_member.username, v3_role) except NotFoundError: diff --git a/server/portal/apps/webhooks/views.py b/server/portal/apps/webhooks/views.py index 54544b23d..f38430c1b 100644 --- a/server/portal/apps/webhooks/views.py +++ b/server/portal/apps/webhooks/views.py @@ -161,6 +161,7 @@ def post(self, request, *args, **kwargs): job_uuid = request.POST.get('job_uuid', None) job_owner = request.POST.get('owner', None) address = request.POST.get('address', None) + message = request.POST.get('message', None) if not address: msg = "Missing required interactive webhook parameter: address" @@ -174,6 +175,9 @@ def post(self, request, *args, **kwargs): Notification.ACTION_LINK: address } + if message: + event_data[Notification.MESSAGE] = message + # confirm that there is a corresponding running tapis job before sending notification try: valid_state = validate_tapis_job(job_uuid, job_owner, TERMINAL_JOB_STATES) From e47ea96472dcf6ab018e3b6c7fdc90180d5a8a7e Mon Sep 17 00:00:00 2001 From: Chandra Y Date: Tue, 10 Oct 2023 10:01:14 -0500 Subject: [PATCH 03/23] Bug/WA-314: Input file fixes for hidden and FIXED types (#880) * WA-314: Input file fixes for hidden and fixed * Task/WP-66: Refactored some variables and props to include proptypes (#876) * Refactored some variables and props to include proptypes * fixed linting errors * WP-299: Add Data Files button dropdown needs minor adjustment in alignment (#878) * small UI change to align dropdown * linting issues fixed --------- Co-authored-by: Chandra Y --------- Co-authored-by: Asim Regmi <54924215+asimregmi@users.noreply.github.com> --- client/package-lock.json | 13 ++ client/package.json | 1 + .../Applications/AppForm/AppForm.jsx | 1 + .../Applications/AppForm/AppForm.test.js | 171 +++++++++++++++++- .../Applications/AppForm/AppFormSchema.js | 9 +- .../AppForm/fixtures/AppForm.app.fixture.js | 46 +++++ 6 files changed, 234 insertions(+), 7 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index f000bf735..530f88263 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -76,6 +76,7 @@ "stylelint": "^14.4.0", "stylelint-config-recommended": "^7.0.0", "stylelint-config-standard": "^25.0.0", + "timekeeper": "^2.3.1", "typescript": "^4.4.3", "vite": "^2.9.16", "weak-key": "^1.0.1" @@ -14617,6 +14618,12 @@ "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==", "dev": true }, + "node_modules/timekeeper": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/timekeeper/-/timekeeper-2.3.1.tgz", + "integrity": "sha512-LeQRS7/4JcC0PgdSFnfUiStQEdiuySlCj/5SJ18D+T1n9BoY7PxKFfCwLulpHXoLUFr67HxBddQdEX47lDGx1g==", + "dev": true + }, "node_modules/tiny-invariant": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz", @@ -25859,6 +25866,12 @@ "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==", "dev": true }, + "timekeeper": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/timekeeper/-/timekeeper-2.3.1.tgz", + "integrity": "sha512-LeQRS7/4JcC0PgdSFnfUiStQEdiuySlCj/5SJ18D+T1n9BoY7PxKFfCwLulpHXoLUFr67HxBddQdEX47lDGx1g==", + "dev": true + }, "tiny-invariant": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz", diff --git a/client/package.json b/client/package.json index db805dab0..8313ccab5 100644 --- a/client/package.json +++ b/client/package.json @@ -98,6 +98,7 @@ "stylelint": "^14.4.0", "stylelint-config-recommended": "^7.0.0", "stylelint-config-standard": "^25.0.0", + "timekeeper": "^2.3.1", "typescript": "^4.4.3", "vite": "^2.9.16", "weak-key": "^1.0.1" diff --git a/client/src/components/Applications/AppForm/AppForm.jsx b/client/src/components/Applications/AppForm/AppForm.jsx index 787dcf255..0667f13aa 100644 --- a/client/src/components/Applications/AppForm/AppForm.jsx +++ b/client/src/components/Applications/AppForm/AppForm.jsx @@ -484,6 +484,7 @@ export const AppSchemaForm = ({ app }) => { targetDir: isTargetPathField(k) ? v : null, }; }) + .filter((v) => v) //filter nulls .reduce((acc, entry) => { // merge input field and targetPath fields into one. const key = getInputFieldFromTargetPathField(entry.name); diff --git a/client/src/components/Applications/AppForm/AppForm.test.js b/client/src/components/Applications/AppForm/AppForm.test.js index d09920342..00ed86b1f 100644 --- a/client/src/components/Applications/AppForm/AppForm.test.js +++ b/client/src/components/Applications/AppForm/AppForm.test.js @@ -1,5 +1,6 @@ import React from 'react'; -import { render, waitFor } from '@testing-library/react'; +import { fireEvent, render, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; import { Provider } from 'react-redux'; import configureStore from 'redux-mock-store'; import { BrowserRouter } from 'react-router-dom'; @@ -15,11 +16,16 @@ import { appTrayExpectedFixture, } from '../../../redux/sagas/fixtures/apptray.fixture'; import { initialAppState } from '../../../redux/reducers/apps.reducers'; -import { helloWorldAppFixture } from './fixtures/AppForm.app.fixture'; +import { + helloWorldAppFixture, + helloWorldAppSubmissionPayloadFixture, +} from './fixtures/AppForm.app.fixture'; import systemsFixture from '../../DataFiles/fixtures/DataFiles.systems.fixture'; import { projectsFixture } from '../../../redux/sagas/fixtures/projects.fixture'; import '@testing-library/jest-dom/extend-expect'; +import timekeeper from 'timekeeper'; +const frozenDate = '2023-10-01'; const mockStore = configureStore(); const initialMockState = { allocations: allocationsFixture, @@ -56,6 +62,11 @@ function renderAppSchemaFormComponent(store, app) { } describe('AppSchemaForm', () => { + beforeAll(() => { + // Lock Time + timekeeper.freeze(new Date(frozenDate)); + }); + it('renders the AppSchemaForm', async () => { const store = mockStore({ ...initialMockState, @@ -257,6 +268,162 @@ describe('AppSchemaForm', () => { expect(getByText(/Activate your Application Name license/)).toBeDefined(); }); }); + + it('job submission with file input mode FIXED', async () => { + const store = mockStore({ + ...initialMockState, + }); + + const { getByText, container } = renderAppSchemaFormComponent(store, { + ...helloWorldAppFixture, + definition: { + ...helloWorldAppFixture.definition, + jobAttributes: { + ...helloWorldAppFixture.definition.jobAttributes, + fileInputs: [ + { + name: 'File to copy', + description: 'A fixed file used by the app', + inputMode: 'FIXED', + autoMountLocal: true, + sourceUrl: + 'tapis://corral-tacc/tacc/aci/secure-test/rallyGolf.jpg', + targetPath: 'rallyGolf.jpg', + }, + ], + }, + }, + }); + const hiddenFileInput = container.querySelector( + 'input[name="fileInputs.File to copy"]' + ); + // FIXED fields are still shown in UI but not submitted. + expect(hiddenFileInput).toBeInTheDocument(); + + const submitButton = getByText(/Submit/); + fireEvent.click(submitButton); + const payload = { + ...helloWorldAppSubmissionPayloadFixture, + job: { + ...helloWorldAppSubmissionPayloadFixture.job, + name: 'hello-world-0.0.1_' + frozenDate + 'T00:00:00', + }, + }; + + await waitFor(() => { + expect(store.getActions()).toEqual([ + { type: 'GET_SYSTEM_MONITOR' }, + { type: 'SUBMIT_JOB', payload: payload }, + ]); + }); + }); + + it('job submission with file input hidden', async () => { + const store = mockStore({ + ...initialMockState, + }); + + const { getByText, container } = renderAppSchemaFormComponent(store, { + ...helloWorldAppFixture, + definition: { + ...helloWorldAppFixture.definition, + jobAttributes: { + ...helloWorldAppFixture.definition.jobAttributes, + fileInputs: [ + { + name: 'File to copy', + description: 'A fixed file used by the app', + inputMode: 'REQUIRED', + autoMountLocal: true, + sourceUrl: + 'tapis://corral-tacc/tacc/aci/secure-test/rallyGolf.jpg', + targetPath: 'rallyGolf.jpg', + notes: { + isHidden: true, + }, + }, + ], + }, + }, + }); + + const hiddenFileInput = container.querySelector( + 'input[name="fileInputs.File to copy"]' + ); + expect(hiddenFileInput).not.toBeInTheDocument(); + + const submitButton = getByText(/Submit/); + fireEvent.click(submitButton); + const payload = { + ...helloWorldAppSubmissionPayloadFixture, + job: { + ...helloWorldAppSubmissionPayloadFixture.job, + name: 'hello-world-0.0.1_' + frozenDate + 'T00:00:00', + }, + }; + + await waitFor(() => { + expect(store.getActions()).toEqual([ + { type: 'GET_SYSTEM_MONITOR' }, + { type: 'SUBMIT_JOB', payload: payload }, + ]); + }); + }); + + it('job submission with custom target path', async () => { + const store = mockStore({ + ...initialMockState, + }); + const { getByText, container } = renderAppSchemaFormComponent(store, { + ...helloWorldAppFixture, + definition: { + ...helloWorldAppFixture.definition, + notes: { + ...helloWorldAppFixture.definition.notes, + showTargetPath: true, + }, + }, + }); + + const fileInput = container.querySelector( + 'input[name="fileInputs.File to modify"]' + ); + const file = 'tapis://foo/bar.txt'; + const targetPathForFile = 'baz.txt'; + fireEvent.change(fileInput, { target: { value: file } }); + const targetPathInput = container.querySelector( + 'input[name="fileInputs._TargetPath_File to modify"]' + ); + fireEvent.change(targetPathInput, { target: { value: targetPathForFile } }); + + const submitButton = getByText(/Submit/); + fireEvent.click(submitButton); + const payload = { + ...helloWorldAppSubmissionPayloadFixture, + job: { + ...helloWorldAppSubmissionPayloadFixture.job, + fileInputs: [ + { + name: 'File to modify', + sourceUrl: file, + targetPath: targetPathForFile, + }, + ], + name: 'hello-world-0.0.1_' + frozenDate + 'T00:00:00', + }, + }; + + await waitFor(() => { + expect(store.getActions()).toEqual([ + { type: 'GET_SYSTEM_MONITOR' }, + { type: 'SUBMIT_JOB', payload: payload }, + ]); + }); + }); + + afterAll(() => { + timekeeper.reset(); + }); }); describe('AppDetail', () => { diff --git a/client/src/components/Applications/AppForm/AppFormSchema.js b/client/src/components/Applications/AppForm/AppFormSchema.js index cfd772f48..b3f26ba09 100644 --- a/client/src/components/Applications/AppForm/AppFormSchema.js +++ b/client/src/components/Applications/AppForm/AppFormSchema.js @@ -106,11 +106,10 @@ const FormSchema = (app) => { app.definition.notes.showTargetPath ?? false; (app.definition.jobAttributes.fileInputs || []).forEach((i) => { const input = i; - /* TODOv3 consider hidden file inputs https://jira.tacc.utexas.edu/browse/WP-102 - if (input.name.startsWith('_') || !input.value.visible) { // TODOv3 visible or hidden - return; - } - */ + if (input.notes?.isHidden) { + return; + } + const field = { label: input.name, description: input.description, diff --git a/client/src/components/Applications/AppForm/fixtures/AppForm.app.fixture.js b/client/src/components/Applications/AppForm/fixtures/AppForm.app.fixture.js index 5aa718ba4..1458448ea 100644 --- a/client/src/components/Applications/AppForm/fixtures/AppForm.app.fixture.js +++ b/client/src/components/Applications/AppForm/fixtures/AppForm.app.fixture.js @@ -237,3 +237,49 @@ export const helloWorldAppFixture = { type: null, }, }; + +export const helloWorldAppSubmissionPayloadFixture = { + job: { + fileInputs: [], + parameterSet: { + appArgs: [ + { + name: 'Greeting', + arg: 'hello', + }, + { + name: 'Target', + arg: 'world', + }, + { + name: 'Sleep Time', + arg: '30', + }, + ], + containerArgs: [], + schedulerOptions: [ + { + name: 'TACC Allocation', + description: 'The TACC allocation associated with this job execution', + include: true, + arg: '-A TACC-ACI', + }, + ], + envVariables: [], + }, + name: 'hello-world-0.0.1', + nodeCount: 1, + coresPerNode: 1, + maxMinutes: 10, + archiveSystemId: 'frontera', + archiveSystemDir: + 'HOST_EVAL($HOME)/tapis-jobs-archive/${JobCreateDate}/${JobName}-${JobUUID}', + archiveOnAppError: true, + appId: 'hello-world', + appVersion: '0.0.1', + execSystemId: 'frontera', + execSystemLogicalQueue: 'development', + }, + licenseType: null, + isInteractive: false, +}; From d7a003e010bfe4b18adba5a8d9a2b6fe39339770 Mon Sep 17 00:00:00 2001 From: Jake Rosenberg Date: Tue, 10 Oct 2023 13:21:29 -0500 Subject: [PATCH 04/23] Fix email confirmation for tickets (#879) Co-authored-by: Jake Rosenberg Co-authored-by: Sal Tijerina --- server/portal/apps/tickets/rtUtil.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/portal/apps/tickets/rtUtil.py b/server/portal/apps/tickets/rtUtil.py index 42c1bd7cd..bd848e585 100644 --- a/server/portal/apps/tickets/rtUtil.py +++ b/server/portal/apps/tickets/rtUtil.py @@ -47,7 +47,7 @@ def create_ticket(self, attachments, subject, problem_description, requestor, cc files=attachments, Subject=subject, Text=problem_description, - Requestors=requestor, + Requestor=requestor, Cc=cc, CF_resource=settings.RT_TAG) From 1b05540376b0cdc3be7d9957d1edbc67a5f8929c Mon Sep 17 00:00:00 2001 From: Asim Regmi <54924215+asimregmi@users.noreply.github.com> Date: Tue, 17 Oct 2023 10:28:28 -0500 Subject: [PATCH 05/23] WP-66: refactor data files components 2 (#885) * Refactored some variables and props to include proptypes * fixed linting errors * fixed DataFiles prop variable names * Added proptypes is required for projectId --- .../components/DataFiles/DataFilesModals/DataFilesCopyModal.jsx | 2 +- .../DataFiles/DataFilesModals/DataFilesSelectModal.jsx | 2 +- .../DataFilesProjectMembers/DataFilesProjectMembers.jsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/components/DataFiles/DataFilesModals/DataFilesCopyModal.jsx b/client/src/components/DataFiles/DataFilesModals/DataFilesCopyModal.jsx index bebffdf3f..c4cd26d68 100644 --- a/client/src/components/DataFiles/DataFilesModals/DataFilesCopyModal.jsx +++ b/client/src/components/DataFiles/DataFilesModals/DataFilesCopyModal.jsx @@ -142,7 +142,7 @@ const DataFilesCopyModal = React.memo(() => { Destination { Select Input Date: Wed, 18 Oct 2023 11:40:23 -0500 Subject: [PATCH 06/23] build(deps-dev): bump @babel/traverse from 7.19.0 to 7.23.2 in /client (#889) Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.19.0 to 7.23.2. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse) --- updated-dependencies: - dependency-name: "@babel/traverse" dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- client/package-lock.json | 296 ++++++++++++++++++++------------------- 1 file changed, 150 insertions(+), 146 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 530f88263..98a48ba2e 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -134,12 +134,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dev": true, "dependencies": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" @@ -185,13 +186,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.19.0.tgz", - "integrity": "sha512-S1ahxf1gZ2dpoiFgA+ohK9DIpz50bJ0CWs7Zlzb54Z4sG8qmdIrGrVqmy1sAtTVRb+9CU6U8VqT9L0Zj7hxHVg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dev": true, "dependencies": { - "@babel/types": "^7.19.0", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" }, "engines": { @@ -298,9 +300,9 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, "engines": { "node": ">=6.9.0" @@ -319,25 +321,25 @@ } }, "node_modules/@babel/helper-function-name": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", - "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "dependencies": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -462,30 +464,30 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz", - "integrity": "sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz", - "integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true, "engines": { "node": ">=6.9.0" @@ -530,13 +532,13 @@ } }, "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -544,9 +546,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.19.0.tgz", - "integrity": "sha512-74bEXKX2h+8rrfQUfsBfuZZHzsEs6Eql4pqy/T4Nn6Y9wNPggQOqD6z6pn5Bl8ZfysKouFZT/UXEH94ummEeQw==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -1927,33 +1929,33 @@ } }, "node_modules/@babel/template": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", - "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.18.10", - "@babel/types": "^7.18.10" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.19.0.tgz", - "integrity": "sha512-4pKpFRDh+utd2mbRC8JLnlsMUii3PMHjpL6a0SZ4NMZy7YFP9aXORxEhdMVOc9CpWtDF09IkciQLEhK7Ml7gRA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.19.0", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.19.0", - "@babel/types": "^7.19.0", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -1962,13 +1964,13 @@ } }, "node_modules/@babel/types": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.0.tgz", - "integrity": "sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.18.10", - "@babel/helper-validator-identifier": "^7.18.6", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -3083,9 +3085,9 @@ } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz", - "integrity": "sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", "dev": true, "engines": { "node": ">=6.0.0" @@ -3101,19 +3103,19 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.11", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz", - "integrity": "sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg==", + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.15", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz", - "integrity": "sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g==", + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", "dev": true, "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@material-ui/core": { @@ -15361,12 +15363,13 @@ } }, "@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dev": true, "requires": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" } }, "@babel/compat-data": { @@ -15399,13 +15402,14 @@ } }, "@babel/generator": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.19.0.tgz", - "integrity": "sha512-S1ahxf1gZ2dpoiFgA+ohK9DIpz50bJ0CWs7Zlzb54Z4sG8qmdIrGrVqmy1sAtTVRb+9CU6U8VqT9L0Zj7hxHVg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dev": true, "requires": { - "@babel/types": "^7.19.0", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" } }, @@ -15482,9 +15486,9 @@ } }, "@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true }, "@babel/helper-explode-assignable-expression": { @@ -15497,22 +15501,22 @@ } }, "@babel/helper-function-name": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", - "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "requires": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" } }, "@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-member-expression-to-functions": { @@ -15607,24 +15611,24 @@ } }, "@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-string-parser": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz", - "integrity": "sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz", - "integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true }, "@babel/helper-validator-option": { @@ -15657,20 +15661,20 @@ } }, "@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.19.0.tgz", - "integrity": "sha512-74bEXKX2h+8rrfQUfsBfuZZHzsEs6Eql4pqy/T4Nn6Y9wNPggQOqD6z6pn5Bl8ZfysKouFZT/UXEH94ummEeQw==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "dev": true }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { @@ -16594,42 +16598,42 @@ } }, "@babel/template": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", - "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.18.10", - "@babel/types": "^7.18.10" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" } }, "@babel/traverse": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.19.0.tgz", - "integrity": "sha512-4pKpFRDh+utd2mbRC8JLnlsMUii3PMHjpL6a0SZ4NMZy7YFP9aXORxEhdMVOc9CpWtDF09IkciQLEhK7Ml7gRA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.19.0", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.19.0", - "@babel/types": "^7.19.0", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" } }, "@babel/types": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.0.tgz", - "integrity": "sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.18.10", - "@babel/helper-validator-identifier": "^7.18.6", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } }, @@ -17401,9 +17405,9 @@ } }, "@jridgewell/resolve-uri": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz", - "integrity": "sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", "dev": true }, "@jridgewell/set-array": { @@ -17413,19 +17417,19 @@ "dev": true }, "@jridgewell/sourcemap-codec": { - "version": "1.4.11", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz", - "integrity": "sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg==", + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", "dev": true }, "@jridgewell/trace-mapping": { - "version": "0.3.15", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz", - "integrity": "sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g==", + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", "dev": true, "requires": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "@material-ui/core": { From c522d1c6ff7221c351b8fa33c8a3810fc6dc952e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Oct 2023 11:40:42 -0500 Subject: [PATCH 07/23] build(deps): bump urllib3 from 1.26.17 to 1.26.18 in /server (#888) Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.17 to 1.26.18. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/1.26.17...1.26.18) --- updated-dependencies: - dependency-name: urllib3 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- server/poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/poetry.lock b/server/poetry.lock index 3e25cce44..aaf7d1064 100644 --- a/server/poetry.lock +++ b/server/poetry.lock @@ -2619,13 +2619,13 @@ files = [ [[package]] name = "urllib3" -version = "1.26.17" +version = "1.26.18" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ - {file = "urllib3-1.26.17-py2.py3-none-any.whl", hash = "sha256:94a757d178c9be92ef5539b8840d48dc9cf1b2709c9d6b588232a055c524458b"}, - {file = "urllib3-1.26.17.tar.gz", hash = "sha256:24d6a242c28d29af46c3fae832c36db3bbebcc533dd1bb549172cd739c82df21"}, + {file = "urllib3-1.26.18-py2.py3-none-any.whl", hash = "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07"}, + {file = "urllib3-1.26.18.tar.gz", hash = "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0"}, ] [package.extras] From f12180a8b9970b0101efb4394f7f5bbe65129a1c Mon Sep 17 00:00:00 2001 From: Shayan Khan Date: Thu, 19 Oct 2023 11:34:37 -0500 Subject: [PATCH 08/23] task/WP-164 Implement Workspace Search (#886) * simplified and fixed search on project listing and project file listing * small fix * fix project file search bug, added search on project id * fixing a test --- server/portal/apps/projects/views.py | 39 +++++++++++++++---- server/portal/apps/search/tasks.py | 7 +++- server/portal/libs/agave/operations.py | 2 +- .../portal/libs/agave/operations_unit_test.py | 2 +- server/portal/libs/elasticsearch/docs/base.py | 38 ++++++------------ server/portal/libs/elasticsearch/utils.py | 22 +++++++++++ 6 files changed, 73 insertions(+), 37 deletions(-) diff --git a/server/portal/apps/projects/views.py b/server/portal/apps/projects/views.py index 8baf098d3..ca03a8479 100644 --- a/server/portal/apps/projects/views.py +++ b/server/portal/apps/projects/views.py @@ -17,7 +17,9 @@ list_projects, get_project, create_shared_workspace,\ update_project, get_workspace_role, change_user_role, add_user_to_workspace,\ remove_user, transfer_ownership - +from portal.apps.search.tasks import tapis_project_listing_indexer +from portal.libs.elasticsearch.indexes import IndexedProject +from elasticsearch_dsl import Q LOGGER = logging.getLogger(__name__) @@ -63,12 +65,35 @@ def get(self, request): } ``` """ - # TODOv3: Support Elasticsearch queries for V3 projects https://jira.tacc.utexas.edu/browse/TV3-160 - # query_string = request.GET.get('query_string') - # offset = int(request.GET.get('offset', 0)) - # limit = int(request.GET.get('limit', 100)) - client = request.user.tapis_oauth.client - listing = list_projects(client) + + query_string = request.GET.get('query_string') + offset = int(request.GET.get('offset', 0)) + limit = int(request.GET.get('limit', 100)) + + listing = [] + + if query_string: + search = IndexedProject.search() + + ngram_query = Q("query_string", query=query_string, + fields=["title", "id"], + minimum_should_match='100%', + default_operator='or') + + wildcard_query = Q("wildcard", title=f'*{query_string}*') | Q("wildcard", id=f'*{query_string}*') + + search = search.query(ngram_query | wildcard_query) + search = search.extra(from_=int(offset), size=int(limit)) + + res = search.execute() + hits = [hit.to_dict() for hit in res] + listing = hits + else: + client = request.user.tapis_oauth.client + listing = list_projects(client) + + tapis_project_listing_indexer.delay(listing) + return JsonResponse({"status": 200, "response": listing}) def post(self, request): # pylint: disable=no-self-use diff --git a/server/portal/apps/search/tasks.py b/server/portal/apps/search/tasks.py index 6dabd8725..61bf00a2e 100644 --- a/server/portal/apps/search/tasks.py +++ b/server/portal/apps/search/tasks.py @@ -3,7 +3,7 @@ from django.conf import settings from celery import shared_task from portal.libs.agave.utils import user_account, service_account -from portal.libs.elasticsearch.utils import index_listing +from portal.libs.elasticsearch.utils import index_listing, index_project_listing from portal.apps.users.utils import get_tas_allocations from portal.apps.projects.models.metadata import ProjectMetadata from portal.libs.elasticsearch.docs.base import (IndexedAllocation, @@ -78,3 +78,8 @@ def index_project(self, project_id): project_doc = IndexedProject(**project_dict) project_doc.meta.id = project_id project_doc.save() + + +@shared_task(bind=True, max_retries=3, queue='default') +def tapis_project_listing_indexer(self, projects): + index_project_listing(projects) diff --git a/server/portal/libs/agave/operations.py b/server/portal/libs/agave/operations.py index 0131dcef8..83264a1a8 100644 --- a/server/portal/libs/agave/operations.py +++ b/server/portal/libs/agave/operations.py @@ -139,7 +139,7 @@ def search(client, system, path='', offset=0, limit=100, query_string='', filter if filter: search = search.filter(filter_query) - search = search.filter('prefix', **{'path._exact': path}) + search = search.filter('prefix', **{'path._exact': path.strip('/')}) search = search.filter('term', **{'system._exact': system}) search = search.extra(from_=int(offset), size=int(limit)) res = search.execute() diff --git a/server/portal/libs/agave/operations_unit_test.py b/server/portal/libs/agave/operations_unit_test.py index 269826025..81ddfaa85 100644 --- a/server/portal/libs/agave/operations_unit_test.py +++ b/server/portal/libs/agave/operations_unit_test.py @@ -74,7 +74,7 @@ def test_search(self, mock_search, mock_listing): "name._exact, name._pattern"], default_operator='and')) - mock_search().query().filter.assert_called_with('prefix', **{'path._exact': '/path'}) + mock_search().query().filter.assert_called_with('prefix', **{'path._exact': 'path'}) mock_search().query().filter().filter.assert_called_with('term', **{'system._exact': 'test.system'}) mock_search().query().filter().filter().extra.assert_called_with(from_=int(0), size=int(100)) self.assertEqual(search_res, {'listing': diff --git a/server/portal/libs/elasticsearch/docs/base.py b/server/portal/libs/elasticsearch/docs/base.py index 9d17e3315..c52b36be7 100644 --- a/server/portal/libs/elasticsearch/docs/base.py +++ b/server/portal/libs/elasticsearch/docs/base.py @@ -17,37 +17,21 @@ class IndexedProject(Document): + id = Keyword(fields={'_exact': Keyword()}) title = Text(fields={'_exact': Keyword()}) description = Text() - created = Date() - lastModified = Date() - projectId = Keyword() + path = Text() + name = Text() + host = Text() owner = Object( - properties={ - 'username': Keyword(), - 'fullName': Text() - } - ) - pi = Object( - properties={ - 'username': Keyword(), - 'fullName': Text() - } - ) - coPIs = Object( - multi=True, - properties={ - 'username': Keyword(), - 'fullName': Text() - } - ) - teamMembers = Object( - multi=True, - properties={ - 'username': Keyword(), - 'fullName': Text() - } + properties={ + 'username': Keyword(), + 'firstName': Text(), + 'lastName': Text(), + 'email': Text() + } ) + updated = Date() @classmethod def from_id(cls, projectId): diff --git a/server/portal/libs/elasticsearch/utils.py b/server/portal/libs/elasticsearch/utils.py index cd2c2cc75..90666209f 100644 --- a/server/portal/libs/elasticsearch/utils.py +++ b/server/portal/libs/elasticsearch/utils.py @@ -219,3 +219,25 @@ def index_listing(files): }) bulk(client, ops) + + +def index_project_listing(projects): + from portal.libs.elasticsearch.docs.base import IndexedProject + + idx = IndexedProject.Index.name + client = get_connection('default') + ops = [] + + for _project in projects: + project_dict = dict(_project) + project_dict['updated'] = current_time() + project_uuid = get_sha256_hash(project_dict['id']) + ops.append({ + '_index': idx, + '_id': project_uuid, + 'doc': project_dict, + '_op_type': 'update', + 'doc_as_upsert': True + }) + + bulk(client, ops) From e427c97155d47a0278b49c75eaba61897e223a4f Mon Sep 17 00:00:00 2001 From: Taylor Grafft Date: Tue, 24 Oct 2023 13:52:44 -0500 Subject: [PATCH 09/23] task/WP-109-remove-unused-django-fields (#887) * task/WP-109-remove-unused-django-fields-v1 * task/WP-109-remove-unused-django-fields-v2 * task/WP-109-remove-unused-django-fields-v3 * task/WP-109-remove-unused-django-fields-v4 --------- Co-authored-by: Taylor Grafft Co-authored-by: Taylor Grafft Co-authored-by: Taylor Grafft Co-authored-by: Sal Tijerina --- .../ManageAccount/ManageAccountTables.jsx | 2 - .../tests/ManageAccountTables.test.js | 8 ---- .../migrations/0006_auto_20231018_1927.py | 37 +++++++++++++++++++ server/portal/apps/accounts/models.py | 7 ---- server/portal/fixtures/accounts.json | 12 ++---- server/portal/fixtures/users.json | 6 --- 6 files changed, 40 insertions(+), 32 deletions(-) create mode 100644 server/portal/apps/accounts/migrations/0006_auto_20231018_1927.py diff --git a/client/src/components/ManageAccount/ManageAccountTables.jsx b/client/src/components/ManageAccount/ManageAccountTables.jsx index 8ad1abbd4..ad360f725 100644 --- a/client/src/components/ManageAccount/ManageAccountTables.jsx +++ b/client/src/components/ManageAccount/ManageAccountTables.jsx @@ -65,8 +65,6 @@ export const ProfileInformation = () => { { Header: 'Institution', accessor: 'institution' }, { Header: 'Country of Residence', accessor: 'country' }, { Header: 'Country of Citizenship', accessor: 'citizenship' }, - { Header: 'Ethnicity', accessor: 'ethnicity' }, - { Header: 'Gender', accessor: 'gender' }, ], [] ); diff --git a/client/src/components/ManageAccount/tests/ManageAccountTables.test.js b/client/src/components/ManageAccount/tests/ManageAccountTables.test.js index 3a655aaae..e10515297 100644 --- a/client/src/components/ManageAccount/tests/ManageAccountTables.test.js +++ b/client/src/components/ManageAccount/tests/ManageAccountTables.test.js @@ -21,12 +21,6 @@ const dummyState = { }, data: { demographics: { - ethnicity: 'Asian', - gender: 'Male', - bio: '', - website: 'http://owais.io', - orcid_id: 'test', - professional_level: 'Staff (support, administration, etc)', username: 'ojamil', email: 'ojamil@tacc.utexas.edu', firstName: 'Owais', @@ -77,8 +71,6 @@ describe('Profile Information Component', () => { 'Institution', 'Country of Residence', 'Country of Citizenship', - 'Ethnicity', - 'Gender', ]; headings.forEach((heading) => { expect(getByText(heading)).toBeInTheDocument(); diff --git a/server/portal/apps/accounts/migrations/0006_auto_20231018_1927.py b/server/portal/apps/accounts/migrations/0006_auto_20231018_1927.py new file mode 100644 index 000000000..183c3ebd7 --- /dev/null +++ b/server/portal/apps/accounts/migrations/0006_auto_20231018_1927.py @@ -0,0 +1,37 @@ +# Generated by Django 3.2.20 on 2023-10-18 19:27 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0005_auto_20210316_1950'), + ] + + operations = [ + migrations.RemoveField( + model_name='portalprofile', + name='bio', + ), + migrations.RemoveField( + model_name='portalprofile', + name='ethnicity', + ), + migrations.RemoveField( + model_name='portalprofile', + name='gender', + ), + migrations.RemoveField( + model_name='portalprofile', + name='orcid_id', + ), + migrations.RemoveField( + model_name='portalprofile', + name='professional_level', + ), + migrations.RemoveField( + model_name='portalprofile', + name='website', + ), + ] diff --git a/server/portal/apps/accounts/models.py b/server/portal/apps/accounts/models.py index 4f18ff8a1..482c363f3 100644 --- a/server/portal/apps/accounts/models.py +++ b/server/portal/apps/accounts/models.py @@ -26,13 +26,6 @@ class PortalProfile(models.Model): related_name='profile', on_delete=models.CASCADE ) - ethnicity = models.CharField(max_length=255) - gender = models.CharField(max_length=255) - bio = models.CharField(max_length=4096, default=None, null=True, blank=True) - website = models.CharField(max_length=256, default=None, null=True, blank=True) - orcid_id = models.CharField(max_length=256, default=None, null=True, blank=True) - professional_level = models.CharField(max_length=256, default=None, null=True) - # Default to False. If PORTAL_USER_ACCOUNT_SETUP_STEPS is empty, # setup_complete will be set to True on first login setup_complete = models.BooleanField(default=False) diff --git a/server/portal/fixtures/accounts.json b/server/portal/fixtures/accounts.json index cf850fd7b..fdd870d93 100644 --- a/server/portal/fixtures/accounts.json +++ b/server/portal/fixtures/accounts.json @@ -3,27 +3,21 @@ "model": "accounts.portalprofile", "pk": 1, "fields": { - "user": 1, - "ethnicity": "", - "gender": "" + "user": 1 } }, { "model": "accounts.portalprofile", "pk": 2, "fields": { - "user": 2, - "ethnicity": "", - "gender": "" + "user": 2 } }, { "model": "accounts.portalprofile", "pk": 3, "fields": { - "user": 3, - "ethnicity": "", - "gender": "" + "user": 3 } }, { diff --git a/server/portal/fixtures/users.json b/server/portal/fixtures/users.json index baca595ba..a5410e9ad 100644 --- a/server/portal/fixtures/users.json +++ b/server/portal/fixtures/users.json @@ -58,12 +58,6 @@ "pk": 1, "fields": { "user": 1, - "ethnicity": "", - "gender": "", - "bio": null, - "website": null, - "orcid_id": null, - "professional_level": null, "setup_complete": true } } From fa8c979762eb56663321b7c7145b0dd3c0e39c77 Mon Sep 17 00:00:00 2001 From: Taylor Grafft Date: Tue, 24 Oct 2023 14:21:23 -0500 Subject: [PATCH 10/23] task/WP-288: Implement Queue Filter (#883) * task/WP-288-QueueFilter --------- Co-authored-by: Taylor Grafft Co-authored-by: Taylor Grafft Co-authored-by: Chandra Y --- .../Applications/AppForm/AppForm.jsx | 20 +++++-- .../Applications/AppForm/AppForm.test.js | 55 +++++++++++++++++++ 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/client/src/components/Applications/AppForm/AppForm.jsx b/client/src/components/Applications/AppForm/AppForm.jsx index 0667f13aa..f197be6cd 100644 --- a/client/src/components/Applications/AppForm/AppForm.jsx +++ b/client/src/components/Applications/AppForm/AppForm.jsx @@ -692,11 +692,21 @@ export const AppSchemaForm = ({ app }) => { ) .map((q) => q.name) .sort() - .map((queueName) => ( - - )) + .map((queueName) => + app.definition.notes.queueFilter ? ( + app.definition.notes.queueFilter.includes( + queueName + ) && ( + + ) + ) : ( + + ) + ) .sort()} )} diff --git a/client/src/components/Applications/AppForm/AppForm.test.js b/client/src/components/Applications/AppForm/AppForm.test.js index 00ed86b1f..2a6b119df 100644 --- a/client/src/components/Applications/AppForm/AppForm.test.js +++ b/client/src/components/Applications/AppForm/AppForm.test.js @@ -442,3 +442,58 @@ describe('AppDetail', () => { ).toBeDefined(); }); }); + +const mockAppWithQueueFilter = { + ...helloWorldAppFixture, + definition: { + ...helloWorldAppFixture.definition, + notes: { + ...helloWorldAppFixture.definition.notes, + queueFilter: ['rtx', 'small'], + }, + }, +}; + +const mockAppWithoutQueueFilter = { + ...helloWorldAppFixture, + definition: { + ...helloWorldAppFixture.definition, + notes: { + ...helloWorldAppFixture.definition.notes, + queueFilter: null, + }, + }, +}; + +describe('AppSchemaForm queueFilter tests', () => { + it('renders only the queues specified in the queueFilter', () => { + const { container } = renderAppSchemaFormComponent( + mockStore(initialMockState), + mockAppWithQueueFilter + ); + + const targetDropdown = container.querySelector( + 'select[name="execSystemLogicalQueue"]' + ); + const options = Array.from(targetDropdown.querySelectorAll('option')); + expect(options).toHaveLength(2); + expect(options[0].textContent).toBe('rtx'); + expect(options[1].textContent).toBe('small'); + }); + + it('renders all queues when no queueFilter is present', () => { + const { container } = renderAppSchemaFormComponent( + mockStore(initialMockState), + mockAppWithoutQueueFilter + ); + + const targetDropdown = container.querySelector( + 'select[name="execSystemLogicalQueue"]' + ); + const options = Array.from(targetDropdown.querySelectorAll('option')); + expect(options).toHaveLength(3); + expect(options[0].textContent).toBe('development'); + expect(options[1].textContent).toBe('rtx'); + expect(options[2].textContent).toBe('small'); + }); +}); From 31af29e901bc8ce42572969b034e2716563812e7 Mon Sep 17 00:00:00 2001 From: Asim Regmi <54924215+asimregmi@users.noreply.github.com> Date: Tue, 24 Oct 2023 15:07:42 -0500 Subject: [PATCH 11/23] task/WP-100: Display all jobAttributes via getJobDisplayInformation (#868) * filtered out hidden inputs * added more jobAttributes * added a new function filterHiddenObjects to combine filters * added test suites for filter input and parameters --------- Co-authored-by: Chandra Y --- .../History/HistoryViews/JobHistoryModal.jsx | 2 + .../redux/sagas/fixtures/appdetail.fixture.js | 16 +++++ .../redux/sagas/fixtures/jobdetail.fixture.js | 4 +- client/src/utils/jobsUtil.js | 68 +++++++------------ 4 files changed, 43 insertions(+), 47 deletions(-) diff --git a/client/src/components/History/HistoryViews/JobHistoryModal.jsx b/client/src/components/History/HistoryViews/JobHistoryModal.jsx index 37c155bcc..a522b5b10 100644 --- a/client/src/components/History/HistoryViews/JobHistoryModal.jsx +++ b/client/src/components/History/HistoryViews/JobHistoryModal.jsx @@ -122,6 +122,8 @@ function JobHistoryContent({ const outputDataObj = { 'Job Name': jobName, 'Output Location': outputLocation, + 'Archive System': jobDetails.archiveSystemId, + 'Archive Directory': jobDetails.archiveSystemDir, }; const resubmitJob = () => { diff --git a/client/src/redux/sagas/fixtures/appdetail.fixture.js b/client/src/redux/sagas/fixtures/appdetail.fixture.js index 9d654b14a..3fc531b23 100644 --- a/client/src/redux/sagas/fixtures/appdetail.fixture.js +++ b/client/src/redux/sagas/fixtures/appdetail.fixture.js @@ -62,6 +62,13 @@ const appDetailFixture = { inputMode: 'REQUIRED', notes: { fieldType: 'number' }, }, + { + arg: 'OpenSeesSP', + name: 'mainProgram', + description: '', + inputMode: 'FIXED', + notes: { isHidden: true }, + }, ], containerArgs: [], schedulerOptions: [ @@ -86,6 +93,15 @@ const appDetailFixture = { sourceUrl: null, targetPath: 'in.txt', }, + { + name: 'hello world', + description: 'hello world description', + inputMode: 'FIXED', + autoMountLocal: true, + sourceUrl: null, + targetPath: '.', + notes: { isHidden: true }, + }, ], fileInputArrays: [], nodeCount: 1, diff --git a/client/src/redux/sagas/fixtures/jobdetail.fixture.js b/client/src/redux/sagas/fixtures/jobdetail.fixture.js index 4e1b61db1..ffea2139e 100644 --- a/client/src/redux/sagas/fixtures/jobdetail.fixture.js +++ b/client/src/redux/sagas/fixtures/jobdetail.fixture.js @@ -33,9 +33,9 @@ const jobDetailFixture = { memoryMB: 100, maxMinutes: 10, fileInputs: - '[{"name": "File to modify", "optional": true, "sourceUrl": "tapis://test.community/system/1/user/test/in.txt", "targetPath": "in.txt", "description": "The full greeting will be appended to the target .txt file", "autoMountLocal": true, "srcSharedAppCtx": false, "destSharedAppCtx": true}]', + '[{"name": "File to modify", "optional": true, "sourceUrl": "tapis://test.community/system/1/user/test/in.txt", "targetPath": "in.txt", "description": "The full greeting will be appended to the target .txt file", "autoMountLocal": true, "srcSharedAppCtx": false, "destSharedAppCtx": true}, {"name": "hello world", "optional": false, "sourceUrl": "", "targetPath": ".", "description": "hello world description", "autoMountLocal": true, "srcSharedAppCtx": false, "destSharedAppCtx": true, "notes": "{\\"isHidden\\":\\"true\\"}"}]', parameterSet: - '{"appArgs": [{"arg": "hello", "name": "Greeting", "notes": "{\\"enum_values\\":[{\\"hello\\":\\"Hello\\"},{\\"hola\\":\\"Hola\\"},{\\"wassup\\":\\"Wassup\\"}]}", "include": null, "description": "Choose a greeting to give to your target"}, {"arg": "world", "name": "Target", "notes": "{}", "include": null, "description": "Whom to address your greeting"}, {"arg": "1", "name": "Sleep Time", "notes": "{\\"fieldType\\":\\"number\\"}", "include": null, "description": "How long to sleep before app execution"}], "envVariables": [{"key": "_tapisAppId", "value": "hello-world", "description": null}, {"key": "_tapisAppVersion", "value": "0.0.1", "description": null}, {"key": "_tapisArchiveOnAppError", "value": "true", "description": null}, {"key": "_tapisArchiveSystemDir", "value": "/home/user/tapis-jobs-archive/2023-01-24Z/hello-world_2023-01-24T23:52:57-e929ad16-adc5-4bd4-b84f-d41d1b67e5ee-007", "description": null}, {"key": "_tapisArchiveSystemId", "value": "cloud.data", "description": null}, {"key": "_tapisCoresPerNode", "value": "1", "description": null}, {"key": "_tapisDynamicExecSystem", "value": "false", "description": null}, {"key": "_tapisEffectiveUserId", "value": "user", "description": null}, {"key": "_tapisExecSystemExecDir", "value": "/scratch1/12345/user/tapis/e929ad16-adc5-4bd4-b84f-d41d1b67e5ee-007", "description": null}, {"key": "_tapisExecSystemHPCQueue", "value": "development", "description": null}, {"key": "_tapisExecSystemId", "value": "frontera", "description": null}, {"key": "_tapisExecSystemInputDir", "value": "/scratch1/12345/user/tapis/e929ad16-adc5-4bd4-b84f-d41d1b67e5ee-007", "description": null}, {"key": "_tapisExecSystemLogicalQueue", "value": "development", "description": null}, {"key": "_tapisExecSystemOutputDir", "value": "/scratch1/12345/user/tapis/e929ad16-adc5-4bd4-b84f-d41d1b67e5ee-007/output", "description": null}, {"key": "_tapisJobCreateDate", "value": "2023-01-24Z", "description": null}, {"key": "_tapisJobCreateTime", "value": "23:53:10.922143633Z", "description": null}, {"key": "_tapisJobCreateTimestamp", "value": "2023-01-24T23:53:10.922143633Z", "description": null}, {"key": "_tapisJobName", "value": "hello-world_2023-01-24T23:52:57", "description": null}, {"key": "_tapisJobOwner", "value": "user", "description": null}, {"key": "_tapisJobUUID", "value": "e929ad16-adc5-4bd4-b84f-d41d1b67e5ee-007", "description": null}, {"key": "_tapisJobWorkingDir", "value": "/scratch1/12345/user/tapis/e929ad16-adc5-4bd4-b84f-d41d1b67e5ee-007", "description": null}, {"key": "_tapisMaxMinutes", "value": "10", "description": null}, {"key": "_tapisMemoryMB", "value": "100", "description": null}, {"key": "_tapisNodes", "value": "1", "description": null}, {"key": "_tapisSysBatchScheduler", "value": "SLURM", "description": null}, {"key": "_tapisSysHost", "value": "frontera.tacc.utexas.edu", "description": null}, {"key": "_tapisSysRootDir", "value": "/", "description": null}, {"key": "_tapisTenant", "value": "portals", "description": null}, {"key": "_webhook_base_url", "value": "https://dev.a2cps.tacc.utexas.edu/webhooks/", "description": null}], "archiveFilter": {"excludes": [], "includes": [], "includeLaunchFiles": true}, "containerArgs": [], "schedulerOptions": [{"arg": "--tapis-profile tacc", "name": "tacc Scheduler Profile", "notes": "{}", "include": null, "description": "Scheduler profile for HPC clusters at TACC"}, {"arg": "-A TACC-ACI", "name": "TACC Allocation", "notes": null, "include": true, "description": "The allocation associated with this job execution"}]}', + '{"appArgs": [{"arg": "hello", "name": "Greeting", "notes": "{\\"enum_values\\":[{\\"hello\\":\\"Hello\\"},{\\"hola\\":\\"Hola\\"},{\\"wassup\\":\\"Wassup\\"}]}", "include": null, "description": "Choose a greeting to give to your target"}, {"arg": "world", "name": "Target", "notes": "{}", "include": null, "description": "Whom to address your greeting"}, {"arg": "1", "name": "Sleep Time", "notes": "{\\"fieldType\\":\\"number\\"}", "include": null, "description": "How long to sleep before app execution"}, {"arg": "OpenSeesSP", "name": "mainProgram", "notes": "{\\"isHidden\\":\\"true\\"}", "include": null, "description": "null"}, {"arg": "OpenSeesSP", "name": "_mainProgram", "include": null, "description": "null"}], "envVariables": [{"key": "_tapisAppId", "value": "hello-world", "description": null}, {"key": "_tapisAppVersion", "value": "0.0.1", "description": null}, {"key": "_tapisArchiveOnAppError", "value": "true", "description": null}, {"key": "_tapisArchiveSystemDir", "value": "/home/user/tapis-jobs-archive/2023-01-24Z/hello-world_2023-01-24T23:52:57-e929ad16-adc5-4bd4-b84f-d41d1b67e5ee-007", "description": null}, {"key": "_tapisArchiveSystemId", "value": "cloud.data", "description": null}, {"key": "_tapisCoresPerNode", "value": "1", "description": null}, {"key": "_tapisDynamicExecSystem", "value": "false", "description": null}, {"key": "_tapisEffectiveUserId", "value": "user", "description": null}, {"key": "_tapisExecSystemExecDir", "value": "/scratch1/12345/user/tapis/e929ad16-adc5-4bd4-b84f-d41d1b67e5ee-007", "description": null}, {"key": "_tapisExecSystemHPCQueue", "value": "development", "description": null}, {"key": "_tapisExecSystemId", "value": "frontera", "description": null}, {"key": "_tapisExecSystemInputDir", "value": "/scratch1/12345/user/tapis/e929ad16-adc5-4bd4-b84f-d41d1b67e5ee-007", "description": null}, {"key": "_tapisExecSystemLogicalQueue", "value": "development", "description": null}, {"key": "_tapisExecSystemOutputDir", "value": "/scratch1/12345/user/tapis/e929ad16-adc5-4bd4-b84f-d41d1b67e5ee-007/output", "description": null}, {"key": "_tapisJobCreateDate", "value": "2023-01-24Z", "description": null}, {"key": "_tapisJobCreateTime", "value": "23:53:10.922143633Z", "description": null}, {"key": "_tapisJobCreateTimestamp", "value": "2023-01-24T23:53:10.922143633Z", "description": null}, {"key": "_tapisJobName", "value": "hello-world_2023-01-24T23:52:57", "description": null}, {"key": "_tapisJobOwner", "value": "user", "description": null}, {"key": "_tapisJobUUID", "value": "e929ad16-adc5-4bd4-b84f-d41d1b67e5ee-007", "description": null}, {"key": "_tapisJobWorkingDir", "value": "/scratch1/12345/user/tapis/e929ad16-adc5-4bd4-b84f-d41d1b67e5ee-007", "description": null}, {"key": "_tapisMaxMinutes", "value": "10", "description": null}, {"key": "_tapisMemoryMB", "value": "100", "description": null}, {"key": "_tapisNodes", "value": "1", "description": null}, {"key": "_tapisSysBatchScheduler", "value": "SLURM", "description": null}, {"key": "_tapisSysHost", "value": "frontera.tacc.utexas.edu", "description": null}, {"key": "_tapisSysRootDir", "value": "/", "description": null}, {"key": "_tapisTenant", "value": "portals", "description": null}, {"key": "_webhook_base_url", "value": "https://dev.a2cps.tacc.utexas.edu/webhooks/", "description": null}], "archiveFilter": {"excludes": [], "includes": [], "includeLaunchFiles": true}, "containerArgs": [], "schedulerOptions": [{"arg": "--tapis-profile tacc", "name": "tacc Scheduler Profile", "notes": "{}", "include": null, "description": "Scheduler profile for HPC clusters at TACC"}, {"arg": "-A TACC-ACI", "name": "TACC Allocation", "notes": null, "include": true, "description": "The allocation associated with this job execution"}]}', execSystemConstraints: null, subscriptions: '[]', blockedCount: 0, diff --git a/client/src/utils/jobsUtil.js b/client/src/utils/jobsUtil.js index 0d416834f..e20cb79bc 100644 --- a/client/src/utils/jobsUtil.js +++ b/client/src/utils/jobsUtil.js @@ -59,29 +59,34 @@ export function getAllocatonFromDirective(directive) { * Get display values from job, app and execution system info */ export function getJobDisplayInformation(job, app) { - const fileInputs = JSON.parse(job.fileInputs); + const filterHiddenObjects = (objects) => + objects + .filter((obj) => { + const notes = obj.notes ? JSON.parse(obj.notes) : null; + return !notes || !notes.isHidden; + }) + .filter((obj) => !(obj.name || obj.sourceUrl || '').startsWith('_')); + + const fileInputs = filterHiddenObjects(JSON.parse(job.fileInputs)); const parameterSet = JSON.parse(job.parameterSet); - const parameters = parameterSet.appArgs; + const parameters = filterHiddenObjects(parameterSet.appArgs); + const envVariables = parameterSet.envVariables; const schedulerOptions = parameterSet.schedulerOptions; const display = { applicationName: job.appId, systemName: job.execSystemId, - inputs: fileInputs - .map((input) => ({ - label: input.name || 'Unnamed Input', - id: input.sourceUrl, - value: input.sourceUrl, - })) - .filter((obj) => !obj.id?.startsWith('_')), - - parameters: parameters - .map((parameter) => ({ - label: parameter.name, - id: parameter.name, - value: parameter.arg, - })) - .filter((obj) => !obj.id.startsWith('_')), + inputs: fileInputs.map((input) => ({ + label: input.name || 'Unnamed Input', + id: input.sourceUrl, + value: input.sourceUrl, + })), + + parameters: parameters.map((parameter) => ({ + label: parameter.name, + id: parameter.name, + value: parameter.arg, + })), }; if (app) { @@ -96,34 +101,6 @@ export function getJobDisplayInformation(job, app) { display.applicationName = app.definition.notes.label || display.applicationName; - // https://jira.tacc.utexas.edu/browse/WP-100 - // TODOv3: Maybe should filter with includes? some have null/array values - // Note from Sal: We'll probably have to filter with a flag we create - // ourselves with whatever meta object they allow us to - // attach to job input args in the future. For example, - // a webhookUrl will be a required input for interactive jobs, - // but we want to hide that input - - // filter non-visible - // display.inputs.filter((input) => { - // const matchingParameter = app.definition.inputs.find((obj) => { - // return input.id === obj.id; - // }); - // if (matchingParameter) { - // return matchingParameter.value.visible; - // } - // return true; - // }); - // display.parameters.filter((input) => { - // const matchingParameter = app.definition.parameters.find((obj) => { - // return input.id === obj.id; - // }); - // if (matchingParameter) { - // return matchingParameter.value.visible; - // } - // return true; - // }); - const workPath = envVariables.find( (env) => env.key === '_tapisJobWorkingDir' ); @@ -148,6 +125,7 @@ export function getJobDisplayInformation(job, app) { // ignore if there is problem using the app definition to improve display } } + return display; } From f3e7f18dc3cfd6127236b15f5b99dc65612157d0 Mon Sep 17 00:00:00 2001 From: Asim Regmi <54924215+asimregmi@users.noreply.github.com> Date: Tue, 24 Oct 2023 15:16:51 -0500 Subject: [PATCH 12/23] Task/WP-72: Highlight matching search terms (#873) * task/WP-72 highlighted search queries/term in job history infinitescrolltable * fixed formatting * removed highlight and added bold * remove css properties and replaced with * used local css modules instead of global * Changed implementation of HighlightSearchTerm component to be used by Jobs.jsx * added useMemo hook and improved HighlightSearchTerm perf * removed useMemo hook * added unit tests for HighlightSearchTerm component * updated comment and fixed linting --------- Co-authored-by: Shayan Khan Co-authored-by: Chandra Y --- client/src/components/Jobs/Jobs.jsx | 49 +++++++++++----- .../HighlightSearchTerm.jsx | 39 +++++++++++++ .../HighlightSearchTerm.module.scss | 3 + .../HighlightSearchTerm.test.js | 57 +++++++++++++++++++ .../_common/HighlightSearchTerm/index.js | 3 + .../InfiniteScrollTable.jsx | 1 + client/src/components/_common/index.js | 1 + 7 files changed, 138 insertions(+), 15 deletions(-) create mode 100644 client/src/components/_common/HighlightSearchTerm/HighlightSearchTerm.jsx create mode 100644 client/src/components/_common/HighlightSearchTerm/HighlightSearchTerm.module.scss create mode 100644 client/src/components/_common/HighlightSearchTerm/HighlightSearchTerm.test.js create mode 100644 client/src/components/_common/HighlightSearchTerm/index.js diff --git a/client/src/components/Jobs/Jobs.jsx b/client/src/components/Jobs/Jobs.jsx index 1945d1bf7..c10f6abcb 100644 --- a/client/src/components/Jobs/Jobs.jsx +++ b/client/src/components/Jobs/Jobs.jsx @@ -5,6 +5,7 @@ import { Link, useLocation } from 'react-router-dom'; import { AppIcon, InfiniteScrollTable, + HighlightSearchTerm, Message, SectionMessage, Section, @@ -90,8 +91,6 @@ function JobsView({ original: { id, uuid, name }, }, }) => { - const query = queryStringParser.parse(useLocation().search); - // TODOv3: dropV2Jobs const jobsPathname = uuid ? `/jobs/${uuid}` : `/jobsv2/${id}`; return ( @@ -105,11 +104,11 @@ function JobsView({ }} className="wb-link" > - View Details + {query.query_string ? View Details : 'View Details'} ); }, - [] + [query] ); if (error) { @@ -133,15 +132,24 @@ function JobsView({ { Header: 'Job Name', accessor: 'name', - Cell: (el) => ( - - {el.value} - - ), + Cell: (el) => { + return ( + + {query.query_string ? ( + + ) : ( + el.value + )} + + ); + }, }, { Header: 'Job Status', @@ -183,12 +191,20 @@ function JobsView({ // TODOv3: dropV2Jobs if (el.row.original.uuid) { const outputLocation = getOutputPath(el.row.original); + return outputLocation && !hideDataFiles ? ( - {outputLocation} + {query.query_string ? ( + + ) : ( + outputLocation + )} ) : null; } else { @@ -232,7 +248,10 @@ function JobsView({ } getRowProps={rowProps} - columnMemoProps={[version]} /* TODOv3: dropV2Jobs. */ + columnMemoProps={[ + version, + query, + ]} /* TODOv3: dropV2Jobs. Refactor version prop. */ /> ); diff --git a/client/src/components/_common/HighlightSearchTerm/HighlightSearchTerm.jsx b/client/src/components/_common/HighlightSearchTerm/HighlightSearchTerm.jsx new file mode 100644 index 000000000..592085e29 --- /dev/null +++ b/client/src/components/_common/HighlightSearchTerm/HighlightSearchTerm.jsx @@ -0,0 +1,39 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styles from './HighlightSearchTerm.module.scss'; + +const HighlightSearchTerm = ({ searchTerm, content }) => { + if (!searchTerm) { + return <>{content}; + } + + const searchTermRegex = new RegExp(`(${searchTerm})`, 'gi'); + + const highlightParts = () => { + const parts = content.split(searchTermRegex); + return parts.map((part, i) => { + const isSearchTerm = part.match(searchTermRegex); + return isSearchTerm ? ( + + {part} + + ) : ( + part + ); + }); + }; + + return <>{highlightParts()}; +}; + +HighlightSearchTerm.propTypes = { + searchTerm: PropTypes.string, + content: PropTypes.string, +}; + +HighlightSearchTerm.defaultProps = { + searchTerm: '', + content: '', +}; + +export default HighlightSearchTerm; diff --git a/client/src/components/_common/HighlightSearchTerm/HighlightSearchTerm.module.scss b/client/src/components/_common/HighlightSearchTerm/HighlightSearchTerm.module.scss new file mode 100644 index 000000000..a9bb77c2b --- /dev/null +++ b/client/src/components/_common/HighlightSearchTerm/HighlightSearchTerm.module.scss @@ -0,0 +1,3 @@ +.highlight { + font-weight: bold; +} diff --git a/client/src/components/_common/HighlightSearchTerm/HighlightSearchTerm.test.js b/client/src/components/_common/HighlightSearchTerm/HighlightSearchTerm.test.js new file mode 100644 index 000000000..11060513f --- /dev/null +++ b/client/src/components/_common/HighlightSearchTerm/HighlightSearchTerm.test.js @@ -0,0 +1,57 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { + toBeInTheDocument, + toHaveClass, + toBeNull, +} from '@testing-library/jest-dom'; + +import HighlightSearchTerm from './HighlightSearchTerm'; + +describe('HighlightSearchTerm Component', () => { + it('renders content when searchTerm is not provided', () => { + const { getByText } = render(); + expect(getByText('Lorem ipsum')).toBeInTheDocument(); + }); + + it('renders without highlighting when searchTerm in content do not match', () => { + const { getByText } = render( + + ); + expect(getByText('Lorem ipsum dolor sit amet')).toBeInTheDocument(); + expect(document.querySelector('.highlight')).toBeNull(); + }); + + it('renders content when searchTerm is not provided', () => { + const { getByText } = render(); + expect(getByText('Lorem ipsum')).toBeInTheDocument(); + }); + + it('renders content with searchTerm highlighted', () => { + const { getByText } = render( + + ); + const highlightedText = getByText('ipsum'); + expect(highlightedText).toHaveClass('highlight'); + }); + + it('renders content with multiple searchTerm occurrences highlighted', () => { + const { getAllByText } = render( + + ); + const highlightedText = getAllByText('ipsum'); + expect(highlightedText.length).toBe(5); + highlightedText.forEach((element) => { + expect(element).toHaveClass('highlight'); + }); + }); +}); diff --git a/client/src/components/_common/HighlightSearchTerm/index.js b/client/src/components/_common/HighlightSearchTerm/index.js new file mode 100644 index 000000000..ccb240cb7 --- /dev/null +++ b/client/src/components/_common/HighlightSearchTerm/index.js @@ -0,0 +1,3 @@ +import HighlightSearchTerm from './HighlightSearchTerm'; + +export default HighlightSearchTerm; diff --git a/client/src/components/_common/InfiniteScrollTable/InfiniteScrollTable.jsx b/client/src/components/_common/InfiniteScrollTable/InfiniteScrollTable.jsx index a3bbe85e5..d9f673df1 100644 --- a/client/src/components/_common/InfiniteScrollTable/InfiniteScrollTable.jsx +++ b/client/src/components/_common/InfiniteScrollTable/InfiniteScrollTable.jsx @@ -128,6 +128,7 @@ InfiniteScrollTable.propTypes = { noDataText: rowContentPropType, getRowProps: PropTypes.func, columnMemoProps: PropTypes.arrayOf(PropTypes.any), + cell: PropTypes.object, }; InfiniteScrollTable.defaultProps = { onInfiniteScroll: (offset) => {}, diff --git a/client/src/components/_common/index.js b/client/src/components/_common/index.js index 3812495d2..ec663f8ca 100644 --- a/client/src/components/_common/index.js +++ b/client/src/components/_common/index.js @@ -16,6 +16,7 @@ export { default as Icon } from './Icon'; export { default as Message } from './Message'; export { default as InlineMessage } from './InlineMessage'; export { default as SectionMessage } from './SectionMessage'; +export { default as HighlightSearchTerm } from './HighlightSearchTerm'; export { default as Sidebar } from './Sidebar'; export { default as DescriptionList } from './DescriptionList'; export { default as DropdownSelector } from './DropdownSelector'; From 14959147c9323ceded55df10a368cbebe02f4925 Mon Sep 17 00:00:00 2001 From: Taylor Grafft Date: Wed, 25 Oct 2023 15:14:22 -0500 Subject: [PATCH 13/23] task/WP-273: Category icon (#874) * task/WP-273-CategoryIcon * task/WP-273-CategoryIcon-v2 * task/WP-273-CategoryIcon-v3 * task/WP-273-CategoryIcon-v4 * task/WP-273-CategoryIcon-v5 * task/WP-273-CategoryIcon-v6 * Update client/src/components/Applications/AppForm/AppForm.jsx Co-authored-by: Chandra Y * formatting fix --------- Co-authored-by: Taylor Grafft Co-authored-by: Taylor Grafft Co-authored-by: Taylor Grafft Co-authored-by: Taylor Grafft Co-authored-by: Chandra Y Co-authored-by: Taylor Grafft --- .../Applications/AppBrowser/AppBrowser.jsx | 2 +- .../Applications/AppForm/AppForm.jsx | 14 ++++- .../components/_common/AppIcon/AppIcon.jsx | 18 +++++- .../_common/AppIcon/AppIcon.test.js | 58 +++++++++++++------ client/src/utils/doesClassExist.js | 16 +++++ 5 files changed, 86 insertions(+), 22 deletions(-) create mode 100644 client/src/utils/doesClassExist.js diff --git a/client/src/components/Applications/AppBrowser/AppBrowser.jsx b/client/src/components/Applications/AppBrowser/AppBrowser.jsx index f70890fc8..75535098e 100644 --- a/client/src/components/Applications/AppBrowser/AppBrowser.jsx +++ b/client/src/components/Applications/AppBrowser/AppBrowser.jsx @@ -90,7 +90,7 @@ const AppBrowser = () => { } > - + {app.label || app.appId} diff --git a/client/src/components/Applications/AppForm/AppForm.jsx b/client/src/components/Applications/AppForm/AppForm.jsx index f197be6cd..e90801b4a 100644 --- a/client/src/components/Applications/AppForm/AppForm.jsx +++ b/client/src/components/Applications/AppForm/AppForm.jsx @@ -150,6 +150,18 @@ const AdjustValuesWhenQueueChanges = ({ app }) => { }; const AppInfo = ({ app }) => { + const categoryDict = useSelector((state) => state.apps.categoryDict); + const getAppCategory = (appId) => { + for (const [cat, apps] of Object.entries(categoryDict)) { + if (apps.some((app) => app.appId === appId)) { + return cat; + } + } + return null; + }; + + const appCategory = getAppCategory(app.definition.id); + return (
{app.definition.label}
@@ -163,7 +175,7 @@ const AppInfo = ({ app }) => { target="_blank" rel="noreferrer noopener" > - {' '} + {' '} {app.definition.notes.label} Documentation ) : null} diff --git a/client/src/components/_common/AppIcon/AppIcon.jsx b/client/src/components/_common/AppIcon/AppIcon.jsx index 18bcf3592..ac8f8a792 100644 --- a/client/src/components/_common/AppIcon/AppIcon.jsx +++ b/client/src/components/_common/AppIcon/AppIcon.jsx @@ -3,11 +3,16 @@ import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import Icon from '_common/Icon'; import './AppIcon.scss'; +import iconStyles from '../../../styles/trumps/icon.css'; +import iconFontsStyles from '../../../styles/trumps/icon.fonts.css'; +import doesClassExist from 'utils/doesClassExist'; -const AppIcon = ({ appId }) => { +const AppIcon = ({ appId, category }) => { const appIcons = useSelector((state) => state.apps.appIcons); const findAppIcon = (id) => { - let appIcon = 'applications'; + let appIcon = category + ? category.replace(' ', '-').toLowerCase() + : 'applications'; Object.keys(appIcons).forEach((appName) => { if (id.includes(appName)) { appIcon = appIcons[appName].toLowerCase(); @@ -19,6 +24,10 @@ const AppIcon = ({ appId }) => { } else if (id.includes('extract')) { appIcon = 'extract'; } + // Check if the CSS class exists, if not default to 'icon-applications' + if (!doesClassExist(`icon-${appIcon}`, [iconFontsStyles, iconStyles])) { + appIcon = 'applications'; + } return appIcon; }; const iconName = findAppIcon(appId); @@ -27,6 +36,11 @@ const AppIcon = ({ appId }) => { }; AppIcon.propTypes = { appId: PropTypes.string.isRequired, + category: PropTypes.string, +}; + +AppIcon.defaultProps = { + category: 'applications', }; export default AppIcon; diff --git a/client/src/components/_common/AppIcon/AppIcon.test.js b/client/src/components/_common/AppIcon/AppIcon.test.js index c5c64c228..b183b079b 100644 --- a/client/src/components/_common/AppIcon/AppIcon.test.js +++ b/client/src/components/_common/AppIcon/AppIcon.test.js @@ -1,9 +1,6 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { - toHaveAttribute, - toHaveTextContent, -} from '@testing-library/jest-dom/dist/matchers'; +import '@testing-library/jest-dom/extend-expect'; import { Provider } from 'react-redux'; import configureStore from 'redux-mock-store'; import AppIcon from './AppIcon'; @@ -15,43 +12,68 @@ const store = mockStore({ jupyter: 'jupyter', }, }, + categories: { + visualization: ['vasp'], + 'data-processing': ['jupyter'], + }, }); -expect.extend({ toHaveAttribute }); +// Mock document.styleSheets to simulate the existence of the CSS classes we're testing for +Object.defineProperty(document, 'styleSheets', { + value: [ + { + cssRules: [ + { selectorText: '.icon-jupyter::before' }, + { selectorText: '.icon-visualization::before' }, + { selectorText: '.icon-compress::before' }, + { selectorText: '.icon-extract::before' }, + ], + }, + ], + writable: true, +}); -function renderAppIcon(appId) { +function renderAppIcon(appId, category = 'default') { return render( - + ); } describe('AppIcon', () => { it('should render icons for known app IDs', () => { - const { getByRole } = renderAppIcon('jupyter'); - expect(getByRole('img')).toHaveAttribute('class', 'icon icon-jupyter'); + const { container } = renderAppIcon('jupyter', 'data-processing'); + expect(container.firstChild).toHaveClass('icon-jupyter'); }); - it('should show generic icons for apps with no appIcon', () => { - const { getByRole } = renderAppIcon('vasp'); - expect(getByRole('img')).toHaveAttribute('class', 'icon icon-applications'); + + it('should show category icons for apps with no appIcon', () => { + const { container } = renderAppIcon('vasp', 'visualization'); + expect(container.firstChild).toHaveClass('icon-visualization'); }); + it('should render icons for prtl.clone apps', () => { - const { getByRole } = renderAppIcon( + const { container } = renderAppIcon( 'prtl.clone.username.allocation.jupyter' ); - expect(getByRole('img')).toHaveAttribute('class', 'icon icon-jupyter'); + expect(container.firstChild).toHaveClass('icon-jupyter'); }); + it('should render icon for zippy toolbar app', () => { - const { getByRole } = renderAppIcon( + const { container } = renderAppIcon( 'prtl.clone.username.FORK.zippy-0.2u2-2.0' ); - expect(getByRole('img')).toHaveAttribute('class', 'icon icon-compress'); + expect(container.firstChild).toHaveClass('icon-compress'); }); + it('should render icon for extract toolbar app', () => { - const { getByRole } = renderAppIcon( + const { container } = renderAppIcon( 'prtl.clone.username.FORK.extract-0.1u7-7.0' ); - expect(getByRole('img')).toHaveAttribute('class', 'icon icon-extract'); + expect(container.firstChild).toHaveClass('icon-extract'); }); }); diff --git a/client/src/utils/doesClassExist.js b/client/src/utils/doesClassExist.js new file mode 100644 index 000000000..b3ddab017 --- /dev/null +++ b/client/src/utils/doesClassExist.js @@ -0,0 +1,16 @@ +function doesClassExist(className, stylesheets) { + for (let stylesheet of stylesheets) { + //Required to make this work with Jest/identity-obj-proxy + if (typeof stylesheet === 'object') { + if (stylesheet[className]) { + return true; + } + } else if (typeof stylesheet === 'string') { + if (stylesheet.includes(`.${className}::before`)) { + return true; + } + } + } + return false; +} +export default doesClassExist; From 8ac9be3a453a42e50a6337cc6b98ab92080770c4 Mon Sep 17 00:00:00 2001 From: Asim Regmi <54924215+asimregmi@users.noreply.github.com> Date: Thu, 26 Oct 2023 11:46:38 -0500 Subject: [PATCH 14/23] Task/WP-32--onboarding page filter show incomplete (#891) * adding filter incomplete users checkbox * condensed filteredUser function --- .../components/Onboarding/OnboardingAdmin.jsx | 36 ++++++++++++++++--- .../Onboarding/OnboardingAdmin.module.scss | 17 +++++++++ 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/client/src/components/Onboarding/OnboardingAdmin.jsx b/client/src/components/Onboarding/OnboardingAdmin.jsx index 73f8508e7..f4b91790f 100644 --- a/client/src/components/Onboarding/OnboardingAdmin.jsx +++ b/client/src/components/Onboarding/OnboardingAdmin.jsx @@ -6,6 +6,7 @@ import { SectionMessage, Message, Paginator, + Checkbox, } from '_common'; import { v4 as uuidv4 } from 'uuid'; import PropTypes from 'prop-types'; @@ -233,11 +234,20 @@ OnboardingAdminList.propTypes = { const OnboardingAdmin = () => { const dispatch = useDispatch(); const [eventLogModalParams, setEventLogModalParams] = useState(null); + const [showIncompleteOnly, setShowIncompleteOnly] = useState(false); + + const toggleShowIncomplete = () => { + setShowIncompleteOnly((prev) => !prev); + }; const { users, offset, limit, total, query, loading, error } = useSelector( (state) => state.onboarding.admin ); + const filteredUsers = users.filter((user) => + showIncompleteOnly ? !user.setupComplete : true + ); + const paginationCallback = useCallback( (page) => { dispatch({ @@ -289,22 +299,38 @@ const OnboardingAdmin = () => {
Administrator Controls
- +
+ + +
- {users.length === 0 && ( + {filteredUsers.length === 0 && (
No users to show.
)}
- {users.length > 0 && ( + {filteredUsers.length > 0 && ( )}
- {users.length > 0 && ( + {filteredUsers.length > 0 && (
Date: Fri, 27 Oct 2023 15:23:08 -0500 Subject: [PATCH 15/23] task/WP-65-DropdownViewFullPath (#866) * task/WP-65-DropdownViewFullPath * task/WP-65-DropdownViewFullPath-v2 * task/WP-65-DropdownViewFullPath-v3 * task/WP-65-DropdownViewFullPath-v4 * task/WP-65-DropdownViewFullPath-v5 * task/WP-65-DropdownViewFullPathv6 * task/WP-65-DropdownViewFullPath-v7 * task/WP-65-DropdownViewFullPath - dropdown-menu css component (#875) * task/WP-65-DropdownViewFullPath-v8 * task/WP-65-DropdownViewFullPath-v9 * task/WP-65-DropdownViewFullPath-v10 * task/WP-65-DropdownViewFullPath-v11 * task/WP-65-DropdownViewFullPath-v12 * task/WP-65-DropdownViewFullPath-v13 * task/WP-65-DropdownViewFullPath-v14 * task/WP-65-DropdownViewFullPath-v15 * Update client/src/components/DataFiles/DataFilesModals/DataFilesShowPathModal.jsx Co-authored-by: Sal Tijerina * prettier fix --------- Co-authored-by: Taylor Grafft Co-authored-by: Taylor Grafft Co-authored-by: Sal Tijerina Co-authored-by: Wesley B <62723358+wesleyboar@users.noreply.github.com> Co-authored-by: Taylor Grafft Co-authored-by: Taylor Grafft Co-authored-by: Chandra Y --- .../CombinedBreadcrumbs.jsx | 31 ++++ .../CombinedBreadcrumbs.module.scss | 5 + client/src/components/DataFiles/DataFiles.jsx | 4 +- .../DataFilesBreadcrumbs.jsx | 90 +++++------ .../DataFilesBreadcrumbs.scss | 74 +++++++++ .../DataFilesBreadcrumbs.test.js | 99 +++++++----- .../DataFilesDropdown/DataFilesDropdown.jsx | 153 ++++++++++++++++++ .../DataFilesDropdown.test.js | 58 +++++++ .../DataFilesModals/DataFilesSelectModal.jsx | 8 + .../DataFilesShowPathModal.jsx | 74 +++++---- .../DataFilesShowPathModal.module.scss | 24 +++ .../DataFilesSidebar/DataFilesSidebar.scss | 5 +- .../DataFilesSystemSelector.jsx | 4 + .../DataFilesToolbar/DataFilesToolbar.jsx | 1 + .../components/PublicData/PublicData.test.js | 4 +- .../_common/TextCopyField/TextCopyField.jsx | 75 +++++---- .../TextCopyField/TextCopyField.module.scss | 11 ++ .../src/styles/components/dropdown-menu.css | 54 +++++++ 18 files changed, 625 insertions(+), 149 deletions(-) create mode 100644 client/src/components/DataFiles/CombinedBreadcrumbs/CombinedBreadcrumbs.jsx create mode 100644 client/src/components/DataFiles/CombinedBreadcrumbs/CombinedBreadcrumbs.module.scss create mode 100644 client/src/components/DataFiles/DataFilesDropdown/DataFilesDropdown.jsx create mode 100644 client/src/components/DataFiles/DataFilesDropdown/DataFilesDropdown.test.js create mode 100644 client/src/components/DataFiles/DataFilesModals/DataFilesShowPathModal.module.scss create mode 100644 client/src/styles/components/dropdown-menu.css diff --git a/client/src/components/DataFiles/CombinedBreadcrumbs/CombinedBreadcrumbs.jsx b/client/src/components/DataFiles/CombinedBreadcrumbs/CombinedBreadcrumbs.jsx new file mode 100644 index 000000000..7f5ce80c5 --- /dev/null +++ b/client/src/components/DataFiles/CombinedBreadcrumbs/CombinedBreadcrumbs.jsx @@ -0,0 +1,31 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import DataFilesBreadcrumbs from '../DataFilesBreadcrumbs/DataFilesBreadcrumbs.jsx'; +import BreadcrumbsDropdown from '../DataFilesDropdown/DataFilesDropdown.jsx'; +import styles from './CombinedBreadcrumbs.module.scss'; + +const CombinedBreadcrumbs = (props) => { + return ( +
+ + +
+ ); +}; + +CombinedBreadcrumbs.propTypes = { + api: PropTypes.string.isRequired, + scheme: PropTypes.string.isRequired, + system: PropTypes.string.isRequired, + path: PropTypes.string.isRequired, + section: PropTypes.string.isRequired, + isPublic: PropTypes.bool, + className: PropTypes.string, +}; + +CombinedBreadcrumbs.defaultProps = { + isPublic: false, + className: '', +}; + +export default CombinedBreadcrumbs; diff --git a/client/src/components/DataFiles/CombinedBreadcrumbs/CombinedBreadcrumbs.module.scss b/client/src/components/DataFiles/CombinedBreadcrumbs/CombinedBreadcrumbs.module.scss new file mode 100644 index 000000000..7898fea1f --- /dev/null +++ b/client/src/components/DataFiles/CombinedBreadcrumbs/CombinedBreadcrumbs.module.scss @@ -0,0 +1,5 @@ +.combined-breadcrumbs { + display: flex; + align-items: center; + gap: 1rem; +} diff --git a/client/src/components/DataFiles/DataFiles.jsx b/client/src/components/DataFiles/DataFiles.jsx index 650175100..7d67a0596 100644 --- a/client/src/components/DataFiles/DataFiles.jsx +++ b/client/src/components/DataFiles/DataFiles.jsx @@ -15,7 +15,7 @@ import { useFileListing, useSystems } from 'hooks/datafiles'; import DataFilesToolbar from './DataFilesToolbar/DataFilesToolbar'; import DataFilesListing from './DataFilesListing/DataFilesListing'; import DataFilesSidebar from './DataFilesSidebar/DataFilesSidebar'; -import DataFilesBreadcrumbs from './DataFilesBreadcrumbs/DataFilesBreadcrumbs'; +import CombinedBreadcrumbs from './CombinedBreadcrumbs/CombinedBreadcrumbs'; import DataFilesModals from './DataFilesModals/DataFilesModals'; import DataFilesProjectsList from './DataFilesProjectsList/DataFilesProjectsList'; import DataFilesProjectFileListing from './DataFilesProjectFileListing/DataFilesProjectFileListing'; @@ -138,7 +138,7 @@ const DataFiles = () => { listingParams.system === noPHISystem ? 'UNPROTECTED' : 'DATA' } header={ - {children} @@ -46,7 +51,7 @@ const BreadcrumbLink = ({ return ( @@ -120,6 +125,22 @@ const DataFilesBreadcrumbs = ({ const paths = []; const pathComps = []; + const dispatch = useDispatch(); + + const fileData = { + system: system, + path: path, + }; + + const openFullPathModal = (e) => { + e.stopPropagation(); + e.preventDefault(); + dispatch({ + type: 'DATA_FILES_TOGGLE_MODAL', + payload: { operation: 'showpath', props: { file: fileData } }, + }); + }; + const { fetchSelectedSystem } = useSystems(); const selectedSystem = fetchSelectedSystem({ scheme, system, path }); @@ -155,52 +176,29 @@ const DataFilesBreadcrumbs = ({ } }, ''); + const fullPath = paths.slice(-1); + const currentDirectory = pathComps.slice(-1); + return ( -
- {scheme === 'projects' && ( - <> - {' '} - {system && `/ `} - +
+
+ {currentDirectory.length === 0 ? ( + + {truncateMiddle(systemName || 'Shared Workspaces', 30)} + + ) : ( + currentDirectory.map((pathComp, i) => { + if (i === fullPath.length - 1) { + return {truncateMiddle(pathComp, 30)}; + } + }) + )} +
+ {systemName && api === 'tapis' && ( + )} - - <>{systemName} - - {pathComps.map((pathComp, i) => { - if (i < paths.length - 2) { - return ' /... '; - } - if (i === paths.length - 1) { - return / {pathComp}; - } - return ( - - {' '} - /{' '} - - <>{pathComp} - - - ); - })}
); }; diff --git a/client/src/components/DataFiles/DataFilesBreadcrumbs/DataFilesBreadcrumbs.scss b/client/src/components/DataFiles/DataFilesBreadcrumbs/DataFilesBreadcrumbs.scss index 9e9673332..e06f9db5c 100644 --- a/client/src/components/DataFiles/DataFilesBreadcrumbs/DataFilesBreadcrumbs.scss +++ b/client/src/components/DataFiles/DataFilesBreadcrumbs/DataFilesBreadcrumbs.scss @@ -1,9 +1,12 @@ @import '../../../styles/tools/mixins.scss'; +@import '../../../styles/components/dropdown-menu.css'; .breadcrumbs { /* ... */ @include truncate-with-ellipsis; margin-right: 2em; + display: flex; + align-items: center; } .breadcrumb-link, .breadcrumb-link:hover { @@ -34,3 +37,74 @@ max-width: 700px; } } + +#path-button-wrapper { + padding-left: var(--horizontal-buffer); +} + +/* Nested to prevent styles from affecting CMS header dropdown */ +/* HACK: Using ID to increase specificity (until source of problem is fixed) */ +/* HELP: Why does DataFilesSidebar not need such specificity? */ +/* .go-to-button-dropdown { */ +#go-to-button-dropdown { + /* To fix menu not showing */ + /* HELP: Why does DataFilesSidebar not need this fix? */ + .dropdown-menu { + opacity: 1 !important; + pointer-events: auto !important; + } + /* To restyle */ + .dropdown-menu { + margin-top: 38px; + } + .dropdown-menu::before, + .dropdown-menu::after { + left: 23px; + margin-left: 0; + } + .dropdown-menu::after { + top: -9px; + } + + .dropdown-item { + display: inline-block; + } +} + +.breadcrumb-container { + display: flex; + align-items: center; +} + +.truncate { + display: inline-block; + max-width: 600px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding-left: 20px; +} + +#go-to-button-dropdown .complex-dropdown-item-root, +.complex-dropdown-item-project { + display: flex !important; +} + +#go-to-button-dropdown .link-hover:hover { + text-decoration: none; +} + +.multiline-menu-item-wrapper { + display: inline-block; + padding-left: 5px; + line-height: 1.1em; + small { + display: block; + color: var(--global-color-primary--x-dark); + } +} + +.breadcrumbs .vertical-align-separator { + margin-right: 2px; + margin-left: 10px; +} diff --git a/client/src/components/DataFiles/DataFilesBreadcrumbs/DataFilesBreadcrumbs.test.js b/client/src/components/DataFiles/DataFilesBreadcrumbs/DataFilesBreadcrumbs.test.js index 7520d2f06..77f0580b0 100644 --- a/client/src/components/DataFiles/DataFilesBreadcrumbs/DataFilesBreadcrumbs.test.js +++ b/client/src/components/DataFiles/DataFilesBreadcrumbs/DataFilesBreadcrumbs.test.js @@ -7,6 +7,7 @@ import systemsFixture from '../fixtures/DataFiles.systems.fixture'; import filesFixture from '../fixtures/DataFiles.files.fixture'; import { initialSystemState } from '../../../redux/reducers/datafiles.reducers'; import { projectsFixture } from '../../../redux/sagas/fixtures/projects.fixture'; +import { fireEvent } from '@testing-library/react'; const mockStore = configureStore(); @@ -16,8 +17,7 @@ describe('DataFilesBreadcrumbs', () => { systems: systemsFixture, projects: projectsFixture, }); - const history = createMemoryHistory(); - const { getByText, debug } = renderComponent( + const { getByText } = renderComponent( { createMemoryHistory() ); - expect(getByText(/My Data \(Frontera\)/)).toBeDefined(); - expect( - getByText(/My Data \(Frontera\)/) - .closest('a') - .getAttribute('href') - ).toEqual( - '/workbench/data/tapis/private/frontera.home.username/home/username/' - ); - expect(getByText(/the/).closest('a').getAttribute('href')).toEqual( - '/workbench/data/tapis/private/frontera.home.username/home/username/path/to/the/' - ); - expect(getByText(/files/).closest('a')).toBeNull(); + // Check if the last part of the path is rendered as text + const filesText = getByText('files'); + expect(filesText).toBeDefined(); + expect(filesText.closest('a')).toBeNull(); }); it('renders correct breadcrumbs when in root of system', () => { const store = mockStore({ systems: systemsFixture, }); - const history = createMemoryHistory(); - const { getByText, debug } = renderComponent( + const { getByText } = renderComponent( { createMemoryHistory() ); + // Check if the system name is rendered as text when in the root of the system expect(getByText('Frontera')).toBeDefined(); - expect(getByText('Frontera').closest('a').getAttribute('href')).toEqual( - '/workbench/data/tapis/private/frontera.home.username/' - ); }); - it('render breadcrumbs with initial empty systems', () => { + it('render breadcrumbs for projects', () => { const store = mockStore({ - systems: initialSystemState, + systems: systemsFixture, projects: projectsFixture, + files: filesFixture, }); - const history = createMemoryHistory(); - const { getByText, debug } = renderComponent( + const { getByText } = renderComponent( { createMemoryHistory() ); - expect(getByText(/Frontera/)).toBeDefined(); - expect( - getByText(/Frontera/) - .closest('a') - .getAttribute('href') - ).toEqual('/workbench/data/tapis/private/frontera.home.username/'); + // Check if the last part of the path is rendered as text for projects + const filesText = getByText('files'); + expect(filesText).toBeDefined(); + expect(filesText.closest('a')).toBeNull(); }); - it('render breadcrumbs for projects', () => { + it('dispatches action to open full path modal on button click', () => { const store = mockStore({ systems: systemsFixture, projects: projectsFixture, - files: filesFixture, }); - const history = createMemoryHistory(); - const { getByText, debug } = renderComponent( + const { getByText } = renderComponent( , store, createMemoryHistory() ); + const viewFullPathButton = getByText('View Full Path'); + fireEvent.click(viewFullPathButton); + + const actions = store.getActions(); + const expectedActions = { + type: 'DATA_FILES_TOGGLE_MODAL', + payload: { + operation: 'showpath', + props: { + file: { + system: 'frontera.home.username', + path: '/home/username/path/to/the/files', + }, + }, + }, + }; + expect(actions).toContainEqual(expectedActions); + }); - expect(getByText(/Shared Workspaces/)).toBeDefined(); - expect( - getByText(/Shared Workspaces/) - .closest('a') - .getAttribute('href') - ).toEqual('/workbench/data/tapis/projects/'); + it('renders pathComp, which is current directory', () => { + const store = mockStore({ + systems: systemsFixture, + }); + const history = createMemoryHistory(); + const { getByText } = renderComponent( + , + store, + createMemoryHistory() + ); + const pathComp = getByText('files'); + expect(pathComp).toBeDefined(); }); }); diff --git a/client/src/components/DataFiles/DataFilesDropdown/DataFilesDropdown.jsx b/client/src/components/DataFiles/DataFilesDropdown/DataFilesDropdown.jsx new file mode 100644 index 000000000..246f72788 --- /dev/null +++ b/client/src/components/DataFiles/DataFilesDropdown/DataFilesDropdown.jsx @@ -0,0 +1,153 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { Link, useLocation } from 'react-router-dom'; +import { Button } from '_common'; +import { + DropdownToggle, + DropdownMenu, + DropdownItem, + ButtonDropdown, +} from 'reactstrap'; +import { useSystemDisplayName, useSystems } from 'hooks/datafiles'; +import '../DataFilesBreadcrumbs/DataFilesBreadcrumbs.scss'; + +const BreadcrumbsDropdown = ({ + api, + scheme, + system, + path, + section, + isPublic, +}) => { + const paths = []; + const [dropdownOpen, setDropdownOpen] = useState(false); + const toggleDropdown = () => setDropdownOpen(!dropdownOpen); + + const location = useLocation(); + const pathParts = location.pathname.split('/'); + const projectId = pathParts.includes('projects') + ? pathParts[pathParts.indexOf('projects') + 1] + : null; + + const handleNavigation = (targetPath) => { + const basePath = isPublic ? '/public-data' : '/workbench/data'; + let url; + + if (scheme === 'projects' && targetPath === systemName) { + url = `${basePath}/${api}/projects/${projectId}/`; + } else if (scheme === 'projects' && !targetPath) { + url = `${basePath}/${api}/projects/`; + } else if (api === 'googledrive' && !targetPath) { + url = `${basePath}/${api}/${scheme}/${system}/`; + } else if (api === 'tapis' && scheme !== 'projects' && !targetPath) { + url = `${basePath}/${api}/${scheme}/${system}/`; + } else { + url = `${basePath}/${api}/${scheme}/${system}${targetPath}/`; + } + + return url; + }; + + const { fetchSelectedSystem } = useSystems(); + const selectedSystem = fetchSelectedSystem({ scheme, system, path }); + const systemName = useSystemDisplayName({ scheme, system, path }); + const homeDir = selectedSystem?.homeDir; + const isSystemRootPath = !path + .replace(/^\/+/, '') + .startsWith(homeDir?.replace(/^\/+/, '')); + const startingPath = isSystemRootPath ? '' : homeDir; + + const pathComponents = path.split('/').filter((x) => !!x); + const startingPathComponents = startingPath.split('/').filter((x) => !!x); + const overlapIndex = pathComponents.findIndex( + (component, index) => startingPathComponents[index] !== component + ); + + let currentPath = startingPath; + pathComponents.slice(overlapIndex).forEach((component) => { + currentPath = `${currentPath}/${component}`; + paths.push(currentPath); + }); + + const fullPath = paths.reverse(); + const displayPaths = + scheme === 'projects' ? [...fullPath, systemName] : fullPath; + const sliceStart = scheme === 'projects' && systemName ? 0 : 1; + return ( +
+ + Go to ... + + {displayPaths + .slice(sliceStart, displayPaths.length) + .map((path, index) => { + const folderName = path.split('/').pop(); + return ( + + + + + {folderName.length > 20 + ? folderName.substring(0, 20) + : folderName} + {scheme === 'projects' && path === systemName && ( + Project Name + )} + + + + ); + })} + + + + + + {scheme === 'projects' + ? 'Shared Workspaces' + : systemName || 'Shared Workspaces'} + {homeDir ? Root : null} + + + + + +
+ ); +}; + +BreadcrumbsDropdown.propTypes = { + api: PropTypes.string.isRequired, + scheme: PropTypes.string.isRequired, + system: PropTypes.string.isRequired, + path: PropTypes.string.isRequired, + section: PropTypes.string, + isPublic: PropTypes.bool, +}; + +BreadcrumbsDropdown.defaultProps = { + isPublic: false, +}; + +export default BreadcrumbsDropdown; diff --git a/client/src/components/DataFiles/DataFilesDropdown/DataFilesDropdown.test.js b/client/src/components/DataFiles/DataFilesDropdown/DataFilesDropdown.test.js new file mode 100644 index 000000000..c422af617 --- /dev/null +++ b/client/src/components/DataFiles/DataFilesDropdown/DataFilesDropdown.test.js @@ -0,0 +1,58 @@ +import React from 'react'; +import { fireEvent } from '@testing-library/react'; +import configureMockStore from 'redux-mock-store'; +import renderComponent from 'utils/testing'; +import systemsFixture from '../fixtures/DataFiles.systems.fixture'; +import BreadcrumbsDropdown from './DataFilesDropdown'; + +const mockStore = configureMockStore(); + +describe('BreadcrumbsDropdown', () => { + it('renders "Go to ..." dropdown and can be toggled', () => { + const store = mockStore({ + systems: systemsFixture, + }); + + const { getByText } = renderComponent( + , + store + ); + + const dropdownToggle = getByText('Go to ...'); + expect(dropdownToggle).toBeDefined(); + + // Toggle dropdown + fireEvent.click(dropdownToggle); + + // Now, dropdown content should be visible + expect(getByText('to')).toBeDefined(); + expect(getByText('path')).toBeDefined(); + }); + + it('renders root path correctly', () => { + const store = mockStore({ + systems: systemsFixture, + }); + + const { getByText } = renderComponent( + , + store + ); + + const dropdownToggle = getByText('Go to ...'); + fireEvent.click(dropdownToggle); + + // Check if the root path is rendered correctly + expect(getByText('Frontera')).toBeDefined(); + }); +}); diff --git a/client/src/components/DataFiles/DataFilesModals/DataFilesSelectModal.jsx b/client/src/components/DataFiles/DataFilesModals/DataFilesSelectModal.jsx index f019d8fdd..7324d4e15 100644 --- a/client/src/components/DataFiles/DataFilesModals/DataFilesSelectModal.jsx +++ b/client/src/components/DataFiles/DataFilesModals/DataFilesSelectModal.jsx @@ -6,6 +6,7 @@ import DataFilesBreadcrumbs from '../DataFilesBreadcrumbs/DataFilesBreadcrumbs'; import DataFilesModalListingTable from './DataFilesModalTables/DataFilesModalListingTable'; import DataFilesSystemSelector from '../DataFilesSystemSelector/DataFilesSystemSelector'; import DataFilesProjectsList from '../DataFilesProjectsList/DataFilesProjectsList'; +import DataFilesShowPathModal from './DataFilesShowPathModal'; const DataFilesSelectModal = ({ isOpen, toggle, onSelect }) => { const systems = useSelector( @@ -21,6 +22,7 @@ const DataFilesSelectModal = ({ isOpen, toggle, onSelect }) => { (state) => state.files.params.modal, shallowEqual ); + const selectRef = React.useRef(); const onOpened = () => { const systemParams = { @@ -29,6 +31,7 @@ const DataFilesSelectModal = ({ isOpen, toggle, onSelect }) => { system: systems.filter((s) => !s.hidden)[0].system, path: systems.filter((s) => !s.hidden)[0]?.homeDir || '', }; + dispatch({ type: 'FETCH_FILES_MODAL', payload: { ...systemParams, section: 'modal' }, @@ -40,6 +43,10 @@ const DataFilesSelectModal = ({ isOpen, toggle, onSelect }) => { props: {}, }, }); + dispatch({ + type: 'FETCH_SYSTEM_DEFINITION', + payload: systemParams.system, + }); }; const selectCallback = (system, path) => { onSelect(system, path); @@ -91,6 +98,7 @@ const DataFilesSelectModal = ({ isOpen, toggle, onSelect }) => { /> )}
+
{showProjects ? ( diff --git a/client/src/components/DataFiles/DataFilesModals/DataFilesShowPathModal.jsx b/client/src/components/DataFiles/DataFilesModals/DataFilesShowPathModal.jsx index 41b5b5979..0b49d5840 100644 --- a/client/src/components/DataFiles/DataFilesModals/DataFilesShowPathModal.jsx +++ b/client/src/components/DataFiles/DataFilesModals/DataFilesShowPathModal.jsx @@ -2,7 +2,7 @@ import React, { useEffect } from 'react'; import { useSelector, useDispatch, shallowEqual } from 'react-redux'; import { Modal, ModalHeader, ModalBody } from 'reactstrap'; import { TextCopyField } from '_common'; -import DataFilesBreadcrumbs from '../DataFilesBreadcrumbs/DataFilesBreadcrumbs'; +import styles from './DataFilesShowPathModal.module.scss'; const DataFilesShowPathModal = React.memo(() => { const dispatch = useDispatch(); @@ -11,6 +11,22 @@ const DataFilesShowPathModal = React.memo(() => { shallowEqual ); + const modalParams = useSelector( + (state) => state.files.params.modal, + shallowEqual + ); + + const { api: modalApi } = modalParams; + + useEffect(() => { + if (modalApi === 'tapis' && modalParams.system) { + dispatch({ + type: 'FETCH_SYSTEM_DEFINITION', + payload: modalParams.system, + }); + } + }, [modalParams, dispatch]); + useEffect(() => { if (params.api === 'tapis' && params.system) { dispatch({ @@ -54,37 +70,39 @@ const DataFilesShowPathModal = React.memo(() => { toggle={toggle} className="dataFilesModal" > - - Pathnames for {file.name} + + View Full Path - -
- {params.api === 'tapis' && definition && ( - <> -
Storage Host
-
{definition.host}
-
Storage Path
- - )} - {params.api === 'googledrive' && ( - <> -
Storage Location
-
Google Drive
- - )} -
+ {(params.api === 'tapis' || modalApi === 'tapis') && definition && ( + <> + Storage Host + + {definition.host} + + Storage Path -
-
+ + )} + {params.api === 'googledrive' && ( + <> + + Storage Location + + Google Drive + + )}
); diff --git a/client/src/components/DataFiles/DataFilesModals/DataFilesShowPathModal.module.scss b/client/src/components/DataFiles/DataFilesModals/DataFilesShowPathModal.module.scss new file mode 100644 index 000000000..598be3708 --- /dev/null +++ b/client/src/components/DataFiles/DataFilesModals/DataFilesShowPathModal.module.scss @@ -0,0 +1,24 @@ +.custom-modal-header { + padding-left: 15px; +} + +.custom-textcopyfield { + margin-top: 0; +} + +.storage-host, +.storage-path, +.storage-location { + font-weight: bold; + display: block; + margin-top: 10px; + margin-left: 15px; +} + +.storage-path { + margin-bottom: 0; +} + +.storage-values { + margin-left: 15px; +} diff --git a/client/src/components/DataFiles/DataFilesSidebar/DataFilesSidebar.scss b/client/src/components/DataFiles/DataFilesSidebar/DataFilesSidebar.scss index 3b5cd538b..b2c09a31a 100644 --- a/client/src/components/DataFiles/DataFilesSidebar/DataFilesSidebar.scss +++ b/client/src/components/DataFiles/DataFilesSidebar/DataFilesSidebar.scss @@ -1,3 +1,5 @@ +@import '../../../styles/components/dropdown-menu.css'; + .data-files-btn { background-color: var(--global-color-accent--normal); border-color: var(--global-color-accent--normal); @@ -19,7 +21,7 @@ padding-top: 20px; } -/* HACK: Quick solution to prevent styles from cascading into header dropdown */ +/* Nested to prevent styles from affecting CMS header dropdown */ .data-files-sidebar { .dropdown-menu { border-color: var(--global-color-accent--normal); @@ -39,7 +41,6 @@ content: ''; } .dropdown-menu::after { - position: absolute; top: -9px; left: 68px; border-right: 9px solid transparent; diff --git a/client/src/components/DataFiles/DataFilesSystemSelector/DataFilesSystemSelector.jsx b/client/src/components/DataFiles/DataFilesSystemSelector/DataFilesSystemSelector.jsx index 55a477207..019a21ab3 100644 --- a/client/src/components/DataFiles/DataFilesSystemSelector/DataFilesSystemSelector.jsx +++ b/client/src/components/DataFiles/DataFilesSystemSelector/DataFilesSystemSelector.jsx @@ -48,6 +48,10 @@ const DataFilesSystemSelector = ({ section, }, }); + dispatch({ + type: 'FETCH_SYSTEM_DEFINITION', + payload: system.system, + }); dispatch({ type: 'DATA_FILES_SET_MODAL_PROPS', payload: { diff --git a/client/src/components/DataFiles/DataFilesToolbar/DataFilesToolbar.jsx b/client/src/components/DataFiles/DataFilesToolbar/DataFilesToolbar.jsx index 23c776311..63b22155a 100644 --- a/client/src/components/DataFiles/DataFilesToolbar/DataFilesToolbar.jsx +++ b/client/src/components/DataFiles/DataFilesToolbar/DataFilesToolbar.jsx @@ -13,6 +13,7 @@ export const ToolbarButton = ({ text, iconName, onClick, disabled }) => { - -
- + {displayField === 'textarea' ? ( + <> +