Skip to content

Commit

Permalink
feat(website-frontend): improve repository page based on UX feedback (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
xandervedder authored Nov 22, 2024
1 parent efbae37 commit 3934885
Show file tree
Hide file tree
Showing 12 changed files with 173 additions and 27 deletions.
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions packages/common/src/slug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,7 @@ function sanitize(rawSlug: string | undefined) {
return rawSlug;
}
}

export function buildReportUrl(slug: string) {
return `${window.location.origin}/reports/${slug}`;
}
12 changes: 11 additions & 1 deletion packages/common/test/unit/slug.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect } from 'chai';
import { Slug, InvalidSlugError } from '../../src/slug.js';
import { Slug, InvalidSlugError, buildReportUrl } from '../../src/slug.js';

describe(Slug.name, () => {
describe('parse', () => {
Expand Down Expand Up @@ -35,4 +35,14 @@ describe(Slug.name, () => {
.property('message', 'Missing slug');
});
});

describe('buildReportUrl', () => {
it('should build a correct url', () => {
global.window = { location: { origin: 'http://localhost' } } as Window & typeof globalThis;

expect(buildReportUrl('github.com/stryker-mutator/stryker/master')).eq(
`${window.location.origin}/reports/github.com/stryker-mutator/stryker/master`,
);
});
});
});
1 change: 1 addition & 0 deletions packages/e2e/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export default defineConfig({
{
name: 'local',
use: {
headless: false,
baseURL: 'http://localhost:4200',
trace: 'retain-on-failure',
},
Expand Down
12 changes: 12 additions & 0 deletions packages/e2e/po/reports/report-client.po.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ export class ReportClient {
}
}

async disableRepository(slug: string): Promise<void> {
const patchBody: Partial<Repository> = { enabled: true };
const authToken = generateAuthToken();
await this.request.patch(`/api/repositories/${slug}`, {
failOnStatusCode: true,
data: patchBody,
headers: {
Authorization: `Bearer ${authToken}`,
},
});
}

async getUserRepositories(): Promise<Repository[]> {
const auth = generateAuthToken();
const response = await this.request.get('api/user/repositories', {
Expand Down
13 changes: 9 additions & 4 deletions packages/e2e/spec/repositories-page.spec.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,36 @@
import { test, expect, Page } from '@playwright/test';
import { RepositoriesPage } from '../po/repositories/repositories-page.po.js';
import { ReportClient } from '../po/reports/report-client.po.js';

// Example: 0527de29-6436-4564-9c5f-34f417ec68c0
const API_KEY_REGEX = /^[0-9a-z]{8}-(?:[0-9a-z]{4}-){3}[0-9a-z]{12}$/;

test.describe.serial('Repositories page', () => {
let repositoriesPage: RepositoriesPage;
let page: Page;
let client: ReportClient;

const copyText: () => Promise<string> = async () => await page.evaluate('navigator.clipboard.readText()');

const enableRepository = async () => {
await page.waitForSelector('sme-list#disabled-repositories');
const toggleRepository = repositoriesPage.disabledRepositories.first();
await toggleRepository.locator('sme-button > button').click();
await toggleRepository.locator('button[title="Enable repository"]').click();
};

const disableRepository = async () => {
await page.waitForSelector('sme-list#enabled-repositories');
const toggleRepository = repositoriesPage.enabledRepositories.first();
await toggleRepository.locator('sme-button > button').click();
await toggleRepository.locator('button[title="Disable repository"]').click();
};

test.beforeAll(async ({ browser }) => {
test.beforeAll(async ({ browser, request }) => {
const context = await browser.newContext();
await context.grantPermissions(['clipboard-read', 'clipboard-write']);
repositoriesPage = new RepositoriesPage(await context.newPage());
client = new ReportClient(request);

await client.disableRepository('github.com/stryker-mutator-test-organization/hello-org');
page = repositoriesPage.page;
await repositoriesPage.logOn();
await repositoriesPage.navigate();
Expand Down Expand Up @@ -85,7 +90,7 @@ test.describe.serial('Repositories page', () => {
test.beforeAll(async () => {
await enableRepository();
await page.locator('sme-modal div.mt-auto sme-button > button').click();
await repositoriesPage.enabledRepositories.first().click();
await repositoriesPage.enabledRepositories.first().locator('button').first().click();
await page.locator('sme-modal sme-collapsible#badge-collapsible').click();
});

Expand Down
1 change: 1 addition & 0 deletions packages/stryker-elements/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"@storybook/theming": "8.4.5",
"@storybook/web-components": "8.4.5",
"@storybook/web-components-vite": "8.4.5",
"@stryker-mutator/dashboard-common": "0.15.1",
"autoprefixer": "10.4.20",
"postcss": "8.4.49",
"storybook": "8.4.5",
Expand Down
11 changes: 5 additions & 6 deletions packages/stryker-elements/src/lib/atoms/badge.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { html } from 'lit';
import { BaseElement } from '../base-element';
import { customElement, property } from 'lit/decorators.js';

import { buildReportUrl } from '@stryker-mutator/dashboard-common';

import { BaseElement } from '../base-element';

export type BadgeStyle = 'flat' | 'flat-square' | 'plastic' | 'for-the-badge' | 'social';

const BASE_BADGE_URL = 'https://img.shields.io/endpoint';
Expand All @@ -18,11 +21,7 @@ export class Badge extends BaseElement {
slug = '';

render() {
return html`<a href="${this.#buildReportUrl()}"><img src="${this.#buildBadgeUrl()}" /></a>`;
}

#buildReportUrl() {
return `${window.location.origin}/reports/${this.slug}`;
return html`<a href="${buildReportUrl(this.slug)}"><img src="${this.#buildBadgeUrl()}" /></a>`;
}

#buildBadgeUrl() {
Expand Down
47 changes: 47 additions & 0 deletions packages/stryker-elements/src/lib/icons/svg-icons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { svg } from 'lit';

const MinusIcon = svg`
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="white"
class="size-6"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14" />
</svg>
`;

const PlusIcon = svg`
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="white"
class="size-6"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
`;

const SettingsIcon = svg`
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="white"
class="size-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 0 1 1.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.559.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.894.149c-.424.07-.764.383-.929.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 0 1-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.398.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 0 1-.12-1.45l.527-.737c.25-.35.272-.806.108-1.204-.165-.397-.506-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.108-1.204l-.526-.738a1.125 1.125 0 0 1 .12-1.45l.773-.773a1.125 1.125 0 0 1 1.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894Z"
/>
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg>
`;

export { MinusIcon, PlusIcon, SettingsIcon };
72 changes: 66 additions & 6 deletions packages/stryker-elements/src/lib/molecules/toggle-repository.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { when } from 'lit/directives/when.js';
import { ifDefined } from 'lit/directives/if-defined.js';

import { buildReportUrl } from '@stryker-mutator/dashboard-common';

import { BaseElement } from '../base-element.js';
import { Button } from '../atoms/buttons/button.js';
import { MinusIcon, PlusIcon, SettingsIcon } from '../icons/svg-icons.js';

@customElement('sme-toggle-repository')
export class ToggleRepository extends BaseElement {
Expand All @@ -16,13 +21,23 @@ export class ToggleRepository extends BaseElement {
@property()
slug = '';

@property({ type: Boolean })
isToggling = false;

render() {
return html`
<div
@click="${this.#handleClick}"
class="grid cursor-pointer grid-cols-2 rounded-lg border-2 border-neutral-600 p-2"
class="${classMap({
'opacity-50': this.isToggling,
})} grid grid-cols-2 rounded-lg border-2 border-neutral-600 p-2 transition"
>
<span class="ms-2 flex items-center text-lg font-bold text-white">${this.name}</span>
<a
class="ms-2 inline-flex items-center text-lg font-bold text-white underline decoration-transparent transition hover:underline hover:decoration-white"
href="${buildReportUrl(this.slug)}"
>
${this.name}
</a>
${this.#renderToggleButton()}
</div>
`;
Expand All @@ -40,21 +55,66 @@ export class ToggleRepository extends BaseElement {
return html`
<div class="flex">
<div class="ms-auto flex">
${when(this.enabled, () => html`<sme-badge class="me-6 flex items-center" slug="${this.slug}"></sme-badge>`)}
<sme-button class="flex" @click="${this.#toggleRepository}">
${this.enabled ? 'Disable' : 'Enable'}
</sme-button>
${when(
this.enabled,
() => html`<sme-badge class="flex items-center" slug="${ifDefined(this.slug)}"></sme-badge>`,
)}
${when(
this.enabled,
() => this.#renderWhenEnabled(),
() => this.#renderWhenDisabled(),
)}
</div>
</div>
`;
}

#renderWhenEnabled() {
return html`
<button
@click="${this.#openRepositorySettings}"
class="ml-2 rounded bg-blue-600 p-1 transition hover:bg-blue-700"
title="Open repository settings"
?disabled="${this.isToggling}"
>
${SettingsIcon}
</button>
<button
@click="${this.#toggleRepository}"
class="ml-2 rounded bg-red-600 p-1 transition hover:bg-red-700"
title="Disable repository"
?disabled="${this.isToggling}"
>
${MinusIcon}
</button>
`;
}

#renderWhenDisabled() {
return html`
<button
@click="${this.#toggleRepository}"
class="ml-2 rounded bg-green-600 p-1 transition hover:bg-green-700"
title="Enable repository"
?disabled="${this.isToggling}"
>
${PlusIcon}
</button>
`;
}

#openRepositorySettings() {
this.dispatchEvent(new CustomEvent('openRepositorySettings', { detail: { name: this.name } }));
}

#toggleRepository() {
this.isToggling = true;
this.dispatchEvent(
new CustomEvent('repositoryToggled', {
detail: {
slug: this.slug,
checked: !this.enabled,
ref: this,
},
}),
);
Expand Down
17 changes: 11 additions & 6 deletions packages/website-frontend/src/pages/repositories.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ifDefined } from 'lit/directives/if-defined.js';
import { map } from 'lit/directives/map.js';
import { when } from 'lit/directives/when.js';

import { ToggleRepository } from '@stryker-mutator/stryker-elements';
import { Repository } from '@stryker-mutator/dashboard-contract';

import { authService } from '../services/auth.service';
Expand Down Expand Up @@ -107,8 +108,8 @@ export class RepositoriesPage extends LitElement {
return html`
<sme-toggle-repository
?hidden="${shouldHide()}"
@repositoryClicked="${() => this.#handleRepositoryClick(repository)}"
@repositoryToggled="${(event: CustomEvent<{ checked: boolean }>) =>
@openRepositorySettings="${() => this.#handleRepositoryClick(repository)}"
@repositoryToggled="${(event: CustomEvent<{ checked: boolean; ref: ToggleRepository }>) =>
this.#handleRepositoryToggled(event, repository)}"
.enabled="${repository.enabled}"
name="${repository.name}"
Expand Down Expand Up @@ -160,9 +161,9 @@ export class RepositoriesPage extends LitElement {
<sme-collapsible id="usage-collapsible" title="Usage">
<sme-text>
See the
<sme-link href="https://stryker-mutator.io/docs/General/dashboard/" inline unStyled
><b><u>Stryker dashboard documentation ↗</u></b></sme-link
>
<sme-link href="https://stryker-mutator.io/docs/General/dashboard/" inline unStyled>
<b><u>Stryker dashboard documentation ↗</u></b>
</sme-link>
for an explanation on how you can configure the dashboard reporter or use cURL to send your report to the
dashboard.
</sme-text>
Expand All @@ -175,8 +176,12 @@ export class RepositoriesPage extends LitElement {
return `${repository.slug}/${repository.defaultBranch}`;
}

async #handleRepositoryToggled(event: CustomEvent<{ checked: boolean }>, repository: Repository) {
async #handleRepositoryToggled(
event: CustomEvent<{ checked: boolean; ref: ToggleRepository }>,
repository: Repository,
) {
const response = await repositoriesService.enableRepository(repository.slug, event.detail.checked);
event.detail.ref.isToggling = false;
repository.enabled = response === null ? false : true;
if (repository.enabled) {
this.#openModal(repository, response!.apiKey);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ describe(RepositoriesPage.name, () => {
const enabledRepository = loader.querySelector(
'sme-list#enabled-repositories > sme-toggle-repository:not([hidden])',
);
const button = enabledRepository?.shadowRoot?.querySelector('sme-button');
const button = enabledRepository?.shadowRoot?.querySelectorAll('button')[1];
button?.click();

await sut.whenStable();
Expand All @@ -211,7 +211,7 @@ describe(RepositoriesPage.name, () => {
const disabledRepository = loader.querySelector(
'sme-list#disabled-repositories > sme-toggle-repository:not([hidden])',
);
const button = disabledRepository?.shadowRoot?.querySelector('sme-button');
const button = disabledRepository?.shadowRoot?.querySelector('button');
button?.click();

// Assert
Expand All @@ -229,7 +229,7 @@ describe(RepositoriesPage.name, () => {
expect(noEnabledRepositoriesNotification).toHaveTextContent("You don't have any repositories to enable.");
});

it('should open the modal when an enabled repository is clicked', async () => {
it('should open the modal when an configure repository button is clicked', async () => {
// Act
sut.connect();
await sut.whenStable();
Expand All @@ -239,7 +239,8 @@ describe(RepositoriesPage.name, () => {
const disabledRepository = loader.querySelector(
'sme-list#enabled-repositories > sme-toggle-repository:not([hidden])',
)!;
disabledRepository.dispatchEvent(new Event('repositoryClicked'));
const configureButton = disabledRepository.shadowRoot!.querySelector('button') as HTMLElement;
configureButton.click();

// Assert
await sut.waitFor(() => sut.element.shadowRoot?.querySelector('sme-modal') !== null);
Expand Down

0 comments on commit 3934885

Please sign in to comment.