mirror of
https://github.com/bitwarden/browser
synced 2026-01-08 11:33:28 +00:00
[PM-12273] Admin Console Integration Page (#11883)
* Integration page initial implementation * replace placeholder integrations * fix linting and tests * fix locales * update locale * Change logos, add link to SCIM page * refactor to standalone components, add integration filtering pipe * refactor modules and imports. Remove hyperlink text from integration card template * refactor i18n usage to be more generic * Add storybooks * fix tests * minify svgs, include spec files in TS config, fix stories * Update apps/web/src/locales/en/messages.json Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> * fix imports, add static dir for stories * Add darkmode svgs for integrations * hide nav link for non enterprise orgs * add router guard * change rxjs selector * Remove tailwind class causing style issues --------- Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
This commit is contained in:
4
apps/web/src/app/shared/components/index.ts
Normal file
4
apps/web/src/app/shared/components/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./integrations/integration-card/integration-card.component";
|
||||
export * from "./integrations/integration-grid/integration-grid.component";
|
||||
export * from "./integrations/integrations.pipe";
|
||||
export * from "./integrations/models";
|
||||
@@ -0,0 +1,32 @@
|
||||
<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 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-bg-secondary-100 tw-items-center tw-justify-end tw-pt-4 tw-pr-4">
|
||||
<i class="bwi bwi-external-link"></i>
|
||||
</div>
|
||||
<div
|
||||
class="tw-flex tw-h-32 tw-bg-secondary-100 tw-items-center tw-justify-center tw-pb-2 tw-px-6 lg:tw-pb-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-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-w-full after:tw-h-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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,183 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
|
||||
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 { SharedModule } from "@bitwarden/components/src/shared";
|
||||
import { I18nPipe } from "@bitwarden/components/src/shared/i18n.pipe";
|
||||
|
||||
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({
|
||||
imports: [IntegrationCardComponent, SharedModule],
|
||||
providers: [
|
||||
{
|
||||
provide: ThemeStateService,
|
||||
useValue: { selectedTheme$: usersPreferenceTheme$ },
|
||||
},
|
||||
{
|
||||
provide: SYSTEM_THEME_OBSERVABLE,
|
||||
useValue: systemTheme$,
|
||||
},
|
||||
{
|
||||
provide: I18nPipe,
|
||||
useValue: mock<I18nPipe>(),
|
||||
},
|
||||
{
|
||||
provide: I18nService,
|
||||
useValue: mock<I18nService>(),
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(IntegrationCardComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
component.name = "Integration Name";
|
||||
component.image = "test-image.png";
|
||||
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");
|
||||
|
||||
expect(name.textContent).toBe("Integration Name");
|
||||
});
|
||||
|
||||
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,96 @@
|
||||
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";
|
||||
|
||||
import { SharedModule } from "../../../shared.module";
|
||||
|
||||
@Component({
|
||||
selector: "app-integration-card",
|
||||
templateUrl: "./integration-card.component.html",
|
||||
standalone: true,
|
||||
imports: [SharedModule],
|
||||
})
|
||||
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() 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,63 @@
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
import { I18nMockService } from "@bitwarden/components";
|
||||
|
||||
import { SharedModule } from "../../../shared.module";
|
||||
|
||||
import { IntegrationCardComponent } from "./integration-card.component";
|
||||
|
||||
class MockThemeService implements Partial<ThemeStateService> {}
|
||||
|
||||
export default {
|
||||
title: "Web/Integration Layout/Integration Card",
|
||||
component: IntegrationCardComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [SharedModule],
|
||||
providers: [
|
||||
{
|
||||
provide: I18nService,
|
||||
useFactory: () => {
|
||||
return new I18nMockService({});
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ThemeStateService,
|
||||
useClass: MockThemeService,
|
||||
},
|
||||
{
|
||||
provide: SYSTEM_THEME_OBSERVABLE,
|
||||
useValue: of(ThemeTypes.Light),
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
args: {
|
||||
integrations: [],
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
type Story = StoryObj<IntegrationCardComponent>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: /*html*/ `
|
||||
<app-integration-card
|
||||
[name]="name"
|
||||
[image]="image"
|
||||
[linkURL]="linkURL"
|
||||
></app-integration-card>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
name: "Bitwarden",
|
||||
image: "/integrations/bitwarden-vertical-blue.svg",
|
||||
linkURL: "https://bitwarden.com",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
<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"
|
||||
[title]="tooltipI18nKey | i18n: integration.name"
|
||||
[attr.aria-label]="ariaI18nKey | i18n: integration.name"
|
||||
>
|
||||
<app-integration-card
|
||||
[name]="integration.name"
|
||||
[linkURL]="integration.linkURL"
|
||||
[image]="integration.image"
|
||||
[imageDarkMode]="integration.imageDarkMode"
|
||||
[externalURL]="integration.type === IntegrationType.SDK"
|
||||
[newBadgeExpiration]="integration.newBadgeExpiration"
|
||||
></app-integration-card>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -0,0 +1,100 @@
|
||||
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 "@bitwarden/angular/services/injection-tokens";
|
||||
import { IntegrationType } from "@bitwarden/common/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
import { SharedModule } from "@bitwarden/components/src/shared";
|
||||
import { I18nPipe } from "@bitwarden/components/src/shared/i18n.pipe";
|
||||
|
||||
import { IntegrationCardComponent } from "../integration-card/integration-card.component";
|
||||
import { Integration } from "../models";
|
||||
|
||||
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",
|
||||
linkURL: "https://example.com/1",
|
||||
type: IntegrationType.Integration,
|
||||
},
|
||||
{
|
||||
name: "SDK 2",
|
||||
image: "test-image2.png",
|
||||
linkURL: "https://example.com/2",
|
||||
type: IntegrationType.SDK,
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [IntegrationGridComponent, IntegrationCardComponent, SharedModule],
|
||||
providers: [
|
||||
{
|
||||
provide: ThemeStateService,
|
||||
useValue: mock<ThemeStateService>(),
|
||||
},
|
||||
{
|
||||
provide: SYSTEM_THEME_OBSERVABLE,
|
||||
useValue: of(ThemeTypes.Light),
|
||||
},
|
||||
{
|
||||
provide: I18nPipe,
|
||||
useValue: mock<I18nPipe>(),
|
||||
},
|
||||
{
|
||||
provide: I18nService,
|
||||
useValue: mock<I18nService>({ t: (key, p1) => key + " " + p1 }),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(IntegrationGridComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.integrations = integrations;
|
||||
component.ariaI18nKey = "integrationCardAriaLabel";
|
||||
component.tooltipI18nKey = "integrationCardTooltip";
|
||||
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.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);
|
||||
});
|
||||
|
||||
it("has a tool tip and aria label attributes", () => {
|
||||
const card: HTMLElement = fixture.debugElement.queryAll(By.css("li"))[0].nativeElement;
|
||||
expect(card.title).toBe("integrationCardTooltip" + " " + integrations[0].name);
|
||||
expect(card.getAttribute("aria-label")).toBe(
|
||||
"integrationCardAriaLabel" + " " + integrations[0].name,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Component, Input } from "@angular/core";
|
||||
|
||||
import { IntegrationType } from "@bitwarden/common/enums";
|
||||
|
||||
import { SharedModule } from "../../../shared.module";
|
||||
import { IntegrationCardComponent } from "../integration-card/integration-card.component";
|
||||
import { Integration } from "../models";
|
||||
|
||||
@Component({
|
||||
selector: "app-integration-grid",
|
||||
templateUrl: "./integration-grid.component.html",
|
||||
standalone: true,
|
||||
imports: [IntegrationCardComponent, SharedModule],
|
||||
})
|
||||
export class IntegrationGridComponent {
|
||||
@Input() integrations: Integration[];
|
||||
|
||||
@Input() ariaI18nKey: string = "integrationCardAriaLabel";
|
||||
@Input() tooltipI18nKey: string = "integrationCardTooltip";
|
||||
|
||||
protected IntegrationType = IntegrationType;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { IntegrationType } from "@bitwarden/common/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
import { I18nMockService } from "@bitwarden/components";
|
||||
|
||||
import { SharedModule } from "../../../shared.module";
|
||||
import { IntegrationCardComponent } from "../integration-card/integration-card.component";
|
||||
import { IntegrationGridComponent } from "../integration-grid/integration-grid.component";
|
||||
|
||||
class MockThemeService implements Partial<ThemeStateService> {}
|
||||
|
||||
export default {
|
||||
title: "Web/Integration Layout/Integration Grid",
|
||||
component: IntegrationGridComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [IntegrationCardComponent, SharedModule],
|
||||
providers: [
|
||||
{
|
||||
provide: I18nService,
|
||||
useFactory: () => {
|
||||
return new I18nMockService({
|
||||
integrationCardAriaLabel: "Go to integration",
|
||||
integrationCardTooltip: "Go to integration",
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ThemeStateService,
|
||||
useClass: MockThemeService,
|
||||
},
|
||||
{
|
||||
provide: SYSTEM_THEME_OBSERVABLE,
|
||||
useValue: of(ThemeTypes.Dark),
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
} as Meta;
|
||||
|
||||
type Story = StoryObj<IntegrationGridComponent>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: /*html*/ `
|
||||
<app-integration-grid [integrations]="integrations"></app-integration-grid>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
integrations: [
|
||||
{
|
||||
name: "Card 1",
|
||||
linkURL: "https://bitwarden.com",
|
||||
image: "/integrations/bitwarden-vertical-blue.svg",
|
||||
type: IntegrationType.SSO,
|
||||
},
|
||||
{
|
||||
name: "Card 2",
|
||||
linkURL: "https://bitwarden.com",
|
||||
image: "/integrations/bitwarden-vertical-blue.svg",
|
||||
type: IntegrationType.SDK,
|
||||
},
|
||||
{
|
||||
name: "Card 3",
|
||||
linkURL: "https://bitwarden.com",
|
||||
image: "/integrations/bitwarden-vertical-blue.svg",
|
||||
type: IntegrationType.SCIM,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Pipe, PipeTransform } from "@angular/core";
|
||||
|
||||
import { IntegrationType } from "@bitwarden/common/enums";
|
||||
|
||||
import { Integration } from "../../../shared/components/integrations/models";
|
||||
|
||||
@Pipe({
|
||||
name: "filterIntegrations",
|
||||
standalone: true,
|
||||
})
|
||||
export class FilterIntegrationsPipe implements PipeTransform {
|
||||
transform(integrations: Integration[], type: IntegrationType): Integration[] {
|
||||
return integrations.filter((integration) => integration.type === type);
|
||||
}
|
||||
}
|
||||
20
apps/web/src/app/shared/components/integrations/models.ts
Normal file
20
apps/web/src/app/shared/components/integrations/models.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
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;
|
||||
type: IntegrationType;
|
||||
/**
|
||||
* Shows the "New" badge until the defined date.
|
||||
* When omitted, the badge is never shown.
|
||||
*
|
||||
* @example "2024-12-31"
|
||||
*/
|
||||
newBadgeExpiration?: string;
|
||||
};
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./shared.module";
|
||||
export * from "./loose-components.module";
|
||||
export * from "./components/index";
|
||||
|
||||
Reference in New Issue
Block a user