Skip to content

Commit

Permalink
feat: add catalog UI to browse the catalog (#72)
Browse files Browse the repository at this point in the history
* feat: add catalog UI to browse the catalog

related to podman-desktop/podman-desktop#8972

Signed-off-by: Florent Benoit <[email protected]>
Change-Id: I9e121490ccf65275ec1273f716d9d7a8e26d791a
  • Loading branch information
benoitf authored Nov 6, 2024
1 parent 8ad4a81 commit 3cef4a9
Show file tree
Hide file tree
Showing 25 changed files with 1,750 additions and 489 deletions.
18 changes: 18 additions & 0 deletions src/app.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
/**********************************************************************
* Copyright (C) 2024 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/

/// <reference types="svelte" />

// See https://kit.svelte.dev/docs/types#app
Expand Down
114 changes: 114 additions & 0 deletions src/lib/Appearance.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**********************************************************************
* Copyright (C) 2024 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/

import '@testing-library/jest-dom/vitest';

import { render } from '@testing-library/svelte';
import { beforeEach, expect, test, vi } from 'vitest';

import Appearance from './Appearance.svelte';

const addEventListenerMock = vi.fn();

beforeEach(() => {
vi.resetAllMocks();
window.matchMedia = vi.fn().mockReturnValue({
matches: false,
addEventListener: addEventListenerMock,
removeEventListener: vi.fn(),
});
});

function getRootElement(container: HTMLElement): HTMLElement {
// get root html element
let rootElement: HTMLElement | null = container;
let loop = 0;
while (rootElement?.parentElement && loop < 10) {
rootElement = container.parentElement;
loop++;
}
return rootElement as HTMLElement;
}

function getRootElementClassesValue(container: HTMLElement): string | undefined {
return getRootElement(container).classList.value;
}

test('check initial light theme', async () => {
const { baseElement } = render(Appearance, {});
// expect to have no (dark) class as OS is using light
await vi.waitFor(() => expect(getRootElementClassesValue(baseElement)).toBe('light'));
});

test('check initial dark theme', async () => {
window.matchMedia = vi.fn().mockReturnValue({
matches: true,
addEventListener: addEventListenerMock,
removeEventListener: vi.fn(),
});
const { baseElement } = render(Appearance, {});
// expect to have no (dark) class as OS is using light
await vi.waitFor(() => expect(getRootElementClassesValue(baseElement)).toBe('dark'));
});

test('Expect event being changed when changing the default appearance on the operating system', async () => {
// initial is dark
window.matchMedia = vi.fn().mockReturnValue({
matches: true,
addEventListener: addEventListenerMock,
removeEventListener: vi.fn(),
});

let userCallback: () => void = () => {};
addEventListenerMock.mockImplementation((event: string, callback: () => void) => {
if (event === 'change') {
userCallback = callback;
}
});

const { baseElement } = render(Appearance, {});

// check it's dark
expect(getRootElementClassesValue(baseElement)).toBe('dark');

// now change to light
window.matchMedia = vi.fn().mockReturnValue({
matches: false,
addEventListener: addEventListenerMock,
removeEventListener: vi.fn(),
});

// call the callback on matchMedia
userCallback();

// check if it's now light
await vi.waitFor(() => expect(getRootElementClassesValue(baseElement)).toBe('light'));

// now change to dark
window.matchMedia = vi.fn().mockReturnValue({
matches: true,
addEventListener: addEventListenerMock,
removeEventListener: vi.fn(),
});

// call again the callback on matchMedia
userCallback();

// check if it's now dark
await vi.waitFor(() => expect(getRootElementClassesValue(baseElement)).toBe('dark'));
});
27 changes: 27 additions & 0 deletions src/lib/Appearance.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<script lang="ts">
import { onMount } from 'svelte';
let isDarkTheme = $state(window.matchMedia('(prefers-color-scheme: dark)').matches);
$effect(() => {
const html = document.documentElement;
// toggle the dark class on the html element
if (isDarkTheme) {
html.classList.add('dark');
html.classList.remove('light');
html.setAttribute('style', 'color-scheme: dark;');
} else {
html.classList.remove('dark');
html.classList.add('light');
html.setAttribute('style', 'color-scheme: light;');
}
});
onMount(async () => {
// add a listener for the appearance change in case user change setting on the Operating System
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
isDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches;
});
});
</script>
103 changes: 103 additions & 0 deletions src/lib/Markdown.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<script lang="ts">
import { micromark } from 'micromark';
import { directive } from 'micromark-extension-directive';
let html: string | undefined = $state(undefined);
// content to use in the markdown
const { markdown }: { markdown: string } = $props();
$effect(() => {
const tempHtml = micromark(markdown, {
extensions: [directive()],
htmlExtensions: [],
});
if (!tempHtml) {
return;
}
const parser = new DOMParser();
const doc = parser.parseFromString(tempHtml, 'text/html');
const links = doc.querySelectorAll('a');
for (const link of links) {
const currentHref = link.getAttribute('href');
// remove and replace href attribute if matching
if (currentHref?.startsWith('#')) {
// get current value of href
link.removeAttribute('href');
// remove from current href the #
const withoutHashHRef = currentHref.substring(1);
// add an attribute to handle onclick
link.setAttribute('data-pd-jump-in-page', withoutHashHRef);
// add a class for cursor
link.classList.add('cursor-pointer');
}
}
// for all h1/h2/h3/h4/h5/h6, add an id attribute being the name of the attibute all in lowercase without spaces (replaced by -)
const headers = doc.querySelectorAll('h1, h2, h3, h4, h5, h6');
for (const header of headers) {
const headerText = header.textContent;
const headerId = headerText?.toLowerCase().replace(/\s/g, '-');
if (headerId) {
header.setAttribute('id', headerId);
}
}
html = doc.body.innerHTML;
});
</script>

<section class="markdown" aria-label="markdown-content">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html html}
</section>

<!-- The markdown rendered has it's own style that you'll have to customize / check against podman desktop
UI guidelines -->
<style lang="postcss">
.markdown > :global(p) {
line-height: normal;
padding-bottom: 8px;
margin-bottom: 8px;
}
.markdown > :global(h1),
:global(h2),
:global(h3),
:global(h4),
:global(h5) {
font-size: revert;
line-height: normal;
font-weight: revert;
border-bottom: 1px solid #444;
margin-bottom: 20px;
}
.markdown > :global(ul) {
line-height: normal;
list-style: revert;
margin: revert;
padding: revert;
}
.markdown > :global(b),
:global(strong) {
font-weight: 600;
}
.markdown > :global(blockquote) {
opacity: 0.8;
line-height: normal;
}
.markdown :global(a) {
color: theme(colors.purple.500);
text-decoration: none;
}
.markdown :global(a):hover {
color: theme(colors.purple.400);
text-decoration: underline;
}
</style>
48 changes: 48 additions & 0 deletions src/lib/api/extensions-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**********************************************************************
* Copyright (C) 2024 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/
export interface CatalogExtensionVersionFileInfo {
assetType: 'icon' | 'LICENSE' | 'README';
data: string;
}

export interface CatalogExtensionVersionInfo {
version: string;
podmanDesktopVersion?: string;
ociUri: string;
preview: boolean;
lastUpdated: Date;
files: CatalogExtensionVersionFileInfo[];
}

export interface CatalogExtensionInfo {
id: string;
publisherName: string;
publisherDisplayName: string;
extensionName: string;
categories: string[];
shortDescription: string;
displayName: string;
keywords: string[];
unlisted: boolean;
versions: CatalogExtensionVersionInfo[];
}

export interface ExtensionByCategoryInfo {
category: string;
extensions: CatalogExtensionInfo[];
}
59 changes: 59 additions & 0 deletions src/lib/extensions.svelte.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**********************************************************************
* Copyright (C) 2024 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/

import { beforeEach, describe, expect, test, vi } from 'vitest';

import catalogOfExtensions from '../../static/api/extensions.json';
import type { CatalogExtensionInfo } from './api/extensions-info';
import { catalogExtensions, getCurrentExtension, initCatalog, setCurrentExtension } from './extensions.svelte';

const fetchMock = vi.fn();

describe('check catalog', () => {
beforeEach(() => {
vi.resetAllMocks();
catalogExtensions.length = 0;
window.fetch = fetchMock;
});

test('check fetch', async () => {
fetchMock.mockResolvedValue({
ok: true,
json: async () => ({ extensions: catalogOfExtensions.extensions }),
});
await initCatalog();

expect(vi.mocked(fetch)).toHaveBeenCalledWith('https://registry.podman-desktop.io/api/extensions.json');

// check we have extensions in the catalog
expect(catalogExtensions.length).toBeGreaterThan(0);
});
});

test('check current extension', () => {
const currentExtension = getCurrentExtension();
expect(currentExtension.value).toBeUndefined();
setCurrentExtension({ id: 'dummy' } as unknown as CatalogExtensionInfo);

// check again the value
expect(currentExtension.value).toEqual({ id: 'dummy' });

// unset
setCurrentExtension(undefined);
expect(currentExtension.value).toBeUndefined();
});
Loading

0 comments on commit 3cef4a9

Please sign in to comment.