Skip to content

Commit

Permalink
feat: allow extensions to customize icons (containers#1899)
Browse files Browse the repository at this point in the history
Signed-off-by: lstocchi <lstocchi@redhat.com>
  • Loading branch information
lstocchi committed Jul 6, 2023
1 parent 762d35f commit 694c5a1
Show file tree
Hide file tree
Showing 22 changed files with 1,899 additions and 4 deletions.
Binary file added extensions/kind/kind-icon.woff2
Binary file not shown.
17 changes: 17 additions & 0 deletions extensions/kind/package.json
Original file line number Diff line number Diff line change
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
1 change: 1 addition & 0 deletions packages/main/src/plugin/api/container-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface ContainerInfo extends Dockerode.ContainerInfo {
status: string;
engineId: string;
};
icon?: string;
}

export interface SimpleContainerInfo extends Dockerode.ContainerInfo {
Expand Down
22 changes: 22 additions & 0 deletions packages/main/src/plugin/api/view-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**********************************************************************
* 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 interface ViewContribution {
id: string;
when: string | null | undefined;
icon: string;
}
2 changes: 2 additions & 0 deletions packages/main/src/plugin/authentication.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import type { Proxy } from './proxy.js';
import type { IconRegistry } from './icon-registry.js';
import type { Directories } from './directories.js';
import type { CustomPickRegistry } from './custompick/custompick-registry.js';
import type { ViewRegistry } from './view-registry.js';

function randomNumber(n = 5) {
return Math.round(Math.random() * 10 * n);
Expand Down Expand Up @@ -255,6 +256,7 @@ suite('Authentication', () => {
authentication,
vi.fn() as unknown as IconRegistry,
vi.fn() as unknown as Telemetry,
vi.fn() as unknown as ViewRegistry,
directories,
);
providerMock = {
Expand Down
4 changes: 3 additions & 1 deletion packages/main/src/plugin/container-registry.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type { Proxy } from '/@/plugin/proxy.js';
import { ImageRegistry } from '/@/plugin/image-registry.js';
import type { ApiSenderType } from '/@/plugin/api.js';
import type Dockerode from 'dockerode';
import { ViewRegistry } from './view-registry.js';

/* eslint-disable @typescript-eslint/no-empty-function */

Expand Down Expand Up @@ -56,7 +57,8 @@ beforeEach(() => {
} as unknown as Proxy;

const imageRegistry = new ImageRegistry({} as ApiSenderType, telemetry, certificates, proxy);
containerRegistry = new TestContainerProviderRegistry({} as ApiSenderType, imageRegistry, telemetry);
const viewRegistry = new ViewRegistry();
containerRegistry = new TestContainerProviderRegistry({} as ApiSenderType, imageRegistry, telemetry, viewRegistry);
});

test('tag should reject if no provider', async () => {
Expand Down
24 changes: 24 additions & 0 deletions packages/main/src/plugin/container-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ import { Emitter } from './events/emitter.js';
import fs from 'node:fs';
import { pipeline } from 'node:stream/promises';
import type { ApiSenderType } from './api.js';
import { ContextKeyService } from './contextkey/contextKeyService.js';
import { ContextKeyExpr } from './contextkey/contextKey.js';
import type { ViewRegistry } from './view-registry.js';
export interface InternalContainerProvider {
name: string;
id: string;
Expand Down Expand Up @@ -79,6 +82,7 @@ export class ContainerProviderRegistry {
private apiSender: ApiSenderType,
private imageRegistry: ImageRegistry,
private telemetryService: Telemetry,
private viewRegistry: ViewRegistry,
) {
const libPodDockerode = new LibpodDockerode();
libPodDockerode.enhancePrototypeWithLibPod();
Expand Down Expand Up @@ -266,6 +270,8 @@ export class ContainerProviderRegistry {

async listContainers(): Promise<ContainerInfo[]> {
let telemetryOptions = {};
const viewContribution = this.viewRegistry.getViewContribution('icons/containersList');
const containerContext = new ContextKeyService();
const containers = await Promise.all(
Array.from(this.internalProviders.values()).map(async provider => {
try {
Expand Down Expand Up @@ -312,6 +318,22 @@ export class ContainerProviderRegistry {
StartedAt = '';
}

// it prepares the context for the icon contributions
const contextId = containerContext.createChildContext();
const context = containerContext.getContextValuesContainer(contextId);
context.setValue('containerLabelKeys', Object.keys(container.Labels));
let icon;

// it checks if someone contributed to the containerlist view
viewContribution.every(contribution => {
const contextExprDeserialized = ContextKeyExpr.deserialize(contribution.when);
if (contextExprDeserialized?.evaluate(context)) {
icon = contribution.icon;
return false;
}
return true;
});

// do we have a matching pod for this container ?
let pod;
const matchingPod = pods.find(pod =>
Expand All @@ -332,6 +354,7 @@ export class ContainerProviderRegistry {
engineId: provider.id,
engineType: provider.connection.type,
StartedAt,
icon,
};
return containerInfo;
}),
Expand All @@ -343,6 +366,7 @@ export class ContainerProviderRegistry {
}
}),
);
containerContext.dispose();
const flatttenedContainers = containers.flat();
this.telemetryService
.track('listContainers', Object.assign({ total: flatttenedContainers.length }, telemetryOptions))
Expand Down
64 changes: 64 additions & 0 deletions packages/main/src/plugin/contextkey/contextKey.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**********************************************************************
* 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 */
/* eslint-disable @typescript-eslint/no-non-null-assertion */

import * as assert from 'assert';
import { suite, test } from 'vitest';

import { ContextKeyExpr } from './contextKey.js';

function createContext(ctx: any) {
return {
getValue: (key: string) => {
return ctx[key];
},
};
}

suite('ContextKeyExpr', () => {
test('ContextKeyInExpr', () => {
const ainb = ContextKeyExpr.deserialize('a in b')!;
assert.strictEqual(ainb.evaluate(createContext({ a: 3, b: [3, 2, 1] })), true);
assert.strictEqual(ainb.evaluate(createContext({ a: 3, b: [1, 2, 3] })), true);
assert.strictEqual(ainb.evaluate(createContext({ a: 3, b: [1, 2] })), false);
assert.strictEqual(ainb.evaluate(createContext({ a: 3 })), false);
assert.strictEqual(ainb.evaluate(createContext({ a: 3, b: null })), false);
assert.strictEqual(ainb.evaluate(createContext({ a: 'x', b: ['x'] })), true);
assert.strictEqual(ainb.evaluate(createContext({ a: 'x', b: ['y'] })), false);
assert.strictEqual(ainb.evaluate(createContext({ a: 'x', b: {} })), false);
assert.strictEqual(ainb.evaluate(createContext({ a: 'x', b: { x: false } })), true);
assert.strictEqual(ainb.evaluate(createContext({ a: 'x', b: { x: true } })), true);
assert.strictEqual(ainb.evaluate(createContext({ a: 'prototype', b: {} })), false);
});

test('ContextKeyNotInExpr', () => {
const aNotInB = ContextKeyExpr.deserialize('a not in b')!;
assert.strictEqual(aNotInB.evaluate(createContext({ a: 3, b: [3, 2, 1] })), false);
assert.strictEqual(aNotInB.evaluate(createContext({ a: 3, b: [1, 2, 3] })), false);
assert.strictEqual(aNotInB.evaluate(createContext({ a: 3, b: [1, 2] })), true);
assert.strictEqual(aNotInB.evaluate(createContext({ a: 3 })), true);
assert.strictEqual(aNotInB.evaluate(createContext({ a: 3, b: null })), true);
assert.strictEqual(aNotInB.evaluate(createContext({ a: 'x', b: ['x'] })), false);
assert.strictEqual(aNotInB.evaluate(createContext({ a: 'x', b: ['y'] })), true);
assert.strictEqual(aNotInB.evaluate(createContext({ a: 'x', b: {} })), true);
assert.strictEqual(aNotInB.evaluate(createContext({ a: 'x', b: { x: false } })), false);
assert.strictEqual(aNotInB.evaluate(createContext({ a: 'x', b: { x: true } })), false);
assert.strictEqual(aNotInB.evaluate(createContext({ a: 'prototype', b: {} })), true);
});
});

0 comments on commit 694c5a1

Please sign in to comment.