diff --git a/Dockerfile b/Dockerfile index e5dd783e..2805fc09 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,9 @@ # Node builder image -FROM uselagoon/node-20-builder:latest as builder +FROM uselagoon/node-20-builder:latest AS builder COPY . /app/ -RUN yarn install +RUN yarn install --network-timeout 300000 # Node service image @@ -28,4 +28,4 @@ ENV GRAPHQL_API=$GRAPHQL_API RUN yarn run build EXPOSE 3000 -CMD ["yarn", "start"] \ No newline at end of file +CMD ["yarn", "start"] diff --git a/README.md b/README.md index b4f9f429..c89932bf 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ A couple of environment variables are required: - user_owner - user with owner role - user_orguser - Organization user - user_orgviewer - Organization viewer +- user_orgadmin - Organization admin - user_orgowner - Organization owner - user_platformowner - Platform owner diff --git a/cypress/cypress.config.ts b/cypress/cypress.config.ts index 9954393f..c5ca49da 100644 --- a/cypress/cypress.config.ts +++ b/cypress/cypress.config.ts @@ -2,6 +2,7 @@ import { defineConfig } from 'cypress'; export default defineConfig({ requestTimeout: 15000, + defaultCommandTimeout: 8000, e2e: { env: { api: 'http://0.0.0.0:33000/graphql', @@ -15,6 +16,7 @@ export default defineConfig({ // orgs user_orguser: 'orguser@example.com', user_orgviewer: 'orgviewer@example.com', + user_orgadmin: 'orgadmin@example.com', user_orgowner: 'orgowner@example.com', // top level user for all default tests user_platformowner: 'platformowner@example.com', diff --git a/cypress/e2e/organizations/manage.cy.ts b/cypress/e2e/organizations/manage.cy.ts index 4c0c6332..5ba490be 100644 --- a/cypress/e2e/organizations/manage.cy.ts +++ b/cypress/e2e/organizations/manage.cy.ts @@ -17,9 +17,14 @@ describe('Org Manage page', () => { it('Adds a org viewer', () => { manage.doAddOrgViewer(testData.organizations.manage.user); }); - it('Should upgrade org viewer to owner', () => { - manage.doEditOrgViewer(testData.organizations.manage.user); + it('Should upgrade org viewer to admin', () => { + manage.doEditOrgViewerToAdmin(testData.organizations.manage.user); }); + + it('Should upgrade org admin to owner', () => { + manage.doEditOrgViewerToOwner(testData.organizations.manage.user); + }); + it('Deletes user', () => { manage.doDeleteUser(testData.organizations.manage.user); }); diff --git a/cypress/e2e/organizations/overview.cy.ts b/cypress/e2e/organizations/overview.cy.ts index a02daa23..18debc2e 100644 --- a/cypress/e2e/organizations/overview.cy.ts +++ b/cypress/e2e/organizations/overview.cy.ts @@ -23,7 +23,7 @@ describe('Organization overview page', () => { overview.doQuotaFieldCheck(); }); - it.only('Changes org friendly name/description', () => { + it('Changes org friendly name/description', () => { registerIdleHandler('idle'); overview.changeOrgFriendlyname(testData.organizations.overview.friendlyName); diff --git a/cypress/e2e/rbac/developer.cy.ts b/cypress/e2e/rbac/developer.cy.ts index da29806a..a12fc488 100644 --- a/cypress/e2e/rbac/developer.cy.ts +++ b/cypress/e2e/rbac/developer.cy.ts @@ -114,7 +114,7 @@ describe('DEVELOPER permission test suites', () => { environmentOverview.doDeleteEnvironmentError('main'); }); - it('Deletes stating environment', () => { + it('Deletes staging environment', () => { cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-staging`); cy.intercept('POST', Cypress.env('api'), req => { @@ -147,7 +147,7 @@ describe('DEVELOPER permission test suites', () => { }); it('Fails to cancel any deployment - no permission to CANCEL for DEVELOPER', () => { - cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-staging/deployments`); + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/deployments`); registerIdleHandler('idle'); diff --git a/cypress/e2e/rbac/guest.cy.ts b/cypress/e2e/rbac/guest.cy.ts index 6a5679f6..0bed5fe7 100644 --- a/cypress/e2e/rbac/guest.cy.ts +++ b/cypress/e2e/rbac/guest.cy.ts @@ -146,7 +146,7 @@ describe('GUEST permission test suites', () => { }); it('Fails to do cancel a deployment - no permission for GUEST', () => { - cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-staging/deployments`); + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/deployments`); registerIdleHandler('idle'); cy.intercept('POST', Cypress.env('api'), req => { diff --git a/cypress/e2e/rbac/maintainer.cy.ts b/cypress/e2e/rbac/maintainer.cy.ts index 9809a2b2..e7492e52 100644 --- a/cypress/e2e/rbac/maintainer.cy.ts +++ b/cypress/e2e/rbac/maintainer.cy.ts @@ -136,7 +136,7 @@ describe('MAINTAINER permission test suites', () => { environmentOverview.doDeleteEnvironmentError('main'); }); - it('Deletes stating environment', () => { + it('Deletes staging environment', () => { cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-staging`); cy.intercept('POST', Cypress.env('api'), req => { @@ -169,7 +169,7 @@ describe('MAINTAINER permission test suites', () => { }); it('Cancels a staging deployment', () => { - cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-staging/deployments`); + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/deployments`); registerIdleHandler('idle'); diff --git a/cypress/e2e/rbac/organizations/orgAdmin.cy.ts b/cypress/e2e/rbac/organizations/orgAdmin.cy.ts new file mode 100644 index 00000000..78533124 --- /dev/null +++ b/cypress/e2e/rbac/organizations/orgAdmin.cy.ts @@ -0,0 +1,170 @@ +import { testData } from 'cypress/fixtures/variables'; +import GroupAction from 'cypress/support/actions/organizations/GroupsAction'; +import NotificationsAction from 'cypress/support/actions/organizations/NotificationsAction'; +import OverviewAction from 'cypress/support/actions/organizations/OverviewAction'; +import ProjectsActions from 'cypress/support/actions/organizations/ProjectsActions'; +import { aliasMutation, aliasQuery, registerIdleHandler } from 'cypress/utils/aliasQuery'; + +const overview = new OverviewAction(); +const group = new GroupAction(); +const project = new ProjectsActions(); +const notifications = new NotificationsAction(); + +const orgAdmin = [Cypress.env('user_orgadmin')]; + +orgAdmin.forEach(admin => { + const desc = { + [Cypress.env('user_orgadmin')]: 'Org admin', + }; + + describe(`Organizations ${desc[admin]} journey`, () => { + beforeEach(() => { + // register interceptors/idle handler + cy.intercept('POST', Cypress.env('api'), req => { + aliasQuery(req, 'getOrganization'); + aliasMutation(req, 'updateOrganizationFriendlyName'); + aliasMutation(req, 'addUserToGroup'); + aliasMutation(req, 'addGroupToOrganization'); + }); + + registerIdleHandler('idle'); + + cy.login(admin, admin); + cy.visit(`${Cypress.env('url')}/organizations/lagoon-demo-organization`); + }); + + if (admin === Cypress.env('user_orgadmin')) { + it('Fails to change org name and desc - no permission for ORGADMIN', () => { + overview.doFailedChangeOrgFriendlyname(testData.organizations.overview.friendlyName); + overview.closeModal(); + overview.doFailedChangeOrgDescription(testData.organizations.overview.description); + overview.closeModal(); + }); + } else { + it('Changes org name and desc', () => { + overview.changeOrgFriendlyname(testData.organizations.overview.friendlyName); + overview.changeOrgDescription(testData.organizations.overview.description); + }); + } + + it('Navigates to groups and creates', () => { + cy.waitForNetworkIdle('@idle', 500); + + const group1 = testData.organizations.groups.newGroupName; + const group2 = testData.organizations.groups.newGroupName2; + + cy.get('.groups').click(); + cy.location('pathname').should('equal', '/organizations/lagoon-demo-organization/groups'); + + group.doAddGroup(group1, group2); + registerIdleHandler('groupQuery'); + group.doAddMemberToGroup(testData.organizations.users.email, group1); + }); + + it('Navigates to projects and creates a new one', () => { + registerIdleHandler('projectsQuery'); + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'addProjectToOrganization'); + }); + + cy.waitForNetworkIdle('@idle', 500); + + cy.get('.projects').click(); + cy.location('pathname').should('equal', '/organizations/lagoon-demo-organization/projects'); + cy.waitForNetworkIdle('@projectsQuery', 1000); + + project.doAddProject(testData.organizations.project); + }); + + it('Navigates to notifications and creates a couple', () => { + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'addNotificationSlack'); + aliasMutation(req, 'UpdateNotificationSlack'); + aliasMutation(req, 'addNotificationRocketChat'); + aliasMutation(req, 'addNotificationMicrosoftTeams'); + aliasMutation(req, 'addNotificationEmail'); + aliasMutation(req, 'addNotificationWebhook'); + }); + + registerIdleHandler('notificationsQuery'); + + cy.waitForNetworkIdle('@idle', 500); + cy.get('.notifications').click(); + cy.location('pathname').should('equal', '/organizations/lagoon-demo-organization/notifications'); + cy.waitForNetworkIdle('@notificationsQuery', 1000); + + const { slack: slackData, email: emailData, webhook: webhookData } = testData.organizations.notifications; + + notifications.doAddNotification('slack', slackData); + notifications.doAddNotification('email', emailData); + notifications.doAddNotification('webhook', webhookData); + }); + + it('Navigates to a project, adds a group and notifications', () => { + cy.visit( + `${Cypress.env('url')}/organizations/lagoon-demo-organization/projects/${ + testData.organizations.project.projectName + }` + ); + + cy.getBySel('addGroupToProject').click(); + + cy.get('.react-select__indicator').click({ force: true }); + cy.get('#react-select-2-option-0').click(); + + cy.getBySel('addGroupToProjectConfirm').click(); + + cy.log('add notifications'); + + cy.getBySel('addNotificationToProject').click(); + + cy.get('[class$=control]').click({ force: true }); + cy.get('#react-select-3-option-0').click(); + + cy.getBySel('addNotificationToProjectConfirm').click(); + }); + + // cleanup + after(() => { + registerIdleHandler('projectsQuery'); + registerIdleHandler('groupQuery'); + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'removeNotification'); + aliasMutation(req, 'deleteGroup'); + aliasMutation(req, 'deleteProject'); + }); + + cy.waitForNetworkIdle('@idle', 500); + cy.get('.groups').click(); + + group.doDeleteGroup(testData.organizations.groups.newGroupName); + cy.wait('@gqldeleteGroupMutation'); + + group.doDeleteGroup(testData.organizations.groups.newGroupName2); + cy.wait('@gqldeleteGroupMutation'); + + cy.waitForNetworkIdle('@idle', 500); + cy.get('.projects').click(); + + cy.waitForNetworkIdle('@projectsQuery', 1000); + + project.doDeleteProject(testData.organizations.project.projectName); + + cy.get('.notifications').click(); + + cy.waitForNetworkIdle('@idle', 500); + + const { + webhook: { name: webhooknName }, + email: { name: emailName }, + slack: { name: slackName }, + } = testData.organizations.notifications; + + notifications.doDeleteNotification(webhooknName); + cy.wait('@gqlremoveNotificationMutation'); // wait for a delete mutation instead + notifications.doDeleteNotification(emailName); + cy.wait('@gqlremoveNotificationMutation'); + notifications.doDeleteNotification(slackName); + }); + }); +}); diff --git a/cypress/e2e/rbac/reporter.cy.ts b/cypress/e2e/rbac/reporter.cy.ts index 48c90573..4f916a7b 100644 --- a/cypress/e2e/rbac/reporter.cy.ts +++ b/cypress/e2e/rbac/reporter.cy.ts @@ -143,7 +143,7 @@ describe('REPORTER permission test suites', () => { }); it('Fails to do cancel a deployment - no permission for REPORTER', () => { - cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-staging/deployments`); + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/deployments`); registerIdleHandler('idle'); @@ -168,6 +168,8 @@ describe('REPORTER permission test suites', () => { cy.waitForNetworkIdle('@idle', 500); deployment.navigateToRunningDeployment(); + + cy.waitForNetworkIdle('@idle', 500); deployment.doFailedCancelDeployment(); }); }); diff --git a/cypress/support/actions/organizations/ManageAction.ts b/cypress/support/actions/organizations/ManageAction.ts index 6a8bb8c0..f22f11d3 100644 --- a/cypress/support/actions/organizations/ManageAction.ts +++ b/cypress/support/actions/organizations/ManageAction.ts @@ -16,10 +16,26 @@ export default class ManageAction { }); } - doEditOrgViewer(user: string) { + doEditOrgViewerToAdmin(user: string) { manageRepo.getUserRows().contains(user).parents('.tableRow').find('.link').click(); - manageRepo.getUserIsOwnerCheckbox().check(); + // admin + manageRepo.getUserRoleDropdown().click({ force: true }); + manageRepo.getUserAdminRoleOption().click(); + + manageRepo.getUpdateBtn().click(); + + cy.wait('@gqlAddUserToOrganizationMutation'); + + manageRepo.getUserRows().contains(user).parents('.tableRow').find(':contains("ORG ADMIN")').should('exist'); + } + + doEditOrgViewerToOwner(user: string) { + manageRepo.getUserRows().contains(user).parents('.tableRow').find('.link').click(); + + // owner + manageRepo.getUserRoleDropdown().click({ force: true }); + manageRepo.getUserOwnerRoleOption().click(); manageRepo.getUpdateBtn().click(); diff --git a/cypress/support/actions/organizations/OverviewAction.ts b/cypress/support/actions/organizations/OverviewAction.ts index d087f5b0..0742173c 100644 --- a/cypress/support/actions/organizations/OverviewAction.ts +++ b/cypress/support/actions/organizations/OverviewAction.ts @@ -34,7 +34,8 @@ export default class OverviewAction { changeOrgFriendlyname(friendlyName: string) { overviewRepo.getNameEditButton('edit-name').click(); - overviewRepo.getEditField().type(friendlyName); + + overviewRepo.getEditField().clear().type(friendlyName); overviewRepo.getSubmitButton().click(); cy.wait('@gqlupdateOrganizationFriendlyNameMutation'); @@ -53,7 +54,7 @@ export default class OverviewAction { } doFailedChangeOrgFriendlyname(friendlyName: string) { overviewRepo.getNameEditButton('edit-name').click(); - overviewRepo.getEditField().type(friendlyName); + overviewRepo.getEditField().clear().type(friendlyName); overviewRepo.getSubmitButton().click(); cy.wait('@gqlupdateOrganizationFriendlyNameMutation').then(interception => { @@ -68,7 +69,7 @@ export default class OverviewAction { changeOrgDescription(description: string) { overviewRepo.getDescEditButton('edit-description').click(); - overviewRepo.getEditField().type(description); + overviewRepo.getEditField().clear().type(description); overviewRepo.getSubmitButton().click(); cy.wait('@gqlupdateOrganizationFriendlyNameMutation'); @@ -85,7 +86,7 @@ export default class OverviewAction { } doFailedChangeOrgDescription(description: string) { overviewRepo.getDescEditButton('edit-description').click(); - overviewRepo.getEditField().type(description); + overviewRepo.getEditField().clear().type(description); overviewRepo.getSubmitButton().click(); cy.wait('@gqlupdateOrganizationFriendlyNameMutation').then(interception => { diff --git a/cypress/support/actions/organizations/ProjectsActions.ts b/cypress/support/actions/organizations/ProjectsActions.ts index 98bbc50b..dae3d962 100644 --- a/cypress/support/actions/organizations/ProjectsActions.ts +++ b/cypress/support/actions/organizations/ProjectsActions.ts @@ -17,7 +17,7 @@ export default class ProjectsActions { projects.selectTarget(); - projects.getAddConfirm().click(); + projects.getAddConfirm().click({ force: true }); cy.wait('@gqladdProjectToOrganizationMutation'); @@ -32,7 +32,7 @@ export default class ProjectsActions { projects.selectTarget(); - projects.getAddConfirm().click(); + projects.getAddConfirm().click({ force: true }); cy.wait('@gqladdProjectToOrganizationMutation').then(interception => { expect(interception.response?.statusCode).to.eq(200); @@ -44,7 +44,13 @@ export default class ProjectsActions { }); } doDeleteProject(projectName: string) { - projects.getDeleteBtn().eq(1).click(); + projects.getDeleteBtn(projectName).click(); + + cy.get('.highlight') + .invoke('text') + .then(highlightText => { + projects.getDeleteConfirmInput().focus().type(highlightText); + }); projects.getDeleteConfirm().click(); diff --git a/cypress/support/repositories/organizations/ManageRepository.ts b/cypress/support/repositories/organizations/ManageRepository.ts index 5d05690a..710eaa13 100644 --- a/cypress/support/repositories/organizations/ManageRepository.ts +++ b/cypress/support/repositories/organizations/ManageRepository.ts @@ -6,9 +6,17 @@ export default class ManageRepository { getUserEmailField() { return cy.getBySel('manageEmail'); } - getUserIsOwnerCheckbox() { - return cy.getBySel('userIsOwner'); + getUserRoleDropdown() { + return cy.get('.react-select__indicator'); } + + getUserAdminRoleOption() { + return cy.get('#react-select-2-option-1'); + } + getUserOwnerRoleOption() { + return cy.get('#react-select-2-option-2'); + } + getSubmitBtn() { return cy.getBySel('addUserConfirm'); } diff --git a/cypress/support/repositories/organizations/ProjectsRepository.ts b/cypress/support/repositories/organizations/ProjectsRepository.ts index 65c6ed9f..dc43de46 100644 --- a/cypress/support/repositories/organizations/ProjectsRepository.ts +++ b/cypress/support/repositories/organizations/ProjectsRepository.ts @@ -23,8 +23,11 @@ export default class ProjectsRepository { getProjectRows() { return cy.getBySel('table-row'); } - getDeleteBtn() { - return cy.get("[aria-label='delete']"); + getDeleteBtn(projectName: string) { + return cy.getBySel('table-row').filter(`:contains(${projectName})`).find('[data-cy=deleteProject]'); + } + getDeleteConfirmInput() { + return cy.getBySel('deleteProjectConfirm'); } getDeleteConfirm() { return cy.getBySel('deleteConfirm'); diff --git a/src/components/AddVariable/StyledAddVariable.tsx b/src/components/AddVariable/StyledAddVariable.tsx index 8cc30cab..3ec7f03d 100644 --- a/src/components/AddVariable/StyledAddVariable.tsx +++ b/src/components/AddVariable/StyledAddVariable.tsx @@ -33,7 +33,8 @@ export const NewVariableModal = styled.div` font-family: 'source-code-pro', sans-serif; font-weight: bold; } - input { + input, + textarea { margin-right: 10px; width: 100%; } diff --git a/src/components/AddVariable/index.js b/src/components/AddVariable/index.js index 789e0ba3..0bf3674f 100644 --- a/src/components/AddVariable/index.js +++ b/src/components/AddVariable/index.js @@ -145,16 +145,15 @@ export const AddVariable = ({ />
- - Value +
diff --git a/src/components/AddVariable/logic.js b/src/components/AddVariable/logic.js index 8b1237a8..4ad019ff 100644 --- a/src/components/AddVariable/logic.js +++ b/src/components/AddVariable/logic.js @@ -28,9 +28,11 @@ const withModalHandlers = withHandlers({ () => setOpen(true), closeModal: - ({ setOpen }) => - () => - setOpen(false), + ({ setOpen, setClear }) => + () => { + setOpen(false); + setClear(); + }, }); export default compose( diff --git a/src/components/DeploymentsByFilter/index.js b/src/components/DeploymentsByFilter/index.js index f9849b9f..93be94a9 100644 --- a/src/components/DeploymentsByFilter/index.js +++ b/src/components/DeploymentsByFilter/index.js @@ -118,10 +118,12 @@ const DeploymentsByFilter = ({ deployments }) => { }); }; + const sortedFilteredItems = sortedItems.filter(deployment => filterResults(deployment)); + return (
- + { - {!sortedItems.filter(deployment => filterResults(deployment)).length && ( -
No deployments
- )} - {sortedItems - .filter(deployment => filterResults(deployment)) - .map(deployment => { - return ( -
-
- - {formatString(deployment.environment.project.name, 'project')} - -
-
- - {formatString(deployment.environment.name, 'environment')} - -
-
{formatString(deployment.environment.openshift.name, 'cluster')}
-
- - {deployment.name} - -
-
{deployment.priority}
-
- {moment.utc(deployment.created).local().format('DD MMM YYYY, HH:mm:ss (Z)')} -
-
- {deployment.status.charAt(0).toUpperCase() + deployment.status.slice(1)} - - {!['complete', 'cancelled', 'failed'].includes(deployment.status) && deployment.buildStep && ( - - )} -
-
{getDeploymentDuration(deployment)}
-
- {['new', 'pending', 'queued', 'running'].includes(deployment.status) && ( - - )} -
+ {!sortedFilteredItems.length &&
No deployments
} + {sortedFilteredItems.map(deployment => { + return ( +
+
+ + {formatString(deployment.environment.project.name, 'project')} + +
+
+ + {formatString(deployment.environment.name, 'environment')} + +
+
{formatString(deployment.environment.openshift.name, 'cluster')}
+
+ + {deployment.name} + +
+
{deployment.priority}
+
+ {moment.utc(deployment.created).local().format('DD MMM YYYY, HH:mm:ss (Z)')} +
+
+ {deployment.status.charAt(0).toUpperCase() + deployment.status.slice(1)} + + {!['complete', 'cancelled', 'failed'].includes(deployment.status) && deployment.buildStep && ( + + )} +
+
{getDeploymentDuration(deployment)}
+
+ {['new', 'pending', 'queued', 'running'].includes(deployment.status) && ( + + )}
- ); - })} +
+ ); + })} ); diff --git a/src/components/Organizations/AddUserToOrganization/index.js b/src/components/Organizations/AddUserToOrganization/index.js index 895415f8..600591ce 100644 --- a/src/components/Organizations/AddUserToOrganization/index.js +++ b/src/components/Organizations/AddUserToOrganization/index.js @@ -7,12 +7,15 @@ import Button from 'components/Button'; import withLogic from 'components/Organizations/AddUserToOrganization/logic'; import gql from 'graphql-tag'; +import { userTypeOptions } from '../Manage'; import { Footer } from '../SharedStyles'; import { NewUser } from './Styles'; export const ADD_USER_MUTATION = gql` - mutation AddUserToOrganization($email: String!, $organization: Int!, $owner: Boolean) { - addUserToOrganization(input: { user: { email: $email }, organization: $organization, owner: $owner }) { + mutation AddUserToOrganization($email: String!, $organization: Int!, $owner: Boolean, $admin: Boolean) { + addUserToOrganization( + input: { user: { email: $email }, organization: $organization, owner: $owner, admin: $admin } + ) { id } } @@ -21,17 +24,11 @@ export const ADD_USER_MUTATION = gql` /** * Adds/edits user to an organization */ -export const AddUserToOrganization = ({ - organization, - close, - inputValueEmail, - setInputValue, - checkboxValueOwner, - setCheckboxValueOwner, - onAddUser, - users, -}) => { +export const AddUserToOrganization = ({ organization, close, inputValueEmail, setInputValue, onAddUser, users }) => { const userAlreadyExists = users.find(u => u.email === inputValueEmail); + + const [newUserType, setNewUserType] = useState('viewer'); + return ( console.error(err)}> {(addUser, { called, error, data }) => { @@ -60,17 +57,31 @@ export const AddUserToOrganization = ({ />
+
-
- ); - }} - - -
- - + }); + }} + > + Continue + + ); + }} + + + + + )} console.error(e)}> {(removeNotification, { called, error, data }) => { if (data) { @@ -316,93 +318,95 @@ const OrgNotifications = ({ - - -
- -
- -
- -
- -
- -
-
-
- console.error(e)}> - {(updateRocketChat, { called, error, data }) => { - if (error) { - return
{error.message}
; - } - if (data) { - refresh().then(() => { - closeEditModal(); - }); - } - return ( - - ); - }} -
- -
-
+ }); + }} + > + Continue + + ); + }} +
+ + + + )} console.error(e)}> {(removeNotification, { called, error, data }) => { if (error) { @@ -449,82 +453,84 @@ const OrgNotifications = ({ - - -
- -
- -
- - {!isValidEmail &&

Invalid email address

} -
-
-
- console.error(e)}> - {(updateEmail, { called, error, data }) => { - if (error) { - return
{error.message}
; - } - if (data) { - refresh().then(() => { - closeEditModal(); - }); - } - return ( - - ); - }} -
- -
-
+ {editState.current && editState.current.name === notification.name && ( + + +
+ +
+ +
+ + {!isValidEmail &&

Invalid email address

} +
+
+
+ console.error(e)}> + {(updateEmail, { called, error, data }) => { + if (error) { + return
{error.message}
; + } + if (data) { + refresh().then(() => { + closeEditModal(); + }); + } + return ( + + ); + }} +
+ +
+
+ )} console.error(e)}> {(removeNotification, { called, error, data }) => { if (data) { @@ -574,80 +580,81 @@ const OrgNotifications = ({ - - -
- -
- -
- -
-
-
- console.error(e)}> - {(updateWebhook, { called, error, data }) => { - if (error) { - return
{error.message}
; - } - if (data) { - refresh().then(() => { - closeEditModal(); - }); - } - return ( - - ); - }} -
- -
-
- + }); + }} + > + Continue + + ); + }} +
+ + + + )} console.error(e)}> {(removeNotification, { called, error, data }) => { if (data) { @@ -693,79 +700,82 @@ const OrgNotifications = ({ - - -
- -
- -
- -
-
-
- console.error(e)}> - {(updateTeams, { called, error, data }) => { - if (error) { - return
{error.message}
; - } - if (data) { - refresh().then(() => { - closeEditModal(); - }); - } - return ( - - ); - }} -
- -
-
+ }); + }} + > + Continue + + ); + }} +
+ + + + )} console.error(e)}> {(removeNotification, { called, error, data }) => { if (data) { diff --git a/src/components/Organizations/Organization/Styles.tsx b/src/components/Organizations/Organization/Styles.tsx index ae4c6179..f96b965a 100644 --- a/src/components/Organizations/Organization/Styles.tsx +++ b/src/components/Organizations/Organization/Styles.tsx @@ -18,6 +18,14 @@ export const StyledOrganization = styled.div` border-radius: 4px; box-shadow: 0px 4px 8px 0px rgba(0, 0, 0, 0.03); } + .admin-label { + color: ${color.black}; + background-color: ${color.lightBlue}; + margin-left: 10px; + padding: 0px 5px 0px 5px; + border-radius: 4px; + box-shadow: 0px 4px 8px 0px rgba(0, 0, 0, 0.03); + } .viewer-label { color: ${color.black}; background-color: ${color.lightestBlue}; diff --git a/src/components/Organizations/Organization/index.js b/src/components/Organizations/Organization/index.js index ddc226e6..30fd0a35 100644 --- a/src/components/Organizations/Organization/index.js +++ b/src/components/Organizations/Organization/index.js @@ -48,10 +48,10 @@ const Organization = ({ organization, refetch }) => { } const [nameModalOpen, setNameModalOpen] = useState(false); - const [friendlyName, setFriendlyName] = useState(''); + const [friendlyName, setFriendlyName] = useState(organization.friendlyName); const [descModalOpen, setDescModalOpen] = useState(false); - const [description, setDescription] = useState(''); + const [description, setDescription] = useState(organization.description); const quotaDisplay = (quota, quotaNumber, quotaLimit, showLink = true) => { const pluralName = quota + 's'; @@ -93,9 +93,29 @@ const Organization = ({ organization, refetch }) => { }; const renderEditBtn = type => ( - + { + if (type === 'name') { + setFriendlyName(organization.friendlyName); + } + if (type === 'description') { + setDescription(organization.description); + } + + modalAction('open', type)(); + }} + /> ); + const handleFriendlyNameChange = e => { + setFriendlyName(e.target.value); + }; + + const handleDescriptionChange = e => { + setDescription(e.target.value); + }; return ( @@ -117,12 +137,12 @@ const Organization = ({ organization, refetch }) => {
@@ -188,12 +208,12 @@ const Organization = ({ organization, refetch }) => {
@@ -276,6 +296,10 @@ const Organization = ({ organization, refetch }) => { ORG OWNER + ) : owner.admin ? ( + + ORG ADMIN + ) : ( ORG VIEWER diff --git a/src/components/Organizations/Organizations/OrganizationsSkeleton.tsx b/src/components/Organizations/Organizations/OrganizationsSkeleton.tsx index 40191e0c..e0d9a676 100644 --- a/src/components/Organizations/Organizations/OrganizationsSkeleton.tsx +++ b/src/components/Organizations/Organizations/OrganizationsSkeleton.tsx @@ -5,7 +5,11 @@ import Box from 'components/Box'; import { Organization, OrganizationsPage, OrgsHeader, SearchInput } from './StyledOrganizations'; -const OrganizationsSkeleton = () => { +interface Props { + setSearch: React.Dispatch>; +} + +const OrganizationsSkeleton = ({ setSearch }: Props) => { const RenderSkeletonBox = (index: number) => { return ( @@ -27,7 +31,13 @@ const OrganizationsSkeleton = () => { - + setSearch(e.target.value)} + aria-labelledby="search" + className="searchInput" + type="text" + placeholder="Type to search" + /> <>{[...Array(numberOfItems)].map((_, idx) => RenderSkeletonBox(idx))} diff --git a/src/components/Organizations/Organizations/index.tsx b/src/components/Organizations/Organizations/index.tsx index 8d95711a..00ac463c 100644 --- a/src/components/Organizations/Organizations/index.tsx +++ b/src/components/Organizations/Organizations/index.tsx @@ -1,8 +1,11 @@ -import React, { useState } from 'react'; +import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import Highlighter from 'react-highlight-words'; +import { LoadingOutlined } from '@ant-design/icons'; +import { Spin } from 'antd'; import Box from 'components/Box'; import OrganizationLink from 'components/link/Organizations/Organization'; +import { debounce } from 'lib/util'; import { Organization, OrganizationsPage, OrgsHeader, SearchInput } from './StyledOrganizations'; @@ -14,29 +17,112 @@ export interface IOrganization { __typename: 'Organization'; } +interface OrganizationProps { + organizations: IOrganization[]; + initialSearch: string; +} /** * The primary list of organizations. */ -const Organizations = ({ organizations = [] }: { organizations: IOrganization[] }) => { - const [searchInput, setSearchInput] = useState(''); +const Organizations: FC = ({ organizations = [], initialSearch }) => { + const [searchInput, setSearchInput] = useState(initialSearch || ''); + + const [isFiltering, setIsFiltering] = useState(false); + const [filteredOrgs, setFilteredOrgs] = useState(organizations); + + const searchInputRef = useRef(null); + + useEffect(() => { + if (initialSearch && searchInputRef.current) { + searchInputRef.current.focus(); + } + }, []); + + const timerLengthPercentage = useMemo( + () => Math.min(1000, Math.max(40, Math.floor(organizations.length * 0.0725))), + [organizations.length] + ); + + const debouncedSearch = useCallback( + debounce((searchVal: string) => { + setSearchInput(searchVal); + }, timerLengthPercentage), + [] + ); + + const handleSearch = (searchVal: string) => { + setIsFiltering(true); + debouncedSearch(searchVal); + }; - const filteredOrganizations = organizations.filter(key => { - const sortByName = key.name.toLowerCase().includes(searchInput.toLowerCase()); - const sortByUrl = ''; - return ['name', 'environments', '__typename'].includes(key.name) ? false : (true && sortByName) || sortByUrl; - }); + useEffect(() => { + const filterOrgs = async (): Promise => { + return new Promise(resolve => { + const filteredOrganizations = organizations.filter(org => { + const searchStrLowerCase = searchInput.toLowerCase(); + const filterFn = (key?: string) => key?.toLowerCase().includes(searchStrLowerCase); + + const sortByName = filterFn(org.name); + const sortByDesc = filterFn(org.description); + const sortByFriendlyName = filterFn(org.friendlyName); + + if (['__typename', 'name', 'id'].includes(org.name)) { + return false; + } + return sortByName || sortByFriendlyName || sortByDesc; + }); + + resolve(filteredOrganizations); + }); + }; + + void filterOrgs() + .then(filtered => setFilteredOrgs(filtered)) + .finally(() => setIsFiltering(false)); + }, [searchInput, organizations]); + + const filteredMappedOrgs = useMemo(() => { + return filteredOrgs.map(organization => ( + + + +

+ +

+
+ +
+
+
+
+
+ )); + }, [filteredOrgs]); return ( - + setSearchInput(e.target.value)} + onChange={e => handleSearch(e.target.value)} placeholder="Type to search" disabled={organizations.length === 0} /> @@ -48,37 +134,14 @@ const Organizations = ({ organizations = [] }: { organizations: IOrganization[]
)} - {searchInput && !filteredOrganizations.length && ( + {searchInput && !filteredMappedOrgs.length && (

No organizations matching "{searchInput}"

)} - {filteredOrganizations.map(organization => ( - - - -

- -

-
- -
-
-
-
-
- ))} + {filteredMappedOrgs} ); }; diff --git a/src/components/Organizations/Projects/index.js b/src/components/Organizations/Projects/index.js index a40eec5f..7b6651ad 100644 --- a/src/components/Organizations/Projects/index.js +++ b/src/components/Organizations/Projects/index.js @@ -13,7 +13,14 @@ import { IconDashboard } from '../CustomIcons/OrganizationIcons'; import { DeleteButton } from '../Groups/Styles'; import NewProject from '../NewProject'; import PaginatedTable from '../PaginatedTable/PaginatedTable'; -import { Footer, RemoveModalHeader, RemoveModalParagraph, TableActions, Tag } from '../SharedStyles'; +import { + Footer, + RemoveModalConfirmInput, + RemoveModalHeader, + RemoveModalParagraph, + TableActions, + Tag, +} from '../SharedStyles'; import { ProjectDashboard, StyledOrgProjects } from './Styles'; const DELETE_PROJECT = gql` @@ -29,6 +36,7 @@ const OrgProjects = ({ projects = [], organizationId, organizationName, refresh, const [modalState, setModalState] = useState({ open: false, current: null, + confirmValue: '', }); const Columns = [ @@ -94,6 +102,7 @@ const OrgProjects = ({ projects = [], organizationId, organizationName, refresh, { setModalState({ open: true, current: project.name }); }} @@ -106,9 +115,20 @@ const OrgProjects = ({ projects = [], organizationId, organizationName, refresh, > Are you sure? - This action will delete project {project.name} from Lagoon and the organization. + This action will delete project {project.name} from Lagoon and the + organization. + Type the name of the project to confirm. + + { + setModalState({ ...modalState, confirmValue: e.target.value }); + }} + /> +