Skip to content

Commit

Permalink
feat: allow extensions to customize icons (#1899) (#3131)
Browse files Browse the repository at this point in the history
* feat: allow extensions to customize icons (#1899)

Signed-off-by: lstocchi <lstocchi@redhat.com>

* fix: keep storage to server and move evaluation to renderer

Signed-off-by: lstocchi <lstocchi@redhat.com>

* fix: clean and add tests

Signed-off-by: lstocchi <lstocchi@redhat.com>

* fix: rename file and fix function signature

Signed-off-by: lstocchi <lstocchi@redhat.com>

* fix: move contextKeyValue type

Signed-off-by: lstocchi <lstocchi@redhat.com>

* fix: clean after messed rebase

Signed-off-by: lstocchi <lstocchi@redhat.com>

* fix: replace icon and refactor

Signed-off-by: lstocchi <lstocchi@redhat.com>

* chore: replace icon with one with margin

Signed-off-by: lstocchi <lstocchi@redhat.com>

---------

Signed-off-by: lstocchi <lstocchi@redhat.com>
  • Loading branch information
lstocchi committed Jul 21, 2023
1 parent c426d00 commit 1ebe4c7
Show file tree
Hide file tree
Showing 9 changed files with 305 additions and 57 deletions.
Binary file added extensions/kind/kind-icon.woff2
Binary file not shown.
17 changes: 17 additions & 0 deletions extensions/kind/package.json
Expand Up @@ -50,13 +50,30 @@
}
}
},
"icons": {
"kind-icon": {
"description": "Kind icon",
"default": {
"fontPath": "kind-icon.woff2",
"fontCharacter": "\\EA01"
}
}
},
"menus": {
"dashboard/image": [
{
"command": "kind.image.move",
"title": "Push image to Kind cluster"
}
]
},
"views": {
"icons/containersList": [
{
"when": "io.x-k8s.kind.cluster in containerLabelKeys",
"icon": "${kind-icon}"
}
]
}
},
"scripts": {
Expand Down
144 changes: 90 additions & 54 deletions packages/renderer/src/lib/ContainerList.svelte
@@ -1,9 +1,10 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import { filtered, searchPattern, containersInfos } from '../stores/containers';
import { viewsContributions } from '../stores/views';
import { contexts } from '../stores/contexts';
import type { ContainerInfo } from '../../../main/src/plugin/api/container-info';
import ContainerIcon from './images/ContainerIcon.svelte';
import PodIcon from './images/PodIcon.svelte';
import StatusIcon from './images/StatusIcon.svelte';
import { router } from 'tinro';
Expand Down Expand Up @@ -32,13 +33,19 @@ import type { PodInfo } from '../../../main/src/plugin/api/pod-info';
import { PodUtils } from '../lib/pod/pod-utils';
import ComposeActions from './compose/ComposeActions.svelte';
import Spinner from './ui/Spinner.svelte';
import { CONTAINER_LIST_VIEW } from './view/views';
import type { ViewInfoUI } from '../../../main/src/plugin/api/view-info';
import type { ContextUI } from './context/context';
const containerUtils = new ContainerUtils();
let openChoiceModal = false;
let enginesList: EngineInfoUI[];
// groups of containers that will be displayed
let containerGroups: ContainerGroupInfoUI[] = [];
let viewContributions: ViewInfoUI[] = [];
let extensionsContext: ContextUI[] = [];
let containersInfo: ContainerInfo[] = [];
export let searchTerm = '';
$: searchPattern.set(searchTerm);
Expand Down Expand Up @@ -205,72 +212,95 @@ function createPodFromContainers() {
}
let containersUnsubscribe: Unsubscriber;
let contextsUnsubscribe: Unsubscriber;
let podUnsubscribe: Unsubscriber;
let viewsUnsubscribe: Unsubscriber;
let pods: PodInfo[];
onMount(async () => {
// grab previous groups
containerGroups = get(containerGroupsInfo);
containersUnsubscribe = filtered.subscribe(value => {
const currentContainers = value.map((containerInfo: ContainerInfo) => {
return containerUtils.getContainerInfoUI(containerInfo);
});
contextsUnsubscribe = contexts.subscribe(value => {
extensionsContext = value;
if (containersInfo.length > 0) {
updateContainers(containersInfo, extensionsContext, viewContributions);
}
});
// Map engineName, engineId and engineType from currentContainers to EngineInfoUI[]
const engines = currentContainers.map(container => {
return {
name: container.engineName,
id: container.engineId,
};
});
viewsUnsubscribe = viewsContributions.subscribe(value => {
viewContributions = value.filter(view => view.viewId === CONTAINER_LIST_VIEW) || [];
if (containersInfo.length > 0) {
updateContainers(containersInfo, extensionsContext, viewContributions);
}
});
// Remove duplicates from engines by name
const uniqueEngines = engines.filter(
(engine, index, self) => index === self.findIndex(t => t.name === engine.name),
);
containersUnsubscribe = filtered.subscribe(value => {
updateContainers(value, extensionsContext, viewContributions);
});
if (uniqueEngines.length > 1) {
multipleEngines = true;
} else {
multipleEngines = false;
}
podUnsubscribe = podsInfos.subscribe(podInfos => {
pods = podInfos;
});
});
// Set the engines to the global variable for the Prune functionality button
enginesList = uniqueEngines;
// create groups
const computedContainerGroups = containerUtils.getContainerGroups(currentContainers);
// update selected items based on current selected items
computedContainerGroups.forEach(group => {
const matchingGroup = containerGroups.find(currentGroup => currentGroup.name === group.name);
if (matchingGroup) {
group.selected = matchingGroup.selected;
group.expanded = matchingGroup.expanded;
group.containers.forEach(container => {
const matchingContainer = matchingGroup.containers.find(
currentContainer => currentContainer.id === container.id,
);
if (matchingContainer) {
container.actionError = matchingContainer.actionError;
container.selected = matchingContainer.selected;
}
});
}
});
function updateContainers(
containers: ContainerInfo[],
extensionsContext: ContextUI[],
viewContributions: ViewInfoUI[],
) {
containersInfo = containers;
const currentContainers = containers.map((containerInfo: ContainerInfo) => {
return containerUtils.getContainerInfoUI(containerInfo, extensionsContext, viewContributions);
});
// update the value
containerGroups = computedContainerGroups;
// Map engineName, engineId and engineType from currentContainers to EngineInfoUI[]
const engines = currentContainers.map(container => {
return {
name: container.engineName,
id: container.engineId,
};
});
// compute refresh interval
const interval = computeInterval();
refreshTimeouts.push(setTimeout(refreshUptime, interval));
// Remove duplicates from engines by name
const uniqueEngines = engines.filter((engine, index, self) => index === self.findIndex(t => t.name === engine.name));
podUnsubscribe = podsInfos.subscribe(podInfos => {
pods = podInfos;
});
if (uniqueEngines.length > 1) {
multipleEngines = true;
} else {
multipleEngines = false;
}
// Set the engines to the global variable for the Prune functionality button
enginesList = uniqueEngines;
// create groups
const computedContainerGroups = containerUtils.getContainerGroups(currentContainers);
// update selected items based on current selected items
computedContainerGroups.forEach(group => {
const matchingGroup = containerGroups.find(currentGroup => currentGroup.name === group.name);
if (matchingGroup) {
group.selected = matchingGroup.selected;
group.expanded = matchingGroup.expanded;
group.containers.forEach(container => {
const matchingContainer = matchingGroup.containers.find(
currentContainer => currentContainer.id === container.id,
);
if (matchingContainer) {
container.actionError = matchingContainer.actionError;
container.selected = matchingContainer.selected;
}
});
}
});
});
// update the value
containerGroups = computedContainerGroups;
// compute refresh interval
const interval = computeInterval();
refreshTimeouts.push(setTimeout(refreshUptime, interval));
}
onDestroy(() => {
// store current groups for later
Expand All @@ -284,9 +314,15 @@ onDestroy(() => {
if (containersUnsubscribe) {
containersUnsubscribe();
}
if (contextsUnsubscribe) {
contextsUnsubscribe();
}
if (podUnsubscribe) {
podUnsubscribe();
}
if (viewsUnsubscribe) {
viewsUnsubscribe();
}
});
function openDetailsContainer(container: ContainerInfoUI) {
Expand Down Expand Up @@ -515,7 +551,7 @@ function errorCallback(container: ContainerInfoUI, errorMessage: string): void {
</td>
<td class="flex flex-row justify-center h-12">
<div class="grid place-content-center ml-3 mr-4">
<StatusIcon icon="{ContainerIcon}" status="{container.state}" />
<StatusIcon icon="{container.icon}" status="{container.state}" />
</div>
</td>
<td
Expand Down
2 changes: 2 additions & 0 deletions packages/renderer/src/lib/container/ContainerInfoUI.ts
Expand Up @@ -64,6 +64,8 @@ export interface ContainerInfoUI {
created: number;
actionInProgress?: boolean;
actionError?: string;
labels: { [label: string]: string };
icon?: any;
}

export interface ContainerGroupInfoUI extends ContainerGroupPartInfoUI {
Expand Down
48 changes: 48 additions & 0 deletions packages/renderer/src/lib/container/container-utils.spec.ts
Expand Up @@ -20,6 +20,8 @@ import { beforeEach, expect, test, vi } from 'vitest';
import { ContainerUtils } from './container-utils';
import type { ContainerInfo } from '../../../../main/src/plugin/api/container-info';
import { ContainerGroupInfoTypeUI } from './ContainerInfoUI';
import { ContextUI } from '../context/context';
import type { ViewInfoUI } from '../../../../main/src/plugin/api/view-info';

let containerUtils: ContainerUtils;

Expand Down Expand Up @@ -201,3 +203,49 @@ test('container group status should be degraded when the pod status is degraded'
expect(group.type).toBe(ContainerGroupInfoTypeUI.POD);
expect(group.status).toBe('DEGRADED');
});

test('should expect icon to be undefined if no context/view is passed', async () => {
const containerInfo = {
Image: 'docker.io/kindest/node:foobar',
Ports: [
{
PublicPort: 80,
},
{
PublicPort: 8022,
},
],
} as unknown as ContainerInfo;
const icon = containerUtils.iconClass(containerInfo);
expect(icon).toBe(undefined);
});

test('should expect icon to be valid value with context/view set', async () => {
const context = new ContextUI(1, undefined, 'extension');
const view: ViewInfoUI = {
extensionId: 'extension',
viewId: 'id',
icon: '${kind-icon}',
when: 'io.x-k8s.kind.cluster in containerLabelKeys',
};
const containerInfo = {
Image: 'docker.io/kindest/node:foobar',
Labels: {
'io.x-k8s.kind.cluster': 'ok',
},
} as unknown as ContainerInfo;
const icon = containerUtils.iconClass(containerInfo, [context], [view]);
expect(icon).toBe('podman-desktop-icon-kind-icon');
});

test('should expect icon to be ContainerIcon if no context/view is passed', async () => {
const containerInfo = {
Id: 'container1',
Image: 'docker.io/kindest/node:foobar',
Names: ['container1'],
State: 'STOPPED',
} as unknown as ContainerInfo;
const containerUI = containerUtils.getContainerInfoUI(containerInfo);
expect(containerUI.icon).toBeDefined();
expect(typeof containerUI.icon !== 'string').toBe(true);
});
54 changes: 53 additions & 1 deletion packages/renderer/src/lib/container/container-utils.ts
Expand Up @@ -23,6 +23,11 @@ import moment from 'moment';
import humanizeDuration from 'humanize-duration';
import { filesize } from 'filesize';
import type { Port } from '@podman-desktop/api';
import type { ContextUI } from '../context/context';
import type { ViewInfoUI } from '../../../../main/src/plugin/api/view-info';
import { ContextKeyExpr } from '../context/contextKey';
import ContainerIcon from '../images/ContainerIcon.svelte';

export class ContainerUtils {
getName(containerInfo: ContainerInfo) {
// part of a compose ?
Expand Down Expand Up @@ -113,7 +118,11 @@ export class ContainerUtils {
return containerInfo.engineName;
}

getContainerInfoUI(containerInfo: ContainerInfo): ContainerInfoUI {
getContainerInfoUI(
containerInfo: ContainerInfo,
extensionsContext?: ContextUI[],
viewContributions?: ViewInfoUI[],
): ContainerInfoUI {
return {
id: containerInfo.Id,
shortId: containerInfo.Id.substring(0, 8),
Expand All @@ -135,6 +144,8 @@ export class ContainerUtils {
groupInfo: this.getContainerGroup(containerInfo),
selected: false,
created: containerInfo.Created,
labels: containerInfo.Labels,
icon: this.iconClass(containerInfo, extensionsContext, viewContributions) || ContainerIcon,
};
}

Expand Down Expand Up @@ -219,4 +230,45 @@ export class ContainerUtils {
return '';
}
}

iconClass(
container: ContainerInfo,
extensionsContext?: ContextUI[],
viewContributions?: ViewInfoUI[],
): string | undefined {
if (!extensionsContext || !viewContributions) {
return undefined;
}

let icon;
// loop over all contribution for this view
for (const contribution of viewContributions) {
// retrieve the extension from the contribution and fetch its context
const extensionContext: ContextUI = extensionsContext.find(ctx => {
return ctx.extension === contribution.extensionId;
});
if (extensionContext) {
// adapt the context to work with containers (e.g save container labels into the context)
this.adaptContextOnContainer(extensionContext, container);
// deserialize the when clause
const whenDeserialized = ContextKeyExpr.deserialize(contribution.when);
// if the when clause has to be applied to this container
if (whenDeserialized?.evaluate(extensionContext)) {
// handle ${} in icon class
// and interpret the value and replace with the class-name
const match = contribution.icon.match(/\$\{(.*)\}/);
if (match && match.length === 2) {
const className = match[1];
icon = contribution.icon.replace(match[0], `podman-desktop-icon-${className}`);
return icon;
}
}
}
}
return icon;
}

adaptContextOnContainer(context: ContextUI, container: ContainerInfo): void {
context.setValue('containerLabelKeys', Object.keys(container.Labels));
}
}

0 comments on commit 1ebe4c7

Please sign in to comment.