1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-06 18:43:25 +00:00

[PM-23825] setup crowdstrike card (#15728)

This commit is contained in:
Vijay Oommen
2025-07-24 08:53:03 -05:00
committed by GitHub
parent d0d1359ff4
commit df8e0ed094
9 changed files with 153 additions and 16 deletions

View File

@@ -1,8 +1,8 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnInit } from "@angular/core";
import { Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { Observable, switchMap } from "rxjs";
import { Observable, Subject, switchMap, takeUntil } from "rxjs";
import {
getOrganizationById,
@@ -11,6 +11,8 @@ import {
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { IntegrationType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { HeaderModule } from "../../../layouts/header/header.module";
import { SharedModule } from "../../../shared/shared.module";
@@ -30,10 +32,12 @@ import { Integration } from "../shared/components/integrations/models";
FilterIntegrationsPipe,
],
})
export class AdminConsoleIntegrationsComponent implements OnInit {
export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
integrationsList: Integration[] = [];
tabIndex: number;
organization$: Observable<Organization>;
isEventBasedIntegrationsEnabled: boolean = false;
private destroy$ = new Subject<void>();
ngOnInit(): void {
this.organization$ = this.route.params.pipe(
@@ -53,7 +57,15 @@ export class AdminConsoleIntegrationsComponent implements OnInit {
private route: ActivatedRoute,
private organizationService: OrganizationService,
private accountService: AccountService,
private configService: ConfigService,
) {
this.configService
.getFeatureFlag$(FeatureFlag.EventBasedOrganizationIntegrations)
.pipe(takeUntil(this.destroy$))
.subscribe((isEnabled) => {
this.isEventBasedIntegrationsEnabled = isEnabled;
});
this.integrationsList = [
{
name: "AD FS",
@@ -229,6 +241,22 @@ export class AdminConsoleIntegrationsComponent implements OnInit {
type: IntegrationType.DEVICE,
},
];
if (this.isEventBasedIntegrationsEnabled) {
this.integrationsList.push({
name: "Crowdstrike",
linkURL: "",
image: "../../../../../../../images/integrations/logo-crowdstrike-black.svg",
type: IntegrationType.EVENT,
description: "crowdstrikeEventIntegrationDesc",
isConnected: false,
canSetupConnection: true,
});
}
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
get IntegrationType(): typeof IntegrationType {

View File

@@ -17,16 +17,40 @@
</div>
</div>
<div class="tw-p-5">
<h3 class="tw-text-main tw-text-lg tw-font-semibold">{{ name }}</h3>
<a
class="tw-block tw-mb-0 tw-font-bold 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"
[href]="linkURL"
rel="noopener noreferrer"
target="_blank"
>
</a>
<span *ngIf="showNewBadge()" bitBadge class="tw-mt-3" variant="secondary">
{{ "new" | i18n }}
</span>
<h3 class="tw-text-main tw-text-lg tw-font-semibold">
{{ name }}
@if (showConnectedBadge()) {
<span class="tw-ml-3">
@if (isConnected) {
<span bitBadge variant="success">{{ "on" | i18n }}</span>
}
@if (!isConnected) {
<span bitBadge>{{ "off" | i18n }}</span>
}
</span>
}
</h3>
<p class="tw-mb-0">{{ description }}</p>
@if (canSetupConnection) {
<button type="button" class="tw-mt-3" bitButton (click)="setupConnection(name)">
<span>{{ "connectIntegrationButtonDesc" | i18n: name }}</span>
</button>
}
@if (linkURL) {
<a
class="tw-block tw-mb-0 tw-font-bold 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"
[href]="linkURL"
rel="noopener noreferrer"
target="_blank"
>
</a>
}
@if (showNewBadge()) {
<span bitBadge class="tw-mt-3" variant="secondary">
{{ "new" | i18n }}
</span>
}
</div>
</div>

View File

@@ -16,6 +16,7 @@ import { IntegrationCardComponent } from "./integration-card.component";
describe("IntegrationCardComponent", () => {
let component: IntegrationCardComponent;
let fixture: ComponentFixture<IntegrationCardComponent>;
const mockI18nService = mock<I18nService>();
const systemTheme$ = new BehaviorSubject<ThemeType>(ThemeType.Light);
const usersPreferenceTheme$ = new BehaviorSubject<ThemeType>(ThemeType.Light);
@@ -41,7 +42,7 @@ describe("IntegrationCardComponent", () => {
},
{
provide: I18nService,
useValue: mock<I18nService>(),
useValue: mockI18nService,
},
],
}).compileComponents();
@@ -55,6 +56,7 @@ describe("IntegrationCardComponent", () => {
component.image = "test-image.png";
component.linkURL = "https://example.com/";
mockI18nService.t.mockImplementation((key) => key);
fixture.detectChanges();
});
@@ -67,7 +69,7 @@ describe("IntegrationCardComponent", () => {
it("renders card body", () => {
const name = fixture.nativeElement.querySelector("h3");
expect(name.textContent).toBe("Integration Name");
expect(name.textContent).toContain("Integration Name");
});
it("assigns external rel attribute", () => {
@@ -182,4 +184,28 @@ describe("IntegrationCardComponent", () => {
});
});
});
describe("connected badge", () => {
it("shows connected badge when isConnected is true", () => {
component.isConnected = true;
expect(component.showConnectedBadge()).toBe(true);
});
it("does not show connected badge when isConnected is false", () => {
component.isConnected = false;
fixture.detectChanges();
const name = fixture.nativeElement.querySelector("h3 > span > span > span");
expect(name.textContent).toContain("off");
// when isConnected is true/false, the badge should be shown as on/off
// when isConnected is undefined, the badge should not be shown
expect(component.showConnectedBadge()).toBe(true);
});
it("does not show connected badge when isConnected is undefined", () => {
component.isConnected = undefined;
expect(component.showConnectedBadge()).toBe(false);
});
});
});

View File

@@ -41,6 +41,9 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
* @example "2024-12-31"
*/
@Input() newBadgeExpiration?: string;
@Input() description?: string;
@Input() isConnected?: boolean;
@Input() canSetupConnection?: boolean;
constructor(
private themeStateService: ThemeStateService,
@@ -93,4 +96,14 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
return expirationDate > new Date();
}
showConnectedBadge(): boolean {
return this.isConnected !== undefined;
}
setupConnection(app: string) {
// This method can be used to handle the connection logic for the integration
// For example, it could open a modal or redirect to a setup page
this.isConnected = !this.isConnected; // Toggle connection state for demonstration
}
}

View File

@@ -13,6 +13,9 @@
[imageDarkMode]="integration.imageDarkMode"
[externalURL]="integration.type === IntegrationType.SDK"
[newBadgeExpiration]="integration.newBadgeExpiration"
[description]="integration.description | i18n"
[isConnected]="integration.isConnected"
[canSetupConnection]="integration.canSetupConnection"
></app-integration-card>
</li>
</ul>

View File

@@ -17,4 +17,7 @@ export type Integration = {
* @example "2024-12-31"
*/
newBadgeExpiration?: string;
description?: string;
isConnected?: boolean;
canSetupConnection?: boolean;
};