-
Notifications
You must be signed in to change notification settings - Fork 310
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Allow to install in one click featured extensions (if not insta…
…lled) related to #1901 widget available from dashboard page and extensions list Change-Id: I495ad06dd7f27a3779195c3fc503dae3dd6e0cc3 Signed-off-by: Florent Benoit <fbenoit@redhat.com> Change-Id: I1559329c48dd4d9390e133d8ddcf647340dded92
- Loading branch information
Showing
6 changed files
with
466 additions
and
103 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
98 changes: 98 additions & 0 deletions
98
packages/renderer/src/lib/featured/FeaturedExtensionDownload.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
/********************************************************************** | ||
* Copyright (C) 2023 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 | ||
***********************************************************************/ | ||
|
||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
|
||
import '@testing-library/jest-dom'; | ||
import { beforeAll, test, expect, vi } from 'vitest'; | ||
import { fireEvent, render, screen } from '@testing-library/svelte'; | ||
import FeaturedExtensionDownload from './FeaturedExtensionDownload.svelte'; | ||
import type { FeaturedExtension } from '../../../../main/src/plugin/featured/featured-api'; | ||
|
||
const extensionInstallFromImageMock = vi.fn(); | ||
|
||
// fake the window.events object | ||
beforeAll(() => { | ||
(window as any).extensionInstallFromImage = extensionInstallFromImageMock; | ||
(window.events as unknown) = { | ||
receive: (_channel: string, func: any) => { | ||
func(); | ||
}, | ||
}; | ||
}); | ||
|
||
test('Expect that the install button is hidden if extension is not installable', async () => { | ||
const featuredExtension: FeaturedExtension = { | ||
builtin: true, | ||
id: 'foo.bar', | ||
displayName: 'FooBar', | ||
description: 'This is FooBar description', | ||
icon: 'data:image/png;base64,foobar', | ||
categories: [], | ||
fetchable: false, | ||
installed: false, | ||
}; | ||
|
||
await render(FeaturedExtensionDownload, { featuredExtension }); | ||
|
||
// expect to have the button if installable | ||
const installButton = screen.queryByRole('button', { name: 'Install foo.bar Extension' }); | ||
// expect the button is not there | ||
expect(installButton).not.toBeInTheDocument(); | ||
}); | ||
|
||
test('Expect that we can see the button and click on the install', async () => { | ||
const featuredExtension: FeaturedExtension = { | ||
builtin: true, | ||
id: 'foo.bar', | ||
displayName: 'FooBar', | ||
description: 'This is FooBar description', | ||
icon: 'data:image/png;base64,foobar', | ||
categories: [], | ||
fetchable: true, | ||
fetchLink: 'oci-hello/world', | ||
fetchVersion: '1.2.3', | ||
installed: false, | ||
}; | ||
|
||
const { component } = await render(FeaturedExtensionDownload, { featuredExtension }); | ||
|
||
// expect to have the button if installable | ||
const installButton = screen.getByRole('button', { name: 'Install foo.bar Extension' }); | ||
// expect the button to be there | ||
expect(installButton).toBeInTheDocument(); | ||
|
||
// mock the install function | ||
extensionInstallFromImageMock.mockImplementation(async () => { | ||
featuredExtension.installed = true; | ||
featuredExtension.fetchable = false; | ||
await component.$set({ featuredExtension }); | ||
}); | ||
|
||
// click on the button | ||
await fireEvent.click(installButton); | ||
|
||
// install should have been called | ||
expect(extensionInstallFromImageMock).toHaveBeenCalled(); | ||
|
||
// now, expect the button to be gone | ||
// expect the button to be there | ||
const installButtonAfterClick = screen.queryByRole('button', { name: 'Install foo.bar Extension' }); | ||
// expect the button is not there | ||
expect(installButtonAfterClick).not.toBeInTheDocument(); | ||
}); |
79 changes: 79 additions & 0 deletions
79
packages/renderer/src/lib/featured/FeaturedExtensionDownload.svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
<script lang="ts"> | ||
import type { FeaturedExtension } from '../../../../main/src/plugin/featured/featured-api'; | ||
import { faCheckCircle, faDownload } from '@fortawesome/free-solid-svg-icons'; | ||
import Fa from 'svelte-fa/src/fa.svelte'; | ||
import LoadingIcon from '../ui/LoadingIcon.svelte'; | ||
import ErrorMessage from '../ui/ErrorMessage.svelte'; | ||
export let featuredExtension: FeaturedExtension; | ||
let installInProgress = false; | ||
let logs = []; | ||
let errorInstall = ''; | ||
let percentage = '0%'; | ||
async function installExtension() { | ||
console.log('User asked to install the extension with the following properties', featuredExtension); | ||
logs = []; | ||
installInProgress = true; | ||
// do a trim on the image name | ||
const ociImage = featuredExtension.fetchLink.trim(); | ||
try { | ||
// download image | ||
await window.extensionInstallFromImage( | ||
ociImage, | ||
(data: string) => { | ||
logs = [...logs, data]; | ||
console.log('data', data); | ||
// try to extract percentage from string like | ||
// data Downloading sha256:e8d2c9e5c69499c41ba39b7828c00e55087572884cac466b4d1b47243b085c7d.tar - 11% - (55132/521578) | ||
const percentageMatch = data.match(/(\d+)%/); | ||
if (percentageMatch) { | ||
percentage = percentageMatch[1] + '%'; | ||
} | ||
}, | ||
(error: string) => { | ||
console.log(`got an error when installing ${featuredExtension.id}`, error); | ||
installInProgress = false; | ||
errorInstall = error; | ||
}, | ||
); | ||
logs = [...logs, '☑️ installation finished !']; | ||
percentage = '100%'; | ||
} catch (error) { | ||
console.log('error', error); | ||
} | ||
installInProgress = false; | ||
} | ||
</script> | ||
|
||
<button | ||
aria-label="Install {featuredExtension.id} Extension" | ||
on:click="{() => installExtension()}" | ||
hidden="{!featuredExtension.fetchable}" | ||
title="Install {featuredExtension.displayName} v{featuredExtension.fetchVersion} Extension" | ||
class="border-2 relative rounded border-dustypurple-700 text-dustypurple-700 hover:bg-charcoal-800 hover:text-dustypurple-600 w-10 p-2 text-center cursor-pointer flex flex-row"> | ||
<!--<Fa class="ml-1.5" size="16" icon={faDownload} />--> | ||
<span class="ml-0.5"></span> | ||
<LoadingIcon | ||
icon="{faDownload}" | ||
iconSize="16" | ||
loadingWidthClass="w-7" | ||
loadingHeightClass="h-7" | ||
positionTopClass="top-[2px]" | ||
positionLeftClass="left-[4px]" | ||
loading="{installInProgress}" /> | ||
<span | ||
class:hidden="{!installInProgress}" | ||
class="absolute -top-[15px] right-0 text-dustypurple-500" | ||
style="font-size: 8px">{percentage}</span> | ||
<div class:hidden="{!errorInstall}" class="absolute w-56 -top-[25px] right-0" style="font-size: 8px"> | ||
<ErrorMessage error="{errorInstall}" /> | ||
</div> | ||
</button> |
127 changes: 127 additions & 0 deletions
127
packages/renderer/src/lib/featured/FeaturedExtensions.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
/********************************************************************** | ||
* Copyright (C) 2023 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 | ||
***********************************************************************/ | ||
|
||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
|
||
import '@testing-library/jest-dom'; | ||
import { beforeAll, test, expect, vi } from 'vitest'; | ||
import { render, screen } from '@testing-library/svelte'; | ||
import { get } from 'svelte/store'; | ||
import FeaturedExtensions from './FeaturedExtensions.svelte'; | ||
import { featuredExtensionInfos } from '/@/stores/featuredExtensions'; | ||
import type { FeaturedExtension } from '../../../../main/src/plugin/featured/featured-api'; | ||
|
||
const getFeaturedExtensionsMock = vi.fn(); | ||
|
||
// fake the window.events object | ||
beforeAll(() => { | ||
(window as any).getFeaturedExtensions = getFeaturedExtensionsMock; | ||
(window.events as unknown) = { | ||
receive: (_channel: string, func: any) => { | ||
func(); | ||
}, | ||
}; | ||
}); | ||
|
||
test('Expect that featured extensions are displayed', async () => { | ||
const featuredExtension1: FeaturedExtension = { | ||
builtin: true, | ||
id: 'foo.bar', | ||
displayName: 'FooBar', | ||
description: 'This is FooBar description', | ||
icon: 'data:image/png;base64,foobar', | ||
categories: [], | ||
fetchable: true, | ||
fetchLink: 'oci-hello/world', | ||
fetchVersion: '1.2.3', | ||
installed: false, | ||
}; | ||
|
||
const featuredExtension2: FeaturedExtension = { | ||
builtin: true, | ||
id: 'foo.baz', | ||
displayName: 'FooBaz', | ||
description: 'Foobaz description', | ||
icon: 'data:image/png;base64,foobaz', | ||
categories: [], | ||
fetchable: false, | ||
installed: true, | ||
}; | ||
|
||
const featuredExtension3: FeaturedExtension = { | ||
builtin: true, | ||
id: 'foo.bar', | ||
displayName: 'Bar', | ||
description: 'FooBar not fetchable description', | ||
icon: 'data:image/png;base64,bar', | ||
categories: [], | ||
fetchable: false, | ||
installed: false, | ||
}; | ||
|
||
getFeaturedExtensionsMock.mockResolvedValue([featuredExtension1, featuredExtension2, featuredExtension3]); | ||
|
||
// ask to update the featured Extensions store | ||
window.dispatchEvent(new CustomEvent('system-ready')); | ||
|
||
// wait store are populated | ||
while (get(featuredExtensionInfos).length === 0) { | ||
await new Promise(resolve => setTimeout(resolve, 500)); | ||
} | ||
|
||
await render(FeaturedExtensions); | ||
|
||
// get by title | ||
const firstExtension = screen.getByTitle('This is FooBar description'); | ||
expect(firstExtension).toBeInTheDocument(); | ||
|
||
const imageExt1 = screen.getByRole('img', { name: 'FooBar logo' }); | ||
// expect the image to be there | ||
expect(imageExt1).toBeInTheDocument(); | ||
// expect image source is correct | ||
expect(imageExt1).toHaveAttribute('src', 'data:image/png;base64,foobar'); | ||
|
||
// Not installed so it should have a button to install | ||
const installButton = screen.getByRole('button', { name: 'Install foo.bar Extension' }); | ||
// expect the button to be there | ||
expect(installButton).toBeInTheDocument(); | ||
|
||
// get by title | ||
const secondExtension = screen.getByTitle('Foobaz description'); | ||
expect(secondExtension).toBeInTheDocument(); | ||
// contains the text installed | ||
expect(secondExtension).toHaveTextContent(/.*installed/); | ||
|
||
const imageExt2 = screen.getByRole('img', { name: 'FooBaz logo' }); | ||
// expect the image to be there | ||
expect(imageExt2).toBeInTheDocument(); | ||
// expect image source is correct | ||
expect(imageExt2).toHaveAttribute('src', 'data:image/png;base64,foobaz'); | ||
|
||
// get by title | ||
const thirdExtension = screen.getByTitle('FooBar not fetchable description'); | ||
expect(thirdExtension).toBeInTheDocument(); | ||
// contains the text installed | ||
expect(thirdExtension).toHaveTextContent(/.*N\/A/); | ||
|
||
const imageExt3 = screen.getByRole('img', { name: 'Bar logo' }); | ||
// expect the image to be there | ||
expect(imageExt3).toBeInTheDocument(); | ||
// expect image source is correct | ||
expect(imageExt3).toHaveAttribute('src', 'data:image/png;base64,bar'); | ||
}); |
46 changes: 46 additions & 0 deletions
46
packages/renderer/src/lib/featured/FeaturedExtensions.svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
<script lang="ts"> | ||
import { featuredExtensionInfos } from '/@/stores/featuredExtensions'; | ||
import { faCheckCircle, faCircleXmark, faDownload } from '@fortawesome/free-solid-svg-icons'; | ||
import Fa from 'svelte-fa/src/fa.svelte'; | ||
import FeaturedExtensionDownload from './FeaturedExtensionDownload.svelte'; | ||
</script> | ||
|
||
<!--Title--> | ||
<p class="text-lg first-letter:uppercase font-bold">featured extensions:</p> | ||
<div class="grid md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-3 pb-4"> | ||
{#each $featuredExtensionInfos as featuredExtension} | ||
<div | ||
title="{featuredExtension.description}" | ||
class="rounded-md | ||
bg-charcoal-800 flex flex-row justify-center p-4 h-20 border-2 border-charcoal-800 hover:border-dustypurple-500"> | ||
<div class=" flex flex-col flex-1"> | ||
<div class="flex flex-row place-items-center flex-1"> | ||
<div> | ||
<img | ||
class="w-12 h-12 object-contain" | ||
alt="{featuredExtension.displayName} logo" | ||
src="{featuredExtension.icon}" /> | ||
</div> | ||
<div class="flex flex-1 mx-2 cursor-default font-bold justify-start"> | ||
{featuredExtension.displayName} | ||
</div> | ||
<div class="h-full w-18 flex flex-col items-end place-content-center"> | ||
{#if featuredExtension.installed} | ||
<div class="text-dustypurple-700 p-1 text-center flex flex-row place-items-center"> | ||
<Fa class="ml-1.5 mr-2" size="18" icon="{faCheckCircle}" /> | ||
<div class="uppercase font-bold text-xs cursor-default">installed</div> | ||
</div> | ||
{:else if featuredExtension.fetchable} | ||
<FeaturedExtensionDownload featuredExtension="{featuredExtension}" /> | ||
{:else} | ||
<div class="text-charcoal-300 p-1 text-center flex flex-row place-items-center"> | ||
<Fa class="ml-1.5 mr-1" size="18" icon="{faCircleXmark}" /> | ||
<div class="uppercase text-xs cursor-default font-extralight">N/A</div> | ||
</div> | ||
{/if} | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
{/each} | ||
</div> |
Oops, something went wrong.