mirror of
https://github.com/bitwarden/browser
synced 2025-12-27 21:53:25 +00:00
[SM-956] Secret Manager: Integrations Page (#8701)
* add navigation item for integrations and SDKs page * Initial routing to Integrations & SDKs page * Initial add of integrations component * Initial add of SDKs component * add secret manage integration images * remove integration & sdk components in favor of a single component * add integration & integration grid components * add integrations & sdks * rename page & components to integration after design discussion * add external rel attribute for SDK links * remove ts extension * refactor: use pseudo element to cover as a link * refactor: change secondaryText to linkText to align with usage * update icon for integrations * add new badge option for integration cards * hardcode integration/sdk names * add dark mode images for integrations and sdks * update integration/sdk card with dark mode image when applicable * refactor integration types to be an enum * fix enum typings in integration grid test --------- Co-authored-by: Robyn MacCallum <robyntmaccallum@gmail.com>
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
<div
|
||||
class="tw-block tw-h-full tw-overflow-hidden tw-rounded tw-border tw-border-solid tw-border-secondary-600 tw-relative tw-transition-all xl:tw-w-64 hover:tw-scale-105 focus-within:tw-outline-none focus-within:tw-ring focus-within:tw-ring-primary-700 focus-within:tw-ring-offset-2"
|
||||
>
|
||||
<div
|
||||
class="tw-flex tw-h-36 tw-bg-secondary-100 tw-items-center tw-justify-center tw-py-2 tw-px-6 lg:tw-py-4 lg:tw-px-12"
|
||||
>
|
||||
<div class="tw-flex tw-items-center tw-justify-center tw-h-28 tw-w-28 lg:tw-w-40">
|
||||
<img
|
||||
#imageEle
|
||||
[src]="image"
|
||||
alt=""
|
||||
class="tw-block tw-mx-auto tw-h-auto tw-max-w-full tw-max-h-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw-p-5">
|
||||
<h3 class="tw-mb-4 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-w-full after:tw-h-full after:tw-left-0 after:tw-top-0"
|
||||
[href]="linkURL"
|
||||
[rel]="[externalURL ? 'noopener noreferrer' : null]"
|
||||
>
|
||||
{{ linkText }}
|
||||
</a>
|
||||
<span *ngIf="showNewBadge()" bitBadge class="tw-mt-3" variant="secondary">
|
||||
{{ "new" | i18n }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,174 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { SYSTEM_THEME_OBSERVABLE } from "../../../../../../../libs/angular/src/services/injection-tokens";
|
||||
import { ThemeType } from "../../../../../../../libs/common/src/platform/enums";
|
||||
import { ThemeStateService } from "../../../../../../../libs/common/src/platform/theming/theme-state.service";
|
||||
|
||||
import { IntegrationCardComponent } from "./integration-card.component";
|
||||
|
||||
describe("IntegrationCardComponent", () => {
|
||||
let component: IntegrationCardComponent;
|
||||
let fixture: ComponentFixture<IntegrationCardComponent>;
|
||||
|
||||
const systemTheme$ = new BehaviorSubject<ThemeType>(ThemeType.Light);
|
||||
const usersPreferenceTheme$ = new BehaviorSubject<ThemeType>(ThemeType.Light);
|
||||
|
||||
beforeEach(async () => {
|
||||
// reset system theme
|
||||
systemTheme$.next(ThemeType.Light);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [IntegrationCardComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: ThemeStateService,
|
||||
useValue: { selectedTheme$: usersPreferenceTheme$ },
|
||||
},
|
||||
{
|
||||
provide: SYSTEM_THEME_OBSERVABLE,
|
||||
useValue: systemTheme$,
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(IntegrationCardComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
component.name = "Integration Name";
|
||||
component.image = "test-image.png";
|
||||
component.linkText = "Get started with integration";
|
||||
component.linkURL = "https://example.com/";
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("assigns link href", () => {
|
||||
const link = fixture.nativeElement.querySelector("a");
|
||||
|
||||
expect(link.href).toBe("https://example.com/");
|
||||
});
|
||||
|
||||
it("renders card body", () => {
|
||||
const name = fixture.nativeElement.querySelector("h3");
|
||||
const link = fixture.nativeElement.querySelector("a");
|
||||
|
||||
expect(name.textContent).toBe("Integration Name");
|
||||
expect(link.textContent.trim()).toBe("Get started with integration");
|
||||
});
|
||||
|
||||
it("assigns external rel attribute", () => {
|
||||
component.externalURL = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
const link = fixture.nativeElement.querySelector("a");
|
||||
|
||||
expect(link.rel).toBe("noopener noreferrer");
|
||||
});
|
||||
|
||||
describe("new badge", () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date("2023-09-01"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("shows when expiration is in the future", () => {
|
||||
component.newBadgeExpiration = "2023-09-02";
|
||||
expect(component.showNewBadge()).toBe(true);
|
||||
});
|
||||
|
||||
it("does not show when expiration is not set", () => {
|
||||
expect(component.showNewBadge()).toBe(false);
|
||||
});
|
||||
|
||||
it("does not show when expiration is in the past", () => {
|
||||
component.newBadgeExpiration = "2023-08-31";
|
||||
expect(component.showNewBadge()).toBe(false);
|
||||
});
|
||||
|
||||
it("does not show when expiration is today", () => {
|
||||
component.newBadgeExpiration = "2023-09-01";
|
||||
expect(component.showNewBadge()).toBe(false);
|
||||
});
|
||||
|
||||
it("does not show when expiration is invalid", () => {
|
||||
component.newBadgeExpiration = "not-a-date";
|
||||
expect(component.showNewBadge()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("imageDarkMode", () => {
|
||||
it("ignores theme changes when darkModeImage is not set", () => {
|
||||
systemTheme$.next(ThemeType.Dark);
|
||||
usersPreferenceTheme$.next(ThemeType.Dark);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.imageEle.nativeElement.src).toContain("test-image.png");
|
||||
});
|
||||
|
||||
describe("user prefers the system theme", () => {
|
||||
beforeEach(() => {
|
||||
component.imageDarkMode = "test-image-dark.png";
|
||||
});
|
||||
|
||||
it("sets image src to imageDarkMode", () => {
|
||||
usersPreferenceTheme$.next(ThemeType.System);
|
||||
systemTheme$.next(ThemeType.Dark);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
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";
|
||||
|
||||
usersPreferenceTheme$.next(ThemeType.System);
|
||||
systemTheme$.next(ThemeType.Light);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.imageEle.nativeElement.src).toContain("test-image.png");
|
||||
});
|
||||
});
|
||||
|
||||
describe("user prefers dark mode", () => {
|
||||
beforeEach(() => {
|
||||
component.imageDarkMode = "test-image-dark.png";
|
||||
});
|
||||
|
||||
it("updates image to dark mode", () => {
|
||||
systemTheme$.next(ThemeType.Light); // system theme shouldn't matter
|
||||
usersPreferenceTheme$.next(ThemeType.Dark);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.imageEle.nativeElement.src).toContain("test-image-dark.png");
|
||||
});
|
||||
});
|
||||
|
||||
describe("user prefers light mode", () => {
|
||||
beforeEach(() => {
|
||||
component.imageDarkMode = "test-image-dark.png";
|
||||
});
|
||||
|
||||
it("updates image to light mode", () => {
|
||||
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");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
ElementRef,
|
||||
Inject,
|
||||
Input,
|
||||
OnDestroy,
|
||||
ViewChild,
|
||||
} from "@angular/core";
|
||||
import { Observable, Subject, combineLatest, takeUntil } from "rxjs";
|
||||
|
||||
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
|
||||
@Component({
|
||||
selector: "sm-integration-card",
|
||||
templateUrl: "./integration-card.component.html",
|
||||
})
|
||||
export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
private destroyed$: Subject<void> = new Subject();
|
||||
@ViewChild("imageEle") imageEle: ElementRef<HTMLImageElement>;
|
||||
|
||||
@Input() name: string;
|
||||
@Input() image: string;
|
||||
@Input() imageDarkMode?: string;
|
||||
@Input() linkText: string;
|
||||
@Input() linkURL: string;
|
||||
|
||||
/** Adds relevant `rel` attribute to external links */
|
||||
@Input() externalURL?: boolean;
|
||||
|
||||
/**
|
||||
* Date of when the new badge should be hidden.
|
||||
* When omitted, the new badge is never shown.
|
||||
*
|
||||
* @example "2024-12-31"
|
||||
*/
|
||||
@Input() newBadgeExpiration?: string;
|
||||
|
||||
constructor(
|
||||
private themeStateService: ThemeStateService,
|
||||
@Inject(SYSTEM_THEME_OBSERVABLE)
|
||||
private systemTheme$: Observable<ThemeType>,
|
||||
) {}
|
||||
|
||||
ngAfterViewInit() {
|
||||
combineLatest([this.themeStateService.selectedTheme$, this.systemTheme$])
|
||||
.pipe(takeUntil(this.destroyed$))
|
||||
.subscribe(([theme, systemTheme]) => {
|
||||
// When the card doesn't have a dark mode image, exit early
|
||||
if (!this.imageDarkMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (theme === ThemeType.System) {
|
||||
// When the user's preference is the system theme,
|
||||
// use the system theme to determine the image
|
||||
const prefersDarkMode =
|
||||
systemTheme === ThemeType.Dark || systemTheme === ThemeType.SolarizedDark;
|
||||
|
||||
this.imageEle.nativeElement.src = prefersDarkMode ? this.imageDarkMode : this.image;
|
||||
} else if (theme === ThemeType.Dark || theme === ThemeType.SolarizedDark) {
|
||||
// When the user's preference is dark mode, use the dark mode image
|
||||
this.imageEle.nativeElement.src = this.imageDarkMode;
|
||||
} else {
|
||||
// Otherwise use the light mode image
|
||||
this.imageEle.nativeElement.src = this.image;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed$.next();
|
||||
this.destroyed$.complete();
|
||||
}
|
||||
|
||||
/** Show the "new" badge when expiration is in the future */
|
||||
showNewBadge() {
|
||||
if (!this.newBadgeExpiration) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const expirationDate = new Date(this.newBadgeExpiration);
|
||||
|
||||
// Do not show the new badge for invalid dates
|
||||
if (isNaN(expirationDate.getTime())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return expirationDate > new Date();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<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"
|
||||
>
|
||||
<li *ngFor="let integration of integrations">
|
||||
<sm-integration-card
|
||||
[name]="integration.name"
|
||||
[linkText]="integration.linkText"
|
||||
[linkURL]="integration.linkURL"
|
||||
[image]="integration.image"
|
||||
[imageDarkMode]="integration.imageDarkMode"
|
||||
[externalURL]="integration.type === IntegrationType.SDK"
|
||||
[newBadgeExpiration]="integration.newBadgeExpiration"
|
||||
></sm-integration-card>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -0,0 +1,81 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { SYSTEM_THEME_OBSERVABLE } from "../../../../../../../libs/angular/src/services/injection-tokens";
|
||||
import { IntegrationType } from "../../../../../../../libs/common/src/enums";
|
||||
import { ThemeType } from "../../../../../../../libs/common/src/platform/enums";
|
||||
import { ThemeStateService } from "../../../../../../../libs/common/src/platform/theming/theme-state.service";
|
||||
import { IntegrationCardComponent } from "../integration-card/integration-card.component";
|
||||
import { Integration } from "../models/integration";
|
||||
|
||||
import { IntegrationGridComponent } from "./integration-grid.component";
|
||||
|
||||
describe("IntegrationGridComponent", () => {
|
||||
let component: IntegrationGridComponent;
|
||||
let fixture: ComponentFixture<IntegrationGridComponent>;
|
||||
const integrations: Integration[] = [
|
||||
{
|
||||
name: "Integration 1",
|
||||
image: "test-image1.png",
|
||||
linkText: "Get started with integration 1",
|
||||
linkURL: "https://example.com/1",
|
||||
type: IntegrationType.Integration,
|
||||
},
|
||||
{
|
||||
name: "SDK 2",
|
||||
image: "test-image2.png",
|
||||
linkText: "View SDK 2",
|
||||
linkURL: "https://example.com/2",
|
||||
type: IntegrationType.SDK,
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [IntegrationGridComponent, IntegrationCardComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: ThemeStateService,
|
||||
useValue: mock<ThemeStateService>(),
|
||||
},
|
||||
{
|
||||
provide: SYSTEM_THEME_OBSERVABLE,
|
||||
useValue: of(ThemeType.Light),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(IntegrationGridComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.integrations = integrations;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("lists all integrations", () => {
|
||||
expect(component.integrations).toEqual(integrations);
|
||||
|
||||
const cards = fixture.debugElement.queryAll(By.directive(IntegrationCardComponent));
|
||||
|
||||
expect(cards.length).toBe(integrations.length);
|
||||
});
|
||||
|
||||
it("assigns the correct attributes to IntegrationCardComponent", () => {
|
||||
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.linkText).toBe("View SDK 2");
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Component, Input } from "@angular/core";
|
||||
|
||||
import { IntegrationType } from "@bitwarden/common/enums";
|
||||
|
||||
import { Integration } from "../models/integration";
|
||||
|
||||
@Component({
|
||||
selector: "sm-integration-grid",
|
||||
templateUrl: "./integration-grid.component.html",
|
||||
})
|
||||
export class IntegrationGridComponent {
|
||||
@Input() integrations: Integration[];
|
||||
|
||||
protected IntegrationType = IntegrationType;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { IntegrationsComponent } from "./integrations.component";
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: "",
|
||||
component: IntegrationsComponent,
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class IntegrationsRoutingModule {}
|
||||
@@ -0,0 +1,16 @@
|
||||
<app-header>
|
||||
<sm-new-menu></sm-new-menu>
|
||||
</app-header>
|
||||
|
||||
<section class="tw-mb-9">
|
||||
<p bitTypography="body1">{{ "integrationsDesc" | i18n }}</p>
|
||||
<sm-integration-grid [integrations]="integrations"></sm-integration-grid>
|
||||
</section>
|
||||
|
||||
<section class="tw-mb-9">
|
||||
<h2 bitTypography="h2">
|
||||
{{ "sdks" | i18n }}
|
||||
</h2>
|
||||
<p bitTypography="body1">{{ "sdksDesc" | i18n }}</p>
|
||||
<sm-integration-grid [integrations]="sdks"></sm-integration-grid>
|
||||
</section>
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { SYSTEM_THEME_OBSERVABLE } from "../../../../../../libs/angular/src/services/injection-tokens";
|
||||
import { I18nService } from "../../../../../../libs/common/src/platform/abstractions/i18n.service";
|
||||
import { ThemeType } from "../../../../../../libs/common/src/platform/enums";
|
||||
import { ThemeStateService } from "../../../../../../libs/common/src/platform/theming/theme-state.service";
|
||||
import { I18nPipe } from "../../../../../../libs/components/src/shared/i18n.pipe";
|
||||
|
||||
import { IntegrationCardComponent } from "./integration-card/integration-card.component";
|
||||
import { IntegrationGridComponent } from "./integration-grid/integration-grid.component";
|
||||
import { IntegrationsComponent } from "./integrations.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-header",
|
||||
template: "<div></div>",
|
||||
})
|
||||
class MockHeaderComponent {}
|
||||
|
||||
@Component({
|
||||
selector: "sm-new-menu",
|
||||
template: "<div></div>",
|
||||
})
|
||||
class MockNewMenuComponent {}
|
||||
|
||||
describe("IntegrationsComponent", () => {
|
||||
let fixture: ComponentFixture<IntegrationsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
IntegrationsComponent,
|
||||
IntegrationGridComponent,
|
||||
IntegrationCardComponent,
|
||||
MockHeaderComponent,
|
||||
MockNewMenuComponent,
|
||||
I18nPipe,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: I18nService,
|
||||
useValue: mock<I18nService>({ t: (key) => key }),
|
||||
},
|
||||
{
|
||||
provide: ThemeStateService,
|
||||
useValue: mock<ThemeStateService>(),
|
||||
},
|
||||
{
|
||||
provide: SYSTEM_THEME_OBSERVABLE,
|
||||
useValue: of(ThemeType.Light),
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
fixture = TestBed.createComponent(IntegrationsComponent);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("divides Integrations & SDKS", () => {
|
||||
const [integrationList, sdkList] = fixture.debugElement.queryAll(
|
||||
By.directive(IntegrationGridComponent),
|
||||
);
|
||||
|
||||
// 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"]);
|
||||
|
||||
expect(
|
||||
(sdkList.componentInstance as IntegrationGridComponent).integrations.map((i) => i.name),
|
||||
).toEqual(["C#", "C++", "Go", "Java", "JS WebAssembly", "php", "Python", "Ruby"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { IntegrationType } from "@bitwarden/common/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
import { Integration } from "./models/integration";
|
||||
|
||||
@Component({
|
||||
selector: "sm-integrations",
|
||||
templateUrl: "./integrations.component.html",
|
||||
})
|
||||
export class IntegrationsComponent {
|
||||
private integrationsAndSdks: Integration[] = [];
|
||||
|
||||
constructor(i18nService: I18nService) {
|
||||
this.integrationsAndSdks = [
|
||||
{
|
||||
name: "GitHub Actions",
|
||||
linkText: i18nService.t("setUpGithubActions"),
|
||||
linkURL: "https://bitwarden.com/help/github-actions-integration/",
|
||||
image: "../../../../../../../images/secrets-manager/integrations/github.svg",
|
||||
imageDarkMode: "../../../../../../../images/secrets-manager/integrations/github-white.svg",
|
||||
type: IntegrationType.Integration,
|
||||
},
|
||||
{
|
||||
name: "GitLab CI/CD",
|
||||
linkText: i18nService.t("setUpGitlabCICD"),
|
||||
linkURL: "https://bitwarden.com/help/gitlab-integration/",
|
||||
image: "../../../../../../../images/secrets-manager/integrations/gitlab.svg",
|
||||
imageDarkMode: "../../../../../../../images/secrets-manager/integrations/gitlab-white.svg",
|
||||
type: IntegrationType.Integration,
|
||||
},
|
||||
{
|
||||
name: "Ansible",
|
||||
linkText: i18nService.t("setUpAnsible"),
|
||||
linkURL: "https://bitwarden.com/help/ansible-integration/",
|
||||
image: "../../../../../../../images/secrets-manager/integrations/ansible.svg",
|
||||
type: IntegrationType.Integration,
|
||||
},
|
||||
{
|
||||
name: "C#",
|
||||
linkText: i18nService.t("cSharpSDKRepo"),
|
||||
linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/csharp",
|
||||
image: "../../../../../../../images/secrets-manager/sdks/c-sharp.svg",
|
||||
type: IntegrationType.SDK,
|
||||
},
|
||||
{
|
||||
name: "C++",
|
||||
linkText: i18nService.t("cPlusPlusSDKRepo"),
|
||||
linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/cpp",
|
||||
image: "../../../../../../../images/secrets-manager/sdks/c-plus-plus.png",
|
||||
type: IntegrationType.SDK,
|
||||
},
|
||||
{
|
||||
name: "Go",
|
||||
linkText: i18nService.t("goSDKRepo"),
|
||||
linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/go",
|
||||
image: "../../../../../../../images/secrets-manager/sdks/go.svg",
|
||||
type: IntegrationType.SDK,
|
||||
},
|
||||
{
|
||||
name: "Java",
|
||||
linkText: i18nService.t("javaSDKRepo"),
|
||||
linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/java",
|
||||
image: "../../../../../../../images/secrets-manager/sdks/java.svg",
|
||||
imageDarkMode: "../../../../../../../images/secrets-manager/sdks/java-white.svg",
|
||||
type: IntegrationType.SDK,
|
||||
},
|
||||
{
|
||||
name: "JS WebAssembly",
|
||||
linkText: i18nService.t("jsWebAssemblySDKRepo"),
|
||||
linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/js",
|
||||
image: "../../../../../../../images/secrets-manager/sdks/wasm.svg",
|
||||
type: IntegrationType.SDK,
|
||||
},
|
||||
{
|
||||
name: "php",
|
||||
linkText: i18nService.t("phpSDKRepo"),
|
||||
linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/php",
|
||||
image: "../../../../../../../images/secrets-manager/sdks/php.svg",
|
||||
type: IntegrationType.SDK,
|
||||
},
|
||||
{
|
||||
name: "Python",
|
||||
linkText: i18nService.t("pythonSDKRepo"),
|
||||
linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/python",
|
||||
image: "../../../../../../../images/secrets-manager/sdks/python.svg",
|
||||
type: IntegrationType.SDK,
|
||||
},
|
||||
{
|
||||
name: "Ruby",
|
||||
linkText: i18nService.t("rubySDKRepo"),
|
||||
linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/ruby",
|
||||
image: "../../../../../../../images/secrets-manager/sdks/ruby.png",
|
||||
type: IntegrationType.SDK,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** Filter out content for the integrations sections */
|
||||
get integrations(): Integration[] {
|
||||
return this.integrationsAndSdks.filter(
|
||||
(integration) => integration.type === IntegrationType.Integration,
|
||||
);
|
||||
}
|
||||
|
||||
/** Filter out content for the SDKs section */
|
||||
get sdks(): Integration[] {
|
||||
return this.integrationsAndSdks.filter(
|
||||
(integration) => integration.type === IntegrationType.SDK,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { SecretsManagerSharedModule } from "../shared/sm-shared.module";
|
||||
|
||||
import { IntegrationCardComponent } from "./integration-card/integration-card.component";
|
||||
import { IntegrationGridComponent } from "./integration-grid/integration-grid.component";
|
||||
import { IntegrationsRoutingModule } from "./integrations-routing.module";
|
||||
import { IntegrationsComponent } from "./integrations.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [SecretsManagerSharedModule, IntegrationsRoutingModule],
|
||||
declarations: [IntegrationsComponent, IntegrationGridComponent, IntegrationCardComponent],
|
||||
providers: [],
|
||||
})
|
||||
export class IntegrationsModule {}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { IntegrationType } from "@bitwarden/common/enums";
|
||||
|
||||
/** Integration or SDK */
|
||||
export type Integration = {
|
||||
name: string;
|
||||
image: string;
|
||||
/**
|
||||
* Optional image shown in dark mode.
|
||||
*/
|
||||
imageDarkMode?: string;
|
||||
linkURL: string;
|
||||
linkText: string;
|
||||
type: IntegrationType;
|
||||
/**
|
||||
* Shows the "New" badge until the defined date.
|
||||
* When omitted, the badge is never shown.
|
||||
*
|
||||
* @example "2024-12-31"
|
||||
*/
|
||||
newBadgeExpiration?: string;
|
||||
};
|
||||
@@ -22,6 +22,12 @@
|
||||
route="machine-accounts"
|
||||
[relativeTo]="route.parent"
|
||||
></bit-nav-item>
|
||||
<bit-nav-item
|
||||
icon="bwi-providers"
|
||||
[text]="'integrations' | i18n"
|
||||
route="integrations"
|
||||
[relativeTo]="route.parent"
|
||||
></bit-nav-item>
|
||||
<bit-nav-item
|
||||
icon="bwi-trash"
|
||||
[text]="'trash' | i18n"
|
||||
|
||||
@@ -5,6 +5,7 @@ import { AuthGuard } from "@bitwarden/angular/auth/guards";
|
||||
|
||||
import { organizationEnabledGuard } from "./guards/sm-org-enabled.guard";
|
||||
import { canActivateSM } from "./guards/sm.guard";
|
||||
import { IntegrationsModule } from "./integrations/integrations.module";
|
||||
import { LayoutComponent } from "./layout/layout.component";
|
||||
import { NavigationComponent } from "./layout/navigation.component";
|
||||
import { OverviewModule } from "./overview/overview.module";
|
||||
@@ -60,6 +61,13 @@ const routes: Routes = [
|
||||
titleId: "machineAccounts",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "integrations",
|
||||
loadChildren: () => IntegrationsModule,
|
||||
data: {
|
||||
titleId: "integrations",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "trash",
|
||||
loadChildren: () => TrashModule,
|
||||
|
||||
Reference in New Issue
Block a user