From 09c3bc8f1b529a973d4ce3c660a92ed6c7976d65 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Wed, 2 Nov 2022 09:57:25 -0700 Subject: [PATCH] [EC-7] Org Admin Vault Refresh Client V1 (#3925) * [EC-8] Restructure Tabs (#3109) * Cherry pick pending PR for tabs component [CL-17] Tabs - Routing * Update organization tabs from 4 to 6 * Create initial 'Members' tab * Create initial 'Groups' tab * Add initial "Reporting" tab * Use correct report label/layout by product type * Create initial 'Billing' tab * Breakup billing payment and billing history pages * Cleanup org routing and nav permission service * More org tab permission cleanup * Refactor organization billing to use a module * Refactor organization reporting to use module * Cherry pick finished/merged tabs component [CL-17] Tabs - Router (#2952) * This partially reverts commit 24bb775 to fix tracking of people.component.html rename. * Fix people component file rename * Recover lost member page changes * Undo members component rename as it was causing difficult merge conflicts * Fix member and group page container * Remove unnecessary organization lookup * [EC-8] Some PR suggestions * [EC-8] Reuse user billing history for orgs * [EC-8] Renamed user billing history component * [EC-8] Repurpose payment method component Update end user payment method component to be usable for organizations. * [EC-8] Fix missing verify bank condition * [EC-8] Remove org payment method component * [EC-8] Use CL in payment method component * [EC-8] Extend maxWidth Tailwind theme config * [EC-8] Add lazy loading to org reports * [EC-8] Add lazy loading to org billing * [EC-8] Prettier * [EC-8] Cleanup org reporting component redundancy * [EC-8] Use different class for negative margin * [EC-8] Make billing history component "dumb" * Revert "[EC-8] Cleanup org reporting component redundancy" This reverts commit eca337e89bb0be4600af4351640636aa8a498cff. * [EC-8] Create and export shared reports module * [EC-8] Use shared reports module in orgs * [EC-8] Use takeUntil pattern * [EC-8] Move org reporting module out of old modules folder * [EC-8] Move org billing module out of old modules folder * [EC-8] Fix some remaining merge conflicts * [EC-8] Move maxWidth into 'extend' key for Tailwind config * [EC-8] Remove unused module * [EC-8] Rename org report list component * Prettier Co-authored-by: Vincent Salucci * [EC-451] Org Admin Refresh Permissions Refactor (#3320) * [EC-451] Update new org permissions for new tabs * [EC-451] Remove redudant route guards * [EC-451] Remove canAccessManageTab() * [EC-451] Use canAccess* callbacks in org routing module * Fix org api service refactor and linting after pulling in master * Fix broken org people and group pages after merge * [EC-18] Reporting side nav direction (#3420) * [EC-18] Re-order side nav for org reports according to Figma * [EC-18] Fix rxjs linter errors and redundant org flag * [EC-526] Default to Event Logs page for Reporting Tab (#3470) * [EC-526] Default to the Events Logs page when navigating to the Reporting tab * [EC-526] Undo default routing redirect when the child path is missing. Avoids defaulting to "/events" in case a user/org doesn't have access to event logs. * [EC-19] Update Organization Settings Page (#3251) * [EC-19] Refactor existing organization settings components to its own module * [EC-19] Move SSO page to settings tab * [EC-19] Move Policies page to Settings tab Refactor Policy components into its own module * [EC-19] Move ImageSubscriptionHiddenComponent * [EC-19] Lazy load org settings module * [EC-19] Add SSO Id to SSO config view * [EC-19] Remove SSO identfier from org info page * [EC-19] Update org settings/policies to follow ADR-0011 * [EC-19] Update two-step login setup description * [EC-19] Revert nested policy components folder * [EC-19] Revert nested org setting components folder * [EC-19] Remove left over image component * [EC-19] Prettier * [EC-19] Fix missing i18n * [EC-19] Update SSO form to use CL * [EC-19] Remove unused SSO input components * [EC-19] Fix bad SSO locale identifier * [EC-19] Fix import order linting * [EC-19] Add explicit whitespace check for launch click directive * [EC-19] Add restricted import paths to eslint config * [EC-19] Tag deprecated field with Jira issue to cleanup in future release * [EC-19] Remove out of date comment * [EC-19] Move policy components to policies module * [EC-19] Remove dityRequired validator * [EC-19] Use explicit type for SSO config form * [EC-19] Fix rxjs linter errors * [EC-19] Fix RxJS eslint comments in org settings component * [EC-19] Use explicit ControlsOf helper for nested SSO form groups. * [EC-19] Attribute source of ControlsOf helper * [EC-19] Fix missing settings side nav links * [EC-19] Fix member/user language for policy modals * [EC-551] Update Event Logs Client Column (#3572) * [EC-551] Fix RxJS warnings * [EC-551] Update page to use CL components and Tailwind classes * [EC-551] Update Client column to use text instead of icon. Update language and i18n. * [EC-14] Refactor vault filter (#3440) * [EC-14] initial refactoring of vault filter * [EC-14] return observable trees for all filters with head node * [EC-14] Remove bindings on callbacks * [EC-14] fix formatting on disabled orgs * [EC-14] hide MyVault if personal org policy * [EC-14] add check for single org policy * [EC-14] add policies to org and change node constructor * [EC-14] don't show options if personal vault policy * [EC-14] default to all vaults * [EC-14] add default selection to filters * [EC-14] finish filter model callbacks * [EC-14] finish filter functionality and begin cleaning up * [EC-14] clean up old components and start on org vault * [EC-14] loop through filters for presentation * [EC-14] refactor VaultFilterService and put filter presentation data back into Vault Filter component. Remove VaultService * [EC-14] begin refactoring org vault * [EC-14] Refactor Vault Filter Service to use observables * [EC-14] finish org vault filter * [EC-14] fix vault model tests * [EC-14] fix org service calls * [EC-14] pull refactor out of shared code * [EC-14] include head node for collections even if collections aren't loaded yet * [EC-14] fix url params for vaults * [EC-14] remove comments * [EC-14] Remove unnecesary getter for org on vault filter * [EC-14] fix linter * [EC-14] fix prettier * [EC-14] add deprecated methods to collection service for desktop and browser * [EC-14] simplify cipher type node check * [EC-14] add getters to vault filter model * [EC-14] refactor how we build the filter list into methods * [EC-14] add getters to build filter method * [EC-14] remove param ids if false * [EC-14] fix collapsing nodes * [EC-14] add specific type to search placeholder * [EC-14] remove extra constructor and comment from org vault filter * [EC-14] extract subscription callback to methods * [EC-14] Remove unecessary await * [EC-14] Remove ternary operators while building org filter * [EC-14] remove unnecessary deps array in vault filter service declaration * [EC-14] consolidate new models into one file * [EC-14] initialize nested observable inside of service Signed-off-by: Jacob Fink * [EC-14] change how we load orgs into the vault filter and select the default filter * [EC-14] remove get from getters name * [EC-14] remove eslint-disable comment * [EC-14] move vault filter service abstraction to angular folder and separate * [EC-14] rename filter types and delete VaultFilterLabel * [EC-14] remove changes to workspace file * [EC-14] remove deprecated service from jslib module * [EC-14] remove any remaining files from common code * [EC-14] consolidate vault filter components into components folder * [EC-14] simplify method call * [EC-14] refactor the vault filter service - orgs now have observable property - BehaviorSubjects have been migrated to ReplaySubjects if they don't need starting value - added unit tests - fix small error when selecting org badge of personal vault - renamed some properties * [EC-14] replace mergeMap with switchMap in vault filter service * [EC-14] early return to prevent nesting * [EC-14] clean up filterCollections method * [EC-14] use isDeleted helper in html * [EC-14] add jsdoc comments to ServiceUtils * [EC-14] fix linter * [EC-14] use array.slice instead of setting length * Update apps/web/src/app/vault/vault-filter/services/vault-filter.service.ts Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> * [EC-14] add missing high level jsdoc description * [EC-14] fix storybook absolute imports * [EC-14] delete vault-shared.module * [EC-14] change search placeholder text to getter and add missing strings * [EC-14] remove two way binding from search text in vault filter * [EC-14] removed all binding from search text and just use input event * [EC-14] remove async from apply vault filter * [EC-14] remove circular observable calls in vault filter service Co-authored-by: Thomas Rittson * [EC-14] move collapsed nodes to vault filter section * [EC-14] deconstruct filter section inside component * [EC-14] fix merge conflicts and introduce refactored organization service to vault filter service * [EC-14] remove mutation from filter builders * [EC-14] fix styling on buildFolderTree * [EC-14] remove leftover folder-filters reference and use ternary for collapse icon * [EC-14] remove unecessary checks * [EC-14] stop rebuilding filters when the organization changes * [EC-14] Move subscription out of setter in vault filter section * [EC-14] remove extra policy service methods from vault filter service * [EC-14] remove new methods from old vault-filter.service * [EC-14] Use vault filter service in vault components * [EC-14] reload collections from vault now that we have vault filter service * [EC-14] remove currentFilterCollections in vault filter component * [EC-14] change VaultFilterType to more specific OrganizationFilter in organization-options * [EC-14] include org check in isNodeSelected * [EC-14] add getters to filter function, fix storybook, and add test for All Collections * [EC-14] show org options even if there's a personal vault policy * [EC-14] use !"AllCollections" instead of just !null * [EC-14] Remove extra org Subject in vault filter service * [EC-14] remove null check from vault search text * [EC-14] replace store/build names with set/get. Remove extra call to setOrganizationFilter * [EC-14] add take(1) to subscribe in test * [EC-14] move init logic in org vault filter component to ngOnInit * [EC-14] Fix linter * [EC-14] revert change to vault filter model * [EC-14] be specific about ignoring All Collections * [EC-14] move observable init logic to beforeEach in test * [EC-14] make buildAllFilters return something to reduce side effects Signed-off-by: Jacob Fink Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Co-authored-by: Thomas Rittson * [EC-97] Organization Billing Language / RxJS Warnings (#3688) * [EC-97] Update copy to use the word members in a few places * [EC-97] Cleanup RxJS warnings and unused properties in org billing components * [EC-599] Access Selector Component (#3717) * Add Access Selector Component and Stories * Cherry pick FormSelectionList * Fix some problems caused from cherry-pick * Fix some Web module problems caused from cherry-pick * Move AccessSelector out of the root components directory. Move UserType pipe to AccessSelectorModule * Fix broken member access selector story * Add organization feature module * Undo changes to messages.json * Fix messages.json * Remove redundant CommonModule * [EC-599] Fix avatar/icon sizing * [EC-599] Remove padding in permission column * [EC-599] Make FormSelectionList operations immutable * [EC-599] Integrate the multi-select component * [EC-599] Handle readonly/access all edge cases * [EC-599] Add initial unit tests Also cleans up public interface for the AccessSelectorComponent. Fixes a bug found during unit test creation. * [EC-599] Include item name in control labels * [EC-599] Cleanup member email display * [EC-599] Review suggestions - Change PermissionMode to Enum - Rename permControl to permissionControl to be more clear - Rename FormSelectionList file to kebab case. - Move permission row boolean logic to named function for readability * [EC-599] Cleanup AccessSelectorComponent tests - Clarify test states - Add tests for column rendering - Add tests for permission mode - Add id to column headers for testing - Fix small permissionControl bug found during testing * [EC-599] Add FormSelectionList unit tests * [EC-599] Fix unit test and linter * [EC-599] Update Enums to Pascal case * [EC-599] Undo change to Enum values * [EC-7] fix: broken build * [EC-646] Org Admin Vault Refresh November Release Prep (#3913) * [EC-646] Remove links from Manage component These links are no longer necessary as they are now located in the new OAVR tabs. * [EC-646] Re-introduce the canAccessManageTab helper * [EC-646] Re-introduce /manage route in Organization routing module - Add the parent /manage route - Add child routes for collections, people, and groups * [EC-646] Adjust Org admin tabs Re-introduce the Manage tab and remove Groups and Members tabs. * [EC-646] Change Members title back to People * [EC-646] Move missing billing components Some billing components were in the org settings module and needed to be moved the org billing module * [EC-646] Fix import file upload button -Update to use click event handler and tailwind class to hide input. Avoids inline styles/js blocked by CSP - Fix broken async pipe * [EC-646] Fix groups and people page overflow Remove the container and page-content wrapper as the pages are no longer on their own tab * [EC-646] Change People to Members Change the text regarding managing members from People to Members to more closely follow changes coming later in the OAVR. Also update the URL to use /manage/members * [EC-646] Cherry-pick ae39afe to fix tab text color * [EC-646] Fix org routing permissions helpers - Add canAccessVaultTab helper - Update canAccessOrgAdmin include check for vault tab access - Simplify canManageCollections * [EC-646] Fix Manage tab conditional logic - Add *ngIf condition for rendering Manage tab - Re-introduce dynamic route for Manage tab * Revert "[EC-14] Refactor vault filter (#3440)" (#3926) This reverts commit 4d83b81d824de467719e1cff68c0f22c1264d89d. * Remove old reference to bit-submit-button that no longer exists (#3927) * [EC-593] Top align event logs row content (#3813) * [EC-593] Top align event log row contents * [EC-593] Prevent event log timestamp from wrapping * [EC-593] Add alignContent input to bitRow directive * [EC-593] Remove ineffective inline styles (CSP) * [EC-593] Remove templated tailwind classes Tailwind minimizes the bundled stylesheet by removing classes that aren't used in code. Using a string template for the classes causes those classes to be ignored. * [EC-593] Introduce alignContent input to table story * [EC-657] Hide Billing History and Payment Method for selfhosted orgs (#3935) Signed-off-by: Jacob Fink Co-authored-by: Vincent Salucci Co-authored-by: Andreas Coroiu Co-authored-by: Jake Fink Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Co-authored-by: Thomas Rittson --- apps/web/.eslintrc.json | 2 + apps/web/src/app/app.component.ts | 20 +- apps/web/src/app/core/event.service.ts | 8 +- apps/web/src/app/core/policy-list.service.ts | 2 +- .../adjust-subscription.component.html | 0 .../adjust-subscription.component.ts | 0 .../billing-sync-api-key.component.html | 0 .../billing-sync-api-key.component.ts | 0 .../change-plan.component.html | 0 .../change-plan.component.ts | 0 .../download-license.component.html | 0 .../download-license.component.ts | 0 ...zation-billing-history-view.component.html | 27 + ...nization-billing-history-view.component.ts | 51 ++ .../organization-billing-routing.module.ts | 48 ++ .../organization-billing-tab.component.html | 33 ++ .../organization-billing-tab.component.ts | 14 + .../billing/organization-billing.module.ts | 26 + .../organization-subscription.component.html | 0 .../organization-subscription.component.ts | 47 +- .../subscription-hidden.icon.ts | 0 .../access-selector.component.html | 136 +++++ .../access-selector.component.spec.ts | 250 ++++++++ .../access-selector.component.ts | 290 ++++++++++ .../access-selector/access-selector.models.ts | 107 ++++ .../access-selector/access-selector.module.ts | 13 + .../access-selector.stories.ts | 302 ++++++++++ .../components/access-selector/index.ts | 3 + .../access-selector/user-type.pipe.ts | 29 + .../organization-layout.component.html | 71 +-- .../layouts/organization-layout.component.ts | 60 +- .../manage/events.component.html | 100 ++-- .../organizations/manage/events.component.ts | 37 +- .../organizations/manage/groups.component.ts | 2 +- .../manage/manage.component.html | 36 +- .../manage/people.component.html | 6 +- .../organizations/navigation-permissions.ts | 0 .../organization-routing.module.ts | 161 +----- .../app/organizations/organization.module.ts | 11 + .../src/app/organizations/policies/index.ts | 12 + .../policies.component.html | 0 .../policies.component.ts | 2 +- .../organizations/policies/policies.module.ts | 46 ++ .../policy-edit.component.html | 0 .../policy-edit.component.ts | 2 +- .../organization-reporting-routing.module.ts | 87 +++ .../organization-reporting.module.ts | 14 + .../reporting/reporting.component.html | 30 + .../reporting/reporting.component.ts | 36 ++ .../reporting/reports-home.component.html | 20 + .../reporting/reports-home.component.ts | 65 +++ .../settings/account.component.html | 37 +- .../settings/account.component.ts | 8 - .../src/app/organizations/settings/index.ts | 2 + .../organization-billing.component.html | 212 ------- .../organization-billing.component.ts | 153 ----- .../organization-settings-routing.module.ts | 52 ++ .../settings/organization-settings.module.ts | 21 + .../settings/settings.component.html | 45 +- .../settings/settings.component.ts | 38 +- ...families-for-enterprise-setup.component.ts | 2 +- apps/web/src/app/oss-routing.module.ts | 4 +- apps/web/src/app/reports/index.ts | 4 +- .../reports/pages/reports-home.component.ts | 3 +- apps/web/src/app/reports/reports.module.ts | 7 +- apps/web/src/app/reports/reports.ts | 2 +- apps/web/src/app/reports/shared/index.ts | 3 + .../{ => shared}/models/report-entry.ts | 0 .../{ => shared}/models/report-variant.ts | 0 .../report-card/report-card.component.html | 2 +- .../report-card/report-card.component.ts | 0 .../report-card/report-card.stories.ts | 4 +- .../report-list/report-list.component.html | 0 .../report-list/report-list.component.ts | 0 .../report-list/report-list.stories.ts | 6 +- .../reports/shared/reports-shared.module.ts | 14 + .../billing-history-view.component.html | 27 + ...t.ts => billing-history-view.component.ts} | 25 +- .../settings/billing-history.component.html | 65 +++ .../app/settings/billing-history.component.ts | 41 ++ .../settings/payment-method.component.html | 85 ++- .../app/settings/payment-method.component.ts | 124 +++- .../settings/subscription-routing.module.ts | 4 +- .../settings/two-factor-setup.component.html | 13 +- .../user-billing-history.component.html | 98 ---- .../src/app/shared/loose-components.module.ts | 71 +-- apps/web/src/app/shared/shared.module.ts | 11 +- .../tools/import-export/import.component.html | 2 +- .../organization-filter.component.html | 8 - apps/web/src/locales/en/messages.json | 120 +++- .../components/base-cva.component.ts | 7 +- .../input-text-readonly.component.html | 26 - .../input-text-readonly.component.ts | 25 - .../components/input-text.component.html | 33 -- .../components/input-text.component.ts | 48 -- .../components/select.component.html | 19 - .../components/select.component.ts | 14 - .../organizations/manage/sso.component.html | 533 ++++++++++-------- .../app/organizations/manage/sso.component.ts | 157 +++--- .../organizations-routing.module.ts | 10 +- .../app/organizations/organizations.module.ts | 14 +- .../src/directives/copy-click.directive.ts | 16 + .../src/directives/launch-click.directive.ts | 19 + libs/angular/src/jslib.module.ts | 6 + libs/angular/src/types/controls-of.ts | 20 + .../src/utils/form-selection-list.spec.ts | 253 +++++++++ libs/angular/src/utils/form-selection-list.ts | 201 +++++++ .../angular/src/validators/dirty.validator.ts | 9 - .../organization.service.abstraction.ts | 44 +- libs/common/src/models/api/sso-config.api.ts | 2 +- .../request/organization-update.request.ts | 4 + .../organization/organization-sso.request.ts | 1 + .../src/models/response/billing.response.ts | 4 + .../organization/organization-sso.response.ts | 2 + .../common/src/models/view/sso-config.view.ts | 2 +- .../src/form-field/error-summary.component.ts | 4 + .../src/form-field/error.component.ts | 2 + libs/components/src/index.ts | 1 + .../multi-select/models/select-item-view.ts | 4 +- libs/components/src/table/row.directive.ts | 18 +- libs/components/src/table/table.stories.ts | 19 +- .../tabs/shared/tab-list-item.directive.ts | 29 +- libs/components/tailwind.config.base.js | 5 +- 123 files changed, 3493 insertions(+), 1577 deletions(-) rename apps/web/src/app/organizations/{settings => billing}/adjust-subscription.component.html (100%) rename apps/web/src/app/organizations/{settings => billing}/adjust-subscription.component.ts (100%) rename apps/web/src/app/organizations/{settings => billing}/billing-sync-api-key.component.html (100%) rename apps/web/src/app/organizations/{settings => billing}/billing-sync-api-key.component.ts (100%) rename apps/web/src/app/organizations/{settings => billing}/change-plan.component.html (100%) rename apps/web/src/app/organizations/{settings => billing}/change-plan.component.ts (100%) rename apps/web/src/app/organizations/{settings => billing}/download-license.component.html (100%) rename apps/web/src/app/organizations/{settings => billing}/download-license.component.ts (100%) create mode 100644 apps/web/src/app/organizations/billing/organization-billing-history-view.component.html create mode 100644 apps/web/src/app/organizations/billing/organization-billing-history-view.component.ts create mode 100644 apps/web/src/app/organizations/billing/organization-billing-routing.module.ts create mode 100644 apps/web/src/app/organizations/billing/organization-billing-tab.component.html create mode 100644 apps/web/src/app/organizations/billing/organization-billing-tab.component.ts create mode 100644 apps/web/src/app/organizations/billing/organization-billing.module.ts rename apps/web/src/app/organizations/{settings => billing}/organization-subscription.component.html (100%) rename apps/web/src/app/organizations/{settings => billing}/organization-subscription.component.ts (93%) rename apps/web/src/app/organizations/{settings => billing}/subscription-hidden.icon.ts (100%) create mode 100644 apps/web/src/app/organizations/components/access-selector/access-selector.component.html create mode 100644 apps/web/src/app/organizations/components/access-selector/access-selector.component.spec.ts create mode 100644 apps/web/src/app/organizations/components/access-selector/access-selector.component.ts create mode 100644 apps/web/src/app/organizations/components/access-selector/access-selector.models.ts create mode 100644 apps/web/src/app/organizations/components/access-selector/access-selector.module.ts create mode 100644 apps/web/src/app/organizations/components/access-selector/access-selector.stories.ts create mode 100644 apps/web/src/app/organizations/components/access-selector/index.ts create mode 100644 apps/web/src/app/organizations/components/access-selector/user-type.pipe.ts create mode 100644 apps/web/src/app/organizations/navigation-permissions.ts create mode 100644 apps/web/src/app/organizations/organization.module.ts create mode 100644 apps/web/src/app/organizations/policies/index.ts rename apps/web/src/app/organizations/{manage => policies}/policies.component.html (100%) rename apps/web/src/app/organizations/{manage => policies}/policies.component.ts (98%) create mode 100644 apps/web/src/app/organizations/policies/policies.module.ts rename apps/web/src/app/organizations/{manage => policies}/policy-edit.component.html (100%) rename apps/web/src/app/organizations/{manage => policies}/policy-edit.component.ts (97%) create mode 100644 apps/web/src/app/organizations/reporting/organization-reporting-routing.module.ts create mode 100644 apps/web/src/app/organizations/reporting/organization-reporting.module.ts create mode 100644 apps/web/src/app/organizations/reporting/reporting.component.html create mode 100644 apps/web/src/app/organizations/reporting/reporting.component.ts create mode 100644 apps/web/src/app/organizations/reporting/reports-home.component.html create mode 100644 apps/web/src/app/organizations/reporting/reports-home.component.ts create mode 100644 apps/web/src/app/organizations/settings/index.ts delete mode 100644 apps/web/src/app/organizations/settings/organization-billing.component.html create mode 100644 apps/web/src/app/organizations/settings/organization-settings-routing.module.ts create mode 100644 apps/web/src/app/organizations/settings/organization-settings.module.ts create mode 100644 apps/web/src/app/reports/shared/index.ts rename apps/web/src/app/reports/{ => shared}/models/report-entry.ts (100%) rename apps/web/src/app/reports/{ => shared}/models/report-variant.ts (100%) rename apps/web/src/app/reports/{ => shared}/report-card/report-card.component.html (74%) rename apps/web/src/app/reports/{ => shared}/report-card/report-card.component.ts (100%) rename apps/web/src/app/reports/{ => shared}/report-card/report-card.stories.ts (88%) rename apps/web/src/app/reports/{ => shared}/report-list/report-list.component.html (100%) rename apps/web/src/app/reports/{ => shared}/report-list/report-list.component.ts (100%) rename apps/web/src/app/reports/{ => shared}/report-list/report-list.stories.ts (84%) create mode 100644 apps/web/src/app/reports/shared/reports-shared.module.ts create mode 100644 apps/web/src/app/settings/billing-history-view.component.html rename apps/web/src/app/settings/{user-billing-history.component.ts => billing-history-view.component.ts} (53%) create mode 100644 apps/web/src/app/settings/billing-history.component.html create mode 100644 apps/web/src/app/settings/billing-history.component.ts delete mode 100644 apps/web/src/app/settings/user-billing-history.component.html delete mode 100644 bitwarden_license/bit-web/src/app/organizations/components/input-text-readonly.component.html delete mode 100644 bitwarden_license/bit-web/src/app/organizations/components/input-text-readonly.component.ts delete mode 100644 bitwarden_license/bit-web/src/app/organizations/components/input-text.component.html delete mode 100644 bitwarden_license/bit-web/src/app/organizations/components/input-text.component.ts delete mode 100644 bitwarden_license/bit-web/src/app/organizations/components/select.component.html delete mode 100644 bitwarden_license/bit-web/src/app/organizations/components/select.component.ts create mode 100644 libs/angular/src/directives/copy-click.directive.ts create mode 100644 libs/angular/src/directives/launch-click.directive.ts create mode 100644 libs/angular/src/types/controls-of.ts create mode 100644 libs/angular/src/utils/form-selection-list.spec.ts create mode 100644 libs/angular/src/utils/form-selection-list.ts delete mode 100644 libs/angular/src/validators/dirty.validator.ts diff --git a/apps/web/.eslintrc.json b/apps/web/.eslintrc.json index 6c519d70ae..69dae5e732 100644 --- a/apps/web/.eslintrc.json +++ b/apps/web/.eslintrc.json @@ -10,6 +10,8 @@ "**/app/core/*", "**/reports/*", "**/app/shared/*", + "**/organizations/settings/*", + "**/organizations/policies/*", "@bitwarden/web-vault/*", "src/**/*" ], diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index b1700a8551..9bd0fda6fc 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -27,15 +27,17 @@ import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.ab import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service"; import { PolicyListService, RouterService } from "./core"; -import { DisableSendPolicy } from "./organizations/policies/disable-send.component"; -import { MasterPasswordPolicy } from "./organizations/policies/master-password.component"; -import { PasswordGeneratorPolicy } from "./organizations/policies/password-generator.component"; -import { PersonalOwnershipPolicy } from "./organizations/policies/personal-ownership.component"; -import { RequireSsoPolicy } from "./organizations/policies/require-sso.component"; -import { ResetPasswordPolicy } from "./organizations/policies/reset-password.component"; -import { SendOptionsPolicy } from "./organizations/policies/send-options.component"; -import { SingleOrgPolicy } from "./organizations/policies/single-org.component"; -import { TwoFactorAuthenticationPolicy } from "./organizations/policies/two-factor-authentication.component"; +import { + DisableSendPolicy, + MasterPasswordPolicy, + PasswordGeneratorPolicy, + PersonalOwnershipPolicy, + RequireSsoPolicy, + ResetPasswordPolicy, + SendOptionsPolicy, + SingleOrgPolicy, + TwoFactorAuthenticationPolicy, +} from "./organizations/policies"; const BroadcasterSubscriptionId = "AppComponent"; const IdleTimeout = 60000 * 10; // 10 minutes diff --git a/apps/web/src/app/core/event.service.ts b/apps/web/src/app/core/event.service.ts index 6205418810..4c4e488ff0 100644 --- a/apps/web/src/app/core/event.service.ts +++ b/apps/web/src/app/core/event.service.ts @@ -479,16 +479,14 @@ export class EventService implements OnInit, OnDestroy { private formatGroupId(ev: EventResponse) { const shortId = this.getShortId(ev.groupId); const a = this.makeAnchor(shortId); - a.setAttribute( - "href", - "#/organizations/" + ev.organizationId + "/manage/groups?search=" + shortId - ); + a.setAttribute("href", "#/organizations/" + ev.organizationId + "/groups?search=" + shortId); return a.outerHTML; } private formatCollectionId(ev: EventResponse) { const shortId = this.getShortId(ev.collectionId); const a = this.makeAnchor(shortId); + // TODO: Update view/edit collection link after EC-14 is completed a.setAttribute( "href", "#/organizations/" + ev.organizationId + "/manage/collections?search=" + shortId @@ -503,7 +501,7 @@ export class EventService implements OnInit, OnDestroy { "href", "#/organizations/" + ev.organizationId + - "/manage/people?search=" + + "/members?search=" + shortId + "&viewEvents=" + ev.organizationUserId diff --git a/apps/web/src/app/core/policy-list.service.ts b/apps/web/src/app/core/policy-list.service.ts index 70857ef819..bb20700690 100644 --- a/apps/web/src/app/core/policy-list.service.ts +++ b/apps/web/src/app/core/policy-list.service.ts @@ -1,4 +1,4 @@ -import { BasePolicy } from "../organizations/policies/base-policy.component"; +import { BasePolicy } from "../organizations/policies"; export class PolicyListService { private policies: BasePolicy[] = []; diff --git a/apps/web/src/app/organizations/settings/adjust-subscription.component.html b/apps/web/src/app/organizations/billing/adjust-subscription.component.html similarity index 100% rename from apps/web/src/app/organizations/settings/adjust-subscription.component.html rename to apps/web/src/app/organizations/billing/adjust-subscription.component.html diff --git a/apps/web/src/app/organizations/settings/adjust-subscription.component.ts b/apps/web/src/app/organizations/billing/adjust-subscription.component.ts similarity index 100% rename from apps/web/src/app/organizations/settings/adjust-subscription.component.ts rename to apps/web/src/app/organizations/billing/adjust-subscription.component.ts diff --git a/apps/web/src/app/organizations/settings/billing-sync-api-key.component.html b/apps/web/src/app/organizations/billing/billing-sync-api-key.component.html similarity index 100% rename from apps/web/src/app/organizations/settings/billing-sync-api-key.component.html rename to apps/web/src/app/organizations/billing/billing-sync-api-key.component.html diff --git a/apps/web/src/app/organizations/settings/billing-sync-api-key.component.ts b/apps/web/src/app/organizations/billing/billing-sync-api-key.component.ts similarity index 100% rename from apps/web/src/app/organizations/settings/billing-sync-api-key.component.ts rename to apps/web/src/app/organizations/billing/billing-sync-api-key.component.ts diff --git a/apps/web/src/app/organizations/settings/change-plan.component.html b/apps/web/src/app/organizations/billing/change-plan.component.html similarity index 100% rename from apps/web/src/app/organizations/settings/change-plan.component.html rename to apps/web/src/app/organizations/billing/change-plan.component.html diff --git a/apps/web/src/app/organizations/settings/change-plan.component.ts b/apps/web/src/app/organizations/billing/change-plan.component.ts similarity index 100% rename from apps/web/src/app/organizations/settings/change-plan.component.ts rename to apps/web/src/app/organizations/billing/change-plan.component.ts diff --git a/apps/web/src/app/organizations/settings/download-license.component.html b/apps/web/src/app/organizations/billing/download-license.component.html similarity index 100% rename from apps/web/src/app/organizations/settings/download-license.component.html rename to apps/web/src/app/organizations/billing/download-license.component.html diff --git a/apps/web/src/app/organizations/settings/download-license.component.ts b/apps/web/src/app/organizations/billing/download-license.component.ts similarity index 100% rename from apps/web/src/app/organizations/settings/download-license.component.ts rename to apps/web/src/app/organizations/billing/download-license.component.ts diff --git a/apps/web/src/app/organizations/billing/organization-billing-history-view.component.html b/apps/web/src/app/organizations/billing/organization-billing-history-view.component.html new file mode 100644 index 0000000000..6622245ad1 --- /dev/null +++ b/apps/web/src/app/organizations/billing/organization-billing-history-view.component.html @@ -0,0 +1,27 @@ + + + + {{ "loading" | i18n }} + + + + diff --git a/apps/web/src/app/organizations/billing/organization-billing-history-view.component.ts b/apps/web/src/app/organizations/billing/organization-billing-history-view.component.ts new file mode 100644 index 0000000000..c352bb83f1 --- /dev/null +++ b/apps/web/src/app/organizations/billing/organization-billing-history-view.component.ts @@ -0,0 +1,51 @@ +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { ActivatedRoute } from "@angular/router"; +import { concatMap, Subject, takeUntil } from "rxjs"; + +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction"; +import { BillingHistoryResponse } from "@bitwarden/common/models/response/billing-history.response"; + +@Component({ + selector: "app-org-billing-history-view", + templateUrl: "organization-billing-history-view.component.html", +}) +export class OrgBillingHistoryViewComponent implements OnInit, OnDestroy { + loading = false; + firstLoaded = false; + billing: BillingHistoryResponse; + organizationId: string; + + private destroy$ = new Subject(); + + constructor( + private organizationApiService: OrganizationApiServiceAbstraction, + private route: ActivatedRoute + ) {} + + async ngOnInit() { + this.route.params + .pipe( + concatMap(async (params) => { + this.organizationId = params.organizationId; + await this.load(); + this.firstLoaded = true; + }), + takeUntil(this.destroy$) + ) + .subscribe(); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + async load() { + if (this.loading) { + return; + } + this.loading = true; + this.billing = await this.organizationApiService.getBilling(this.organizationId); + this.loading = false; + } +} diff --git a/apps/web/src/app/organizations/billing/organization-billing-routing.module.ts b/apps/web/src/app/organizations/billing/organization-billing-routing.module.ts new file mode 100644 index 0000000000..0e410d9734 --- /dev/null +++ b/apps/web/src/app/organizations/billing/organization-billing-routing.module.ts @@ -0,0 +1,48 @@ +import { NgModule } from "@angular/core"; +import { RouterModule, Routes } from "@angular/router"; + +import { canAccessBillingTab } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; + +import { PaymentMethodComponent } from "../../settings/payment-method.component"; +import { OrganizationPermissionsGuard } from "../guards/org-permissions.guard"; + +import { OrgBillingHistoryViewComponent } from "./organization-billing-history-view.component"; +import { OrganizationBillingTabComponent } from "./organization-billing-tab.component"; +import { OrganizationSubscriptionComponent } from "./organization-subscription.component"; + +const routes: Routes = [ + { + path: "", + component: OrganizationBillingTabComponent, + canActivate: [OrganizationPermissionsGuard], + data: { organizationPermissions: canAccessBillingTab }, + children: [ + { path: "", pathMatch: "full", redirectTo: "subscription" }, + { + path: "subscription", + component: OrganizationSubscriptionComponent, + data: { titleId: "subscription" }, + }, + { + path: "payment-method", + component: PaymentMethodComponent, + data: { + titleId: "paymentMethod", + }, + }, + { + path: "history", + component: OrgBillingHistoryViewComponent, + data: { + titleId: "billingHistory", + }, + }, + ], + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class OrganizationBillingRoutingModule {} diff --git a/apps/web/src/app/organizations/billing/organization-billing-tab.component.html b/apps/web/src/app/organizations/billing/organization-billing-tab.component.html new file mode 100644 index 0000000000..7f755fccbb --- /dev/null +++ b/apps/web/src/app/organizations/billing/organization-billing-tab.component.html @@ -0,0 +1,33 @@ + diff --git a/apps/web/src/app/organizations/billing/organization-billing-tab.component.ts b/apps/web/src/app/organizations/billing/organization-billing-tab.component.ts new file mode 100644 index 0000000000..5eb207dee9 --- /dev/null +++ b/apps/web/src/app/organizations/billing/organization-billing-tab.component.ts @@ -0,0 +1,14 @@ +import { Component } from "@angular/core"; + +import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; + +@Component({ + selector: "app-org-billing-tab", + templateUrl: "organization-billing-tab.component.html", +}) +export class OrganizationBillingTabComponent { + showPaymentAndHistory: boolean; + constructor(private platformUtilsService: PlatformUtilsService) { + this.showPaymentAndHistory = !this.platformUtilsService.isSelfHost(); + } +} diff --git a/apps/web/src/app/organizations/billing/organization-billing.module.ts b/apps/web/src/app/organizations/billing/organization-billing.module.ts new file mode 100644 index 0000000000..513b7ba766 --- /dev/null +++ b/apps/web/src/app/organizations/billing/organization-billing.module.ts @@ -0,0 +1,26 @@ +import { NgModule } from "@angular/core"; + +import { LooseComponentsModule, SharedModule } from "../../shared"; + +import { AdjustSubscription } from "./adjust-subscription.component"; +import { BillingSyncApiKeyComponent } from "./billing-sync-api-key.component"; +import { ChangePlanComponent } from "./change-plan.component"; +import { DownloadLicenseComponent } from "./download-license.component"; +import { OrgBillingHistoryViewComponent } from "./organization-billing-history-view.component"; +import { OrganizationBillingRoutingModule } from "./organization-billing-routing.module"; +import { OrganizationBillingTabComponent } from "./organization-billing-tab.component"; +import { OrganizationSubscriptionComponent } from "./organization-subscription.component"; + +@NgModule({ + imports: [SharedModule, LooseComponentsModule, OrganizationBillingRoutingModule], + declarations: [ + AdjustSubscription, + BillingSyncApiKeyComponent, + ChangePlanComponent, + DownloadLicenseComponent, + OrganizationBillingTabComponent, + OrganizationSubscriptionComponent, + OrgBillingHistoryViewComponent, + ], +}) +export class OrganizationBillingModule {} diff --git a/apps/web/src/app/organizations/settings/organization-subscription.component.html b/apps/web/src/app/organizations/billing/organization-subscription.component.html similarity index 100% rename from apps/web/src/app/organizations/settings/organization-subscription.component.html rename to apps/web/src/app/organizations/billing/organization-subscription.component.html diff --git a/apps/web/src/app/organizations/settings/organization-subscription.component.ts b/apps/web/src/app/organizations/billing/organization-subscription.component.ts similarity index 93% rename from apps/web/src/app/organizations/settings/organization-subscription.component.ts rename to apps/web/src/app/organizations/billing/organization-subscription.component.ts index dbcdfd907a..803be05097 100644 --- a/apps/web/src/app/organizations/settings/organization-subscription.component.ts +++ b/apps/web/src/app/organizations/billing/organization-subscription.component.ts @@ -1,5 +1,6 @@ -import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; +import { Component, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; +import { concatMap, Subject, takeUntil } from "rxjs"; import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -27,17 +28,13 @@ import { SubscriptionHiddenIcon } from "./subscription-hidden.icon"; selector: "app-org-subscription", templateUrl: "organization-subscription.component.html", }) -// eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class OrganizationSubscriptionComponent implements OnInit { +export class OrganizationSubscriptionComponent implements OnInit, OnDestroy { @ViewChild("setupBillingSyncTemplate", { read: ViewContainerRef, static: true }) setupBillingSyncModalRef: ViewContainerRef; loading = false; firstLoaded = false; organizationId: string; - adjustSeatsAdd = true; - showAdjustSeats = false; - showAdjustSeatAutoscale = false; adjustStorageAdd = true; showAdjustStorage = false; showUpdateLicense = false; @@ -61,6 +58,8 @@ export class OrganizationSubscriptionComponent implements OnInit { subscriptionHiddenIcon = SubscriptionHiddenIcon; + private destroy$ = new Subject(); + constructor( private apiService: ApiService, private platformUtilsService: PlatformUtilsService, @@ -76,19 +75,27 @@ export class OrganizationSubscriptionComponent implements OnInit { } async ngOnInit() { - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - this.route.parent.parent.params.subscribe(async (params) => { - this.organizationId = params.organizationId; - await this.load(); - this.firstLoaded = true; - }); + this.route.params + .pipe( + concatMap(async (params) => { + this.organizationId = params.organizationId; + await this.load(); + this.firstLoaded = true; + }), + takeUntil(this.destroy$) + ) + .subscribe(); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); } async load() { if (this.loading) { return; } - this.loading = true; this.userOrg = this.organizationService.get(this.organizationId); if (this.userOrg.canManageBilling) { @@ -175,7 +182,7 @@ export class OrganizationSubscriptionComponent implements OnInit { this.showChangePlan = !this.showChangePlan; } - closeChangePlan(changed: boolean) { + closeChangePlan() { this.showChangePlan = false; } @@ -192,10 +199,14 @@ export class OrganizationSubscriptionComponent implements OnInit { comp.hasBillingToken = this.hasBillingSyncToken; } ); - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - ref.onClosed.subscribe(async () => { - await this.load(); - }); + ref.onClosed + .pipe( + concatMap(async () => { + await this.load(); + }), + takeUntil(this.destroy$) + ) + .subscribe(); } closeDownloadLicense() { diff --git a/apps/web/src/app/organizations/settings/subscription-hidden.icon.ts b/apps/web/src/app/organizations/billing/subscription-hidden.icon.ts similarity index 100% rename from apps/web/src/app/organizations/settings/subscription-hidden.icon.ts rename to apps/web/src/app/organizations/billing/subscription-hidden.icon.ts diff --git a/apps/web/src/app/organizations/components/access-selector/access-selector.component.html b/apps/web/src/app/organizations/components/access-selector/access-selector.component.html new file mode 100644 index 0000000000..10390b3e05 --- /dev/null +++ b/apps/web/src/app/organizations/components/access-selector/access-selector.component.html @@ -0,0 +1,136 @@ +
+ + {{ "permission" | i18n }} + + + + + {{ selectorLabelText }} + + {{ selectorHelpText }} + +
+ + + + + {{ columnHeader }} + + {{ "permission" | i18n }} + + {{ "role" | i18n }} + {{ "group" | i18n }} + + + + + + +
+ +
+
+ {{ item.labelName }} + + {{ "invited" | i18n }} + +
+
{{ item.email }}
+
+
+ +
+ + {{ item.labelName }} +
+ + + + + + + + + +
+ {{ "canEdit" | i18n }} + +
+ +
+ {{ permissionLabelId(item.readonlyPermission) | i18n }} +
+
+ + + + {{ item.role | userType: "-" }} + + + + {{ item.viaGroupName ?? "-" }} + + + + + + + + {{ emptySelectionText }} + +
+
diff --git a/apps/web/src/app/organizations/components/access-selector/access-selector.component.spec.ts b/apps/web/src/app/organizations/components/access-selector/access-selector.component.spec.ts new file mode 100644 index 0000000000..3b2ba911aa --- /dev/null +++ b/apps/web/src/app/organizations/components/access-selector/access-selector.component.spec.ts @@ -0,0 +1,250 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { OrganizationUserStatusType } from "@bitwarden/common/enums/organizationUserStatusType"; +import { OrganizationUserType } from "@bitwarden/common/enums/organizationUserType"; +import { + AvatarModule, + BadgeModule, + ButtonModule, + FormFieldModule, + IconButtonModule, + TableModule, + TabsModule, +} from "@bitwarden/components"; +import { SelectItemView } from "@bitwarden/components/src/multi-select/models/select-item-view"; + +import { PreloadedEnglishI18nModule } from "../../../tests/preloaded-english-i18n.module"; + +import { AccessSelectorComponent, PermissionMode } from "./access-selector.component"; +import { AccessItemType, CollectionPermission } from "./access-selector.models"; +import { UserTypePipe } from "./user-type.pipe"; + +/** + * Helper class that makes it easier to test the AccessSelectorComponent by + * exposing some protected methods/properties + */ +class TestableAccessSelectorComponent extends AccessSelectorComponent { + selectItems(items: SelectItemView[]) { + super.selectItems(items); + } + deselectItem(id: string) { + this.selectionList.deselectItem(id); + } + + /** + * Helper used to simulate a user selecting a new permission for a table row + * @param index - "Row" index + * @param perm - The new permission value + */ + changeSelectedItemPerm(index: number, perm: CollectionPermission) { + this.selectionList.formArray.at(index).patchValue({ + permission: perm, + }); + } +} + +describe("AccessSelectorComponent", () => { + let component: TestableAccessSelectorComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + ButtonModule, + FormFieldModule, + AvatarModule, + BadgeModule, + ReactiveFormsModule, + FormsModule, + TabsModule, + TableModule, + PreloadedEnglishI18nModule, + JslibModule, + IconButtonModule, + ], + declarations: [TestableAccessSelectorComponent, UserTypePipe], + providers: [], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TestableAccessSelectorComponent); + component = fixture.componentInstance; + + component.emptySelectionText = "Nothing selected"; + + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); + + describe("item selection", () => { + beforeEach(() => { + component.items = [ + { + id: "123", + type: AccessItemType.Group, + labelName: "Group 1", + listName: "Group 1", + }, + ]; + fixture.detectChanges(); + }); + + it("should show the empty row when nothing is selected", () => { + const emptyTableCell = fixture.nativeElement.querySelector("tbody tr td"); + expect(emptyTableCell?.textContent).toEqual("Nothing selected"); + }); + + it("should show one row when one value is selected", () => { + component.selectItems([{ id: "123" } as any]); + fixture.detectChanges(); + const firstColSpan = fixture.nativeElement.querySelector("tbody tr td span"); + expect(firstColSpan.textContent).toEqual("Group 1"); + }); + + it("should emit value change when a value is selected", () => { + // Arrange + const mockChange = jest.fn(); + component.registerOnChange(mockChange); + component.permissionMode = PermissionMode.Edit; + + // Act + component.selectItems([{ id: "123" } as any]); + + // Assert + expect(mockChange.mock.calls.length).toEqual(1); + expect(mockChange.mock.lastCall[0]).toHaveProperty("[0].id", "123"); + }); + + it("should emit value change when a row is modified", () => { + // Arrange + const mockChange = jest.fn(); + component.permissionMode = PermissionMode.Edit; + component.selectItems([{ id: "123" } as any]); + component.registerOnChange(mockChange); // Register change listener after setup + + // Act + component.changeSelectedItemPerm(0, CollectionPermission.Edit); + + // Assert + expect(mockChange.mock.calls.length).toEqual(1); + expect(mockChange.mock.lastCall[0]).toHaveProperty("[0].id", "123"); + expect(mockChange.mock.lastCall[0]).toHaveProperty( + "[0].permission", + CollectionPermission.Edit + ); + }); + + it("should emit value change when a row is removed", () => { + // Arrange + const mockChange = jest.fn(); + component.permissionMode = PermissionMode.Edit; + component.selectItems([{ id: "123" } as any]); + component.registerOnChange(mockChange); // Register change listener after setup + + // Act + component.deselectItem("123"); + + // Assert + expect(mockChange.mock.calls.length).toEqual(1); + expect(mockChange.mock.lastCall[0].length).toEqual(0); + }); + + it("should emit permission values when in edit mode", () => { + // Arrange + const mockChange = jest.fn(); + component.registerOnChange(mockChange); + component.permissionMode = PermissionMode.Edit; + + // Act + component.selectItems([{ id: "123" } as any]); + + // Assert + expect(mockChange.mock.calls.length).toEqual(1); + expect(mockChange.mock.lastCall[0]).toHaveProperty("[0].id", "123"); + expect(mockChange.mock.lastCall[0]).toHaveProperty("[0].permission"); + }); + + it("should not emit permission values when not in edit mode", () => { + // Arrange + const mockChange = jest.fn(); + component.registerOnChange(mockChange); + component.permissionMode = PermissionMode.Hidden; + + // Act + component.selectItems([{ id: "123" } as any]); + + // Assert + expect(mockChange.mock.calls.length).toEqual(1); + expect(mockChange.mock.lastCall[0]).toHaveProperty("[0].id", "123"); + expect(mockChange.mock.lastCall[0]).not.toHaveProperty("[0].permission"); + }); + }); + + describe("column rendering", () => { + beforeEach(() => { + component.items = [ + { + id: "g1", + type: AccessItemType.Group, + labelName: "Group 1", + listName: "Group 1", + }, + { + id: "m1", + type: AccessItemType.Member, + labelName: "Member 1", + listName: "Member 1 (member1@email.com)", + email: "member1@email.com", + role: OrganizationUserType.Manager, + status: OrganizationUserStatusType.Confirmed, + }, + ]; + fixture.detectChanges(); + }); + + test.each([true, false])("should show the role column when enabled", (columnEnabled) => { + // Act + component.showMemberRoles = columnEnabled; + fixture.detectChanges(); + + // Assert + const colHeading = fixture.nativeElement.querySelector("#roleColHeading"); + expect(!!colHeading).toEqual(columnEnabled); + }); + + test.each([true, false])("should show the group column when enabled", (columnEnabled) => { + // Act + component.showGroupColumn = columnEnabled; + fixture.detectChanges(); + + // Assert + const colHeading = fixture.nativeElement.querySelector("#groupColHeading"); + expect(!!colHeading).toEqual(columnEnabled); + }); + + const permissionColumnCases = [ + [PermissionMode.Hidden, false], + [PermissionMode.Edit, true], + [PermissionMode.Readonly, true], + ]; + + test.each(permissionColumnCases)( + "should show the permission column when enabled", + (mode: PermissionMode, shouldShowColumn) => { + // Act + component.permissionMode = mode; + fixture.detectChanges(); + + // Assert + const colHeading = fixture.nativeElement.querySelector("#permissionColHeading"); + expect(!!colHeading).toEqual(shouldShowColumn); + } + ); + }); +}); diff --git a/apps/web/src/app/organizations/components/access-selector/access-selector.component.ts b/apps/web/src/app/organizations/components/access-selector/access-selector.component.ts new file mode 100644 index 0000000000..98a49d5c3a --- /dev/null +++ b/apps/web/src/app/organizations/components/access-selector/access-selector.component.ts @@ -0,0 +1,290 @@ +import { Component, forwardRef, Input, OnDestroy, OnInit } from "@angular/core"; +import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from "@angular/forms"; +import { Subject, takeUntil } from "rxjs"; + +import { FormSelectionList } from "@bitwarden/angular/utils/form-selection-list"; +import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; +import { SelectItemView } from "@bitwarden/components/src/multi-select/models/select-item-view"; + +import { + AccessItemType, + AccessItemValue, + AccessItemView, + CollectionPermission, +} from "./access-selector.models"; + +export enum PermissionMode { + /** + * No permission controls or column present. No permission values are emitted. + */ + Hidden = "hidden", + + /** + * No permission controls. Column rendered an if available on an item. No permission values are emitted + */ + Readonly = "readonly", + + /** + * Permission Controls and column present. Permission values are emitted. + */ + Edit = "edit", +} + +@Component({ + selector: "bit-access-selector", + templateUrl: "access-selector.component.html", + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AccessSelectorComponent), + multi: true, + }, + ], +}) +export class AccessSelectorComponent implements ControlValueAccessor, OnInit, OnDestroy { + private destroy$ = new Subject(); + private notifyOnChange: (v: unknown) => void; + private notifyOnTouch: () => void; + private pauseChangeNotification: boolean; + + /** + * The internal selection list that tracks the value of this form control / component. + * It's responsible for keeping items sorted and synced with the rendered form controls + * @protected + */ + protected selectionList = new FormSelectionList((item) => { + const permissionControl = this.formBuilder.control(this.initialPermission); + + const fg = this.formBuilder.group({ + id: item.id, + type: item.type, + permission: permissionControl, + }); + + // Disable entire row form group if readonly + if (item.readonly) { + fg.disable(); + } + + // Disable permission control if accessAllItems is enabled + if (item.accessAllItems || this.permissionMode != PermissionMode.Edit) { + permissionControl.disable(); + } + + return fg; + }, this._itemComparator.bind(this)); + + /** + * Internal form group for this component. + * @protected + */ + protected formGroup = this.formBuilder.group({ + items: this.selectionList.formArray, + }); + + protected itemType = AccessItemType; + protected permissionList = [ + { perm: CollectionPermission.View, labelId: "canView" }, + { perm: CollectionPermission.ViewExceptPass, labelId: "canViewExceptPass" }, + { perm: CollectionPermission.Edit, labelId: "canEdit" }, + { perm: CollectionPermission.EditExceptPass, labelId: "canEditExceptPass" }, + ]; + protected initialPermission = CollectionPermission.View; + + disabled: boolean; + + /** + * List of all selectable items that. Sorted internally. + */ + @Input() + get items(): AccessItemView[] { + return this.selectionList.allItems; + } + + set items(val: AccessItemView[]) { + const selected = (this.selectionList.formArray.getRawValue() ?? []).concat( + val.filter((m) => m.readonly) + ); + this.selectionList.populateItems( + val.map((m) => { + m.icon = m.icon ?? this.itemIcon(m); // Ensure an icon is set + return m; + }), + selected + ); + } + + /** + * Permission mode that controls if the permission form controls and column should be present. + */ + @Input() + get permissionMode(): PermissionMode { + return this._permissionMode; + } + + set permissionMode(value: PermissionMode) { + this._permissionMode = value; + // Toggle any internal permission controls + for (const control of this.selectionList.formArray.controls) { + if (value == PermissionMode.Edit) { + control.get("permission").enable(); + } else { + control.get("permission").disable(); + } + } + } + private _permissionMode: PermissionMode = PermissionMode.Hidden; + + /** + * Column header for the selected items table + */ + @Input() columnHeader: string; + + /** + * Label used for the ng selector + */ + @Input() selectorLabelText: string; + + /** + * Helper text displayed under the ng selector + */ + @Input() selectorHelpText: string; + + /** + * Text that is shown in the table when no items are selected + */ + @Input() emptySelectionText: string; + + /** + * Flag for if the member roles column should be present + */ + @Input() showMemberRoles: boolean; + + /** + * Flag for if the group column should be present + */ + @Input() showGroupColumn: boolean; + + constructor( + private readonly formBuilder: FormBuilder, + private readonly i18nService: I18nService + ) {} + + /** Required for NG_VALUE_ACCESSOR */ + registerOnChange(fn: any): void { + this.notifyOnChange = fn; + } + + /** Required for NG_VALUE_ACCESSOR */ + registerOnTouched(fn: any): void { + this.notifyOnTouch = fn; + } + + /** Required for NG_VALUE_ACCESSOR */ + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + + // Keep the internal FormGroup in sync + if (this.disabled) { + this.formGroup.disable(); + } else { + this.formGroup.enable(); + } + } + + /** Required for NG_VALUE_ACCESSOR */ + writeValue(selectedItems: AccessItemValue[]): void { + // Modifying the selection list, mistakenly fires valueChanges in the + // internal form array, so we need to know to pause external notification + this.pauseChangeNotification = true; + + // Always clear the internal selection list on a new value + this.selectionList.deselectAll(); + + // We need to also select any read only items to appear in the table + this.selectionList.selectItems(this.items.filter((m) => m.readonly).map((m) => m.id)); + + // If the new value is null, then we're done + if (selectedItems == null) { + this.pauseChangeNotification = false; + return; + } + + // Unable to handle other value types, throw + if (!Array.isArray(selectedItems)) { + throw new Error("The access selector component only supports Array form values!"); + } + + // Iterate and internally select each item + for (const value of selectedItems) { + this.selectionList.selectItem(value.id, value); + } + + this.pauseChangeNotification = false; + } + + ngOnInit() { + // Watch the internal formArray for changes and propagate them + this.selectionList.formArray.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((v) => { + if (!this.notifyOnChange || this.pauseChangeNotification) { + return; + } + this.notifyOnChange(v); + }); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + protected handleBlur() { + if (!this.notifyOnTouch) { + return; + } + + this.notifyOnTouch(); + } + + protected selectItems(items: SelectItemView[]) { + this.pauseChangeNotification = true; + this.selectionList.selectItems(items.map((i) => i.id)); + this.pauseChangeNotification = false; + if (this.notifyOnChange != undefined) { + this.notifyOnChange(this.selectionList.formArray.value); + } + } + + protected itemIcon(item: AccessItemView) { + switch (item.type) { + case AccessItemType.Collection: + return "bwi-collection"; + case AccessItemType.Group: + return "bwi-users"; + case AccessItemType.Member: + return "bwi-user"; + } + } + + protected permissionLabelId(perm: CollectionPermission) { + return this.permissionList.find((p) => p.perm == perm)?.labelId; + } + + protected accessAllLabelId(item: AccessItemView) { + return item.type == AccessItemType.Group ? "groupAccessAll" : "memberAccessAll"; + } + + protected canEditItemPermission(item: AccessItemView) { + return this.permissionMode == PermissionMode.Edit && !item.readonly && !item.accessAllItems; + } + + private _itemComparator(a: AccessItemView, b: AccessItemView) { + if (a.type != b.type) { + return a.type - b.type; + } + return this.i18nService.collator.compare( + a.listName + a.labelName + a.readonly, + b.listName + b.labelName + b.readonly + ); + } +} diff --git a/apps/web/src/app/organizations/components/access-selector/access-selector.models.ts b/apps/web/src/app/organizations/components/access-selector/access-selector.models.ts new file mode 100644 index 0000000000..d621de271e --- /dev/null +++ b/apps/web/src/app/organizations/components/access-selector/access-selector.models.ts @@ -0,0 +1,107 @@ +import { OrganizationUserStatusType } from "@bitwarden/common/enums/organizationUserStatusType"; +import { OrganizationUserType } from "@bitwarden/common/enums/organizationUserType"; +import { SelectionReadOnlyRequest } from "@bitwarden/common/models/request/selection-read-only.request"; +import { SelectionReadOnlyResponse } from "@bitwarden/common/models/response/selection-read-only.response"; +import { SelectItemView } from "@bitwarden/components/src/multi-select/models/select-item-view"; + +/** + * Permission options that replace/correspond with readOnly and hidePassword server fields. + */ +export enum CollectionPermission { + View = "view", + ViewExceptPass = "viewExceptPass", + Edit = "edit", + EditExceptPass = "editExceptPass", +} + +export enum AccessItemType { + Collection, + Group, + Member, +} + +/** + * A "generic" type that describes an item that can be selected from a + * ng-select list and have its collection permission modified. + * + * Currently, it supports Collections, Groups, and Members. Members require some additional + * details to render in the AccessSelectorComponent so their type is defined separately + * and then joined back with the base type. + * + */ +export type AccessItemView = + | SelectItemView & { + /** + * Flag that this group/member can access all items. + * This will disable the permission editor for this item. + */ + accessAllItems?: boolean; + + /** + * Flag that this item cannot be modified. + * This will disable the permission editor and will keep + * the item always selected. + */ + readonly?: boolean; + + /** + * Optional permission that will be rendered for this + * item if it set to readonly. + */ + readonlyPermission?: CollectionPermission; + } & ( + | { + type: AccessItemType.Collection; + viaGroupName?: string; + } + | { + type: AccessItemType.Group; + } + | { + type: AccessItemType.Member; // Members have a few extra details required to display, so they're added here + email: string; + role: OrganizationUserType; + status: OrganizationUserStatusType; + } + ); + +/** + * A type that is emitted as a value for the ngControl + */ +export type AccessItemValue = { + id: string; + permission?: CollectionPermission; + type: AccessItemType; +}; + +/** + * Converts the older SelectionReadOnly interface to one of the new CollectionPermission values + * for the dropdown in the AccessSelectorComponent + * @param value + */ +export const convertToPermission = (value: SelectionReadOnlyResponse) => { + if (value.readOnly) { + return value.hidePasswords ? CollectionPermission.ViewExceptPass : CollectionPermission.View; + } else { + return value.hidePasswords ? CollectionPermission.EditExceptPass : CollectionPermission.Edit; + } +}; + +/** + * Converts an AccessItemValue back into a SelectionReadOnly class using the CollectionPermission + * to determine the values for `readOnly` and `hidePassword` + * @param value + */ +export const convertToSelectionReadOnly = (value: AccessItemValue) => { + return new SelectionReadOnlyRequest( + value.id, + readOnly(value.permission), + hidePassword(value.permission) + ); +}; + +const readOnly = (perm: CollectionPermission) => + [CollectionPermission.View, CollectionPermission.ViewExceptPass].includes(perm); + +const hidePassword = (perm: CollectionPermission) => + [CollectionPermission.ViewExceptPass, CollectionPermission.EditExceptPass].includes(perm); diff --git a/apps/web/src/app/organizations/components/access-selector/access-selector.module.ts b/apps/web/src/app/organizations/components/access-selector/access-selector.module.ts new file mode 100644 index 0000000000..cbb01137b4 --- /dev/null +++ b/apps/web/src/app/organizations/components/access-selector/access-selector.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from "@angular/core"; + +import { SharedModule } from "../../../shared"; + +import { AccessSelectorComponent } from "./access-selector.component"; +import { UserTypePipe } from "./user-type.pipe"; + +@NgModule({ + imports: [SharedModule], + declarations: [AccessSelectorComponent, UserTypePipe], + exports: [AccessSelectorComponent], +}) +export class AccessSelectorModule {} diff --git a/apps/web/src/app/organizations/components/access-selector/access-selector.stories.ts b/apps/web/src/app/organizations/components/access-selector/access-selector.stories.ts new file mode 100644 index 0000000000..059fb1c430 --- /dev/null +++ b/apps/web/src/app/organizations/components/access-selector/access-selector.stories.ts @@ -0,0 +1,302 @@ +import { FormBuilder, FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { action } from "@storybook/addon-actions"; +import { Meta, moduleMetadata, Story } from "@storybook/angular"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { OrganizationUserStatusType } from "@bitwarden/common/enums/organizationUserStatusType"; +import { OrganizationUserType } from "@bitwarden/common/enums/organizationUserType"; +import { + AvatarModule, + BadgeModule, + ButtonModule, + FormFieldModule, + IconButtonModule, + TableModule, + TabsModule, +} from "@bitwarden/components"; + +import { PreloadedEnglishI18nModule } from "../../../tests/preloaded-english-i18n.module"; + +import { AccessSelectorComponent } from "./access-selector.component"; +import { AccessItemType, AccessItemView, CollectionPermission } from "./access-selector.models"; +import { UserTypePipe } from "./user-type.pipe"; + +export default { + title: "Web/Organizations/Access Selector", + decorators: [ + moduleMetadata({ + declarations: [AccessSelectorComponent, UserTypePipe], + imports: [ + ButtonModule, + FormFieldModule, + AvatarModule, + BadgeModule, + ReactiveFormsModule, + FormsModule, + TabsModule, + TableModule, + PreloadedEnglishI18nModule, + JslibModule, + IconButtonModule, + ], + providers: [], + }), + ], + parameters: {}, + argTypes: { + formObj: { table: { disable: true } }, + }, +} as Meta; + +const actionsData = { + onValueChanged: action("onValueChanged"), + onSubmit: action("onSubmit"), +}; + +/** + * Factory to help build semi-realistic looking items + * @param n - The number of items to build + * @param type - Which type to build + */ +const itemsFactory = (n: number, type: AccessItemType) => { + return [...Array(n)].map((_: unknown, id: number) => { + const item: AccessItemView = { + id: id.toString(), + type: type, + } as AccessItemView; + + switch (item.type) { + case AccessItemType.Collection: + item.labelName = item.listName = `Collection ${id}`; + item.id = item.id + "c"; + item.parentGrouping = "Collection Parent Group " + ((id % 2) + 1); + break; + case AccessItemType.Group: + item.labelName = item.listName = `Group ${id}`; + item.id = item.id + "g"; + break; + case AccessItemType.Member: + item.id = item.id + "m"; + item.email = `member${id}@email.com`; + item.status = id % 3 == 0 ? 0 : 2; + item.labelName = item.status == 2 ? `Member ${id}` : item.email; + item.listName = item.status == 2 ? `${item.labelName} (${item.email})` : item.email; + item.role = id % 5; + break; + } + + return item; + }); +}; + +const sampleMembers = itemsFactory(10, AccessItemType.Member); +const sampleGroups = itemsFactory(6, AccessItemType.Group); + +const StandaloneAccessSelectorTemplate: Story = ( + args: AccessSelectorComponent +) => ({ + props: { + items: [], + valueChanged: actionsData.onValueChanged, + initialValue: [], + ...args, + }, + template: ` + +`, +}); + +const memberCollectionAccessItems = itemsFactory(3, AccessItemType.Collection).concat([ + { + id: "c1-group1", + type: AccessItemType.Collection, + labelName: "Collection 1", + listName: "Collection 1", + viaGroupName: "Group 1", + readonlyPermission: CollectionPermission.View, + readonly: true, + }, + { + id: "c1-group2", + type: AccessItemType.Collection, + labelName: "Collection 1", + listName: "Collection 1", + viaGroupName: "Group 2", + readonlyPermission: CollectionPermission.ViewExceptPass, + readonly: true, + }, +]); + +export const MemberCollectionAccess = StandaloneAccessSelectorTemplate.bind({}); +MemberCollectionAccess.args = { + permissionMode: "edit", + showMemberRoles: false, + showGroupColumn: true, + columnHeader: "Collection", + selectorLabelText: "Select Collections", + selectorHelpText: "Some helper text describing what this does", + emptySelectionText: "No collections added", + disabled: false, + initialValue: [], + items: memberCollectionAccessItems, +}; +MemberCollectionAccess.story = { + parameters: { + docs: { + storyDescription: ` + Example of an access selector for modifying the collections a member has access to. + Includes examples of a readonly group and member that cannot be edited. + `, + }, + }, +}; + +export const MemberGroupAccess = StandaloneAccessSelectorTemplate.bind({}); +MemberGroupAccess.args = { + permissionMode: "readonly", + showMemberRoles: false, + columnHeader: "Groups", + selectorLabelText: "Select Groups", + selectorHelpText: "Some helper text describing what this does", + emptySelectionText: "No groups added", + disabled: false, + initialValue: [{ id: "3g" }, { id: "0g" }], + items: itemsFactory(4, AccessItemType.Group).concat([ + { + id: "admin", + type: AccessItemType.Group, + listName: "Admin Group", + labelName: "Admin Group", + accessAllItems: true, + }, + ]), +}; +MemberGroupAccess.story = { + parameters: { + docs: { + storyDescription: ` + Example of an access selector for selecting which groups an individual member belongs too. + `, + }, + }, +}; + +export const GroupMembersAccess = StandaloneAccessSelectorTemplate.bind({}); +GroupMembersAccess.args = { + permissionMode: "hidden", + showMemberRoles: true, + columnHeader: "Members", + selectorLabelText: "Select Members", + selectorHelpText: "Some helper text describing what this does", + emptySelectionText: "No members added", + disabled: false, + initialValue: [{ id: "2m" }, { id: "0m" }], + items: sampleMembers, +}; +GroupMembersAccess.story = { + parameters: { + docs: { + storyDescription: ` + Example of an access selector for selecting which members belong to an specific group. + `, + }, + }, +}; + +export const CollectionAccess = StandaloneAccessSelectorTemplate.bind({}); +CollectionAccess.args = { + permissionMode: "edit", + showMemberRoles: false, + columnHeader: "Groups/Members", + selectorLabelText: "Select groups and members", + selectorHelpText: + "Permissions set for a member will replace permissions set by that member's group", + emptySelectionText: "No members or groups added", + disabled: false, + initialValue: [ + { id: "3g", permission: CollectionPermission.EditExceptPass }, + { id: "0m", permission: CollectionPermission.View }, + ], + items: sampleGroups.concat(sampleMembers).concat([ + { + id: "admin-group", + type: AccessItemType.Group, + listName: "Admin Group", + labelName: "Admin Group", + accessAllItems: true, + readonly: true, + }, + { + id: "admin-member", + type: AccessItemType.Member, + listName: "Admin Member (admin@email.com)", + labelName: "Admin Member", + status: OrganizationUserStatusType.Confirmed, + role: OrganizationUserType.Admin, + email: "admin@email.com", + accessAllItems: true, + readonly: true, + }, + ]), +}; +GroupMembersAccess.story = { + parameters: { + docs: { + storyDescription: ` + Example of an access selector for selecting which members/groups have access to a specific collection. + `, + }, + }, +}; + +const fb = new FormBuilder(); + +const ReactiveFormAccessSelectorTemplate: Story = ( + args: AccessSelectorComponent +) => ({ + props: { + items: [], + onSubmit: actionsData.onSubmit, + ...args, + }, + template: ` +
+ + +
+`, +}); + +export const ReactiveForm = ReactiveFormAccessSelectorTemplate.bind({}); +ReactiveForm.args = { + formObj: fb.group({ formItems: [[{ id: "1g" }]] }), + permissionMode: "edit", + showMemberRoles: false, + columnHeader: "Groups/Members", + selectorLabelText: "Select groups and members", + selectorHelpText: + "Permissions set for a member will replace permissions set by that member's group", + emptySelectionText: "No members or groups added", + items: sampleGroups.concat(sampleMembers), +}; diff --git a/apps/web/src/app/organizations/components/access-selector/index.ts b/apps/web/src/app/organizations/components/access-selector/index.ts new file mode 100644 index 0000000000..86624f8e94 --- /dev/null +++ b/apps/web/src/app/organizations/components/access-selector/index.ts @@ -0,0 +1,3 @@ +export * from "./access-selector.component"; +export * from "./access-selector.module"; +export * from "./access-selector.models"; diff --git a/apps/web/src/app/organizations/components/access-selector/user-type.pipe.ts b/apps/web/src/app/organizations/components/access-selector/user-type.pipe.ts new file mode 100644 index 0000000000..6ef78cb65e --- /dev/null +++ b/apps/web/src/app/organizations/components/access-selector/user-type.pipe.ts @@ -0,0 +1,29 @@ +import { Pipe, PipeTransform } from "@angular/core"; + +import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; +import { OrganizationUserType } from "@bitwarden/common/enums/organizationUserType"; + +@Pipe({ + name: "userType", +}) +export class UserTypePipe implements PipeTransform { + constructor(private i18nService: I18nService) {} + + transform(value?: OrganizationUserType, unknownText?: string): string { + if (value == null) { + return unknownText ?? this.i18nService.t("unknown"); + } + switch (value) { + case OrganizationUserType.Owner: + return this.i18nService.t("owner"); + case OrganizationUserType.Admin: + return this.i18nService.t("admin"); + case OrganizationUserType.User: + return this.i18nService.t("user"); + case OrganizationUserType.Manager: + return this.i18nService.t("manager"); + case OrganizationUserType.Custom: + return this.i18nService.t("custom"); + } + } +} diff --git a/apps/web/src/app/organizations/layouts/organization-layout.component.html b/apps/web/src/app/organizations/layouts/organization-layout.component.html index 7c04938cf3..5f5aa497f7 100644 --- a/apps/web/src/app/organizations/layouts/organization-layout.component.html +++ b/apps/web/src/app/organizations/layouts/organization-layout.component.html @@ -1,49 +1,32 @@ - -
-
- +
+
+
+ + + {{ "vault" | i18n }} + + {{ "manage" | i18n }} + + + {{ getReportTabLabel(organization) | i18n }} + + {{ + "billing" | i18n + }} + {{ + "settings" | i18n + }} +
- +
+ diff --git a/apps/web/src/app/organizations/layouts/organization-layout.component.ts b/apps/web/src/app/organizations/layouts/organization-layout.component.ts index fe9d9de8b1..8c871f74db 100644 --- a/apps/web/src/app/organizations/layouts/organization-layout.component.ts +++ b/apps/web/src/app/organizations/layouts/organization-layout.component.ts @@ -3,11 +3,14 @@ import { ActivatedRoute } from "@angular/router"; import { map, mergeMap, Observable, Subject, takeUntil } from "rxjs"; import { - OrganizationService, - getOrganizationById, + canAccessBillingTab, + canAccessGroupsTab, canAccessManageTab, + canAccessMembersTab, + canAccessReportingTab, canAccessSettingsTab, - canAccessToolsTab, + getOrganizationById, + OrganizationService, } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/models/domain/organization"; @@ -17,7 +20,6 @@ import { Organization } from "@bitwarden/common/models/domain/organization"; }) export class OrganizationLayoutComponent implements OnInit, OnDestroy { organization$: Observable; - businessTokenPromise: Promise; private _destroy = new Subject(); @@ -43,27 +45,43 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy { this._destroy.complete(); } - canShowManageTab(organization: Organization): boolean { - return canAccessManageTab(organization); - } - - canShowToolsTab(organization: Organization): boolean { - return canAccessToolsTab(organization); - } - canShowSettingsTab(organization: Organization): boolean { return canAccessSettingsTab(organization); } - getToolsRoute(organization: Organization): string { - return organization.canAccessImportExport ? "tools/import" : "tools/exposed-passwords-report"; + canShowManageTab(organization: Organization): boolean { + return canAccessManageTab(organization); + } + + canShowMembersTab(organization: Organization): boolean { + return canAccessMembersTab(organization); + } + + canShowGroupsTab(organization: Organization): boolean { + return canAccessGroupsTab(organization); + } + + canShowReportsTab(organization: Organization): boolean { + return canAccessReportingTab(organization); + } + + canShowBillingTab(organization: Organization): boolean { + return canAccessBillingTab(organization); + } + + getReportTabLabel(organization: Organization): string { + return organization.useEvents ? "reporting" : "reports"; + } + + getReportRoute(organization: Organization): string { + return organization.useEvents ? "reporting/events" : "reporting/reports"; } getManageRoute(organization: Organization): string { let route: string; switch (true) { case organization.canManageUsers: - route = "manage/people"; + route = "manage/members"; break; case organization.canViewAssignedCollections || organization.canViewAllCollections: route = "manage/collections"; @@ -71,18 +89,6 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy { case organization.canManageGroups: route = "manage/groups"; break; - case organization.canManagePolicies: - route = "manage/policies"; - break; - case organization.canManageSso: - route = "manage/sso"; - break; - case organization.canManageScim: - route = "manage/scim"; - break; - case organization.canAccessEventLogs: - route = "manage/events"; - break; } return route; } diff --git a/apps/web/src/app/organizations/manage/events.component.html b/apps/web/src/app/organizations/manage/events.component.html index d27b53e8d9..a7468bcf37 100644 --- a/apps/web/src/app/organizations/manage/events.component.html +++ b/apps/web/src/app/organizations/manage/events.component.html @@ -1,54 +1,57 @@ -