mirror of
https://github.com/bitwarden/browser
synced 2026-02-26 09:33:22 +00:00
[PM-32447] Integration card after edits (#19203)
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 28.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 540 540" style="enable-background:new 0 0 540 540;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#01C5D1;}
|
||||
</style>
|
||||
<g id="Logo">
|
||||
<path class="st0" d="M351.4,486.7v-14.3h22.8v-10h-22.1c-3.3,10.8-7.5,21.3-12.8,31.3v3.6h38.9v-10.6H351.4z"/>
|
||||
<path class="st0" d="M378.1,437.7h-21.9c-0.1,3.5-0.4,7.1-1,10.5h23L378.1,437.7z"/>
|
||||
<path class="st0" d="M67.3,497.2v-59.5h12.1v24.5h21.8v-24.5h12.1v59.5h-12.1v-24.5H79.4v24.5H67.3z"/>
|
||||
<path class="st0" d="M158.2,437.7h12.1v40c0,6.5-2,11.6-6.2,15.3s-9.7,5.5-16.8,5.5s-12.7-1.9-16.8-5.5s-6.2-8.8-6.2-15.3v-40h12.1
|
||||
v40.5c0,5.2,3.7,9.3,11,9.3s10.8-4.1,10.8-9.3L158.2,437.7z"/>
|
||||
<path class="st0" d="M181.4,497.2v-59.5h11.1l22.8,37.1v-37.1h12.1v59.5h-11l-22.8-37.1v37.1L181.4,497.2z"/>
|
||||
<path class="st0" d="M250.1,497.2v-49h-16.8v-10.5h45.5v10.5h-16.7v49L250.1,497.2z"/>
|
||||
<path class="st0" d="M284.8,497.2v-59.5h25.8c12,0,18.5,6.9,18.5,18.5c0,8.2-3.7,14.2-9.8,17.2l11,19.8v4h-11.8l-12.4-22.4h-9.2
|
||||
v22.4L284.8,497.2z M296.9,464.6h11.5c5.5,0,8.3-2.7,8.3-8.2s-2.8-8.2-8.3-8.2h-11.5V464.6z"/>
|
||||
<path class="st0" d="M389.5,493.1c-4.3-3.6-6.5-8.7-6.5-15.2h12.1c0,3.5,1.2,6,3.6,7.4c2.5,1.5,5.4,2.2,8.3,2.1
|
||||
c6.2,0,9.8-2.6,9.8-6.5c0-1.8-0.9-3.2-2.9-4.3c-1.1-0.7-2.2-1.2-3.4-1.6c-0.7-0.4-1.4-0.6-2.1-0.9c-1.1-0.4-4-1.5-5.4-2l-2.5-0.9
|
||||
c-4.7-1.8-7.2-3-10.7-5.6c-3.4-2.7-5.4-6.9-5.2-11.2c0-10.9,8.4-17.9,22-17.9s22.5,6.3,22.5,20h-12.1c0-6.4-4.4-9-10.5-9
|
||||
s-9.7,2.4-9.7,6.2c-0.1,1.7,0.8,3.4,2.3,4.2c0.8,0.5,1.7,1,2.6,1.4c0.7,0.3,1.2,0.5,1.5,0.6l3.6,1.3l2.4,0.9c2.3,0.8,4,1.5,5.3,2
|
||||
l4.7,2c1.5,0.7,3,1.5,4.3,2.5c3.3,2.5,6.5,6.5,6.1,11.8c0,10.8-8.3,18.1-23.2,18.1C399.6,498.5,394,496.7,389.5,493.1z"/>
|
||||
<path class="st0" d="M441.7,493.1c-4.3-3.6-6.5-8.7-6.5-15.2h12.1c0,3.5,1.2,6,3.6,7.4c2.5,1.5,5.4,2.2,8.3,2.1
|
||||
c6.2,0,9.8-2.6,9.8-6.5c0-1.8-0.9-3.2-2.9-4.3c-1.1-0.7-2.2-1.2-3.4-1.6c-0.7-0.4-1.4-0.6-2.1-0.9c-1.1-0.4-4-1.5-5.4-2l-2.5-0.9
|
||||
c-4.7-1.8-7.2-3-10.7-5.6c-3.4-2.7-5.4-6.9-5.2-11.2c0-10.9,8.4-17.9,22-17.9s22.4,6.4,22.4,20.1H469c0-6.4-4.4-9-10.5-9
|
||||
s-9.7,2.4-9.7,6.2c-0.1,1.7,0.8,3.4,2.3,4.2c0.8,0.5,1.7,1,2.6,1.4c0.7,0.3,1.2,0.5,1.5,0.6l3.6,1.3l2.4,0.9c2.3,0.8,4,1.5,5.3,2
|
||||
l4.7,2c1.6,0.7,3,1.5,4.3,2.5c3.3,2.5,6.5,6.5,6.1,11.8c0,10.8-8.3,18.1-23.2,18.1C451.7,498.5,446.1,496.7,441.7,493.1z"/>
|
||||
<path class="st0" d="M111.4,251c12.1-8.8,29.3-15.3,29.3-15.3s-9.9-5.2-25.2-12.4C113.7,228.1,111.4,251,111.4,251z"/>
|
||||
<path class="st0" d="M403.7,241.4c-7.3,0.7-14.7,0.6-22-0.3c-4.3-0.4-8.6-1.1-12.7-2.3c-16.5-4-24.7-12.3-24.7-12.3
|
||||
c33.2,8.4,48.1,8.5,52.9,8.2l1.9-0.2c-2-12.1-9.4-37-35-73.5c-0.1,9.6-1.4,19.2-3.8,28.5c-14.4-35.4-25.9-43.5-37.7-53.3
|
||||
c4.6,14.2,4.8,28.6,1.8,42.6c-10.2-36.3-46.4-53.5-46.4-53.5s38,63,4.5,110.3c40.9-126.9-124-109.1-101.7-217.6
|
||||
c-47.2,75.5,2.9,120.3,23.6,134.6c-49.2-31.2-43.4-41.2-96.4-45.5c21.5,76.4,74.9,85,123.3,94.3c-58.1-2.6-86-32.2-131.8-10.5
|
||||
c34.8,58.6,120.4,47.1,140.1,41.2c-52.7,20.8-77-16.2-135.3,37c71,22.9,136.4-10.1,136.4-10.1c-25.9,18.7-76,16.4-90.7,57.6
|
||||
c31.8,3,69.9-17.7,95.2-39.5c-28,31.6-74,34.1-69.9,67.4c56.1-8.5,79-55.3,80.5-57.9c-23.1,47.2-49,40.2-46.5,73.5
|
||||
c28.4,0,57-30.6,68.8-55.6c-4.2-15.2-2.9-31.3,3.8-45.6c0.3,35.6,16.4,69.3,44,91.9c-19.2-4.7-35.2-17.8-43.7-35.6v-0.1
|
||||
c0,0.2-0.1,0.4-0.2,0.6c-8.5,26.3-37.5,48.3-59.8,58c26,3.4,121.9,34.5,118.1,107.3c15.4-35.7,13.3-62.8,4.2-82.9
|
||||
c-10.4-23.2-7.9-50.1,6.6-70.9c-27.1-34.4-31-67.5-22.5-92c14.9,27,52.2,28.6,64.8,26.1c0,0-5.6,41.8,27.1,64.8
|
||||
C406.9,306.3,399.8,274.8,403.7,241.4z"/>
|
||||
<path class="st0" d="M407.1,161.7C366,107.4,295.7,84,230.3,102.8c7.7,4.8,20.6,12.8,29.3,18.3c3.1-0.3,6.1-0.6,9.2-0.8
|
||||
c-2-5-5.7-11.3-7.1-12.7c0,0,13.7,6.8,25.1,13c8.9,0.7,17.6,2.2,26.2,4.6c-0.9-4-2.3-7.9-4-11.6c6.6,5.1,12.9,10.6,18.8,16.5
|
||||
c11,4.4,21.4,10.1,31,17.1l-1-14.3c0,0,10.1,14.4,16.6,27.5c20.9,20.5,34.9,46.9,40.1,75.6l0,0c1.5,8.4,2.2,16.9,2.2,25.4
|
||||
c0,5.9,0.5,11.9,1.5,17.7c2.3,12.9,6,27,12,39.4C449.8,265.7,441.1,206.5,407.1,161.7z"/>
|
||||
<path class="st0" d="M135.5,168.4c-1.9,2.9-3.7,5.9-5.5,9c5.1,0.3,11.4,0.8,17.9,1.3C143,174.8,138.3,170.9,135.5,168.4z"/>
|
||||
<path class="st0" d="M273.5,402c-67.5-1.1-124.7-49.8-136.5-116.2c-8.4-0.5-16.7-1.9-24.7-4.3c11.3,90.3,93.6,154.3,183.9,143
|
||||
c3.1-0.4,6.3-0.9,9.4-1.4C300.9,417.8,291.6,410.8,273.5,402z"/>
|
||||
<path class="st0" d="M404.5,331.4c-13.3-9.1-21.5-25.8-23.3-39.8c3.7,0,7.1-0.9,9.4-3.2c-2.7,1.1-5.6,1.6-8.5,1.5
|
||||
c3.2-0.3,6.8-1.8,7.6-6.6c-1.4,2.7-3.9,4.5-6.9,5c3.8-1.8,4.4-6.7,4.4-6.7c-1.9,3.8-4.4,5.1-6.4,5.6c-0.1-5.8,1-11,3.5-14.6
|
||||
c-15.2-2.3-33.7-2.2-49-13.8c-0.2,15.8,4.9,37.1,24.2,61.8l4.9,6.2l-4.6,6.4c-1.5,2.1-2.8,4.3-4,6.5c-8.8,16.8-9.4,36.7-1.6,54
|
||||
c5,11.2,7.7,23.3,8,35.6l0.2,0.1c0,0.1,0,0.2,0,0.4l0.7,0.1c0,0,16.2-6.1,15-17.6c-1.1-11-1.8-26.4,10.7-27.4
|
||||
c0.3,0,0.6-0.2,0.9-0.4c0,0,5.1-4.1-4.5-11.8c-1.1-0.3-2.2-0.7-3.3-1.1c3.7,0.6,7.1-0.6,10.9-2.4c4.3-2.4,8.2-5.9,4.6-9.8l0.1-0.1
|
||||
c-2.9-2.4-5.5-5.2-7.6-8.3c-0.9-1.4-0.5-3.2,0.9-4.1c0.4-0.3,0.9-0.4,1.3-0.5c4.1-0.4,9.1-3.3,12.7-13.1
|
||||
C405.4,332.7,405.2,331.9,404.5,331.4z"/>
|
||||
</g>
|
||||
<path class="st0" d="M484.5,442.8v-4.1h2.1c0.2,0,0.3,0,0.5,0.1c0.2,0.1,0.4,0.2,0.5,0.4c0.1,0.2,0.2,0.4,0.2,0.7
|
||||
c0,0.3-0.1,0.6-0.2,0.8c-0.1,0.2-0.3,0.4-0.5,0.5c-0.2,0.1-0.4,0.2-0.6,0.2H485v-0.7h1.2c0.1,0,0.3-0.1,0.4-0.2
|
||||
c0.1-0.1,0.2-0.3,0.2-0.5c0-0.3-0.1-0.4-0.2-0.5c-0.1-0.1-0.3-0.1-0.4-0.1h-0.9v3.4H484.5z M486.9,440.9l1,1.9h-1l-1-1.9H486.9z
|
||||
M486,445.3c-0.6,0-1.2-0.1-1.7-0.3c-0.5-0.2-1-0.5-1.4-1c-0.4-0.4-0.7-0.9-1-1.4c-0.2-0.5-0.3-1.1-0.3-1.7c0-0.6,0.1-1.2,0.3-1.7
|
||||
c0.2-0.5,0.5-1,1-1.4c0.4-0.4,0.9-0.7,1.4-1c0.5-0.2,1.1-0.3,1.7-0.3c0.6,0,1.2,0.1,1.7,0.3c0.5,0.2,1,0.5,1.4,1
|
||||
c0.4,0.4,0.7,0.9,1,1.4c0.2,0.5,0.3,1.1,0.3,1.7c0,0.6-0.1,1.2-0.3,1.7c-0.2,0.5-0.5,1-1,1.4s-0.9,0.7-1.4,1
|
||||
C487.2,445.2,486.6,445.3,486,445.3z M486,444.2c0.6,0,1.2-0.2,1.7-0.5c0.5-0.3,0.9-0.7,1.2-1.2c0.3-0.5,0.5-1.1,0.5-1.7
|
||||
c0-0.6-0.2-1.2-0.5-1.7c-0.3-0.5-0.7-0.9-1.2-1.2c-0.5-0.3-1.1-0.5-1.7-0.5c-0.6,0-1.2,0.2-1.7,0.5c-0.5,0.3-0.9,0.7-1.2,1.2
|
||||
s-0.5,1.1-0.5,1.7c0,0.6,0.2,1.2,0.5,1.7c0.3,0.5,0.7,0.9,1.2,1.2S485.4,444.2,486,444.2z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.1 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.1 KiB |
@@ -10624,6 +10624,15 @@
|
||||
"huntressEventIntegrationDesc": {
|
||||
"message": "Send event data to your Huntress SIEM instance"
|
||||
},
|
||||
"integrationConnectedSuccessfully":{
|
||||
"message": "$INTEGRATION$ connected successfully.",
|
||||
"placeholders": {
|
||||
"integration": {
|
||||
"content": "$1",
|
||||
"example": "Crowdstrike"
|
||||
}
|
||||
}
|
||||
},
|
||||
"failedToSaveIntegration": {
|
||||
"message": "Failed to save integration. Please try again later."
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "@bitwarden/common/types/guid";
|
||||
|
||||
import { OrgIntegrationBuilder } from "../models/integration-builder";
|
||||
import { OrganizationIntegration } from "../models/organization-integration";
|
||||
import { OrganizationIntegrationConfigurationRequest } from "../models/organization-integration-configuration-request";
|
||||
import { OrganizationIntegrationConfigurationResponse } from "../models/organization-integration-configuration-response";
|
||||
import { OrganizationIntegrationRequest } from "../models/organization-integration-request";
|
||||
@@ -207,7 +208,11 @@ describe("OrganizationIntegrationService", () => {
|
||||
|
||||
const result = await service.save(orgId, OrganizationIntegrationType.Hec, config, template);
|
||||
|
||||
expect(result).toEqual({ mustBeOwner: false, success: true });
|
||||
expect(result).toEqual({
|
||||
mustBeOwner: false,
|
||||
success: true,
|
||||
organizationIntegrationResult: expect.any(OrganizationIntegration),
|
||||
});
|
||||
expect(integrationApiService.createOrganizationIntegration).toHaveBeenCalledWith(
|
||||
orgId,
|
||||
expect.any(OrganizationIntegrationRequest),
|
||||
@@ -325,7 +330,11 @@ describe("OrganizationIntegrationService", () => {
|
||||
template,
|
||||
);
|
||||
|
||||
expect(result).toEqual({ mustBeOwner: false, success: true });
|
||||
expect(result).toEqual({
|
||||
mustBeOwner: false,
|
||||
success: true,
|
||||
organizationIntegrationResult: expect.any(OrganizationIntegration),
|
||||
});
|
||||
expect(integrationApiService.updateOrganizationIntegration).toHaveBeenCalledWith(
|
||||
orgId,
|
||||
integrationId,
|
||||
@@ -375,7 +384,11 @@ describe("OrganizationIntegrationService", () => {
|
||||
template,
|
||||
);
|
||||
|
||||
expect(result).toEqual({ mustBeOwner: true, success: false });
|
||||
expect(result).toEqual({
|
||||
mustBeOwner: true,
|
||||
success: false,
|
||||
organizationIntegrationResult: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("should rethrow non-404 errors", async () => {
|
||||
|
||||
@@ -29,6 +29,7 @@ import { OrganizationIntegrationConfigurationApiService } from "./organization-i
|
||||
export type IntegrationModificationResult = {
|
||||
mustBeOwner: boolean;
|
||||
success: boolean;
|
||||
organizationIntegrationResult?: OrganizationIntegration | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -113,10 +114,14 @@ export class OrganizationIntegrationService {
|
||||
if (newIntegration !== null) {
|
||||
this._integrations$.next([...this._integrations$.getValue(), newIntegration]);
|
||||
}
|
||||
return { mustBeOwner: false, success: true };
|
||||
return {
|
||||
mustBeOwner: false,
|
||||
success: newIntegration !== null,
|
||||
organizationIntegrationResult: newIntegration ?? undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof ErrorResponse && error.statusCode === 404) {
|
||||
return { mustBeOwner: true, success: false };
|
||||
return { mustBeOwner: true, success: false, organizationIntegrationResult: undefined };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
@@ -178,10 +183,14 @@ export class OrganizationIntegrationService {
|
||||
}
|
||||
this._integrations$.next([...integrations]);
|
||||
}
|
||||
return { mustBeOwner: false, success: true };
|
||||
return {
|
||||
mustBeOwner: false,
|
||||
success: updatedIntegration !== null,
|
||||
organizationIntegrationResult: updatedIntegration ?? undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof ErrorResponse && error.statusCode === 404) {
|
||||
return { mustBeOwner: true, success: false };
|
||||
return { mustBeOwner: true, success: false, organizationIntegrationResult: undefined };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
@@ -221,10 +230,10 @@ export class OrganizationIntegrationService {
|
||||
.filter((i) => i.id !== integrationId);
|
||||
this._integrations$.next(updatedIntegrations);
|
||||
|
||||
return { mustBeOwner: false, success: true };
|
||||
return { mustBeOwner: false, success: true, organizationIntegrationResult: undefined };
|
||||
} catch (error) {
|
||||
if (error instanceof ErrorResponse && error.statusCode === 404) {
|
||||
return { mustBeOwner: true, success: false };
|
||||
return { mustBeOwner: true, success: false, organizationIntegrationResult: undefined };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Pipe, PipeTransform } from "@angular/core";
|
||||
|
||||
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
|
||||
import { IntegrationType } from "@bitwarden/common/enums";
|
||||
|
||||
import { Integration } from "../models/integration";
|
||||
|
||||
@Pipe({
|
||||
name: "filterIntegrations",
|
||||
standalone: true,
|
||||
})
|
||||
export class FilterIntegrationsPipe implements PipeTransform {
|
||||
transform(integrations: Integration[] | null | undefined, type: IntegrationType): Integration[] {
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Signal } from "@angular/core";
|
||||
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
|
||||
import { Integration } from "../models/integration";
|
||||
import { OrganizationIntegration } from "../models/organization-integration";
|
||||
|
||||
export abstract class IntegrationStateService {
|
||||
abstract integrations: Signal<Integration[]>;
|
||||
abstract organization: Signal<Organization | undefined>;
|
||||
abstract setIntegrations(integrations: Integration[]): void;
|
||||
abstract setOrganization(organization: Organization | undefined): void;
|
||||
abstract updateIntegrationSettings(
|
||||
integrationName: string,
|
||||
updatedIntegrationSettings: OrganizationIntegration,
|
||||
): void;
|
||||
abstract deleteIntegrationSettings(integrationName: string): void;
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { FilterIntegrationsPipe } from "@bitwarden/bit-common/dirt/organization-integrations/shared/filter-integrations.pipe";
|
||||
import { IntegrationStateService } from "@bitwarden/bit-common/dirt/organization-integrations/shared/integration-state.service";
|
||||
import { IntegrationType } from "@bitwarden/common/enums/integration-type.enum";
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
import { IntegrationGridComponent } from "../integration-grid/integration-grid.component";
|
||||
import { FilterIntegrationsPipe } from "../integrations.pipe";
|
||||
import { OrganizationIntegrationsState } from "../organization-integrations.state";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@@ -17,7 +17,7 @@ import { OrganizationIntegrationsState } from "../organization-integrations.stat
|
||||
export class DeviceManagementComponent {
|
||||
integrations = this.state.integrations;
|
||||
|
||||
constructor(private state: OrganizationIntegrationsState) {}
|
||||
constructor(private state: IntegrationStateService) {}
|
||||
|
||||
get IntegrationType(): typeof IntegrationType {
|
||||
return IntegrationType;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { FilterIntegrationsPipe } from "@bitwarden/bit-common/dirt/organization-integrations/shared/filter-integrations.pipe";
|
||||
import { IntegrationStateService } from "@bitwarden/bit-common/dirt/organization-integrations/shared/integration-state.service";
|
||||
import { IntegrationType } from "@bitwarden/common/enums/integration-type.enum";
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
import { IntegrationGridComponent } from "../integration-grid/integration-grid.component";
|
||||
import { FilterIntegrationsPipe } from "../integrations.pipe";
|
||||
import { OrganizationIntegrationsState } from "../organization-integrations.state";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@@ -16,7 +16,8 @@ import { OrganizationIntegrationsState } from "../organization-integrations.stat
|
||||
})
|
||||
export class EventManagementComponent {
|
||||
integrations = this.state.integrations;
|
||||
constructor(private state: OrganizationIntegrationsState) {}
|
||||
|
||||
constructor(private state: IntegrationStateService) {}
|
||||
|
||||
get IntegrationType(): typeof IntegrationType {
|
||||
return IntegrationType;
|
||||
|
||||
@@ -10,26 +10,26 @@
|
||||
<div class="tw-flex tw-items-center tw-justify-center tw-size-28 lg:tw-w-40">
|
||||
<img
|
||||
#imageEle
|
||||
[src]="image"
|
||||
[src]="image()"
|
||||
alt=""
|
||||
class="tw-block tw-mx-auto tw-h-auto tw-max-w-full tw-max-h-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@if (linkURL) {
|
||||
@if (linkURL()) {
|
||||
<a
|
||||
class="tw-block tw-mb-0 tw-font-medium hover:tw-no-underline focus:tw-outline-none after:tw-content-[''] after:tw-block after:tw-absolute after:tw-size-full after:tw-left-0 after:tw-top-0 after:tw-w-full after:tw-h-40"
|
||||
[href]="linkURL"
|
||||
[href]="linkURL()"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
title="{{ linkURL }}"
|
||||
title="{{ linkURL() }}"
|
||||
>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
<bit-card-content>
|
||||
<h3 class="tw-text-main tw-m-0 tw-text-lg tw-font-medium">
|
||||
{{ name }}
|
||||
{{ name() }}
|
||||
@if (showConnectedBadge()) {
|
||||
<span class="tw-ml-3">
|
||||
@if (isConnected) {
|
||||
@@ -41,15 +41,15 @@
|
||||
</span>
|
||||
}
|
||||
</h3>
|
||||
@if (description) {
|
||||
<p class="tw-mb-0 tw-mt-2 tw-font-medium">{{ description }}</p>
|
||||
@if (description()) {
|
||||
<p class="tw-mb-0 tw-mt-2 tw-font-medium">{{ description() }}</p>
|
||||
}
|
||||
@if (canSetupConnection) {
|
||||
@if (canSetupConnection()) {
|
||||
<button type="button" class="tw-mt-3" bitButton (click)="setupConnection()">
|
||||
@if (isUpdateAvailable) {
|
||||
<span>{{ "updateIntegrationButtonDesc" | i18n: name }}</span>
|
||||
<span>{{ "updateIntegrationButtonDesc" | i18n: name() }}</span>
|
||||
} @else {
|
||||
<span>{{ "connectIntegrationButtonDesc" | i18n: name }}</span>
|
||||
<span>{{ "connectIntegrationButtonDesc" | i18n: name() }}</span>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { OrgIntegrationBuilder } from "@bitwarden/bit-common/dirt/organization-i
|
||||
import { OrganizationIntegrationServiceName } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-service-type";
|
||||
import { OrganizationIntegrationType } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-type";
|
||||
import { OrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-service";
|
||||
import { IntegrationStateService } from "@bitwarden/bit-common/dirt/organization-integrations/shared/integration-state.service";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
@@ -35,6 +36,7 @@ describe("IntegrationCardComponent", () => {
|
||||
const mockIntegrationService = mock<OrganizationIntegrationService>();
|
||||
const dialogService = mock<DialogService>();
|
||||
const toastService = mock<ToastService>();
|
||||
const stateService = mock<IntegrationStateService>();
|
||||
|
||||
const systemTheme$ = new BehaviorSubject<ThemeType>(ThemeType.Light);
|
||||
const usersPreferenceTheme$ = new BehaviorSubject<ThemeType>(ThemeType.Light);
|
||||
@@ -59,6 +61,7 @@ describe("IntegrationCardComponent", () => {
|
||||
{ provide: OrganizationIntegrationService, useValue: mockIntegrationService },
|
||||
{ provide: ToastService, useValue: toastService },
|
||||
{ provide: DialogService, useValue: dialogService },
|
||||
{ provide: IntegrationStateService, useValue: stateService },
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
@@ -67,9 +70,9 @@ describe("IntegrationCardComponent", () => {
|
||||
fixture = TestBed.createComponent(IntegrationCardComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
component.name = "Integration Name";
|
||||
component.image = "test-image.png";
|
||||
component.linkURL = "https://example.com/";
|
||||
fixture.componentRef.setInput("name", "Integration Name");
|
||||
fixture.componentRef.setInput("image", "test-image.png");
|
||||
fixture.componentRef.setInput("linkURL", "https://example.com/");
|
||||
|
||||
mockI18nService.t.mockImplementation((key) => key);
|
||||
fixture.detectChanges();
|
||||
@@ -88,7 +91,7 @@ describe("IntegrationCardComponent", () => {
|
||||
});
|
||||
|
||||
it("assigns external rel attribute", () => {
|
||||
component.externalURL = true;
|
||||
fixture.componentRef.setInput("externalURL", true);
|
||||
fixture.detectChanges();
|
||||
|
||||
const link = fixture.nativeElement.querySelector("a");
|
||||
@@ -107,26 +110,27 @@ describe("IntegrationCardComponent", () => {
|
||||
});
|
||||
|
||||
it("shows when expiration is in the future", () => {
|
||||
component.newBadgeExpiration = "2023-09-02";
|
||||
fixture.componentRef.setInput("newBadgeExpiration", "2023-09-02");
|
||||
expect(component.showNewBadge()).toBe(true);
|
||||
});
|
||||
|
||||
it("does not show when expiration is not set", () => {
|
||||
fixture.componentRef.setInput("newBadgeExpiration", undefined);
|
||||
expect(component.showNewBadge()).toBe(false);
|
||||
});
|
||||
|
||||
it("does not show when expiration is in the past", () => {
|
||||
component.newBadgeExpiration = "2023-08-31";
|
||||
fixture.componentRef.setInput("newBadgeExpiration", "2023-08-31");
|
||||
expect(component.showNewBadge()).toBe(false);
|
||||
});
|
||||
|
||||
it("does not show when expiration is today", () => {
|
||||
component.newBadgeExpiration = "2023-09-01";
|
||||
fixture.componentRef.setInput("newBadgeExpiration", "2023-09-01");
|
||||
expect(component.showNewBadge()).toBe(false);
|
||||
});
|
||||
|
||||
it("does not show when expiration is invalid", () => {
|
||||
component.newBadgeExpiration = "not-a-date";
|
||||
fixture.componentRef.setInput("newBadgeExpiration", "not-a-date");
|
||||
expect(component.showNewBadge()).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -138,12 +142,12 @@ describe("IntegrationCardComponent", () => {
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.imageEle.nativeElement.src).toContain("test-image.png");
|
||||
expect(component.imageEle().nativeElement.src).toContain("test-image.png");
|
||||
});
|
||||
|
||||
describe("user prefers the system theme", () => {
|
||||
beforeEach(() => {
|
||||
component.imageDarkMode = "test-image-dark.png";
|
||||
fixture.componentRef.setInput("imageDarkMode", "test-image-dark.png");
|
||||
});
|
||||
|
||||
it("sets image src to imageDarkMode", () => {
|
||||
@@ -152,24 +156,24 @@ describe("IntegrationCardComponent", () => {
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.imageEle.nativeElement.src).toContain("test-image-dark.png");
|
||||
expect(component.imageEle().nativeElement.src).toContain("test-image-dark.png");
|
||||
});
|
||||
|
||||
it("sets image src to light mode image", () => {
|
||||
component.imageEle.nativeElement.src = "test-image-dark.png";
|
||||
component.imageEle().nativeElement.src = "test-image-dark.png";
|
||||
|
||||
usersPreferenceTheme$.next(ThemeType.System);
|
||||
systemTheme$.next(ThemeType.Light);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.imageEle.nativeElement.src).toContain("test-image.png");
|
||||
expect(component.imageEle().nativeElement.src).toContain("test-image.png");
|
||||
});
|
||||
});
|
||||
|
||||
describe("user prefers dark mode", () => {
|
||||
beforeEach(() => {
|
||||
component.imageDarkMode = "test-image-dark.png";
|
||||
fixture.componentRef.setInput("imageDarkMode", "test-image-dark.png");
|
||||
});
|
||||
|
||||
it("updates image to dark mode", () => {
|
||||
@@ -178,24 +182,24 @@ describe("IntegrationCardComponent", () => {
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.imageEle.nativeElement.src).toContain("test-image-dark.png");
|
||||
expect(component.imageEle().nativeElement.src).toContain("test-image-dark.png");
|
||||
});
|
||||
});
|
||||
|
||||
describe("user prefers light mode", () => {
|
||||
beforeEach(() => {
|
||||
component.imageDarkMode = "test-image-dark.png";
|
||||
fixture.componentRef.setInput("imageDarkMode", "test-image-dark.png");
|
||||
});
|
||||
|
||||
it("updates image to light mode", () => {
|
||||
component.imageEle.nativeElement.src = "test-image-dark.png";
|
||||
component.imageEle().nativeElement.src = "test-image-dark.png";
|
||||
|
||||
systemTheme$.next(ThemeType.Dark); // system theme shouldn't matter
|
||||
usersPreferenceTheme$.next(ThemeType.Light);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.imageEle.nativeElement.src).toContain("test-image.png");
|
||||
expect(component.imageEle().nativeElement.src).toContain("test-image.png");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -211,57 +215,52 @@ describe("IntegrationCardComponent", () => {
|
||||
});
|
||||
|
||||
it("returns false when newBadgeExpiration is undefined", () => {
|
||||
component.newBadgeExpiration = undefined;
|
||||
fixture.componentRef.setInput("newBadgeExpiration", undefined);
|
||||
expect(component.showNewBadge()).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when newBadgeExpiration is an invalid date", () => {
|
||||
component.newBadgeExpiration = "invalid-date";
|
||||
fixture.componentRef.setInput("newBadgeExpiration", "invalid-date");
|
||||
expect(component.showNewBadge()).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when newBadgeExpiration is in the future", () => {
|
||||
component.newBadgeExpiration = "2024-06-02";
|
||||
fixture.componentRef.setInput("newBadgeExpiration", "2024-06-02");
|
||||
expect(component.showNewBadge()).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when newBadgeExpiration is today", () => {
|
||||
component.newBadgeExpiration = "2024-06-01";
|
||||
fixture.componentRef.setInput("newBadgeExpiration", "2024-06-01");
|
||||
expect(component.showNewBadge()).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when newBadgeExpiration is in the past", () => {
|
||||
component.newBadgeExpiration = "2024-05-31";
|
||||
fixture.componentRef.setInput("newBadgeExpiration", "2024-05-31");
|
||||
expect(component.showNewBadge()).toBe(false);
|
||||
});
|
||||
});
|
||||
describe("showConnectedBadge", () => {
|
||||
it("returns true when canSetupConnection is true", () => {
|
||||
component.canSetupConnection = true;
|
||||
fixture.componentRef.setInput("canSetupConnection", true);
|
||||
expect(component.showConnectedBadge()).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when canSetupConnection is false", () => {
|
||||
component.canSetupConnection = false;
|
||||
expect(component.showConnectedBadge()).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when canSetupConnection is undefined", () => {
|
||||
component.canSetupConnection = undefined;
|
||||
fixture.componentRef.setInput("canSetupConnection", false);
|
||||
expect(component.showConnectedBadge()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setupConnection", () => {
|
||||
beforeEach(() => {
|
||||
component.integrationSettings = {
|
||||
fixture.componentRef.setInput("integrationSettings", {
|
||||
organizationIntegration: {
|
||||
id: "integration-id",
|
||||
configuration: {},
|
||||
integrationConfiguration: [{ id: "config-id" }],
|
||||
},
|
||||
name: OrganizationIntegrationServiceName.CrowdStrike,
|
||||
} as any;
|
||||
} as any);
|
||||
component.organizationId = "org-id" as any;
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
@@ -311,10 +310,10 @@ describe("IntegrationCardComponent", () => {
|
||||
});
|
||||
|
||||
it("should call saveHec if isUpdateAvailable is false", async () => {
|
||||
component.integrationSettings = {
|
||||
fixture.componentRef.setInput("integrationSettings", {
|
||||
organizationIntegration: null,
|
||||
name: OrganizationIntegrationServiceName.CrowdStrike,
|
||||
} as any;
|
||||
} as any);
|
||||
component.organizationId = "org-id" as any;
|
||||
|
||||
(openHecConnectDialog as jest.Mock).mockReturnValue({
|
||||
@@ -376,10 +375,10 @@ describe("IntegrationCardComponent", () => {
|
||||
});
|
||||
|
||||
it("should not call delete if no existing configuration", async () => {
|
||||
component.integrationSettings = {
|
||||
fixture.componentRef.setInput("integrationSettings", {
|
||||
organizationIntegration: null,
|
||||
name: OrganizationIntegrationServiceName.CrowdStrike,
|
||||
} as any;
|
||||
} as any);
|
||||
component.organizationId = "org-id" as any;
|
||||
|
||||
(openHecConnectDialog as jest.Mock).mockReturnValue({
|
||||
|
||||
@@ -3,9 +3,9 @@ import {
|
||||
Component,
|
||||
ElementRef,
|
||||
Inject,
|
||||
Input,
|
||||
input,
|
||||
OnDestroy,
|
||||
ViewChild,
|
||||
viewChild,
|
||||
} from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { Observable, Subject, combineLatest, lastValueFrom, takeUntil } from "rxjs";
|
||||
@@ -20,7 +20,11 @@ import {
|
||||
} from "@bitwarden/bit-common/dirt/organization-integrations/models/integration-builder";
|
||||
import { OrganizationIntegrationServiceName } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-service-type";
|
||||
import { OrganizationIntegrationType } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-type";
|
||||
import { OrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-service";
|
||||
import {
|
||||
IntegrationModificationResult,
|
||||
OrganizationIntegrationService,
|
||||
} from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-service";
|
||||
import { IntegrationStateService } from "@bitwarden/bit-common/dirt/organization-integrations/shared/integration-state.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
@@ -52,30 +56,13 @@ import {
|
||||
})
|
||||
export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
private destroyed$: Subject<void> = new Subject();
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@ViewChild("imageEle") imageEle!: ElementRef<HTMLImageElement>;
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() name: string = "";
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() image: string = "";
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() imageDarkMode: string = "";
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() linkURL: string = "";
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() integrationSettings!: Integration;
|
||||
|
||||
/** Adds relevant `rel` attribute to external links */
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() externalURL?: boolean;
|
||||
readonly imageEle = viewChild.required<ElementRef<HTMLImageElement>>("imageEle");
|
||||
readonly name = input.required<string>();
|
||||
readonly image = input.required<string>();
|
||||
readonly imageDarkMode = input.required<string>();
|
||||
readonly linkURL = input.required<string>();
|
||||
readonly integrationSettings = input.required<Integration>();
|
||||
readonly externalURL = input.required<boolean>();
|
||||
|
||||
/**
|
||||
* Date of when the new badge should be hidden.
|
||||
@@ -83,15 +70,9 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
*
|
||||
* @example "2024-12-31"
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() newBadgeExpiration?: string;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() description?: string;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() canSetupConnection?: boolean;
|
||||
readonly newBadgeExpiration = input<string | undefined>(undefined);
|
||||
readonly description = input<string>("");
|
||||
readonly canSetupConnection = input<boolean>(false);
|
||||
|
||||
organizationId: OrganizationId;
|
||||
|
||||
@@ -104,6 +85,7 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
private organizationIntegrationService: OrganizationIntegrationService,
|
||||
private toastService: ToastService,
|
||||
private i18nService: I18nService,
|
||||
protected state: IntegrationStateService,
|
||||
) {
|
||||
this.organizationId = this.activatedRoute.snapshot.paramMap.get(
|
||||
"organizationId",
|
||||
@@ -115,7 +97,7 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
.pipe(takeUntil(this.destroyed$))
|
||||
.subscribe(([theme, systemTheme]) => {
|
||||
// When the card doesn't have a dark mode image, exit early
|
||||
if (!this.imageDarkMode) {
|
||||
if (!this.imageDarkMode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -124,13 +106,13 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
// use the system theme to determine the image
|
||||
const prefersDarkMode = systemTheme === ThemeType.Dark;
|
||||
|
||||
this.imageEle.nativeElement.src = prefersDarkMode ? this.imageDarkMode : this.image;
|
||||
this.imageEle().nativeElement.src = prefersDarkMode ? this.imageDarkMode() : this.image();
|
||||
} else if (theme === ThemeType.Dark) {
|
||||
// When the user's preference is dark mode, use the dark mode image
|
||||
this.imageEle.nativeElement.src = this.imageDarkMode;
|
||||
this.imageEle().nativeElement.src = this.imageDarkMode();
|
||||
} else {
|
||||
// Otherwise use the light mode image
|
||||
this.imageEle.nativeElement.src = this.image;
|
||||
this.imageEle().nativeElement.src = this.image();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -142,11 +124,11 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
|
||||
/** Show the "new" badge when expiration is in the future */
|
||||
showNewBadge() {
|
||||
if (!this.newBadgeExpiration) {
|
||||
if (!this.newBadgeExpiration()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const expirationDate = new Date(this.newBadgeExpiration);
|
||||
const expirationDate = new Date(this.newBadgeExpiration() ?? "undefined");
|
||||
|
||||
// Do not show the new badge for invalid dates
|
||||
if (isNaN(expirationDate.getTime())) {
|
||||
@@ -157,26 +139,26 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
get isConnected(): boolean {
|
||||
return !!this.integrationSettings.organizationIntegration?.configuration;
|
||||
return !!this.integrationSettings().organizationIntegration?.configuration;
|
||||
}
|
||||
|
||||
showConnectedBadge(): boolean {
|
||||
return this.canSetupConnection ?? false;
|
||||
return this.canSetupConnection();
|
||||
}
|
||||
|
||||
get isUpdateAvailable(): boolean {
|
||||
return !!this.integrationSettings.organizationIntegration;
|
||||
return !!this.integrationSettings().organizationIntegration;
|
||||
}
|
||||
|
||||
async setupConnection() {
|
||||
if (this.integrationSettings?.integrationType === null) {
|
||||
if (this.integrationSettings()?.integrationType === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.integrationSettings?.integrationType === OrganizationIntegrationType.Datadog) {
|
||||
if (this.integrationSettings()?.integrationType === OrganizationIntegrationType.Datadog) {
|
||||
const dialog = openDatadogConnectDialog(this.dialogService, {
|
||||
data: {
|
||||
settings: this.integrationSettings,
|
||||
settings: this.integrationSettings(),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -187,11 +169,11 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
() => this.deleteDatadog(),
|
||||
(res) => this.saveDatadog(res),
|
||||
);
|
||||
} else if (this.integrationSettings.name === OrganizationIntegrationServiceName.Huntress) {
|
||||
} else if (this.integrationSettings().name === OrganizationIntegrationServiceName.Huntress) {
|
||||
// Huntress uses HEC protocol but has its own dialog
|
||||
const dialog = openHuntressConnectDialog(this.dialogService, {
|
||||
data: {
|
||||
settings: this.integrationSettings,
|
||||
settings: this.integrationSettings(),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -206,7 +188,7 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
// invoke the dialog to connect the integration
|
||||
const dialog = openHecConnectDialog(this.dialogService, {
|
||||
data: {
|
||||
settings: this.integrationSettings,
|
||||
settings: this.integrationSettings(),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -228,13 +210,17 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
config: OrgIntegrationConfiguration,
|
||||
template: OrgIntegrationTemplate,
|
||||
): Promise<void> {
|
||||
let response = { mustBeOwner: false, success: false };
|
||||
let response: IntegrationModificationResult = {
|
||||
mustBeOwner: false,
|
||||
success: false,
|
||||
organizationIntegrationResult: undefined,
|
||||
};
|
||||
|
||||
if (this.isUpdateAvailable) {
|
||||
// retrieve org integration and configuration ids
|
||||
const orgIntegrationId = this.integrationSettings.organizationIntegration?.id;
|
||||
const orgIntegrationId = this.integrationSettings().organizationIntegration?.id;
|
||||
const orgIntegrationConfigurationId =
|
||||
this.integrationSettings.organizationIntegration?.integrationConfiguration[0]?.id;
|
||||
this.integrationSettings().organizationIntegration?.integrationConfiguration[0]?.id;
|
||||
|
||||
if (!orgIntegrationId || !orgIntegrationConfigurationId) {
|
||||
throw Error("Organization Integration ID or Configuration ID is missing");
|
||||
@@ -264,10 +250,21 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
// update local state with the new integration settings
|
||||
if (response.success && response.organizationIntegrationResult) {
|
||||
this.state.updateIntegrationSettings(
|
||||
this.integrationSettings().name,
|
||||
response.organizationIntegrationResult,
|
||||
);
|
||||
}
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("success"),
|
||||
message: this.i18nService.t(
|
||||
"integrationConnectedSuccessfully",
|
||||
this.integrationSettings().name,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -275,9 +272,9 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
* Generic delete method
|
||||
*/
|
||||
private async deleteIntegration(): Promise<void> {
|
||||
const orgIntegrationId = this.integrationSettings.organizationIntegration?.id;
|
||||
const orgIntegrationId = this.integrationSettings().organizationIntegration?.id;
|
||||
const orgIntegrationConfigurationId =
|
||||
this.integrationSettings.organizationIntegration?.integrationConfiguration[0]?.id;
|
||||
this.integrationSettings().organizationIntegration?.integrationConfiguration[0]?.id;
|
||||
|
||||
if (!orgIntegrationId || !orgIntegrationConfigurationId) {
|
||||
throw Error("Organization Integration ID or Configuration ID is missing");
|
||||
@@ -294,6 +291,10 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.success) {
|
||||
this.state.deleteIntegrationSettings(this.integrationSettings().name);
|
||||
}
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
@@ -347,11 +348,11 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
const config = OrgIntegrationBuilder.buildHecConfiguration(
|
||||
result.url,
|
||||
result.bearerToken,
|
||||
this.integrationSettings.name as OrganizationIntegrationServiceName,
|
||||
this.integrationSettings().name as OrganizationIntegrationServiceName,
|
||||
);
|
||||
const template = OrgIntegrationBuilder.buildHecTemplate(
|
||||
result.index,
|
||||
this.integrationSettings.name as OrganizationIntegrationServiceName,
|
||||
this.integrationSettings().name as OrganizationIntegrationServiceName,
|
||||
);
|
||||
|
||||
await this.saveIntegration(OrganizationIntegrationType.Hec, config, template);
|
||||
@@ -385,7 +386,7 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
async saveDatadog(result: DatadogConnectDialogResult) {
|
||||
const config = OrgIntegrationBuilder.buildDataDogConfiguration(result.url, result.apiKey);
|
||||
const template = OrgIntegrationBuilder.buildDataDogTemplate(
|
||||
this.integrationSettings.name as OrganizationIntegrationServiceName,
|
||||
this.integrationSettings().name as OrganizationIntegrationServiceName,
|
||||
);
|
||||
|
||||
await this.saveIntegration(OrganizationIntegrationType.Datadog, config, template);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<ul
|
||||
class="tw-inline-grid tw-grid-cols-3 tw-gap-6 tw-m-0 tw-p-0 tw-w-full tw-auto-cols-auto tw-list-none lg:tw-grid-cols-4 lg:tw-gap-10 lg:tw-w-auto"
|
||||
>
|
||||
@for (integration of integrations; track integration) {
|
||||
@for (integration of integrations(); track integration) {
|
||||
<li
|
||||
[title]="tooltipI18nKey | i18n: integration.name"
|
||||
[attr.aria-label]="ariaI18nKey | i18n: integration.name"
|
||||
[title]="tooltipI18nKey() | i18n: integration.name"
|
||||
[attr.aria-label]="ariaI18nKey() | i18n: integration.name"
|
||||
>
|
||||
<app-integration-card
|
||||
[name]="integration.name"
|
||||
|
||||
@@ -7,6 +7,7 @@ import { of } from "rxjs";
|
||||
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
|
||||
import { OrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-service";
|
||||
import { IntegrationStateService } from "@bitwarden/bit-common/dirt/organization-integrations/shared/integration-state.service";
|
||||
import { IntegrationType } from "@bitwarden/common/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||
@@ -74,19 +75,20 @@ describe("IntegrationGridComponent", () => {
|
||||
provide: ToastService,
|
||||
useValue: mock<ToastService>(),
|
||||
},
|
||||
{ provide: IntegrationStateService, useValue: mock<IntegrationStateService>() },
|
||||
],
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(IntegrationGridComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.integrations = integrations;
|
||||
component.ariaI18nKey = "integrationCardAriaLabel";
|
||||
component.tooltipI18nKey = "integrationCardTooltip";
|
||||
fixture.componentRef.setInput("integrations", integrations);
|
||||
fixture.componentRef.setInput("ariaI18nKey", "integrationCardAriaLabel");
|
||||
fixture.componentRef.setInput("tooltipI18nKey", "integrationCardTooltip");
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("lists all integrations", () => {
|
||||
expect(component.integrations).toEqual(integrations);
|
||||
expect(component.integrations()).toEqual(integrations);
|
||||
|
||||
const cards = fixture.debugElement.queryAll(By.directive(IntegrationCardComponent));
|
||||
|
||||
@@ -94,20 +96,20 @@ describe("IntegrationGridComponent", () => {
|
||||
});
|
||||
|
||||
it("assigns the correct attributes to IntegrationCardComponent", () => {
|
||||
expect(component.integrations).toEqual(integrations);
|
||||
expect(component.integrations()).toEqual(integrations);
|
||||
|
||||
const card = fixture.debugElement.queryAll(By.directive(IntegrationCardComponent))[1];
|
||||
|
||||
expect(card.componentInstance.name).toBe("SDK 2");
|
||||
expect(card.componentInstance.image).toBe("test-image2.png");
|
||||
expect(card.componentInstance.linkURL).toBe("https://example.com/2");
|
||||
expect(card.componentInstance.name()).toBe("SDK 2");
|
||||
expect(card.componentInstance.image()).toBe("test-image2.png");
|
||||
expect(card.componentInstance.linkURL()).toBe("https://example.com/2");
|
||||
});
|
||||
|
||||
it("assigns `externalURL` for SDKs", () => {
|
||||
const card = fixture.debugElement.queryAll(By.directive(IntegrationCardComponent));
|
||||
|
||||
expect(card[0].componentInstance.externalURL).toBe(false);
|
||||
expect(card[1].componentInstance.externalURL).toBe(true);
|
||||
expect(card[0].componentInstance.externalURL()).toBe(false);
|
||||
expect(card[1].componentInstance.externalURL()).toBe(true);
|
||||
});
|
||||
|
||||
it("has a tool tip and aria label attributes", () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, Input } from "@angular/core";
|
||||
import { ChangeDetectionStrategy, Component, input } from "@angular/core";
|
||||
|
||||
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
|
||||
import { IntegrationType } from "@bitwarden/common/enums";
|
||||
@@ -6,24 +6,16 @@ import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
import { IntegrationCardComponent } from "../integration-card/integration-card.component";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: "app-integration-grid",
|
||||
templateUrl: "./integration-grid.component.html",
|
||||
imports: [IntegrationCardComponent, SharedModule],
|
||||
})
|
||||
export class IntegrationGridComponent {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() integrations: Integration[] = [];
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() ariaI18nKey: string = "integrationCardAriaLabel";
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() tooltipI18nKey: string = "integrationCardTooltip";
|
||||
readonly integrations = input.required<Integration[]>();
|
||||
readonly ariaI18nKey = input<string>("integrationCardAriaLabel");
|
||||
readonly tooltipI18nKey = input<string>("integrationCardTooltip");
|
||||
|
||||
protected IntegrationType = IntegrationType;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { IntegrationStateService } from "@bitwarden/bit-common/dirt/organization-integrations/shared/integration-state.service";
|
||||
import { IntegrationType } from "@bitwarden/common/enums/integration-type.enum";
|
||||
import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module";
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
import { OrganizationIntegrationsState } from "./organization-integrations.state";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
@@ -16,7 +15,7 @@ import { OrganizationIntegrationsState } from "./organization-integrations.state
|
||||
export class AdminConsoleIntegrationsComponent {
|
||||
organization = this.state.organization;
|
||||
|
||||
constructor(private state: OrganizationIntegrationsState) {}
|
||||
constructor(private state: IntegrationStateService) {}
|
||||
|
||||
// use in the view
|
||||
get IntegrationType(): typeof IntegrationType {
|
||||
|
||||
@@ -7,7 +7,6 @@ import { DeviceManagementComponent } from "./device-management/device-management
|
||||
import { EventManagementComponent } from "./event-management/event-management.component";
|
||||
import { AdminConsoleIntegrationsComponent } from "./integrations.component";
|
||||
import { OrganizationIntegrationsResolver } from "./organization-integrations.resolver";
|
||||
import { OrganizationIntegrationsState } from "./organization-integrations.state";
|
||||
import { SingleSignOnComponent } from "./single-sign-on/single-sign-on.component";
|
||||
import { UserProvisioningComponent } from "./user-provisioning/user-provisioning.component";
|
||||
|
||||
@@ -19,7 +18,7 @@ const routes: Routes = [
|
||||
titleId: "integrations",
|
||||
},
|
||||
component: AdminConsoleIntegrationsComponent,
|
||||
providers: [OrganizationIntegrationsState, OrganizationIntegrationsResolver],
|
||||
providers: [OrganizationIntegrationsResolver],
|
||||
resolve: { integrations: OrganizationIntegrationsResolver },
|
||||
children: [
|
||||
{ path: "", redirectTo: "single-sign-on", pathMatch: "full" },
|
||||
|
||||
@@ -4,6 +4,7 @@ import { DeviceManagementComponent } from "@bitwarden/angular/auth/device-manage
|
||||
import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-api.service";
|
||||
import { OrganizationIntegrationConfigurationApiService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-configuration-api.service";
|
||||
import { OrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-service";
|
||||
import { IntegrationStateService } from "@bitwarden/bit-common/dirt/organization-integrations/shared/integration-state.service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { safeProvider } from "@bitwarden/ui-common";
|
||||
|
||||
@@ -11,6 +12,7 @@ import { EventManagementComponent } from "./event-management/event-management.co
|
||||
import { AdminConsoleIntegrationsComponent } from "./integrations.component";
|
||||
import { OrganizationIntegrationsRoutingModule } from "./organization-integrations-routing.module";
|
||||
import { OrganizationIntegrationsResolver } from "./organization-integrations.resolver";
|
||||
import { OrganizationIntegrationsState } from "./organization-integrations.state";
|
||||
import { SingleSignOnComponent } from "./single-sign-on/single-sign-on.component";
|
||||
import { UserProvisioningComponent } from "./user-provisioning/user-provisioning.component";
|
||||
|
||||
@@ -40,6 +42,11 @@ import { UserProvisioningComponent } from "./user-provisioning/user-provisioning
|
||||
useClass: OrganizationIntegrationConfigurationApiService,
|
||||
deps: [ApiService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: IntegrationStateService,
|
||||
useClass: OrganizationIntegrationsState,
|
||||
useAngularDecorators: true,
|
||||
}),
|
||||
],
|
||||
})
|
||||
export class OrganizationIntegrationsModule {}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Integration } from "@bitwarden/bit-common/dirt/organization-integration
|
||||
import { OrganizationIntegrationServiceName } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-service-type";
|
||||
import { OrganizationIntegrationType } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-type";
|
||||
import { OrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-service";
|
||||
import { IntegrationStateService } from "@bitwarden/bit-common/dirt/organization-integrations/shared/integration-state.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
@@ -15,8 +16,6 @@ import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { getById } from "@bitwarden/common/platform/misc";
|
||||
|
||||
import { OrganizationIntegrationsState } from "./organization-integrations.state";
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationIntegrationsResolver implements Resolve<boolean> {
|
||||
constructor(
|
||||
@@ -24,7 +23,7 @@ export class OrganizationIntegrationsResolver implements Resolve<boolean> {
|
||||
private accountService: AccountService,
|
||||
private configService: ConfigService,
|
||||
private organizationIntegrationService: OrganizationIntegrationService,
|
||||
private state: OrganizationIntegrationsState,
|
||||
private state: IntegrationStateService,
|
||||
) {}
|
||||
|
||||
async resolve(route: ActivatedRouteSnapshot): Promise<boolean> {
|
||||
@@ -262,6 +261,7 @@ export class OrganizationIntegrationsResolver implements Resolve<boolean> {
|
||||
name: OrganizationIntegrationServiceName.Huntress,
|
||||
linkURL: "https://bitwarden.com/help/huntress-siem/",
|
||||
image: "../../../../../../../images/integrations/logo-huntress-siem.svg",
|
||||
imageDarkMode: "../../../../../../../images/integrations/logo-huntress-siem-darkmode.svg",
|
||||
type: IntegrationType.EVENT,
|
||||
description: "huntressEventIntegrationDesc",
|
||||
canSetupConnection: true,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Injectable, signal } from "@angular/core";
|
||||
|
||||
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
|
||||
import { OrganizationIntegration } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration";
|
||||
import { IntegrationStateService } from "@bitwarden/bit-common/dirt/organization-integrations/shared/integration-state.service";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationIntegrationsState {
|
||||
export class OrganizationIntegrationsState implements IntegrationStateService {
|
||||
private readonly _integrations = signal<Integration[]>([]);
|
||||
private readonly _organization = signal<Organization | undefined>(undefined);
|
||||
|
||||
@@ -12,11 +14,38 @@ export class OrganizationIntegrationsState {
|
||||
integrations = this._integrations.asReadonly();
|
||||
organization = this._organization.asReadonly();
|
||||
|
||||
setOrganization(val: Organization | null) {
|
||||
setOrganization(val: Organization | undefined) {
|
||||
this._organization.set(val ?? undefined);
|
||||
}
|
||||
|
||||
setIntegrations(val: Integration[]) {
|
||||
this._integrations.set(val);
|
||||
}
|
||||
|
||||
updateIntegrationSettings(
|
||||
integrationName: string,
|
||||
updatedIntegrationSettings: OrganizationIntegration,
|
||||
) {
|
||||
const integrations = this._integrations();
|
||||
const index = integrations.findIndex((i) => i.name === integrationName);
|
||||
if (index >= 0) {
|
||||
const updatedIntegrations = integrations.map((integration, i) =>
|
||||
i === index
|
||||
? { ...integration, organizationIntegration: updatedIntegrationSettings }
|
||||
: integration,
|
||||
);
|
||||
this.setIntegrations(updatedIntegrations);
|
||||
}
|
||||
}
|
||||
|
||||
deleteIntegrationSettings(integrationName: string) {
|
||||
const integrations = this._integrations();
|
||||
const index = integrations.findIndex((i) => i.name === integrationName);
|
||||
if (index >= 0) {
|
||||
const updatedIntegrations = integrations.map((integration, i) =>
|
||||
i === index ? { ...integration, organizationIntegration: undefined } : integration,
|
||||
);
|
||||
this.setIntegrations(updatedIntegrations);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { FilterIntegrationsPipe } from "@bitwarden/bit-common/dirt/organization-integrations/shared/filter-integrations.pipe";
|
||||
import { IntegrationStateService } from "@bitwarden/bit-common/dirt/organization-integrations/shared/integration-state.service";
|
||||
import { IntegrationType } from "@bitwarden/common/enums/integration-type.enum";
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
import { IntegrationGridComponent } from "../integration-grid/integration-grid.component";
|
||||
import { FilterIntegrationsPipe } from "../integrations.pipe";
|
||||
import { OrganizationIntegrationsState } from "../organization-integrations.state";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@@ -18,5 +18,5 @@ export class SingleSignOnComponent {
|
||||
integrations = this.state.integrations;
|
||||
IntegrationType = IntegrationType;
|
||||
|
||||
constructor(private state: OrganizationIntegrationsState) {}
|
||||
constructor(private state: IntegrationStateService) {}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { FilterIntegrationsPipe } from "@bitwarden/bit-common/dirt/organization-integrations/shared/filter-integrations.pipe";
|
||||
import { IntegrationStateService } from "@bitwarden/bit-common/dirt/organization-integrations/shared/integration-state.service";
|
||||
import { IntegrationType } from "@bitwarden/common/enums/integration-type.enum";
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
import { IntegrationGridComponent } from "../integration-grid/integration-grid.component";
|
||||
import { FilterIntegrationsPipe } from "../integrations.pipe";
|
||||
import { OrganizationIntegrationsState } from "../organization-integrations.state";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@@ -18,7 +18,7 @@ export class UserProvisioningComponent {
|
||||
organization = this.state.organization;
|
||||
integrations = this.state.integrations;
|
||||
|
||||
constructor(private state: OrganizationIntegrationsState) {}
|
||||
constructor(private state: IntegrationStateService) {}
|
||||
|
||||
get IntegrationType(): typeof IntegrationType {
|
||||
return IntegrationType;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<section class="tw-mb-9">
|
||||
<p bitTypography="body1">{{ "integrationsDesc" | i18n }}</p>
|
||||
<app-integration-grid
|
||||
[integrations]="integrations"
|
||||
[integrations]="integrations() | filterIntegrations: IntegrationType.Integration"
|
||||
[tooltipI18nKey]="'smIntegrationTooltip'"
|
||||
[ariaI18nKey]="'smIntegrationCardAriaLabel'"
|
||||
></app-integration-grid>
|
||||
@@ -17,7 +17,7 @@
|
||||
</h2>
|
||||
<p bitTypography="body1">{{ "sdksDesc" | i18n }}</p>
|
||||
<app-integration-grid
|
||||
[integrations]="sdks"
|
||||
[integrations]="integrations() | filterIntegrations: IntegrationType.SDK"
|
||||
[tooltipI18nKey]="'smSdkTooltip'"
|
||||
[ariaI18nKey]="'smSdkAriaLabel'"
|
||||
></app-integration-grid>
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { ChangeDetectionStrategy, Component } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import {} from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
|
||||
import { OrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-service";
|
||||
import { FilterIntegrationsPipe } from "@bitwarden/bit-common/dirt/organization-integrations/shared/filter-integrations.pipe";
|
||||
import { IntegrationStateService } from "@bitwarden/bit-common/dirt/organization-integrations/shared/integration-state.service";
|
||||
import { IntegrationType } from "@bitwarden/common/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
@@ -19,19 +21,18 @@ import { IntegrationCardComponent } from "../../dirt/organization-integrations/i
|
||||
import { IntegrationGridComponent } from "../../dirt/organization-integrations/integration-grid/integration-grid.component";
|
||||
|
||||
import { IntegrationsComponent } from "./integrations.component";
|
||||
import { SecretsIntegrationsState } from "./secrets-integrations.state";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: "app-header",
|
||||
template: "<div></div>",
|
||||
standalone: false,
|
||||
})
|
||||
class MockHeaderComponent {}
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: "sm-new-menu",
|
||||
template: "<div></div>",
|
||||
standalone: false,
|
||||
@@ -39,52 +40,158 @@ class MockHeaderComponent {}
|
||||
class MockNewMenuComponent {}
|
||||
|
||||
describe("IntegrationsComponent", () => {
|
||||
let component: IntegrationsComponent;
|
||||
let fixture: ComponentFixture<IntegrationsComponent>;
|
||||
const orgIntegrationSvc = mock<OrganizationIntegrationService>();
|
||||
let integrationStateService: IntegrationStateService;
|
||||
|
||||
const activatedRouteMock = {
|
||||
snapshot: { paramMap: { get: jest.fn() } },
|
||||
};
|
||||
const mockI18nService = mock<I18nService>();
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [IntegrationsComponent, MockHeaderComponent, MockNewMenuComponent],
|
||||
imports: [JslibModule, IntegrationGridComponent, IntegrationCardComponent],
|
||||
imports: [
|
||||
JslibModule,
|
||||
IntegrationGridComponent,
|
||||
IntegrationCardComponent,
|
||||
FilterIntegrationsPipe,
|
||||
I18nPipe,
|
||||
],
|
||||
providers: [
|
||||
{ provide: I18nService, useValue: mock<I18nService>() },
|
||||
{ provide: ThemeStateService, useValue: mock<ThemeStateService>() },
|
||||
{ provide: SYSTEM_THEME_OBSERVABLE, useValue: of(ThemeType.Light) },
|
||||
{ provide: ActivatedRoute, useValue: activatedRouteMock },
|
||||
{ provide: I18nPipe, useValue: mock<I18nPipe>() },
|
||||
{ provide: I18nService, useValue: mockI18nService },
|
||||
{ provide: OrganizationIntegrationService, useValue: orgIntegrationSvc },
|
||||
{
|
||||
provide: OrganizationIntegrationService,
|
||||
useValue: mock<OrganizationIntegrationService>(),
|
||||
},
|
||||
{ provide: IntegrationStateService, useClass: SecretsIntegrationsState },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(IntegrationsComponent);
|
||||
component = fixture.componentInstance;
|
||||
integrationStateService = TestBed.inject(IntegrationStateService);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("divides Integrations & SDKS", () => {
|
||||
const [integrationList, sdkList] = fixture.debugElement.queryAll(
|
||||
By.directive(IntegrationGridComponent),
|
||||
it("should create", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should initialize integrations in state on construction", () => {
|
||||
const integrations = integrationStateService.integrations();
|
||||
|
||||
expect(integrations.length).toBeGreaterThan(0);
|
||||
expect(integrations).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ name: "GitHub Actions", type: IntegrationType.Integration }),
|
||||
expect.objectContaining({ name: "Rust", type: IntegrationType.SDK }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
// Validate only expected names, as the data is constant
|
||||
expect(
|
||||
(integrationList.componentInstance as IntegrationGridComponent).integrations.map(
|
||||
(i) => i.name,
|
||||
),
|
||||
).toEqual([
|
||||
"GitHub Actions",
|
||||
"GitLab CI/CD",
|
||||
"Ansible",
|
||||
"Kubernetes Operator",
|
||||
"Terraform Provider",
|
||||
]);
|
||||
it("should expose integrations getter from state", () => {
|
||||
const stateIntegrations = integrationStateService.integrations();
|
||||
const componentIntegrations = component.integrations();
|
||||
|
||||
expect(
|
||||
(sdkList.componentInstance as IntegrationGridComponent).integrations.map((i) => i.name),
|
||||
).toEqual(["Rust", "C#", "C++", "Go", "Java", "JS WebAssembly", "php", "Python", "Ruby"]);
|
||||
expect(componentIntegrations).toEqual(stateIntegrations);
|
||||
});
|
||||
|
||||
it("should expose IntegrationType enum for template usage", () => {
|
||||
expect(component.IntegrationType).toBe(IntegrationType);
|
||||
});
|
||||
|
||||
describe("template rendering", () => {
|
||||
it("should render two integration grid sections", () => {
|
||||
const grids = fixture.debugElement.queryAll(By.directive(IntegrationGridComponent));
|
||||
|
||||
expect(grids.length).toBe(2);
|
||||
});
|
||||
|
||||
it("should pass correct integrations to first grid (Integrations)", () => {
|
||||
const [integrationsGrid] = fixture.debugElement.queryAll(
|
||||
By.directive(IntegrationGridComponent),
|
||||
);
|
||||
|
||||
const gridInstance = integrationsGrid.componentInstance as IntegrationGridComponent;
|
||||
const integrationNames = gridInstance.integrations().map((i: Integration) => i.name);
|
||||
|
||||
expect(integrationNames).toContain("GitHub Actions");
|
||||
expect(integrationNames).toContain("GitLab CI/CD");
|
||||
expect(integrationNames).toContain("Ansible");
|
||||
expect(integrationNames).toContain("Kubernetes Operator");
|
||||
expect(integrationNames).toContain("Terraform Provider");
|
||||
expect(integrationNames).not.toContain("Rust");
|
||||
expect(integrationNames).not.toContain("Python");
|
||||
});
|
||||
|
||||
it("should pass correct integrations to second grid (SDKs)", () => {
|
||||
const [, sdksGrid] = fixture.debugElement.queryAll(By.directive(IntegrationGridComponent));
|
||||
|
||||
const gridInstance = sdksGrid.componentInstance as IntegrationGridComponent;
|
||||
const sdkNames = gridInstance.integrations().map((i: Integration) => i.name);
|
||||
|
||||
expect(sdkNames).toContain("Rust");
|
||||
expect(sdkNames).toContain("C#");
|
||||
expect(sdkNames).toContain("C++");
|
||||
expect(sdkNames).toContain("Go");
|
||||
expect(sdkNames).toContain("Java");
|
||||
expect(sdkNames).toContain("JS WebAssembly");
|
||||
expect(sdkNames).toContain("php");
|
||||
expect(sdkNames).toContain("Python");
|
||||
expect(sdkNames).toContain("Ruby");
|
||||
expect(sdkNames).not.toContain("GitHub Actions");
|
||||
});
|
||||
|
||||
it("should pass correct tooltip keys to integration grids", () => {
|
||||
const [integrationsGrid, sdksGrid] = fixture.debugElement.queryAll(
|
||||
By.directive(IntegrationGridComponent),
|
||||
);
|
||||
|
||||
expect(
|
||||
(integrationsGrid.componentInstance as IntegrationGridComponent).tooltipI18nKey(),
|
||||
).toBe("smIntegrationTooltip");
|
||||
expect((sdksGrid.componentInstance as IntegrationGridComponent).tooltipI18nKey()).toBe(
|
||||
"smSdkTooltip",
|
||||
);
|
||||
});
|
||||
|
||||
it("should pass correct aria label keys to integration grids", () => {
|
||||
const [integrationsGrid, sdksGrid] = fixture.debugElement.queryAll(
|
||||
By.directive(IntegrationGridComponent),
|
||||
);
|
||||
|
||||
expect((integrationsGrid.componentInstance as IntegrationGridComponent).ariaI18nKey()).toBe(
|
||||
"smIntegrationCardAriaLabel",
|
||||
);
|
||||
expect((sdksGrid.componentInstance as IntegrationGridComponent).ariaI18nKey()).toBe(
|
||||
"smSdkAriaLabel",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("integration data validation", () => {
|
||||
it("should include required properties for all integrations", () => {
|
||||
const integrations = component.integrations();
|
||||
|
||||
integrations.forEach((integration: Integration) => {
|
||||
expect(integration.name).toBeDefined();
|
||||
expect(integration.linkURL).toBeDefined();
|
||||
expect(integration.image).toBeDefined();
|
||||
expect(integration.type).toBeDefined();
|
||||
expect([IntegrationType.Integration, IntegrationType.SDK]).toContain(integration.type);
|
||||
});
|
||||
});
|
||||
|
||||
it("should have valid link URLs for all integrations", () => {
|
||||
const integrations = component.integrations();
|
||||
|
||||
integrations.forEach((integration: Integration) => {
|
||||
expect(integration.linkURL).toMatch(/^https?:\/\//);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { Component, Signal } from "@angular/core";
|
||||
|
||||
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
|
||||
import { IntegrationStateService } from "@bitwarden/bit-common/dirt/organization-integrations/shared/integration-state.service";
|
||||
import { IntegrationType } from "@bitwarden/common/enums";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
@@ -11,10 +12,8 @@ import { IntegrationType } from "@bitwarden/common/enums";
|
||||
standalone: false,
|
||||
})
|
||||
export class IntegrationsComponent {
|
||||
private integrationsAndSdks: Integration[] = [];
|
||||
|
||||
constructor() {
|
||||
this.integrationsAndSdks = [
|
||||
constructor(private state: IntegrationStateService) {
|
||||
const integrations = [
|
||||
{
|
||||
name: "Rust",
|
||||
linkURL: "https://github.com/bitwarden/sdk-sm",
|
||||
@@ -106,19 +105,16 @@ export class IntegrationsComponent {
|
||||
newBadgeExpiration: "2025-12-12", // December 12, 2025
|
||||
},
|
||||
];
|
||||
|
||||
this.state.setIntegrations(integrations);
|
||||
}
|
||||
|
||||
/** Filter out content for the integrations sections */
|
||||
get integrations(): Integration[] {
|
||||
return this.integrationsAndSdks.filter(
|
||||
(integration) => integration.type === IntegrationType.Integration,
|
||||
);
|
||||
get integrations(): Signal<Integration[]> {
|
||||
return this.state.integrations;
|
||||
}
|
||||
|
||||
/** Filter out content for the SDKs section */
|
||||
get sdks(): Integration[] {
|
||||
return this.integrationsAndSdks.filter(
|
||||
(integration) => integration.type === IntegrationType.SDK,
|
||||
);
|
||||
// use in the view
|
||||
get IntegrationType(): typeof IntegrationType {
|
||||
return IntegrationType;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import { NgModule } from "@angular/core";
|
||||
import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-api.service";
|
||||
import { OrganizationIntegrationConfigurationApiService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-configuration-api.service";
|
||||
import { OrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-service";
|
||||
import { FilterIntegrationsPipe } from "@bitwarden/bit-common/dirt/organization-integrations/shared/filter-integrations.pipe";
|
||||
import { IntegrationStateService } from "@bitwarden/bit-common/dirt/organization-integrations/shared/integration-state.service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { safeProvider } from "@bitwarden/ui-common";
|
||||
|
||||
@@ -12,6 +14,7 @@ import { SecretsManagerSharedModule } from "../shared/sm-shared.module";
|
||||
|
||||
import { IntegrationsRoutingModule } from "./integrations-routing.module";
|
||||
import { IntegrationsComponent } from "./integrations.component";
|
||||
import { SecretsIntegrationsState } from "./secrets-integrations.state";
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -19,6 +22,7 @@ import { IntegrationsComponent } from "./integrations.component";
|
||||
IntegrationsRoutingModule,
|
||||
IntegrationCardComponent,
|
||||
IntegrationGridComponent,
|
||||
FilterIntegrationsPipe,
|
||||
],
|
||||
providers: [
|
||||
safeProvider({
|
||||
@@ -36,6 +40,11 @@ import { IntegrationsComponent } from "./integrations.component";
|
||||
useClass: OrganizationIntegrationConfigurationApiService,
|
||||
deps: [ApiService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: IntegrationStateService,
|
||||
useClass: SecretsIntegrationsState,
|
||||
useAngularDecorators: true,
|
||||
}),
|
||||
],
|
||||
declarations: [IntegrationsComponent],
|
||||
})
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { signal } from "@angular/core";
|
||||
|
||||
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
|
||||
import { OrganizationIntegration } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration";
|
||||
import { IntegrationStateService } from "@bitwarden/bit-common/dirt/organization-integrations/shared/integration-state.service";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
|
||||
export class SecretsIntegrationsState implements IntegrationStateService {
|
||||
private readonly _integrations = signal<Integration[]>([]);
|
||||
private readonly _organization = signal<Organization | undefined>(undefined);
|
||||
|
||||
// Signals
|
||||
integrations = this._integrations.asReadonly();
|
||||
organization = this._organization.asReadonly();
|
||||
|
||||
setOrganization(val: Organization | undefined) {
|
||||
this._organization.set(val ?? undefined);
|
||||
}
|
||||
|
||||
setIntegrations(val: Integration[]) {
|
||||
this._integrations.set(val);
|
||||
}
|
||||
|
||||
updateIntegrationSettings(
|
||||
integrationName: string,
|
||||
updatedIntegrationSettings: OrganizationIntegration,
|
||||
) {
|
||||
const integrations = this._integrations();
|
||||
const index = integrations.findIndex((i) => i.name === integrationName);
|
||||
if (index >= 0) {
|
||||
const updatedIntegrations = integrations.map((integration, i) =>
|
||||
i === index
|
||||
? { ...integration, organizationIntegration: updatedIntegrationSettings }
|
||||
: integration,
|
||||
);
|
||||
this.setIntegrations(updatedIntegrations);
|
||||
}
|
||||
}
|
||||
|
||||
deleteIntegrationSettings(integrationName: string) {
|
||||
const integrations = this._integrations();
|
||||
const index = integrations.findIndex((i) => i.name === integrationName);
|
||||
if (index >= 0) {
|
||||
const updatedIntegrations = integrations.map((integration, i) =>
|
||||
i === index ? { ...integration, organizationIntegration: undefined } : integration,
|
||||
);
|
||||
this.setIntegrations(updatedIntegrations);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
export enum IntegrationType {
|
||||
Integration = "integration",
|
||||
SDK = "sdk",
|
||||
SSO = "sso",
|
||||
SCIM = "scim",
|
||||
BWDC = "bwdc",
|
||||
EVENT = "event",
|
||||
DEVICE = "device",
|
||||
}
|
||||
export const IntegrationType = Object.freeze({
|
||||
Integration: "integration",
|
||||
SDK: "sdk",
|
||||
SSO: "sso",
|
||||
SCIM: "scim",
|
||||
BWDC: "bwdc",
|
||||
EVENT: "event",
|
||||
DEVICE: "device",
|
||||
} as const);
|
||||
|
||||
export type IntegrationType = (typeof IntegrationType)[keyof typeof IntegrationType];
|
||||
|
||||
Reference in New Issue
Block a user