mirror of
https://github.com/podman-desktop/podman-desktop
synced 2026-04-21 17:47:22 +00:00
feat: add status dots for pods (#4646)
* feat: add status dots for pods ### What does this PR do? * Adds dots for the containers when listing a pod (podman or kubernetes) * Hover to see the tooltip of the container name as well as the running status * Dots will change when the container status has changed. * Fixes a small redirection error bug when clicking a kubernetes pod ### Screenshot/screencast of this PR <!-- Please include a screenshot or a screencast explaining what is doing this PR --> ### What issues does this PR fix or reference? <!-- Include any related issues from Podman Desktop repository (or from another issue tracker). --> Closes https://github.com/containers/podman-desktop/issues/3925 Closes https://github.com/containers/podman-desktop/issues/4612 ### How to test this PR? <!-- Please explain steps to reproduce --> Signed-off-by: Charlie Drage <charlie@charliedrage.com> * update based on review Signed-off-by: Charlie Drage <charlie@charliedrage.com> * add outline dots Signed-off-by: Charlie Drage <charlie@charliedrage.com> --------- Signed-off-by: Charlie Drage <charlie@charliedrage.com>
This commit is contained in:
parent
39d36d7465
commit
2da3f543dc
13 changed files with 648 additions and 34 deletions
|
|
@ -59,14 +59,14 @@ import type { Telemetry } from '/@/plugin/telemetry/telemetry.js';
|
|||
function toContainerStatus(state: V1ContainerState | undefined): string {
|
||||
if (state) {
|
||||
if (state.running) {
|
||||
return 'Running';
|
||||
return 'running';
|
||||
} else if (state.terminated) {
|
||||
return 'Terminated';
|
||||
return 'terminated';
|
||||
} else if (state.waiting) {
|
||||
return 'Waiting';
|
||||
return 'waiting';
|
||||
}
|
||||
}
|
||||
return 'Unknown';
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function toPodInfo(pod: V1Pod, contextName?: string): PodInfo {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,14 @@ export enum PodGroupInfoTypeUI {
|
|||
export interface PodInfoContainerUI {
|
||||
Id: string;
|
||||
Names: string;
|
||||
|
||||
// This is a bit odd at the moment as we use the same PodInfoContainerUI for both Kubernetes and Podman Pods.
|
||||
// For PODS:
|
||||
// Status will either be: stopped, running, paused, exited, dead, created, degraded
|
||||
// https://docs.podman.io/en/latest/_static/api.html#tag/pods/operation/PodListLibpod
|
||||
// For Kubernetes:
|
||||
// Status will either be: running, waiting, terminated
|
||||
// see the toContainerStatus function in kubernetes-client.ts
|
||||
Status: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,24 @@ const provider: ProviderInfo = {
|
|||
|
||||
const pod1: PodInfo = {
|
||||
Cgroup: '',
|
||||
Containers: [],
|
||||
// Three containers within the pod, one running, one terminated, one exited
|
||||
Containers: [
|
||||
{
|
||||
Names: 'container1',
|
||||
Id: 'container1',
|
||||
Status: 'running',
|
||||
},
|
||||
{
|
||||
Names: 'container2',
|
||||
Id: 'container2',
|
||||
Status: 'terminated',
|
||||
},
|
||||
{
|
||||
Names: 'container3',
|
||||
Id: 'container3',
|
||||
Status: 'exited',
|
||||
},
|
||||
],
|
||||
Created: '',
|
||||
Id: 'beab25123a40',
|
||||
InfraId: 'pod1',
|
||||
|
|
@ -78,7 +95,13 @@ const pod1: PodInfo = {
|
|||
|
||||
const pod2: PodInfo = {
|
||||
Cgroup: '',
|
||||
Containers: [],
|
||||
Containers: [
|
||||
{
|
||||
Names: 'container4',
|
||||
Id: 'container4',
|
||||
Status: 'running',
|
||||
},
|
||||
],
|
||||
Created: '',
|
||||
Id: 'e8129c5720b3',
|
||||
InfraId: 'pod2',
|
||||
|
|
@ -92,9 +115,90 @@ const pod2: PodInfo = {
|
|||
kind: 'podman',
|
||||
};
|
||||
|
||||
// Pod with 11 containers that shows all the different statuses
|
||||
// running, terminated, waiting, stopped, paused, exited, dead, created, degraded
|
||||
// this makes it so that we "group" them as more than 10 containers equals grouping
|
||||
const manyPod: PodInfo = {
|
||||
Cgroup: '',
|
||||
Containers: [
|
||||
{
|
||||
Names: 'container1',
|
||||
Id: 'container1',
|
||||
Status: 'running',
|
||||
},
|
||||
{
|
||||
Names: 'container2',
|
||||
Id: 'container2',
|
||||
Status: 'terminated',
|
||||
},
|
||||
{
|
||||
Names: 'container3',
|
||||
Id: 'container3',
|
||||
Status: 'waiting',
|
||||
},
|
||||
{
|
||||
Names: 'container4',
|
||||
Id: 'container4',
|
||||
Status: 'stopped',
|
||||
},
|
||||
{
|
||||
Names: 'container5',
|
||||
Id: 'container5',
|
||||
Status: 'paused',
|
||||
},
|
||||
{
|
||||
Names: 'container6',
|
||||
Id: 'container6',
|
||||
Status: 'exited',
|
||||
},
|
||||
{
|
||||
Names: 'container7',
|
||||
Id: 'container7',
|
||||
Status: 'dead',
|
||||
},
|
||||
{
|
||||
Names: 'container8',
|
||||
Id: 'container8',
|
||||
Status: 'created',
|
||||
},
|
||||
{
|
||||
Names: 'container9',
|
||||
Id: 'container9',
|
||||
Status: 'degraded',
|
||||
},
|
||||
{
|
||||
Names: 'container10',
|
||||
Id: 'container10',
|
||||
Status: 'running',
|
||||
},
|
||||
{
|
||||
Names: 'container11',
|
||||
Id: 'container11',
|
||||
Status: 'running',
|
||||
},
|
||||
],
|
||||
Created: '',
|
||||
Id: 'beab25123a40',
|
||||
InfraId: 'manyPod',
|
||||
Labels: {},
|
||||
Name: 'manyPod',
|
||||
Namespace: '',
|
||||
Networks: [],
|
||||
Status: 'running',
|
||||
engineId: 'podman',
|
||||
engineName: 'podman',
|
||||
kind: 'podman',
|
||||
};
|
||||
|
||||
const kubepod1: PodInfo = {
|
||||
Cgroup: '',
|
||||
Containers: [],
|
||||
Containers: [
|
||||
{
|
||||
Names: 'container1',
|
||||
Id: 'container1',
|
||||
Status: 'running',
|
||||
},
|
||||
],
|
||||
Created: '',
|
||||
Id: 'beab25123a40',
|
||||
InfraId: 'kubepod1',
|
||||
|
|
@ -110,7 +214,13 @@ const kubepod1: PodInfo = {
|
|||
|
||||
const kubepod2: PodInfo = {
|
||||
Cgroup: '',
|
||||
Containers: [],
|
||||
Containers: [
|
||||
{
|
||||
Names: 'container1',
|
||||
Id: 'container1',
|
||||
Status: 'running',
|
||||
},
|
||||
],
|
||||
Created: '',
|
||||
Id: 'e8129c5720b3',
|
||||
InfraId: 'kubepod2',
|
||||
|
|
@ -126,7 +236,13 @@ const kubepod2: PodInfo = {
|
|||
|
||||
const ocppod: PodInfo = {
|
||||
Cgroup: '',
|
||||
Containers: [],
|
||||
Containers: [
|
||||
{
|
||||
Names: 'container1',
|
||||
Id: 'container1',
|
||||
Status: 'running',
|
||||
},
|
||||
],
|
||||
Created: '',
|
||||
Id: 'e8129c5720b3',
|
||||
InfraId: 'ocppod',
|
||||
|
|
@ -156,6 +272,14 @@ beforeAll(() => {
|
|||
getContributedMenusMock.mockImplementation(() => Promise.resolve([]));
|
||||
});
|
||||
|
||||
async function waitRender(customProperties: object): Promise<void> {
|
||||
const result = render(PodsList, { ...customProperties });
|
||||
// wait that result.component.$$.ctx[2] is set
|
||||
while (result.component.$$.ctx[2] === undefined) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
}
|
||||
|
||||
test('Expect no pods being displayed', async () => {
|
||||
getProvidersInfoMock.mockResolvedValue([provider]);
|
||||
window.dispatchEvent(new CustomEvent('provider-lifecycle-change'));
|
||||
|
|
@ -185,10 +309,12 @@ test('Expect single podman pod being displayed', async () => {
|
|||
}
|
||||
|
||||
render(PodsList);
|
||||
const pod1Details = screen.getByRole('cell', { name: 'pod1 beab2512 0 container' });
|
||||
const pod1Details = screen.getByRole('cell', { name: 'pod1 beab2512' });
|
||||
expect(pod1Details).toBeInTheDocument();
|
||||
|
||||
// Expect to have three "tooltips" which are the "dots".
|
||||
const pod1Row = screen.getByRole('row', {
|
||||
name: 'Toggle pod pod1 beab2512 0 container podman 0 seconds spinner spinner spinner',
|
||||
name: 'Toggle pod pod1 beab2512 podman tooltip tooltip tooltip 0 seconds spinner spinner spinner',
|
||||
});
|
||||
expect(pod1Row).toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -209,17 +335,14 @@ test('Expect 2 podman pods being displayed', async () => {
|
|||
}
|
||||
|
||||
render(PodsList);
|
||||
const pod1Details = screen.getByRole('cell', { name: 'pod1 beab2512 0 container' });
|
||||
const pod1Details = screen.getByRole('cell', { name: 'pod1 beab2512' });
|
||||
expect(pod1Details).toBeInTheDocument();
|
||||
const pod1Row = screen.getByRole('row', {
|
||||
name: 'Toggle pod pod1 beab2512 0 container podman 0 seconds spinner spinner spinner',
|
||||
name: 'Toggle pod pod1 beab2512 podman tooltip tooltip tooltip 0 seconds spinner spinner spinner',
|
||||
});
|
||||
expect(pod1Row).toBeInTheDocument();
|
||||
|
||||
const pod2Details = screen.getByRole('cell', { name: 'pod2 e8129c57 0 container' });
|
||||
expect(pod2Details).toBeInTheDocument();
|
||||
const pod2Row = screen.getByRole('row', {
|
||||
name: 'Toggle pod pod2 e8129c57 0 container podman 0 seconds spinner spinner spinner',
|
||||
name: 'Toggle pod pod2 e8129c57 podman tooltip 0 seconds spinner spinner spinner',
|
||||
});
|
||||
expect(pod2Row).toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -241,7 +364,7 @@ test('Expect single kubernetes pod being displayed', async () => {
|
|||
|
||||
render(PodsList);
|
||||
const pod1Details = screen.getByRole('row', {
|
||||
name: 'Toggle pod kubepod1 beab2512 0 container kubernetes 0 seconds spinner',
|
||||
name: 'Toggle pod kubepod1 beab2512 kubernetes tooltip 0 seconds spinner',
|
||||
});
|
||||
expect(pod1Details).toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -263,11 +386,11 @@ test('Expect 2 kubernetes pods being displayed', async () => {
|
|||
|
||||
render(PodsList);
|
||||
const pod1Details = screen.getByRole('row', {
|
||||
name: 'Toggle pod kubepod1 beab2512 0 container kubernetes 0 seconds spinner',
|
||||
name: 'Toggle pod kubepod1 beab2512 kubernetes tooltip 0 seconds spinner',
|
||||
});
|
||||
expect(pod1Details).toBeInTheDocument();
|
||||
const pod2Details = screen.getByRole('row', {
|
||||
name: 'Toggle pod kubepod2 e8129c57 0 container kubernetes 0 seconds spinner',
|
||||
name: 'Toggle pod kubepod2 e8129c57 kubernetes tooltip 0 seconds spinner',
|
||||
});
|
||||
expect(pod2Details).toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -311,11 +434,11 @@ test('Expect the route to a pod details page is correctly encoded with an engine
|
|||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
render(PodsList);
|
||||
const podDetails = screen.getByRole('cell', { name: 'ocppod e8129c57 0 container' });
|
||||
const podDetails = screen.getByRole('cell', { name: 'ocppod e8129c57' });
|
||||
expect(podDetails).toBeInTheDocument();
|
||||
|
||||
const podRow = screen.getByRole('row', {
|
||||
name: 'Toggle pod ocppod e8129c57 0 container kubernetes 0 seconds spinner',
|
||||
name: 'Toggle pod ocppod e8129c57 kubernetes tooltip 0 seconds spinner',
|
||||
});
|
||||
expect(podRow).toBeInTheDocument();
|
||||
|
||||
|
|
@ -326,3 +449,92 @@ test('Expect the route to a pod details page is correctly encoded with an engine
|
|||
'/pods/kubernetes/ocppod/userid-dev%2Fapi-sandbox-123-openshiftapps-com%3A6443%2FuserId/logs',
|
||||
);
|
||||
});
|
||||
|
||||
test('Expect the pod1 row to have 3 status dots with the correct colors and the pod2 row to have 1 status dot', async () => {
|
||||
getProvidersInfoMock.mockResolvedValue([provider]);
|
||||
listPodsMock.mockResolvedValue([pod1, pod2]);
|
||||
kubernetesListPodsMock.mockResolvedValue([]);
|
||||
window.dispatchEvent(new CustomEvent('provider-lifecycle-change'));
|
||||
window.dispatchEvent(new CustomEvent('extensions-already-started'));
|
||||
|
||||
while (get(providerInfos).length !== 1) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
while (get(podsInfos).length !== 2) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
waitRender(PodsList);
|
||||
|
||||
// Should render 4 status dots.
|
||||
// 3 for the first pod, 1 for the second pod
|
||||
// this should also appear REORGANIZED and in a different order.
|
||||
const statusDots = screen.getAllByTestId('status-dot');
|
||||
expect(statusDots.length).toBe(4);
|
||||
|
||||
expect(statusDots[0].title).toBe('container1: Running');
|
||||
expect(statusDots[0]).toHaveClass('bg-status-running');
|
||||
|
||||
expect(statusDots[1].title).toBe('container3: Exited');
|
||||
expect(statusDots[1]).toHaveClass('outline-status-exited');
|
||||
|
||||
expect(statusDots[2].title).toBe('container2: Terminated');
|
||||
expect(statusDots[2]).toHaveClass('bg-status-terminated');
|
||||
|
||||
// 2nd row / 2nd pod
|
||||
expect(statusDots[3].title).toBe('container4: Running');
|
||||
expect(statusDots[3]).toHaveClass('bg-status-running');
|
||||
});
|
||||
|
||||
test('Expect the manyPod row to show 9 dots representing every status', async () => {
|
||||
getProvidersInfoMock.mockResolvedValue([provider]);
|
||||
listPodsMock.mockResolvedValue([manyPod]);
|
||||
kubernetesListPodsMock.mockResolvedValue([]);
|
||||
window.dispatchEvent(new CustomEvent('provider-lifecycle-change'));
|
||||
window.dispatchEvent(new CustomEvent('extensions-already-started'));
|
||||
|
||||
while (get(providerInfos).length !== 1) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
while (get(podsInfos).length !== 1) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
waitRender(PodsList);
|
||||
|
||||
// Should render 9 status dots representing all statuses from the 11 containers provided
|
||||
// due to the functoin organizeContainers it will be reorganized and the order will be different
|
||||
// it should be organized as follows:
|
||||
// running, created, paused, waiting, degraded, exited, stopped, terminated, dead
|
||||
const statusDots = screen.getAllByTestId('status-dot');
|
||||
expect(statusDots.length).toBe(9);
|
||||
|
||||
expect(statusDots[0].title).toBe('Running: 3');
|
||||
expect(statusDots[0]).toHaveClass('bg-status-running');
|
||||
|
||||
expect(statusDots[1].title).toBe('Created: 1');
|
||||
expect(statusDots[1]).toHaveClass('outline-status-created');
|
||||
|
||||
expect(statusDots[2].title).toBe('Paused: 1');
|
||||
expect(statusDots[2]).toHaveClass('bg-status-paused');
|
||||
|
||||
expect(statusDots[3].title).toBe('Waiting: 1');
|
||||
expect(statusDots[3]).toHaveClass('bg-status-waiting');
|
||||
|
||||
expect(statusDots[4].title).toBe('Degraded: 1');
|
||||
expect(statusDots[4]).toHaveClass('bg-status-degraded');
|
||||
|
||||
expect(statusDots[5].title).toBe('Exited: 1');
|
||||
expect(statusDots[5]).toHaveClass('outline-status-exited');
|
||||
|
||||
expect(statusDots[6].title).toBe('Stopped: 1');
|
||||
expect(statusDots[6]).toHaveClass('outline-status-stopped');
|
||||
|
||||
expect(statusDots[7].title).toBe('Terminated: 1');
|
||||
expect(statusDots[7]).toHaveClass('bg-status-terminated');
|
||||
|
||||
expect(statusDots[8].title).toBe('Dead: 1');
|
||||
expect(statusDots[8]).toHaveClass('bg-status-dead');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import Button from '../ui/Button.svelte';
|
|||
import { faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||
import StateChange from '../ui/StateChange.svelte';
|
||||
import ProviderInfo from '../ui/ProviderInfo.svelte';
|
||||
import Dots from '../ui/Dots.svelte';
|
||||
|
||||
export let searchTerm = '';
|
||||
$: searchPattern.set(searchTerm);
|
||||
|
|
@ -235,6 +236,7 @@ function errorCallback(pod: PodInfoUI, errorMessage: string): void {
|
|||
<th class="text-center font-extrabold w-10 px-2">Status</th>
|
||||
<th>Name</th>
|
||||
<th class="pl-3">Environment</th>
|
||||
<th class="pl-3">Containers</th>
|
||||
<th class="whitespace-nowrap px-6">Age</th>
|
||||
<th class="text-right pr-2">Actions</th>
|
||||
</tr>
|
||||
|
|
@ -259,12 +261,6 @@ function errorCallback(pod: PodInfoUI, errorMessage: string): void {
|
|||
</div>
|
||||
<div class="flex flex-row items-center">
|
||||
<div class="text-xs text-violet-400">{pod.shortId}</div>
|
||||
<button
|
||||
class="ml-1 text-xs font-extra-light text-gray-900"
|
||||
class:cursor-pointer="{pod.containers.length > 0}"
|
||||
on:click="{() => openContainersFromPod(pod)}">
|
||||
{pod.containers.length} container{pod.containers.length > 1 ? 's' : ''}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -275,6 +271,21 @@ function errorCallback(pod: PodInfoUI, errorMessage: string): void {
|
|||
</div>
|
||||
</td>
|
||||
|
||||
<td class="pl-3 whitespace-nowrap">
|
||||
<!-- If this is podman, make the dots clickable as it'll take us to the container menu
|
||||
this does not work if you click on a kubernetes type pod -->
|
||||
{#if pod.kind === 'podman'}
|
||||
<button
|
||||
class:cursor-pointer="{pod.containers.length > 0}"
|
||||
on:click="{() => openContainersFromPod(pod)}">
|
||||
<Dots containers="{pod.containers}" />
|
||||
</button>
|
||||
{:else}
|
||||
<div class="flex items-center">
|
||||
<Dots containers="{pod.containers}" />
|
||||
</div>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="px-6 py-2 whitespace-nowrap w-10">
|
||||
<div class="flex items-center">
|
||||
<div class="text-sm text-gray-700">
|
||||
|
|
|
|||
118
packages/renderer/src/lib/ui/Dots.spec.ts
Normal file
118
packages/renderer/src/lib/ui/Dots.spec.ts
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2022 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 { getStatusColor, organizeContainers } from './Dots';
|
||||
import type { PodInfoContainerUI } from '../pod/PodInfoUI';
|
||||
|
||||
// Mock a PodInfoContainerUI object that contains containers of all the different statuses
|
||||
// running, terminated, waiting, stopped, paused, exited, dead, created, degraded
|
||||
const mockContainers: PodInfoContainerUI[] = [
|
||||
{
|
||||
Id: '1',
|
||||
Names: 'container1',
|
||||
Status: 'running',
|
||||
},
|
||||
{
|
||||
Id: '2',
|
||||
Names: 'container2',
|
||||
Status: 'terminated',
|
||||
},
|
||||
{
|
||||
Id: '3',
|
||||
Names: 'container3',
|
||||
Status: 'waiting',
|
||||
},
|
||||
{
|
||||
Id: '4',
|
||||
Names: 'container4',
|
||||
Status: 'stopped',
|
||||
},
|
||||
{
|
||||
Id: '5',
|
||||
Names: 'container5',
|
||||
Status: 'paused',
|
||||
},
|
||||
{
|
||||
Id: '6',
|
||||
Names: 'container6',
|
||||
Status: 'exited',
|
||||
},
|
||||
{
|
||||
Id: '7',
|
||||
Names: 'container7',
|
||||
Status: 'dead',
|
||||
},
|
||||
{
|
||||
Id: '8',
|
||||
Names: 'container8',
|
||||
Status: 'created',
|
||||
},
|
||||
{
|
||||
Id: '9',
|
||||
Names: 'container9',
|
||||
Status: 'degraded',
|
||||
},
|
||||
];
|
||||
|
||||
test('test getStatusColor returns the correct colors', () => {
|
||||
expect(getStatusColor('running')).toBe('bg-status-running');
|
||||
expect(getStatusColor('terminated')).toBe('bg-status-terminated');
|
||||
expect(getStatusColor('waiting')).toBe('bg-status-waiting');
|
||||
expect(getStatusColor('stopped')).toBe('outline-status-stopped');
|
||||
expect(getStatusColor('paused')).toBe('bg-status-paused');
|
||||
expect(getStatusColor('exited')).toBe('outline-status-exited');
|
||||
expect(getStatusColor('dead')).toBe('bg-status-dead');
|
||||
expect(getStatusColor('created')).toBe('outline-status-created');
|
||||
expect(getStatusColor('degraded')).toBe('bg-status-degraded');
|
||||
expect(getStatusColor('unknown')).toBe('bg-status-unknown');
|
||||
});
|
||||
|
||||
test('test organizeContainers returns a record of containers organized by status', () => {
|
||||
const organizedContainers = organizeContainers(mockContainers);
|
||||
expect(organizedContainers.running.length).toBe(1);
|
||||
expect(organizedContainers.terminated.length).toBe(1);
|
||||
expect(organizedContainers.waiting.length).toBe(1);
|
||||
expect(organizedContainers.stopped.length).toBe(1);
|
||||
expect(organizedContainers.paused.length).toBe(1);
|
||||
expect(organizedContainers.exited.length).toBe(1);
|
||||
expect(organizedContainers.dead.length).toBe(1);
|
||||
expect(organizedContainers.created.length).toBe(1);
|
||||
expect(organizedContainers.degraded.length).toBe(1);
|
||||
});
|
||||
|
||||
test('randomly re-order the containers and ensure they are still organized correctly after in the correct order', () => {
|
||||
// Copy mockContainers array and shuffle it
|
||||
const shuffledContainers = [...mockContainers].sort(() => Math.random() - 0.5);
|
||||
|
||||
// Organize the shuffled containers
|
||||
const organizedContainers = organizeContainers(shuffledContainers);
|
||||
|
||||
// Make sure it's corrected ordered by:
|
||||
// running, created, paused, waiting, degraded, exited, stopped, terminated, dead
|
||||
expect(organizedContainers.running[0].Id).toBe('1');
|
||||
expect(organizedContainers.created[0].Id).toBe('8');
|
||||
expect(organizedContainers.paused[0].Id).toBe('5');
|
||||
expect(organizedContainers.waiting[0].Id).toBe('3');
|
||||
expect(organizedContainers.degraded[0].Id).toBe('9');
|
||||
expect(organizedContainers.exited[0].Id).toBe('6');
|
||||
expect(organizedContainers.stopped[0].Id).toBe('4');
|
||||
expect(organizedContainers.terminated[0].Id).toBe('2');
|
||||
expect(organizedContainers.dead[0].Id).toBe('7');
|
||||
});
|
||||
26
packages/renderer/src/lib/ui/Dots.svelte
Normal file
26
packages/renderer/src/lib/ui/Dots.svelte
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<script lang="ts">
|
||||
import type { PodInfoContainerUI } from '../pod/PodInfoUI';
|
||||
import StatusDot from './StatusDot.svelte';
|
||||
import { organizeContainers } from './Dots';
|
||||
import { capitalize } from './Util';
|
||||
|
||||
// All the possible statuses that will appear for both Pods and Kubernetes
|
||||
export let containers: PodInfoContainerUI[];
|
||||
|
||||
$: organizedContainers = organizeContainers(containers);
|
||||
</script>
|
||||
|
||||
<!-- If containers is more than 10, we will group them and show the number of containers -->
|
||||
{#if containers.length > 10}
|
||||
{#each Object.entries(organizedContainers) as [status, c]}
|
||||
{#if c.length > 0}
|
||||
<StatusDot status="{status}" tooltip="{capitalize(status)}: {c.length}" number="{c.length}" />
|
||||
{/if}
|
||||
{/each}
|
||||
{:else}
|
||||
{#each Object.entries(organizedContainers) as [status, c]}
|
||||
{#each c as container}
|
||||
<StatusDot status="{status}" name="{container.Names}" />
|
||||
{/each}
|
||||
{/each}
|
||||
{/if}
|
||||
62
packages/renderer/src/lib/ui/Dots.ts
Normal file
62
packages/renderer/src/lib/ui/Dots.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import type { PodInfoContainerUI } from '../pod/PodInfoUI';
|
||||
|
||||
const allStatuses = ['running', 'created', 'paused', 'waiting', 'degraded', 'exited', 'stopped', 'terminated', 'dead'];
|
||||
|
||||
// All the possible statuses that will appear for both Pods and Kubernetes
|
||||
// NOTE: See: https://tailwindcss.com/docs/content-configuration#dynamic-class-names
|
||||
// we cannot do "partial" names like referencing 'bg-'+status because it will
|
||||
// not be shown due to how svelte handles dynamic class names
|
||||
export function getStatusColor(status: string): string {
|
||||
// Define the mapping directly with Record
|
||||
// must be either "bg-" or "outline-" for either solid / outline colors
|
||||
const colors: Record<string, string> = {
|
||||
// Podman & Kubernetes
|
||||
running: 'bg-status-running',
|
||||
|
||||
// Kubernetes-only
|
||||
terminated: 'bg-status-terminated',
|
||||
waiting: 'bg-status-waiting',
|
||||
|
||||
// Podman-only
|
||||
stopped: 'outline-status-stopped',
|
||||
paused: 'bg-status-paused',
|
||||
exited: 'outline-status-exited',
|
||||
dead: 'bg-status-dead',
|
||||
created: 'outline-status-created',
|
||||
degraded: 'bg-status-degraded',
|
||||
};
|
||||
|
||||
// Return the corresponding color class or a default if not found
|
||||
return colors[status] || 'bg-status-unknown';
|
||||
}
|
||||
|
||||
// Organize the containers by returning their status as the key + an array of containers by order of
|
||||
// highest importance (running) to lowest (dead)
|
||||
export function organizeContainers(containers: PodInfoContainerUI[]): Record<string, PodInfoContainerUI[]> {
|
||||
const organizedContainers: Record<string, PodInfoContainerUI[]> = {
|
||||
running: [],
|
||||
created: [],
|
||||
paused: [],
|
||||
waiting: [],
|
||||
degraded: [],
|
||||
exited: [],
|
||||
stopped: [],
|
||||
terminated: [],
|
||||
dead: [],
|
||||
};
|
||||
|
||||
containers.forEach(container => {
|
||||
const statusKey = container.Status.toLowerCase();
|
||||
if (!organizedContainers[statusKey]) {
|
||||
organizedContainers[statusKey] = [container];
|
||||
} else {
|
||||
organizedContainers[statusKey].push(container);
|
||||
}
|
||||
});
|
||||
|
||||
allStatuses.forEach(status => {
|
||||
organizedContainers[status] = organizedContainers[status] || [];
|
||||
});
|
||||
|
||||
return organizedContainers;
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import type { IconDefinition } from '@fortawesome/free-solid-svg-icons';
|
|||
import type { IConnectionStatus } from '../preferences/Util';
|
||||
import LoadingIcon from './LoadingIcon.svelte';
|
||||
import Tooltip from './Tooltip.svelte';
|
||||
import { capitalize } from './Util';
|
||||
|
||||
export let action: string;
|
||||
export let icon: IconDefinition;
|
||||
|
|
@ -19,10 +20,6 @@ $: disable =
|
|||
|
||||
$: loading = state?.inProgress && action === state?.action;
|
||||
|
||||
function capitalizeFirstLetter(text: string): string {
|
||||
return text.charAt(0).toUpperCase() + text.slice(1);
|
||||
}
|
||||
|
||||
function getStyleByState(state: IConnectionStatus | undefined, action: string) {
|
||||
if (
|
||||
(action === 'start' && (state?.inProgress || state?.status !== 'stopped')) ||
|
||||
|
|
@ -36,9 +33,9 @@ function getStyleByState(state: IConnectionStatus | undefined, action: string) {
|
|||
}
|
||||
</script>
|
||||
|
||||
<Tooltip tip="{capitalizeFirstLetter(action)}" bottom>
|
||||
<Tooltip tip="{capitalize(action)}" bottom>
|
||||
<button
|
||||
aria-label="{capitalizeFirstLetter(action)}"
|
||||
aria-label="{capitalize(action)}"
|
||||
class="mx-2.5 my-2 {getStyleByState(state, action)}"
|
||||
on:click="{clickAction}"
|
||||
disabled="{disable}">
|
||||
|
|
|
|||
68
packages/renderer/src/lib/ui/StatusDot.spec.ts
Normal file
68
packages/renderer/src/lib/ui/StatusDot.spec.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import '@testing-library/jest-dom/vitest';
|
||||
import { test, expect } from 'vitest';
|
||||
import { render, screen } from '@testing-library/svelte';
|
||||
import StatusDot from './StatusDot.svelte';
|
||||
|
||||
const renderStatusDot = (containerStatus: string) => {
|
||||
return render(StatusDot, { name: 'foobar', status: containerStatus });
|
||||
};
|
||||
|
||||
test('Expect the dot to have the correct color for running status', () => {
|
||||
renderStatusDot('running');
|
||||
const dot = screen.getByTestId('status-dot');
|
||||
expect(dot).toHaveClass('bg-status-running');
|
||||
});
|
||||
|
||||
test('Expect the dot to have the correct color for terminated status', () => {
|
||||
renderStatusDot('terminated');
|
||||
const dot = screen.getByTestId('status-dot');
|
||||
expect(dot).toHaveClass('bg-status-terminated');
|
||||
});
|
||||
|
||||
test('Expect the dot to have the correct color for waiting status', () => {
|
||||
renderStatusDot('waiting');
|
||||
const dot = screen.getByTestId('status-dot');
|
||||
expect(dot).toHaveClass('bg-status-waiting');
|
||||
});
|
||||
|
||||
test('Expect the dot to have the correct color for stopped status', () => {
|
||||
renderStatusDot('stopped');
|
||||
const dot = screen.getByTestId('status-dot');
|
||||
expect(dot).toHaveClass('outline-status-stopped');
|
||||
});
|
||||
|
||||
test('Expect the dot to have the correct color for paused status', () => {
|
||||
renderStatusDot('paused');
|
||||
const dot = screen.getByTestId('status-dot');
|
||||
expect(dot).toHaveClass('bg-status-paused');
|
||||
});
|
||||
|
||||
test('Expect the dot to have the correct color for exited status', () => {
|
||||
renderStatusDot('exited');
|
||||
const dot = screen.getByTestId('status-dot');
|
||||
expect(dot).toHaveClass('outline-status-exited');
|
||||
});
|
||||
|
||||
test('Expect the dot to have the correct color for dead status', () => {
|
||||
renderStatusDot('dead');
|
||||
const dot = screen.getByTestId('status-dot');
|
||||
expect(dot).toHaveClass('bg-status-dead');
|
||||
});
|
||||
|
||||
test('Expect the dot to have the correct color for created status', () => {
|
||||
renderStatusDot('created');
|
||||
const dot = screen.getByTestId('status-dot');
|
||||
expect(dot).toHaveClass('outline-status-created');
|
||||
});
|
||||
|
||||
test('Expect the dot to have the correct color for degraded status', () => {
|
||||
renderStatusDot('degraded');
|
||||
const dot = screen.getByTestId('status-dot');
|
||||
expect(dot).toHaveClass('bg-status-degraded');
|
||||
});
|
||||
|
||||
test('Expect the dot to have the correct color for unknown status', () => {
|
||||
renderStatusDot('unknown');
|
||||
const dot = screen.getByTestId('status-dot');
|
||||
expect(dot).toHaveClass('bg-status-unknown');
|
||||
});
|
||||
37
packages/renderer/src/lib/ui/StatusDot.svelte
Normal file
37
packages/renderer/src/lib/ui/StatusDot.svelte
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<!-- StatusDot.svelte -->
|
||||
<script lang="ts">
|
||||
import Tooltip from './Tooltip.svelte';
|
||||
import { getStatusColor } from './Dots';
|
||||
import { capitalize } from './Util';
|
||||
|
||||
export let status: string;
|
||||
export let name: string = '';
|
||||
export let tooltip: string = '';
|
||||
export let number: number = 0;
|
||||
|
||||
// If the tooltip is blank, use the container name and status
|
||||
// as the tooltip / title
|
||||
if (tooltip === '' && name !== '' && status !== '') {
|
||||
tooltip = `${name}: ${capitalize(status)}`;
|
||||
}
|
||||
|
||||
// Get the color class for the status
|
||||
// that could be either an outline or a fill
|
||||
let dotClass = getStatusColor(status);
|
||||
|
||||
// If dotClass contains "outline", then we will use 'outline-1 outline-offset-[-2px]
|
||||
</script>
|
||||
|
||||
<Tooltip tip="{tooltip}" top>
|
||||
<div
|
||||
class="w-2.5 h-2.5 mr-0.5 rounded-full text-center {dotClass.includes('outline')
|
||||
? 'outline-2 outline-offset-[-2px] outline'
|
||||
: ''} {getStatusColor(status)} {number ? 'mt-3' : ''}"
|
||||
data-testid="status-dot"
|
||||
title="{tooltip}">
|
||||
</div>
|
||||
<!-- If text -->
|
||||
{#if number}
|
||||
<div class="text-xs text-bold text-gray-600 mr-0.5">{number}</div>
|
||||
{/if}
|
||||
</Tooltip>
|
||||
27
packages/renderer/src/lib/ui/Util.spec.ts
Normal file
27
packages/renderer/src/lib/ui/Util.spec.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
/**********************************************************************
|
||||
* 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 { capitalize } from './Util';
|
||||
|
||||
test('test capitalize function', () => {
|
||||
expect(capitalize('test')).toBe('Test');
|
||||
expect(capitalize('Test')).toBe('Test');
|
||||
expect(capitalize('TEST')).toBe('TEST');
|
||||
});
|
||||
21
packages/renderer/src/lib/ui/Util.ts
Normal file
21
packages/renderer/src/lib/ui/Util.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/**********************************************************************
|
||||
* 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
|
||||
***********************************************************************/
|
||||
|
||||
export function capitalize(text: string): string {
|
||||
return text.charAt(0).toUpperCase() + text.slice(1);
|
||||
}
|
||||
|
|
@ -43,6 +43,33 @@ module.exports = {
|
|||
},
|
||||
},
|
||||
colors: {
|
||||
// The "status" colours to be used for Podman and Kubernetes containers
|
||||
// these can be referenced by in the form of "bg-status-running" or "text-status-running"
|
||||
'status': {
|
||||
// Podman & Kubernetes
|
||||
'running': tailwindColors.green[400],
|
||||
|
||||
// Kubernetes only
|
||||
'terminated': tailwindColors.red[500],
|
||||
'waiting': tailwindColors.amber[600],
|
||||
|
||||
// Podman only
|
||||
|
||||
// Stopped & Exited are the same color / same thing in the eyes of statuses
|
||||
'stopped': tailwindColors.gray[300],
|
||||
'exited': tailwindColors.gray[300],
|
||||
|
||||
// "Warning"
|
||||
'paused': tailwindColors.amber[600],
|
||||
'degraded': tailwindColors.amber[700],
|
||||
|
||||
// Others
|
||||
'created': tailwindColors.green[300],
|
||||
'dead': tailwindColors.red[500],
|
||||
|
||||
// If we don't know the status, use gray
|
||||
'unknown': tailwindColors.gray[100],
|
||||
},
|
||||
'charcoal': {
|
||||
50: '#767676',
|
||||
100: '#707073',
|
||||
|
|
|
|||
Loading…
Reference in a new issue