Skip to content

Commit

Permalink
feat: add purple dot when new content is available in dashboard (podm…
Browse files Browse the repository at this point in the history
…an-desktop#4043) (podman-desktop#4782)

* feat: add purple dot when new content is available in dashboard (podman-desktop#4043)

Signed-off-by: lstocchi <[email protected]>

* fix: create base component

Signed-off-by: lstocchi <[email protected]>

---------

Signed-off-by: lstocchi <[email protected]>
  • Loading branch information
lstocchi authored Nov 17, 2023
1 parent e4bf412 commit 648f22a
Show file tree
Hide file tree
Showing 5 changed files with 296 additions and 17 deletions.
38 changes: 21 additions & 17 deletions packages/renderer/src/AppNavigation.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import VolumeIcon from './lib/images/VolumeIcon.svelte';
import NavItem from './lib/ui/NavItem.svelte';
import type { TinroRouteMeta } from 'tinro';
import type { Unsubscriber } from 'svelte/store';
import NewContentOnDashboardBadge from './lib/dashboard/NewContentOnDashboardBadge.svelte';
let podInfoSubscribe: Unsubscriber;
let containerInfoSubscribe: Unsubscriber;
Expand Down Expand Up @@ -95,24 +96,27 @@ export let meta: TinroRouteMeta;
class="group w-leftnavbar min-w-leftnavbar flex flex-col justify-between hover:overflow-y-none"
aria-label="AppNavigation">
<NavItem href="/" tooltip="Dashboard" bind:meta="{meta}">
<div class="flex items-center w-full h-full">
<svg
id="dashboard"
width="24"
height="24"
viewBox="0.856 0.885 4.454 4.454"
version="1.1"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
><defs></defs><g transform="translate(28.70309,-15.045389)"
><g transform="translate(15.017747,-0.13247817)"
><path
style="color:#000000;fill:#ffffff;stroke-linecap:round;stroke-linejoin:round"
d="m -42.621239,16.087895 c -0.06152,0 -0.120094,0.01566 -0.170016,0.05064 -0.04992,0.03498 -0.09353,0.09781 -0.09353,0.17415 v 0.77928 c 0,0.07634 0.04361,0.138649 0.09353,0.173633 0.04992,0.03498 0.108493,0.05064 0.170016,0.05064 h 3.967199 c 0.06152,0 0.118027,-0.01566 0.167949,-0.05064 0.04992,-0.03498 0.0956,-0.09729 0.0956,-0.173633 v -0.77928 c 0,-0.07634 -0.04568,-0.139166 -0.0956,-0.17415 -0.04992,-0.03498 -0.106426,-0.05064 -0.167949,-0.05064 z m 0,0.26355 h 3.967199 v 0.699182 h -3.967199 z m -0.112138,1.324467 c -0.05186,0.0038 -0.08726,0.03464 -0.104387,0.05633 -0.01826,0.02313 -0.02553,0.04299 -0.03152,0.06098 -0.01197,0.03597 -0.0155,0.06948 -0.0155,0.106971 v 0.77773 c 0,0.03749 0.0036,0.07153 0.0155,0.107487 0.006,0.01798 0.01326,0.03732 0.03152,0.06046 0.01826,0.02313 0.05762,0.0584 0.115239,0.0584 H -41.797 c 0.05761,0 0.09697,-0.03527 0.115238,-0.0584 0.01826,-0.02313 0.02554,-0.04247 0.03152,-0.06046 0.01197,-0.03597 0.0155,-0.07 0.0155,-0.107487 v -0.77773 c 0,-0.03749 -0.0036,-0.07101 -0.0155,-0.106971 -0.006,-0.01798 -0.01326,-0.03784 -0.03152,-0.06098 -0.01826,-0.02313 -0.05762,-0.05633 -0.115238,-0.05633 h -0.925525 c -0.0036,0 -0.0074,-2.51e-4 -0.01085,0 z m 1.623156,0 c -0.05207,0.0038 -0.08934,0.03464 -0.106453,0.05633 -0.01826,0.02313 -0.02553,0.04299 -0.03152,0.06098 -0.01197,0.03597 -0.0155,0.06948 -0.0155,0.106971 v 0.77773 c 0,0.03749 0.0036,0.07153 0.0155,0.107487 0.006,0.01798 0.01326,0.03732 0.03152,0.06046 0.01826,0.02313 0.05969,0.0584 0.117305,0.0584 h 0.923458 c 0.05761,0 0.09698,-0.03527 0.115239,-0.0584 0.01826,-0.02313 0.02709,-0.04247 0.03307,-0.06046 0.01197,-0.03597 0.01602,-0.07 0.01602,-0.107487 v -0.77773 c 0,-0.03749 -0.0041,-0.07101 -0.01602,-0.106971 -0.006,-0.01798 -0.01481,-0.03784 -0.03307,-0.06098 -0.01826,-0.02313 -0.05763,-0.05633 -0.115239,-0.05633 h -0.923458 c -0.0036,0 -0.0074,-2.51e-4 -0.01085,0 z m 1.62109,0 c -0.05186,0.0038 -0.08726,0.03464 -0.104386,0.05633 -0.01826,0.02313 -0.02554,0.04299 -0.03152,0.06098 -0.01197,0.03597 -0.01757,0.06948 -0.01757,0.106971 v 0.77773 c 0,0.03749 0.0056,0.07153 0.01757,0.107487 0.006,0.01798 0.01326,0.03732 0.03152,0.06046 0.01826,0.02313 0.05762,0.0584 0.115238,0.0584 h 0.923458 c 0.05761,0 0.09905,-0.03527 0.117306,-0.0584 0.01826,-0.02313 0.02554,-0.04247 0.03152,-0.06046 0.01197,-0.03597 0.0155,-0.07 0.0155,-0.107487 v -0.77773 c 0,-0.03749 -0.0036,-0.07101 -0.0155,-0.106971 -0.006,-0.01798 -0.01326,-0.03784 -0.03152,-0.06098 -0.01826,-0.02313 -0.05969,-0.05633 -0.117306,-0.05633 h -0.923458 c -0.0036,0 -0.0074,-2.51e-4 -0.01085,0 z m -3.132108,0.26355 h 0.722953 v 0.699182 h -0.722953 z m 1.623156,0 h 0.720887 v 0.699182 h -0.720887 z m 1.62109,0 h 0.722953 v 0.699182 h -0.722953 z m -3.304708,1.324467 c -0.118917,0.0065 -0.203088,0.112408 -0.203088,0.224275 v 0.777214 c 0,0.115477 0.08997,0.224792 0.214974,0.224792 h 2.509407 c 0.125004,0 0.217041,-0.109315 0.217041,-0.224792 v -0.777214 c 0,-0.115475 -0.09203,-0.224275 -0.217041,-0.224275 h -2.509407 c -0.0039,0 -0.008,-2.1e-4 -0.01189,0 z m 3.19257,0 c -0.05186,0.0038 -0.08726,0.03464 -0.104386,0.05633 -0.01826,0.02313 -0.02554,0.04247 -0.03152,0.06046 -0.01197,0.03597 -0.01757,0.06999 -0.01757,0.107487 v 0.777214 c 0,0.03749 0.0056,0.07151 0.01757,0.107487 0.006,0.01798 0.01326,0.03732 0.03152,0.06046 0.01826,0.02313 0.05762,0.05684 0.115238,0.05684 h 0.923458 c 0.05761,0 0.09905,-0.03371 0.117306,-0.05684 0.01826,-0.02313 0.02554,-0.04247 0.03152,-0.06046 0.01197,-0.03597 0.0155,-0.06999 0.0155,-0.107487 v -0.777214 c 0,-0.03749 -0.0036,-0.07151 -0.0155,-0.107487 -0.006,-0.01798 -0.01326,-0.03732 -0.03152,-0.06046 -0.01826,-0.02313 -0.05969,-0.05633 -0.117306,-0.05633 h -0.923458 c -0.0036,0 -0.0074,-2.51e-4 -0.01085,0 z m -3.132108,0.263549 h 2.412255 v 0.699183 h -2.412255 z m 3.244246,0 h 0.722953 v 0.699183 h -0.722953 z"
></path
<div class="relative w-full">
<div class="flex items-center w-full h-full">
<svg
id="dashboard"
width="24"
height="24"
viewBox="0.856 0.885 4.454 4.454"
version="1.1"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
><defs></defs><g transform="translate(28.70309,-15.045389)"
><g transform="translate(15.017747,-0.13247817)"
><path
style="color:#000000;fill:#ffffff;stroke-linecap:round;stroke-linejoin:round"
d="m -42.621239,16.087895 c -0.06152,0 -0.120094,0.01566 -0.170016,0.05064 -0.04992,0.03498 -0.09353,0.09781 -0.09353,0.17415 v 0.77928 c 0,0.07634 0.04361,0.138649 0.09353,0.173633 0.04992,0.03498 0.108493,0.05064 0.170016,0.05064 h 3.967199 c 0.06152,0 0.118027,-0.01566 0.167949,-0.05064 0.04992,-0.03498 0.0956,-0.09729 0.0956,-0.173633 v -0.77928 c 0,-0.07634 -0.04568,-0.139166 -0.0956,-0.17415 -0.04992,-0.03498 -0.106426,-0.05064 -0.167949,-0.05064 z m 0,0.26355 h 3.967199 v 0.699182 h -3.967199 z m -0.112138,1.324467 c -0.05186,0.0038 -0.08726,0.03464 -0.104387,0.05633 -0.01826,0.02313 -0.02553,0.04299 -0.03152,0.06098 -0.01197,0.03597 -0.0155,0.06948 -0.0155,0.106971 v 0.77773 c 0,0.03749 0.0036,0.07153 0.0155,0.107487 0.006,0.01798 0.01326,0.03732 0.03152,0.06046 0.01826,0.02313 0.05762,0.0584 0.115239,0.0584 H -41.797 c 0.05761,0 0.09697,-0.03527 0.115238,-0.0584 0.01826,-0.02313 0.02554,-0.04247 0.03152,-0.06046 0.01197,-0.03597 0.0155,-0.07 0.0155,-0.107487 v -0.77773 c 0,-0.03749 -0.0036,-0.07101 -0.0155,-0.106971 -0.006,-0.01798 -0.01326,-0.03784 -0.03152,-0.06098 -0.01826,-0.02313 -0.05762,-0.05633 -0.115238,-0.05633 h -0.925525 c -0.0036,0 -0.0074,-2.51e-4 -0.01085,0 z m 1.623156,0 c -0.05207,0.0038 -0.08934,0.03464 -0.106453,0.05633 -0.01826,0.02313 -0.02553,0.04299 -0.03152,0.06098 -0.01197,0.03597 -0.0155,0.06948 -0.0155,0.106971 v 0.77773 c 0,0.03749 0.0036,0.07153 0.0155,0.107487 0.006,0.01798 0.01326,0.03732 0.03152,0.06046 0.01826,0.02313 0.05969,0.0584 0.117305,0.0584 h 0.923458 c 0.05761,0 0.09698,-0.03527 0.115239,-0.0584 0.01826,-0.02313 0.02709,-0.04247 0.03307,-0.06046 0.01197,-0.03597 0.01602,-0.07 0.01602,-0.107487 v -0.77773 c 0,-0.03749 -0.0041,-0.07101 -0.01602,-0.106971 -0.006,-0.01798 -0.01481,-0.03784 -0.03307,-0.06098 -0.01826,-0.02313 -0.05763,-0.05633 -0.115239,-0.05633 h -0.923458 c -0.0036,0 -0.0074,-2.51e-4 -0.01085,0 z m 1.62109,0 c -0.05186,0.0038 -0.08726,0.03464 -0.104386,0.05633 -0.01826,0.02313 -0.02554,0.04299 -0.03152,0.06098 -0.01197,0.03597 -0.01757,0.06948 -0.01757,0.106971 v 0.77773 c 0,0.03749 0.0056,0.07153 0.01757,0.107487 0.006,0.01798 0.01326,0.03732 0.03152,0.06046 0.01826,0.02313 0.05762,0.0584 0.115238,0.0584 h 0.923458 c 0.05761,0 0.09905,-0.03527 0.117306,-0.0584 0.01826,-0.02313 0.02554,-0.04247 0.03152,-0.06046 0.01197,-0.03597 0.0155,-0.07 0.0155,-0.107487 v -0.77773 c 0,-0.03749 -0.0036,-0.07101 -0.0155,-0.106971 -0.006,-0.01798 -0.01326,-0.03784 -0.03152,-0.06098 -0.01826,-0.02313 -0.05969,-0.05633 -0.117306,-0.05633 h -0.923458 c -0.0036,0 -0.0074,-2.51e-4 -0.01085,0 z m -3.132108,0.26355 h 0.722953 v 0.699182 h -0.722953 z m 1.623156,0 h 0.720887 v 0.699182 h -0.720887 z m 1.62109,0 h 0.722953 v 0.699182 h -0.722953 z m -3.304708,1.324467 c -0.118917,0.0065 -0.203088,0.112408 -0.203088,0.224275 v 0.777214 c 0,0.115477 0.08997,0.224792 0.214974,0.224792 h 2.509407 c 0.125004,0 0.217041,-0.109315 0.217041,-0.224792 v -0.777214 c 0,-0.115475 -0.09203,-0.224275 -0.217041,-0.224275 h -2.509407 c -0.0039,0 -0.008,-2.1e-4 -0.01189,0 z m 3.19257,0 c -0.05186,0.0038 -0.08726,0.03464 -0.104386,0.05633 -0.01826,0.02313 -0.02554,0.04247 -0.03152,0.06046 -0.01197,0.03597 -0.01757,0.06999 -0.01757,0.107487 v 0.777214 c 0,0.03749 0.0056,0.07151 0.01757,0.107487 0.006,0.01798 0.01326,0.03732 0.03152,0.06046 0.01826,0.02313 0.05762,0.05684 0.115238,0.05684 h 0.923458 c 0.05761,0 0.09905,-0.03371 0.117306,-0.05684 0.01826,-0.02313 0.02554,-0.04247 0.03152,-0.06046 0.01197,-0.03597 0.0155,-0.06999 0.0155,-0.107487 v -0.777214 c 0,-0.03749 -0.0036,-0.07151 -0.0155,-0.107487 -0.006,-0.01798 -0.01326,-0.03732 -0.03152,-0.06046 -0.01826,-0.02313 -0.05969,-0.05633 -0.117306,-0.05633 h -0.923458 c -0.0036,0 -0.0074,-2.51e-4 -0.01085,0 z m -3.132108,0.263549 h 2.412255 v 0.699183 h -2.412255 z m 3.244246,0 h 0.722953 v 0.699183 h -0.722953 z"
></path
></g
></g
></g
></svg>
></svg>
</div>
<NewContentOnDashboardBadge />
</div>
</NavItem>
<NavItem href="/containers" tooltip="Containers{containerCount}" ariaLabel="Containers" bind:meta="{meta}">
Expand Down
124 changes: 124 additions & 0 deletions packages/renderer/src/lib/dashboard/NewContentOnDashboardBadge.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**********************************************************************
* 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
***********************************************************************/
import '@testing-library/jest-dom/vitest';
import { test, expect } from 'vitest';
import { render, screen } from '@testing-library/svelte';
import NewContentOnDashboardBadge from './NewContentOnDashboardBadge.svelte';
import { notificationQueue } from '/@/stores/notifications';
import type { NotificationCard } from '../../../../main/src/plugin/api/notification';
import type { ProviderStatus } from '@podman-desktop/api';
import type { ProviderContainerConnectionInfo, ProviderInfo } from '../../../../main/src/plugin/api/provider-info';
import { providerInfos } from '/@/stores/providers';
import { router } from 'tinro';

async function waitRender(): Promise<void> {
const result = render(NewContentOnDashboardBadge);
// wait that result.component.$$.ctx[3] is set
while (result.component.$$.ctx[3] === undefined) {
await new Promise(resolve => setTimeout(resolve, 100));
}
}

const notification1: NotificationCard = {
id: 1,
extensionId: 'extension',
title: '1',
body: '1',
type: 'info',
highlight: true,
};

const pStatus: ProviderStatus = 'started';
const pInfo: ProviderContainerConnectionInfo = {
name: 'test',
status: 'started',
endpoint: {
socketPath: '',
},
type: 'podman',
};
const providerInfo = {
id: 'test',
internalId: 'id',
name: '',
containerConnections: [pInfo],
kubernetesConnections: undefined,
status: pStatus,
containerProviderConnectionCreation: false,
containerProviderConnectionInitialization: false,
kubernetesProviderConnectionCreation: false,
kubernetesProviderConnectionInitialization: false,
links: undefined,
detectionChecks: undefined,
warnings: undefined,
images: undefined,
installationSupport: undefined,
} as unknown as ProviderInfo;

test('Expect to do not display any dot if active page is Dashboard', async () => {
notificationQueue.set([]);
providerInfos.set([]);
await waitRender();

notificationQueue.set([notification1]);
providerInfos.set([providerInfo]);

await new Promise(resolve => setTimeout(resolve, 200));

const dot = screen.queryByLabelText('New content available');
expect(dot).not.toBeInTheDocument();
});

test('Expect to do not display any dot if active page is not Dashboard but there are no updates', async () => {
notificationQueue.set([]);
providerInfos.set([]);
router.goto('/pods');

await waitRender();

const dot = screen.queryByLabelText('New content available');
expect(dot).not.toBeInTheDocument();
});

test('Expect to display the dot if active page is not Dashboard and there is a new notification', async () => {
notificationQueue.set([]);
providerInfos.set([]);
router.goto('/pods');
await waitRender();

notificationQueue.set([notification1]);

await new Promise(resolve => setTimeout(resolve, 200));

const dot = screen.getByLabelText('New content available');
expect(dot).toBeInTheDocument();
});

test('Expect to display the dot if active page is not Dashboard and there is a new provider', async () => {
notificationQueue.set([]);
providerInfos.set([]);
router.goto('/pods');
await waitRender();

providerInfos.set([providerInfo]);

await new Promise(resolve => setTimeout(resolve, 200));

const dot = screen.getByLabelText('New content available');
expect(dot).toBeInTheDocument();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import { providerInfos } from '/@/stores/providers';
import { notificationQueue } from '/@/stores/notifications';
import type { Unsubscriber } from 'svelte/motion';
import NewContentBadge from '../ui/NewContentBadge.svelte';
let providersId: string[] = [];
let notificationCount: number = 0;
let hasNewProviders = false;
let hasNewNotifications = false;
$: hasNew = hasNewProviders || hasNewNotifications;
let providersUnsubscribe: Unsubscriber;
let notificationsUnsubscribe: Unsubscriber;
onMount(() => {
// if there is a new provider we display the dot
providersUnsubscribe = providerInfos.subscribe(updatedProviders => {
const updatedProvidersId = updatedProviders.map(prov => prov.internalId).sort();
if (!hasNewProviders) {
// if the user is in the dashboard page we do not check for new providers
hasNewProviders = hasNewProvider(providersId, updatedProvidersId);
}
providersId = updatedProvidersId;
});
// if there is a new notification we display the dot
notificationsUnsubscribe = notificationQueue.subscribe(notifications => {
if (!hasNewNotifications) {
// if the user is in the dashboard page we do not check for new notifications
hasNewNotifications = notifications.length > notificationCount;
}
notificationCount = notifications.length;
});
});
onDestroy(() => {
providersUnsubscribe?.();
notificationsUnsubscribe?.();
});
function hasNewProvider(oldProvidersId: string[], newProvidersId: string[]): boolean {
if (oldProvidersId.length < newProvidersId.length) {
return true;
}
for (let [index, id] of newProvidersId.entries()) {
if (id !== oldProvidersId[index]) {
return true;
}
}
return false;
}
function onHide() {
hasNewProviders = false;
hasNewNotifications = false;
}
</script>

<div class="absolute top-0 right-[-9px]">
<NewContentBadge pagePath="/" show="{hasNew}" onHide="{onHide}" />
</div>
55 changes: 55 additions & 0 deletions packages/renderer/src/lib/ui/NewContentBadge.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**********************************************************************
* 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
***********************************************************************/
import '@testing-library/jest-dom/vitest';
import { test, expect } from 'vitest';
import { render, screen } from '@testing-library/svelte';
import NewContentBadge from './NewContentBadge.svelte';
import { router } from 'tinro';

test('Expect to do not display any dot if active page is same from pagePath', async () => {
router.goto('/');
render(NewContentBadge, {
pagePath: '/',
show: true,
});

const dot = screen.queryByLabelText('New content available');
expect(dot).not.toBeInTheDocument();
});

test('Expect to do not display any dot if active page is in pagePath but show prop is false', async () => {
router.goto('/');
render(NewContentBadge, {
pagePath: '/',
show: false,
});

const dot = screen.queryByLabelText('New content available');
expect(dot).not.toBeInTheDocument();
});

test('Expect to display the dot if active page is not pagePath and there is new content', async () => {
router.goto('/test');
render(NewContentBadge, {
pagePath: '/',
show: true,
});

const dot = screen.getByLabelText('New content available');
expect(dot).toBeInTheDocument();
});
33 changes: 33 additions & 0 deletions packages/renderer/src/lib/ui/NewContentBadge.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import { router } from 'tinro';
import type { Unsubscriber } from 'svelte/motion';
export let pagePath: string;
export let show: boolean = false;
export let onHide: () => void = () => {};
let isInPage = $router.path === pagePath;
let hasNew: boolean;
$: hasNew = !isInPage && show;
let routerUnsubscribe: Unsubscriber;
onMount(() => {
// listen to router change, so we can reset the changes and update the dot visibility
routerUnsubscribe = router.subscribe(route => {
isInPage = route.path === pagePath;
if (isInPage) {
onHide();
}
});
});
onDestroy(() => {
routerUnsubscribe?.();
});
</script>

{#if hasNew}
<div aria-label="New content available" class="w-[6px] h-[6px] bg-purple-500 rounded-full"></div>
{/if}

0 comments on commit 648f22a

Please sign in to comment.