1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-18 17:23:37 +00:00

PM-26015 Datadog integration card (#16559)

* PM-26015 adding Datadog integration card

* PM-26015 removing 2 changes

* PM-26015 Removing 1 change

* PM-26015 adding datadog integration card

* PM-26015 fixing code to accept new toast owner changes

* PM-26015 fixing linting error

* PM-26015 fixing pr comment
This commit is contained in:
Graham Walker
2025-10-07 09:37:59 -05:00
committed by GitHub
parent c0d15c19d4
commit 801700d441
23 changed files with 1165 additions and 49 deletions

View File

@@ -5,6 +5,7 @@ import { BehaviorSubject, of } from "rxjs";
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
import { OrganizationIntegrationServiceType } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-service-type";
import { DatadogOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/datadog-organization-integration-service";
import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -29,6 +30,7 @@ describe("IntegrationCardComponent", () => {
const mockI18nService = mock<I18nService>();
const activatedRoute = mock<ActivatedRoute>();
const mockIntegrationService = mock<HecOrganizationIntegrationService>();
const mockDatadogIntegrationService = mock<DatadogOrganizationIntegrationService>();
const dialogService = mock<DialogService>();
const toastService = mock<ToastService>();
@@ -53,6 +55,7 @@ describe("IntegrationCardComponent", () => {
{ provide: I18nService, useValue: mockI18nService },
{ provide: ActivatedRoute, useValue: activatedRoute },
{ provide: HecOrganizationIntegrationService, useValue: mockIntegrationService },
{ provide: DatadogOrganizationIntegrationService, useValue: mockDatadogIntegrationService },
{ provide: ToastService, useValue: toastService },
{ provide: DialogService, useValue: dialogService },
],

View File

@@ -13,17 +13,22 @@ import { Observable, Subject, combineLatest, lastValueFrom, takeUntil } from "rx
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
import { OrganizationIntegrationServiceType } 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 { DatadogOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/datadog-organization-integration-service";
import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-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";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { DialogService, ToastService } from "@bitwarden/components";
import { DialogRef, DialogService, ToastService } from "@bitwarden/components";
import { SharedModule } from "@bitwarden/web-vault/app/shared";
import {
HecConnectDialogResult,
DatadogConnectDialogResult,
HecConnectDialogResultStatus,
DatadogConnectDialogResultStatus,
openDatadogConnectDialog,
openHecConnectDialog,
} from "../integration-dialog/index";
@@ -64,6 +69,7 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
private dialogService: DialogService,
private activatedRoute: ActivatedRoute,
private hecOrganizationIntegrationService: HecOrganizationIntegrationService,
private datadogOrganizationIntegrationService: DatadogOrganizationIntegrationService,
private toastService: ToastService,
private i18nService: I18nService,
) {
@@ -131,42 +137,87 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
}
async setupConnection() {
// invoke the dialog to connect the integration
const dialog = openHecConnectDialog(this.dialogService, {
data: {
settings: this.integrationSettings,
},
});
let dialog: DialogRef<DatadogConnectDialogResult | HecConnectDialogResult, unknown>;
const result = await lastValueFrom(dialog.closed);
// the dialog was cancelled
if (!result || !result.success) {
if (this.integrationSettings?.integrationType === null) {
return;
}
try {
if (result.success === HecConnectDialogResultStatus.Delete) {
await this.deleteHec();
}
} catch {
this.toastService.showToast({
variant: "error",
title: "",
message: this.i18nService.t("failedToDeleteIntegration"),
if (this.integrationSettings?.integrationType === OrganizationIntegrationType.Datadog) {
dialog = openDatadogConnectDialog(this.dialogService, {
data: {
settings: this.integrationSettings,
},
});
}
try {
if (result.success === HecConnectDialogResultStatus.Edited) {
await this.saveHec(result);
const result = await lastValueFrom(dialog.closed);
// the dialog was cancelled
if (!result || !result.success) {
return;
}
} catch {
this.toastService.showToast({
variant: "error",
title: "",
message: this.i18nService.t("failedToSaveIntegration"),
try {
if (result.success === HecConnectDialogResultStatus.Delete) {
await this.deleteDatadog();
}
} catch {
this.toastService.showToast({
variant: "error",
title: "",
message: this.i18nService.t("failedToDeleteIntegration"),
});
}
try {
if (result.success === DatadogConnectDialogResultStatus.Edited) {
await this.saveDatadog(result as DatadogConnectDialogResult);
}
} catch {
this.toastService.showToast({
variant: "error",
title: "",
message: this.i18nService.t("failedToSaveIntegration"),
});
}
} else {
// invoke the dialog to connect the integration
dialog = openHecConnectDialog(this.dialogService, {
data: {
settings: this.integrationSettings,
},
});
const result = await lastValueFrom(dialog.closed);
// the dialog was cancelled
if (!result || !result.success) {
return;
}
try {
if (result.success === HecConnectDialogResultStatus.Delete) {
await this.deleteHec();
}
} catch {
this.toastService.showToast({
variant: "error",
title: "",
message: this.i18nService.t("failedToDeleteIntegration"),
});
}
try {
if (result.success === HecConnectDialogResultStatus.Edited) {
await this.saveHec(result as HecConnectDialogResult);
}
} catch {
this.toastService.showToast({
variant: "error",
title: "",
message: this.i18nService.t("failedToSaveIntegration"),
});
}
}
}
@@ -242,6 +293,69 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
});
}
async saveDatadog(result: DatadogConnectDialogResult) {
if (this.isUpdateAvailable) {
// retrieve org integration and configuration ids
const orgIntegrationId = this.integrationSettings.organizationIntegration?.id;
const orgIntegrationConfigurationId =
this.integrationSettings.organizationIntegration?.integrationConfiguration[0]?.id;
if (!orgIntegrationId || !orgIntegrationConfigurationId) {
throw Error("Organization Integration ID or Configuration ID is missing");
}
// update existing integration and configuration
await this.datadogOrganizationIntegrationService.updateDatadog(
this.organizationId,
orgIntegrationId,
orgIntegrationConfigurationId,
this.integrationSettings.name as OrganizationIntegrationServiceType,
result.url,
result.apiKey,
);
} else {
// create new integration and configuration
await this.datadogOrganizationIntegrationService.saveDatadog(
this.organizationId,
this.integrationSettings.name as OrganizationIntegrationServiceType,
result.url,
result.apiKey,
);
}
this.toastService.showToast({
variant: "success",
title: "",
message: this.i18nService.t("success"),
});
}
async deleteDatadog() {
const orgIntegrationId = this.integrationSettings.organizationIntegration?.id;
const orgIntegrationConfigurationId =
this.integrationSettings.organizationIntegration?.integrationConfiguration[0]?.id;
if (!orgIntegrationId || !orgIntegrationConfigurationId) {
throw Error("Organization Integration ID or Configuration ID is missing");
}
const response = await this.datadogOrganizationIntegrationService.deleteDatadog(
this.organizationId,
orgIntegrationId,
orgIntegrationConfigurationId,
);
if (response.mustBeOwner) {
this.showMustBeOwnerToast();
return;
}
this.toastService.showToast({
variant: "success",
title: "",
message: this.i18nService.t("success"),
});
}
private showMustBeOwnerToast() {
this.toastService.showToast({
variant: "error",

View File

@@ -0,0 +1,58 @@
<form [formGroup]="formGroup" [bitSubmit]="submit">
<bit-dialog dialogSize="large" [loading]="loading">
<span bitDialogTitle>
{{ "connectIntegrationButtonDesc" | i18n: connectInfo.settings.name }}
</span>
<div bitDialogContent class="tw-flex tw-flex-col tw-gap-4">
@if (loading) {
<ng-container #spinner>
<i class="bwi bwi-spinner bwi-lg bwi-spin" aria-hidden="true"></i>
</ng-container>
}
@if (!loading) {
<ng-container>
<bit-form-field>
<bit-label>{{ "url" | i18n }}</bit-label>
<input
bitInput
type="text"
formControlName="url"
placeholder="https://api.<region>.datadoghq.com"
/>
</bit-form-field>
<bit-form-field>
<bit-label>{{ "apiKey" | i18n }}</bit-label>
<input bitInput type="text" formControlName="apiKey" />
<bit-hint>{{ "apiKey" | i18n }}</bit-hint>
</bit-form-field>
</ng-container>
}
</div>
<ng-container bitDialogFooter>
<button type="submit" bitButton bitFormButton buttonType="primary" [disabled]="loading">
@if (isUpdateAvailable) {
{{ "update" | i18n }}
} @else {
{{ "save" | i18n }}
}
</button>
<button type="button" bitButton bitDialogClose buttonType="secondary" [disabled]="loading">
{{ "cancel" | i18n }}
</button>
@if (canDelete) {
<div class="tw-ml-auto">
<button
bitIconButton="bwi-trash"
type="button"
buttonType="danger"
label="'delete' | i18n"
[appA11yTitle]="'delete' | i18n"
[bitAction]="delete"
></button>
</div>
}
</ng-container>
</bit-dialog>
</form>

View File

@@ -0,0 +1,171 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { mock } from "jest-mock-extended";
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
import { IntegrationType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
import { SharedModule } from "@bitwarden/web-vault/app/shared";
import {
ConnectDatadogDialogComponent,
DatadogConnectDialogParams,
DatadogConnectDialogResult,
DatadogConnectDialogResultStatus,
openDatadogConnectDialog,
} from "./connect-dialog-datadog.component";
beforeAll(() => {
// Mock element.animate for jsdom
// the animate function is not available in jsdom, so we provide a mock implementation
// This is necessary for tests that rely on animations
// This mock does not perform any actual animations, it just provides a structure that allows tests
// to run without throwing errors related to missing animate function
if (!HTMLElement.prototype.animate) {
HTMLElement.prototype.animate = function () {
return {
play: () => {},
pause: () => {},
finish: () => {},
cancel: () => {},
reverse: () => {},
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => false,
onfinish: null,
oncancel: null,
startTime: 0,
currentTime: 0,
playbackRate: 1,
playState: "idle",
replaceState: "active",
effect: null,
finished: Promise.resolve(),
id: "",
remove: () => {},
timeline: null,
ready: Promise.resolve(),
} as unknown as Animation;
};
}
});
describe("ConnectDialogDatadogComponent", () => {
let component: ConnectDatadogDialogComponent;
let fixture: ComponentFixture<ConnectDatadogDialogComponent>;
let dialogRefMock = mock<DialogRef<DatadogConnectDialogResult>>();
const mockI18nService = mock<I18nService>();
const integrationMock: Integration = {
name: "Test Integration",
image: "test-image.png",
linkURL: "https://example.com",
imageDarkMode: "test-image-dark.png",
newBadgeExpiration: "2024-12-31",
description: "Test Description",
canSetupConnection: true,
type: IntegrationType.EVENT,
} as Integration;
const connectInfo: DatadogConnectDialogParams = {
settings: integrationMock, // Provide appropriate mock template if needed
};
beforeEach(async () => {
dialogRefMock = mock<DialogRef<DatadogConnectDialogResult>>();
await TestBed.configureTestingModule({
imports: [ReactiveFormsModule, SharedModule, BrowserAnimationsModule],
providers: [
FormBuilder,
{ provide: DIALOG_DATA, useValue: connectInfo },
{ provide: DialogRef, useValue: dialogRefMock },
{ provide: I18nPipe, useValue: mock<I18nPipe>() },
{ provide: I18nService, useValue: mockI18nService },
],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ConnectDatadogDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
mockI18nService.t.mockImplementation((key) => key);
});
it("should create the component", () => {
expect(component).toBeTruthy();
});
it("should initialize form with empty values", () => {
expect(component.formGroup.value).toEqual({
url: "",
apiKey: "",
service: "Test Integration",
});
});
it("should have required validators for all fields", () => {
component.formGroup.setValue({ url: "", apiKey: "", service: "" });
expect(component.formGroup.valid).toBeFalsy();
component.formGroup.setValue({
url: "https://test.com",
apiKey: "token",
service: "Test Service",
});
expect(component.formGroup.valid).toBeTruthy();
});
it("should test url is at least 7 characters long", () => {
component.formGroup.setValue({
url: "test",
apiKey: "token",
service: "Test Service",
});
expect(component.formGroup.valid).toBeFalsy();
component.formGroup.setValue({
url: "https://test.com",
apiKey: "token",
service: "Test Service",
});
expect(component.formGroup.valid).toBeTruthy();
});
it("should call dialogRef.close with correct result on submit", async () => {
component.formGroup.setValue({
url: "https://test.com",
apiKey: "token",
service: "Test Service",
});
await component.submit();
expect(dialogRefMock.close).toHaveBeenCalledWith({
integrationSettings: integrationMock,
url: "https://test.com",
apiKey: "token",
service: "Test Service",
success: DatadogConnectDialogResultStatus.Edited,
});
});
});
describe("openDatadogConnectDialog", () => {
it("should call dialogService.open with correct params", () => {
const dialogServiceMock = mock<DialogService>();
const config: DialogConfig<
DatadogConnectDialogParams,
DialogRef<DatadogConnectDialogResult>
> = {
data: { settings: { name: "Test" } as Integration },
} as any;
openDatadogConnectDialog(dialogServiceMock, config);
expect(dialogServiceMock.open).toHaveBeenCalledWith(ConnectDatadogDialogComponent, config);
});
});

View File

@@ -0,0 +1,121 @@
import { Component, Inject, OnInit } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { DatadogConfiguration } from "@bitwarden/bit-common/dirt/organization-integrations/models/configuration/datadog-configuration";
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
import { HecTemplate } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration-configuration-config/configuration-template/hec-template";
import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components";
import { SharedModule } from "@bitwarden/web-vault/app/shared";
export type DatadogConnectDialogParams = {
settings: Integration;
};
export interface DatadogConnectDialogResult {
integrationSettings: Integration;
url: string;
apiKey: string;
service: string;
success: DatadogConnectDialogResultStatusType | null;
}
export const DatadogConnectDialogResultStatus = {
Edited: "edit",
Delete: "delete",
} as const;
export type DatadogConnectDialogResultStatusType =
(typeof DatadogConnectDialogResultStatus)[keyof typeof DatadogConnectDialogResultStatus];
@Component({
templateUrl: "./connect-dialog-datadog.component.html",
imports: [SharedModule],
})
export class ConnectDatadogDialogComponent implements OnInit {
loading = false;
datadogConfig: DatadogConfiguration | null = null;
hecTemplate: HecTemplate | null = null;
formGroup = this.formBuilder.group({
url: ["", [Validators.required, Validators.minLength(7)]],
apiKey: ["", Validators.required],
service: ["", Validators.required],
});
constructor(
@Inject(DIALOG_DATA) protected connectInfo: DatadogConnectDialogParams,
protected formBuilder: FormBuilder,
private dialogRef: DialogRef<DatadogConnectDialogResult>,
private dialogService: DialogService,
) {}
ngOnInit(): void {
this.datadogConfig =
this.connectInfo.settings.organizationIntegration?.getConfiguration<DatadogConfiguration>() ??
null;
this.hecTemplate =
this.connectInfo.settings.organizationIntegration?.integrationConfiguration?.[0]?.getTemplate<HecTemplate>() ??
null;
this.formGroup.patchValue({
url: this.datadogConfig?.uri || "",
apiKey: this.datadogConfig?.apiKey || "",
service: this.connectInfo.settings.name,
});
}
get isUpdateAvailable(): boolean {
return !!this.datadogConfig;
}
get canDelete(): boolean {
return !!this.datadogConfig;
}
submit = async (): Promise<void> => {
if (this.formGroup.invalid) {
this.formGroup.markAllAsTouched();
return;
}
const result = this.getDatadogConnectDialogResult(DatadogConnectDialogResultStatus.Edited);
this.dialogRef.close(result);
return;
};
delete = async (): Promise<void> => {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "deleteItem" },
content: {
key: "deleteItemConfirmation",
},
type: "warning",
});
if (confirmed) {
const result = this.getDatadogConnectDialogResult(DatadogConnectDialogResultStatus.Delete);
this.dialogRef.close(result);
}
};
private getDatadogConnectDialogResult(
status: DatadogConnectDialogResultStatusType,
): DatadogConnectDialogResult {
const formJson = this.formGroup.getRawValue();
return {
integrationSettings: this.connectInfo.settings,
url: formJson.url || "",
apiKey: formJson.apiKey || "",
service: formJson.service || "",
success: status,
};
}
}
export function openDatadogConnectDialog(
dialogService: DialogService,
config: DialogConfig<DatadogConnectDialogParams, DialogRef<DatadogConnectDialogResult>>,
) {
return dialogService.open<DatadogConnectDialogResult>(ConnectDatadogDialogComponent, config);
}

View File

@@ -1 +1,2 @@
export * from "./connect-dialog/connect-dialog-hec.component";
export * from "./connect-dialog/connect-dialog-datadog.component";

View File

@@ -6,6 +6,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 { DatadogOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/datadog-organization-integration-service";
import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service";
import { IntegrationType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -24,6 +25,7 @@ describe("IntegrationGridComponent", () => {
let fixture: ComponentFixture<IntegrationGridComponent>;
const mockActivatedRoute = mock<ActivatedRoute>();
const mockIntegrationService = mock<HecOrganizationIntegrationService>();
const mockDatadogIntegrationService = mock<DatadogOrganizationIntegrationService>();
const integrations: Integration[] = [
{
name: "Integration 1",
@@ -70,6 +72,7 @@ describe("IntegrationGridComponent", () => {
useValue: mockActivatedRoute,
},
{ provide: HecOrganizationIntegrationService, useValue: mockIntegrationService },
{ provide: DatadogOrganizationIntegrationService, useValue: mockDatadogIntegrationService },
{
provide: ToastService,
useValue: mock<ToastService>(),

View File

@@ -4,6 +4,8 @@ import { firstValueFrom, Observable, Subject, switchMap, takeUntil, takeWhile }
import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
import { OrganizationIntegrationServiceType } 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 { DatadogOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/datadog-organization-integration-service";
import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
@@ -226,6 +228,7 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
// Sets the organization ID which also loads the integrations$
this.organization$.pipe(takeUntil(this.destroy$)).subscribe((org) => {
this.hecOrganizationIntegrationService.setOrganizationIntegrations(org.id);
this.datadogOrganizationIntegrationService.setOrganizationIntegrations(org.id);
});
// For all existing event based configurations loop through and assign the
@@ -253,6 +256,7 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
private accountService: AccountService,
private configService: ConfigService,
private hecOrganizationIntegrationService: HecOrganizationIntegrationService,
private datadogOrganizationIntegrationService: DatadogOrganizationIntegrationService,
) {
this.configService
.getFeatureFlag$(FeatureFlag.EventBasedOrganizationIntegrations)
@@ -270,10 +274,62 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
type: IntegrationType.EVENT,
description: "crowdstrikeEventIntegrationDesc",
canSetupConnection: true,
integrationType: 5, // Assuming 5 corresponds to CrowdStrike in OrganizationIntegrationType
};
this.integrationsList.push(crowdstrikeIntegration);
const datadogIntegration: Integration = {
name: OrganizationIntegrationServiceType.Datadog,
// TODO: Update link when help article is published
linkURL: "",
image: "../../../../../../../images/integrations/logo-datadog-color.svg",
type: IntegrationType.EVENT,
description: "datadogEventIntegrationDesc",
canSetupConnection: true,
integrationType: 6, // Assuming 6 corresponds to Datadog in OrganizationIntegrationType
};
this.integrationsList.push(datadogIntegration);
}
// For all existing event based configurations loop through and assign the
// organizationIntegration for the correct services.
this.hecOrganizationIntegrationService.integrations$
.pipe(takeUntil(this.destroy$))
.subscribe((integrations) => {
// reset all integrations to null first - in case one was deleted
this.integrationsList.forEach((i) => {
if (i.integrationType === OrganizationIntegrationType.Hec) {
i.organizationIntegration = null;
}
});
integrations.map((integration) => {
const item = this.integrationsList.find((i) => i.name === integration.serviceType);
if (item) {
item.organizationIntegration = integration;
}
});
});
this.datadogOrganizationIntegrationService.integrations$
.pipe(takeUntil(this.destroy$))
.subscribe((integrations) => {
// reset all integrations to null first - in case one was deleted
this.integrationsList.forEach((i) => {
if (i.integrationType === OrganizationIntegrationType.Datadog) {
i.organizationIntegration = null;
}
});
integrations.map((integration) => {
const item = this.integrationsList.find((i) => i.name === integration.serviceType);
if (item) {
item.organizationIntegration = integration;
}
});
});
}
ngOnDestroy(): void {
this.destroy$.next();

View File

@@ -1,5 +1,6 @@
import { NgModule } from "@angular/core";
import { DatadogOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/datadog-organization-integration-service";
import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service";
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";
@@ -12,6 +13,11 @@ import { OrganizationIntegrationsRoutingModule } from "./organization-integratio
@NgModule({
imports: [AdminConsoleIntegrationsComponent, OrganizationIntegrationsRoutingModule],
providers: [
safeProvider({
provide: DatadogOrganizationIntegrationService,
useClass: DatadogOrganizationIntegrationService,
deps: [OrganizationIntegrationApiService, OrganizationIntegrationConfigurationApiService],
}),
safeProvider({
provide: HecOrganizationIntegrationService,
useClass: HecOrganizationIntegrationService,

View File

@@ -9,6 +9,7 @@ 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 { DatadogOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/datadog-organization-integration-service";
import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { ThemeType } from "@bitwarden/common/platform/enums";
@@ -37,6 +38,7 @@ class MockNewMenuComponent {}
describe("IntegrationsComponent", () => {
let fixture: ComponentFixture<IntegrationsComponent>;
const hecOrgIntegrationSvc = mock<HecOrganizationIntegrationService>();
const datadogOrgIntegrationSvc = mock<DatadogOrganizationIntegrationService>();
const activatedRouteMock = {
snapshot: { paramMap: { get: jest.fn() } },
@@ -55,6 +57,7 @@ describe("IntegrationsComponent", () => {
{ provide: I18nPipe, useValue: mock<I18nPipe>() },
{ provide: I18nService, useValue: mockI18nService },
{ provide: HecOrganizationIntegrationService, useValue: hecOrgIntegrationSvc },
{ provide: DatadogOrganizationIntegrationService, useValue: datadogOrgIntegrationSvc },
],
}).compileComponents();
fixture = TestBed.createComponent(IntegrationsComponent);