1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-25 12:15:18 +01:00

[SM-1302] Initial config page (#10196)

* Initial config page

* Remove project actions

* Add copy projectId method to the project page

* Update bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/config/config.component.ts

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Update bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.ts

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Update apps/web/src/locales/en/messages.json

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Fix method and  string naming

* Ensure config component load logic happens after params observed

* Remove projectId emitted event

* Update bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/config/config.component.ts

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Adjust load function

* Fix config translation

* Remove unnecceary async from copy functions

* Add project ID translation key

* Update bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/config/config.component.ts

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Simplify load function

* Simplify variable definition

* Add all machine account projects to the config page

* Update bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/config/config.component.ts

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Remove unused variable

* Remove revision date in config project list

---------

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>
Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>
This commit is contained in:
Robyn MacCallum 2024-09-20 12:54:03 -04:00 committed by GitHub
parent ea025b9026
commit cf1f7cc61d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 249 additions and 7 deletions

View File

@ -9002,6 +9002,24 @@
"purchasedSeatsRemoved": { "purchasedSeatsRemoved": {
"message": "purchased seats removed" "message": "purchased seats removed"
}, },
"environmentVariables": {
"message": "Environment variables"
},
"organizationId": {
"message": "Organization ID"
},
"projectIds": {
"message": "Project IDs"
},
"projectId": {
"message": "Project ID"
},
"projectsAccessedByMachineAccount": {
"message": "The following projects can be accessed by this machine account."
},
"config": {
"message": "Config"
},
"learnMoreAboutEmergencyAccess": { "learnMoreAboutEmergencyAccess": {
"message":"Learn more about emergency access" "message":"Learn more about emergency access"
}, },

View File

@ -6,4 +6,5 @@ export class ProjectListView {
revisionDate: string; revisionDate: string;
read: boolean; read: boolean;
write: boolean; write: boolean;
linkable: boolean;
} }

View File

@ -131,6 +131,7 @@ export class ProjectService {
); );
projectListView.creationDate = s.creationDate; projectListView.creationDate = s.creationDate;
projectListView.revisionDate = s.revisionDate; projectListView.revisionDate = s.revisionDate;
projectListView.linkable = true;
return projectListView; return projectListView;
}), }),
); );

View File

@ -0,0 +1,47 @@
<div *ngIf="!loading">
<div class="tw-p-6 tw-border tw-border-solid tw-border-secondary-600 tw-rounded">
<h2 bitTypography="h2">{{ "environmentVariables" | i18n }}</h2>
<div class="tw-flex tw-gap-6 tw-pt-4">
<bit-form-field class="tw-w-2/5 tw-min-w-80" tw>
<bit-label>{{ "identityUrl" | i18n }}</bit-label>
<input bitInput type="text" [(ngModel)]="identityUrl" [disabled]="true" />
<button
bitSuffix
type="button"
type="button"
bitIconButton="bwi-clone"
[bitAction]="copyIdentityUrl"
></button>
</bit-form-field>
<bit-form-field class="tw-w-2/5 tw-min-w-80">
<bit-label>{{ "apiUrl" | i18n }}</bit-label>
<input bitInput type="text" [(ngModel)]="apiUrl" [disabled]="true" />
<button bitSuffix type="button" bitIconButton="bwi-clone" [bitAction]="copyApiUrl"></button>
</bit-form-field>
</div>
<bit-form-field class="tw-w-2/5 tw-min-w-80">
<bit-label>{{ "organizationId" | i18n }}</bit-label>
<input bitInput type="text" [(ngModel)]="organizationId" [disabled]="true" />
<button
bitSuffix
type="button"
bitIconButton="bwi-clone"
[bitAction]="copyOrganizationId"
></button>
</bit-form-field>
</div>
<div class="tw-pt-12">
<h2 slot="summary" class="tw-mb-0" bitTypography="h2" noMargin>{{ "projectIds" | i18n }}</h2>
<p *ngIf="!hasProjects" class="tw-mt-6">{{ "projectsNoItemsTitle" | i18n }}</p>
<p *ngIf="hasProjects" class="tw-mt-4">{{ "projectsAccessedByMachineAccount" | i18n }}</p>
<sm-projects-list
class="tw-mt-8"
*ngIf="hasProjects"
[showMenus]="false"
[projects]="projects"
></sm-projects-list>
</div>
</div>
<div *ngIf="loading" class="tw-items-center tw-justify-center tw-pt-64 tw-text-center">
<i class="bwi bwi-spinner bwi-spin bwi-3x"></i>
</div>

View File

@ -0,0 +1,127 @@
import { Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute, Params } from "@angular/router";
import { Subject, concatMap, takeUntil } from "rxjs";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ToastService } from "@bitwarden/components";
import { ProjectListView } from "../../models/view/project-list.view";
import { ProjectService } from "../../projects/project.service";
import { AccessPolicyService } from "../../shared/access-policies/access-policy.service";
class ServiceAccountConfig {
organizationId: string;
serviceAccountId: string;
identityUrl: string;
apiUrl: string;
projects: ProjectListView[];
}
@Component({
selector: "sm-service-account-config",
templateUrl: "./config.component.html",
})
export class ServiceAccountConfigComponent implements OnInit, OnDestroy {
identityUrl: string;
apiUrl: string;
organizationId: string;
serviceAccountId: string;
projects: ProjectListView[];
hasProjects = false;
private destroy$ = new Subject<void>();
loading = true;
constructor(
private environmentService: EnvironmentService,
private route: ActivatedRoute,
private platformUtilsService: PlatformUtilsService,
private toastService: ToastService,
private i18nService: I18nService,
private projectService: ProjectService,
private accessPolicyService: AccessPolicyService,
) {}
async ngOnInit() {
this.route.params
.pipe(
concatMap(async (params: Params) => {
return await this.load(params.organizationId, params.serviceAccountId);
}),
takeUntil(this.destroy$),
)
.subscribe((smConfig) => {
this.identityUrl = smConfig.identityUrl;
this.apiUrl = smConfig.apiUrl;
this.organizationId = smConfig.organizationId;
this.serviceAccountId = smConfig.serviceAccountId;
this.projects = smConfig.projects;
this.hasProjects = smConfig.projects.length > 0;
this.loading = false;
});
}
async load(organizationId: string, serviceAccountId: string): Promise<ServiceAccountConfig> {
const environment = await this.environmentService.getEnvironment();
const allProjects = await this.projectService.getProjects(organizationId);
const policies = await this.accessPolicyService.getServiceAccountGrantedPolicies(
organizationId,
serviceAccountId,
);
const projects = policies.grantedProjectPolicies.map((policy) => {
return {
id: policy.accessPolicy.grantedProjectId,
name: policy.accessPolicy.grantedProjectName,
organizationId: organizationId,
linkable: allProjects.some(
(project) => project.id === policy.accessPolicy.grantedProjectId,
),
} as ProjectListView;
});
return {
organizationId: organizationId,
serviceAccountId: serviceAccountId,
identityUrl: environment.getIdentityUrl(),
apiUrl: environment.getApiUrl(),
projects: projects,
} as ServiceAccountConfig;
}
copyIdentityUrl = () => {
this.platformUtilsService.copyToClipboard(this.identityUrl);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("valueCopied", this.i18nService.t("identityUrl")),
});
};
copyApiUrl = () => {
this.platformUtilsService.copyToClipboard(this.apiUrl);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("valueCopied", this.i18nService.t("apiUrl")),
});
};
copyOrganizationId = () => {
this.platformUtilsService.copyToClipboard(this.organizationId);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("valueCopied", this.i18nService.t("organizationId")),
});
};
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}

View File

@ -29,6 +29,7 @@
</div> </div>
</bit-tab-link> </bit-tab-link>
<bit-tab-link [route]="['events']">{{ "eventLogs" | i18n }}</bit-tab-link> <bit-tab-link [route]="['events']">{{ "eventLogs" | i18n }}</bit-tab-link>
<bit-tab-link [route]="['config']">{{ "config" | i18n }}</bit-tab-link>
</bit-tab-nav-bar> </bit-tab-nav-bar>
<button <button
type="button" type="button"

View File

@ -2,6 +2,7 @@ import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router"; import { RouterModule, Routes } from "@angular/router";
import { AccessTokenComponent } from "./access/access-tokens.component"; import { AccessTokenComponent } from "./access/access-tokens.component";
import { ServiceAccountConfigComponent } from "./config/config.component";
import { ServiceAccountEventsComponent } from "./event-logs/service-accounts-events.component"; import { ServiceAccountEventsComponent } from "./event-logs/service-accounts-events.component";
import { serviceAccountAccessGuard } from "./guards/service-account-access.guard"; import { serviceAccountAccessGuard } from "./guards/service-account-access.guard";
import { ServiceAccountPeopleComponent } from "./people/service-account-people.component"; import { ServiceAccountPeopleComponent } from "./people/service-account-people.component";
@ -40,6 +41,10 @@ const routes: Routes = [
path: "events", path: "events",
component: ServiceAccountEventsComponent, component: ServiceAccountEventsComponent,
}, },
{
path: "config",
component: ServiceAccountConfigComponent,
},
], ],
}, },
]; ];

View File

@ -9,6 +9,7 @@ import { AccessTokenComponent } from "./access/access-tokens.component";
import { AccessTokenCreateDialogComponent } from "./access/dialogs/access-token-create-dialog.component"; import { AccessTokenCreateDialogComponent } from "./access/dialogs/access-token-create-dialog.component";
import { AccessTokenDialogComponent } from "./access/dialogs/access-token-dialog.component"; import { AccessTokenDialogComponent } from "./access/dialogs/access-token-dialog.component";
import { ExpirationOptionsComponent } from "./access/dialogs/expiration-options.component"; import { ExpirationOptionsComponent } from "./access/dialogs/expiration-options.component";
import { ServiceAccountConfigComponent } from "./config/config.component";
import { ServiceAccountDeleteDialogComponent } from "./dialog/service-account-delete-dialog.component"; import { ServiceAccountDeleteDialogComponent } from "./dialog/service-account-delete-dialog.component";
import { ServiceAccountDialogComponent } from "./dialog/service-account-dialog.component"; import { ServiceAccountDialogComponent } from "./dialog/service-account-dialog.component";
import { ServiceAccountEventsComponent } from "./event-logs/service-accounts-events.component"; import { ServiceAccountEventsComponent } from "./event-logs/service-accounts-events.component";
@ -28,6 +29,7 @@ import { ServiceAccountsComponent } from "./service-accounts.component";
AccessTokenDialogComponent, AccessTokenDialogComponent,
ExpirationOptionsComponent, ExpirationOptionsComponent,
ServiceAccountComponent, ServiceAccountComponent,
ServiceAccountConfigComponent,
ServiceAccountDeleteDialogComponent, ServiceAccountDeleteDialogComponent,
ServiceAccountDialogComponent, ServiceAccountDialogComponent,
ServiceAccountEventsComponent, ServiceAccountEventsComponent,

View File

@ -20,7 +20,7 @@
<bit-table *ngIf="projects?.length >= 1" [dataSource]="dataSource"> <bit-table *ngIf="projects?.length >= 1" [dataSource]="dataSource">
<ng-container header> <ng-container header>
<tr> <tr>
<th bitCell class="tw-w-0"> <th bitCell class="tw-w-0" *ngIf="showMenus">
<label class="!tw-mb-0 tw-flex tw-w-fit tw-gap-2 !tw-font-bold !tw-text-muted"> <label class="!tw-mb-0 tw-flex tw-w-fit tw-gap-2 !tw-font-bold !tw-text-muted">
<input <input
type="checkbox" type="checkbox"
@ -32,7 +32,7 @@
</label> </label>
</th> </th>
<th bitCell bitSortable="name" default>{{ "name" | i18n }}</th> <th bitCell bitSortable="name" default>{{ "name" | i18n }}</th>
<th bitCell bitSortable="revisionDate">{{ "lastEdited" | i18n }}</th> <th bitCell bitSortable="revisionDate" *ngIf="showMenus">{{ "lastEdited" | i18n }}</th>
<th <th
bitCell bitCell
class="tw-w-0" class="tw-w-0"
@ -45,13 +45,14 @@
[bitMenuTriggerFor]="tableMenu" [bitMenuTriggerFor]="tableMenu"
[title]="'options' | i18n" [title]="'options' | i18n"
[attr.aria-label]="'options' | i18n" [attr.aria-label]="'options' | i18n"
*ngIf="showMenus"
></button> ></button>
</th> </th>
</tr> </tr>
</ng-container> </ng-container>
<ng-template body let-rows$> <ng-template body let-rows$>
<tr bitRow *ngFor="let project of rows$ | async"> <tr bitRow *ngFor="let project of rows$ | async">
<td bitCell> <td bitCell *ngIf="showMenus">
<input <input
type="checkbox" type="checkbox"
(change)="$event ? selection.toggle(project.id) : null" (change)="$event ? selection.toggle(project.id) : null"
@ -61,12 +62,32 @@
<td bitCell> <td bitCell>
<div class="tw-flex tw-items-center tw-gap-4 tw-break-all"> <div class="tw-flex tw-items-center tw-gap-4 tw-break-all">
<i class="bwi bwi-collection tw-text-muted" aria-hidden="true"></i> <i class="bwi bwi-collection tw-text-muted" aria-hidden="true"></i>
<a bitLink [routerLink]="['/sm', project.organizationId, 'projects', project.id]">{{ <div>
project.name <a
}}</a> *ngIf="project.linkable"
bitLink
[routerLink]="['/sm', project.organizationId, 'projects', project.id]"
>{{ project.name }}</a
>
<span *ngIf="!project.linkable">{{ project.name }}</span>
<div class="tw-text-sm tw-text-muted tw-block">
{{ project.id }}
<button
type="button"
bitIconButton="bwi-clone"
buttonType="main"
size="small"
[title]="'copyUuid' | i18n"
[attr.aria-label]="'copyUuid' | i18n"
(click)="copyProjectUuidToClipboard(project.id)"
></button>
</div>
</div>
</div> </div>
</td> </td>
<td bitCell class="tw-whitespace-nowrap">{{ project.revisionDate | date: "medium" }}</td> <td bitCell class="tw-whitespace-nowrap" *ngIf="showMenus">
{{ project.revisionDate | date: "medium" }}
</td>
<td bitCell> <td bitCell>
<button <button
type="button" type="button"
@ -75,6 +96,7 @@
[bitMenuTriggerFor]="projectMenu" [bitMenuTriggerFor]="projectMenu"
[title]="'options' | i18n" [title]="'options' | i18n"
[attr.aria-label]="'options' | i18n" [attr.aria-label]="'options' | i18n"
*ngIf="showMenus"
></button> ></button>
</td> </td>
<bit-menu #projectMenu> <bit-menu #projectMenu>

View File

@ -24,6 +24,8 @@ export class ProjectsListComponent {
} }
private _projects: ProjectListView[]; private _projects: ProjectListView[];
@Input() showMenus?: boolean = true;
@Input() @Input()
set search(search: string) { set search(search: string) {
this.selection.clear(); this.selection.clear();
@ -33,6 +35,7 @@ export class ProjectsListComponent {
@Output() editProjectEvent = new EventEmitter<string>(); @Output() editProjectEvent = new EventEmitter<string>();
@Output() deleteProjectEvent = new EventEmitter<ProjectListView[]>(); @Output() deleteProjectEvent = new EventEmitter<ProjectListView[]>();
@Output() newProjectEvent = new EventEmitter(); @Output() newProjectEvent = new EventEmitter();
@Output() copiedProjectUUIdEvent = new EventEmitter<string>();
selection = new SelectionModel<string>(true, []); selection = new SelectionModel<string>(true, []);
protected dataSource = new TableDataSource<ProjectListView>(); protected dataSource = new TableDataSource<ProjectListView>();
@ -90,4 +93,13 @@ export class ProjectsListComponent {
} }
return false; return false;
} }
copyProjectUuidToClipboard(id: string) {
this.platformUtilsService.copyToClipboard(id);
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("valueCopied", this.i18nService.t("projectId")),
);
}
} }

View File

@ -1,10 +1,12 @@
import { NgModule } from "@angular/core"; import { NgModule } from "@angular/core";
import { import {
CardComponent,
MultiSelectModule, MultiSelectModule,
SearchModule, SearchModule,
SelectModule, SelectModule,
NoItemsModule, NoItemsModule,
FormFieldModule,
} from "@bitwarden/components"; } from "@bitwarden/components";
import { CoreOrganizationModule } from "@bitwarden/web-vault/app/admin-console/organizations/core"; import { CoreOrganizationModule } from "@bitwarden/web-vault/app/admin-console/organizations/core";
import { DynamicAvatarComponent } from "@bitwarden/web-vault/app/components/dynamic-avatar.component"; import { DynamicAvatarComponent } from "@bitwarden/web-vault/app/components/dynamic-avatar.component";
@ -31,17 +33,21 @@ import { SecretsListComponent } from "./secrets-list.component";
DynamicAvatarComponent, DynamicAvatarComponent,
SearchModule, SearchModule,
HeaderModule, HeaderModule,
CardComponent,
FormFieldModule,
], ],
exports: [ exports: [
AccessPolicySelectorComponent, AccessPolicySelectorComponent,
BulkConfirmationDialogComponent, BulkConfirmationDialogComponent,
BulkStatusDialogComponent, BulkStatusDialogComponent,
FormFieldModule,
HeaderModule, HeaderModule,
NewMenuComponent, NewMenuComponent,
NoItemsModule, NoItemsModule,
ProjectsListComponent, ProjectsListComponent,
SearchModule, SearchModule,
SecretsListComponent, SecretsListComponent,
CardComponent,
SelectModule, SelectModule,
SharedModule, SharedModule,
], ],