mirror of
https://github.com/podman-desktop/podman-desktop
synced 2026-05-24 10:18:53 +00:00
feat: add config maps and secrets to k8s (renderer code) (#8042)
* feat: add config maps and secrets to k8s (renderer code) ### What does this PR do? * Adds ConfigMaps and Secrets to Kubernetes * Ability to delete / view / edit both configmap and secrets ### Screenshot / video of UI <!-- If this PR is changing UI, please include screenshots or screencasts showing the difference -->  https://github.com/containers/podman-desktop/assets/6422176/41108199-7e2b-4cc2-81f6-2b267b1c887a ### 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/7342 Closes https://github.com/containers/podman-desktop/issues/7190 (its the last part of it) ### How to test this PR? <!-- Please explain steps to verify the functionality, do not forget to provide unit/component tests --> - [X] Tests are covering the bug fix or the new feature 1. Create a ConfigMap and Secrets on your k8s cluster. 2. Delete / Edit / View them Signed-off-by: Charlie Drage <charlie@charliedrage.com> * update based on review Signed-off-by: Charlie Drage <charlie@charliedrage.com> * update namespace column and order in app navigation Signed-off-by: Charlie Drage <charlie@charliedrage.com> * change search to lowercase Signed-off-by: Charlie Drage <charlie@charliedrage.com> --------- Signed-off-by: Charlie Drage <charlie@charliedrage.com>
This commit is contained in:
parent
041f011996
commit
7e577d877c
31 changed files with 1592 additions and 0 deletions
|
|
@ -10,6 +10,9 @@ import type { NavigationRequest } from '/@api/navigation-request';
|
|||
import AppNavigation from './AppNavigation.svelte';
|
||||
import Appearance from './lib/appearance/Appearance.svelte';
|
||||
import ComposeDetails from './lib/compose/ComposeDetails.svelte';
|
||||
import ConfigMapDetails from './lib/configmaps-secrets/ConfigMapDetails.svelte';
|
||||
import ConfigMapSecretList from './lib/configmaps-secrets/ConfigMapSecretList.svelte';
|
||||
import SecretDetails from './lib/configmaps-secrets/SecretDetails.svelte';
|
||||
import ContainerDetails from './lib/container/ContainerDetails.svelte';
|
||||
import ContainerExport from './lib/container/ContainerExport.svelte';
|
||||
import ContainerList from './lib/container/ContainerList.svelte';
|
||||
|
|
@ -244,6 +247,23 @@ window.events?.receive('navigate', (navigationRequest: unknown) => {
|
|||
navigationHint="details">
|
||||
<IngressDetails name="{decodeURI(meta.params.name)}" namespace="{decodeURI(meta.params.namespace)}" />
|
||||
</Route>
|
||||
<Route path="/configmapsSecrets" breadcrumb="ConfigMaps & Secrets" navigationHint="root">
|
||||
<ConfigMapSecretList />
|
||||
</Route>
|
||||
<Route
|
||||
path="/configmapsSecrets/configmap/:name/:namespace/*"
|
||||
breadcrumb="ConfigMap Details"
|
||||
let:meta
|
||||
navigationHint="details">
|
||||
<ConfigMapDetails name="{decodeURI(meta.params.name)}" namespace="{decodeURI(meta.params.namespace)}" />
|
||||
</Route>
|
||||
<Route
|
||||
path="/configmapsSecrets/secret/:name/:namespace/*"
|
||||
breadcrumb="Secret Details"
|
||||
let:meta
|
||||
navigationHint="details">
|
||||
<SecretDetails name="{decodeURI(meta.params.name)}" namespace="{decodeURI(meta.params.namespace)}" />
|
||||
</Route>
|
||||
<Route
|
||||
path="/ingressesRoutes/route/:name/:namespace/*"
|
||||
breadcrumb="Route Details"
|
||||
|
|
|
|||
|
|
@ -52,6 +52,8 @@ test('Test rendering of the navigation bar with empty items', () => {
|
|||
vi.mocked(kubeContextStore).kubernetesCurrentContextIngresses = readable<KubernetesObject[]>([]);
|
||||
vi.mocked(kubeContextStore).kubernetesCurrentContextRoutes = readable<KubernetesObject[]>([]);
|
||||
vi.mocked(kubeContextStore).kubernetesCurrentContextNodes = readable<KubernetesObject[]>([]);
|
||||
vi.mocked(kubeContextStore).kubernetesCurrentContextConfigMaps = readable<KubernetesObject[]>([]);
|
||||
vi.mocked(kubeContextStore).kubernetesCurrentContextSecrets = readable<KubernetesObject[]>([]);
|
||||
vi.mocked(kubeContextStore).kubernetesCurrentContextPersistentVolumeClaims = readable<KubernetesObject[]>([]);
|
||||
|
||||
render(AppNavigation, {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import type { ImageInfo } from '/@api/image-info';
|
|||
import { CommandRegistry } from './lib/CommandRegistry';
|
||||
import NewContentOnDashboardBadge from './lib/dashboard/NewContentOnDashboardBadge.svelte';
|
||||
import { ImageUtils } from './lib/image/image-utils';
|
||||
import ConfigMapSecretIcon from './lib/images/ConfigMapSecretIcon.svelte';
|
||||
import DashboardIcon from './lib/images/DashboardIcon.svelte';
|
||||
import DeploymentIcon from './lib/images/DeploymentIcon.svelte';
|
||||
import ExtensionIcon from './lib/images/ExtensionIcon.svelte';
|
||||
|
|
@ -34,11 +35,13 @@ import { contributions } from './stores/contribs';
|
|||
import { imagesInfos } from './stores/images';
|
||||
import { kubernetesContexts } from './stores/kubernetes-contexts';
|
||||
import {
|
||||
kubernetesCurrentContextConfigMaps,
|
||||
kubernetesCurrentContextDeployments,
|
||||
kubernetesCurrentContextIngresses,
|
||||
kubernetesCurrentContextNodes,
|
||||
kubernetesCurrentContextPersistentVolumeClaims,
|
||||
kubernetesCurrentContextRoutes,
|
||||
kubernetesCurrentContextSecrets,
|
||||
kubernetesCurrentContextServices,
|
||||
} from './stores/kubernetes-contexts-state';
|
||||
import { podsInfos } from './stores/pods';
|
||||
|
|
@ -55,12 +58,17 @@ let persistentVolumeClaimsSubscribe: Unsubscriber;
|
|||
let servicesSubscribe: Unsubscriber;
|
||||
let ingressesSubscribe: Unsubscriber;
|
||||
let routesSubscribe: Unsubscriber;
|
||||
let configmapsSubscribe: Unsubscriber;
|
||||
let secretsSubscribe: Unsubscriber;
|
||||
let combinedInstalledExtensionsSubscribe: Unsubscriber;
|
||||
|
||||
let podCount = '';
|
||||
let containerCount = '';
|
||||
let imageCount = '';
|
||||
let volumeCount = '';
|
||||
let configmapsCount = 0;
|
||||
let secretsCount = 0;
|
||||
let configmapSecretsCount = '';
|
||||
let persistentVolumeClaimsCount = '';
|
||||
let contextCount = 0;
|
||||
let deploymentCount = '';
|
||||
|
|
@ -145,6 +153,15 @@ onMount(async () => {
|
|||
routesCount = value.length;
|
||||
updateIngressesRoutesCount(ingressesCount + routesCount);
|
||||
});
|
||||
|
||||
configmapsSubscribe = kubernetesCurrentContextConfigMaps.subscribe(value => {
|
||||
configmapsCount = value.length;
|
||||
updateConfigMapSecretsCount(configmapsCount + secretsCount);
|
||||
});
|
||||
secretsSubscribe = kubernetesCurrentContextSecrets.subscribe(value => {
|
||||
secretsCount = value.length;
|
||||
updateConfigMapSecretsCount(configmapsCount + secretsCount);
|
||||
});
|
||||
contextsSubscribe = kubernetesContexts.subscribe(value => {
|
||||
contextCount = value.length;
|
||||
});
|
||||
|
|
@ -185,8 +202,16 @@ onDestroy(() => {
|
|||
if (servicesSubscribe) {
|
||||
servicesSubscribe();
|
||||
}
|
||||
if (configmapsSubscribe) {
|
||||
configmapsSubscribe();
|
||||
}
|
||||
if (secretsSubscribe) {
|
||||
secretsSubscribe();
|
||||
}
|
||||
ingressesSubscribe?.();
|
||||
routesSubscribe?.();
|
||||
configmapsSubscribe?.();
|
||||
secretsSubscribe?.();
|
||||
combinedInstalledExtensionsSubscribe?.();
|
||||
});
|
||||
|
||||
|
|
@ -198,6 +223,14 @@ function updateIngressesRoutesCount(count: number) {
|
|||
}
|
||||
}
|
||||
|
||||
function updateConfigMapSecretsCount(count: number) {
|
||||
if (count > 0) {
|
||||
configmapSecretsCount = ' (' + count + ')';
|
||||
} else {
|
||||
configmapSecretsCount = '';
|
||||
}
|
||||
}
|
||||
|
||||
function clickSettings(b: boolean) {
|
||||
if (b) {
|
||||
exitSettingsCallback();
|
||||
|
|
@ -262,6 +295,13 @@ export let meta: TinroRouteMeta;
|
|||
bind:meta="{meta}">
|
||||
<PVCIcon size="{iconSize}" />
|
||||
</NavItem>
|
||||
<NavItem
|
||||
href="/configmapsSecrets"
|
||||
tooltip="ConfigMaps & Secrets{configmapSecretsCount}"
|
||||
ariaLabel="ConfigMaps & Secrets"
|
||||
bind:meta="{meta}">
|
||||
<ConfigMapSecretIcon size="{iconSize}" />
|
||||
</NavItem>
|
||||
</NavSection>
|
||||
{/if}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2024 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 type { KubernetesObject, V1ConfigMap } from '@kubernetes/client-node';
|
||||
import { render, screen } from '@testing-library/svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import { beforeAll, expect, test, vi } from 'vitest';
|
||||
|
||||
import * as kubeContextStore from '/@/stores/kubernetes-contexts-state';
|
||||
|
||||
import ConfigMapDetails from './ConfigMapDetails.svelte';
|
||||
|
||||
const configMap: V1ConfigMap = {
|
||||
metadata: {
|
||||
name: 'my-configmap',
|
||||
namespace: 'default',
|
||||
},
|
||||
data: {},
|
||||
};
|
||||
|
||||
vi.mock('/@/stores/kubernetes-contexts-state', async () => {
|
||||
return {
|
||||
kubernetesCurrentContextConfigMaps: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
(window as any).kubernetesReadNamespacedConfigMap = vi.fn();
|
||||
});
|
||||
|
||||
test('Confirm renders configmap details', async () => {
|
||||
// mock object store
|
||||
const configMaps = writable<KubernetesObject[]>([configMap]);
|
||||
vi.mocked(kubeContextStore).kubernetesCurrentContextConfigMaps = configMaps;
|
||||
|
||||
render(ConfigMapDetails, { name: 'my-configmap', namespace: 'default' });
|
||||
|
||||
expect(screen.getByText('my-configmap')).toBeInTheDocument();
|
||||
expect(screen.getByText('default')).toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
<script lang="ts">
|
||||
import type { V1ConfigMap } from '@kubernetes/client-node';
|
||||
import { StatusIcon, Tab } from '@podman-desktop/ui-svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { router } from 'tinro';
|
||||
import { stringify } from 'yaml';
|
||||
|
||||
import { kubernetesCurrentContextConfigMaps } from '/@/stores/kubernetes-contexts-state';
|
||||
|
||||
import Route from '../../Route.svelte';
|
||||
import MonacoEditor from '../editor/MonacoEditor.svelte';
|
||||
import ConfigMapIcon from '../images/ConfigMapSecretIcon.svelte';
|
||||
import KubeEditYAML from '../kube/KubeEditYAML.svelte';
|
||||
import DetailsPage from '../ui/DetailsPage.svelte';
|
||||
import StateChange from '../ui/StateChange.svelte';
|
||||
import { getTabUrl, isTabSelected } from '../ui/Util';
|
||||
import { ConfigMapSecretUtils } from './configmap-secret-utils';
|
||||
import ConfigMapDetailsSummary from './ConfigMapDetailsSummary.svelte';
|
||||
import ConfigMapSecretActions from './ConfigMapSecretActions.svelte';
|
||||
import type { ConfigMapSecretUI } from './ConfigMapSecretUI';
|
||||
|
||||
export let name: string;
|
||||
export let namespace: string;
|
||||
|
||||
let configMap: ConfigMapSecretUI;
|
||||
let detailsPage: DetailsPage;
|
||||
let kubeConfigMap: V1ConfigMap | undefined;
|
||||
let kubeError: string;
|
||||
|
||||
onMount(() => {
|
||||
const configMapUtils = new ConfigMapSecretUtils();
|
||||
// loading configMap info
|
||||
return kubernetesCurrentContextConfigMaps.subscribe(configMaps => {
|
||||
const matchingConfigMap = configMaps.find(
|
||||
configMap => configMap.metadata?.name === name && configMap.metadata?.namespace === namespace,
|
||||
);
|
||||
if (matchingConfigMap) {
|
||||
try {
|
||||
configMap = configMapUtils.getConfigMapSecretUI(matchingConfigMap);
|
||||
loadDetails();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
} else if (detailsPage) {
|
||||
// the configMap has been deleted
|
||||
detailsPage.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
async function loadDetails() {
|
||||
const getKubeConfigMap = await window.kubernetesReadNamespacedConfigMap(configMap.name, namespace);
|
||||
if (getKubeConfigMap) {
|
||||
kubeConfigMap = getKubeConfigMap;
|
||||
} else {
|
||||
kubeError = `Unable to retrieve Kubernetes details for ${configMap.name}`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if configMap}
|
||||
<DetailsPage title="{configMap.name}" subtitle="{configMap.namespace}" bind:this="{detailsPage}">
|
||||
<StatusIcon slot="icon" icon="{ConfigMapIcon}" size="{24}" status="{configMap.status}" />
|
||||
<svelte:fragment slot="actions">
|
||||
<ConfigMapSecretActions
|
||||
configMapSecret="{configMap}"
|
||||
detailed="{true}"
|
||||
on:update="{() => (configMap = configMap)}" />
|
||||
</svelte:fragment>
|
||||
<div slot="detail" class="flex py-2 w-full justify-end text-sm text-gray-700">
|
||||
<StateChange state="{configMap.status}" />
|
||||
</div>
|
||||
<svelte:fragment slot="tabs">
|
||||
<Tab
|
||||
title="Summary"
|
||||
selected="{isTabSelected($router.path, 'summary')}"
|
||||
url="{getTabUrl($router.path, 'summary')}" />
|
||||
<Tab
|
||||
title="Inspect"
|
||||
selected="{isTabSelected($router.path, 'inspect')}"
|
||||
url="{getTabUrl($router.path, 'inspect')}" />
|
||||
<Tab title="Kube" selected="{isTabSelected($router.path, 'kube')}" url="{getTabUrl($router.path, 'kube')}" />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content">
|
||||
<Route path="/summary" breadcrumb="Summary" navigationHint="tab">
|
||||
<ConfigMapDetailsSummary configMap="{kubeConfigMap}" kubeError="{kubeError}" />
|
||||
</Route>
|
||||
<Route path="/inspect" breadcrumb="Inspect" navigationHint="tab">
|
||||
<MonacoEditor content="{JSON.stringify(kubeConfigMap, undefined, 2)}" language="json" />
|
||||
</Route>
|
||||
<Route path="/kube" breadcrumb="Kube" navigationHint="tab">
|
||||
<KubeEditYAML content="{stringify(kubeConfigMap)}" />
|
||||
</Route>
|
||||
</svelte:fragment>
|
||||
</DetailsPage>
|
||||
{/if}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2024 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 type { V1ConfigMap } from '@kubernetes/client-node';
|
||||
import { render, screen } from '@testing-library/svelte';
|
||||
import { beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import ConfigMapDetailsSummary from './ConfigMapDetailsSummary.svelte';
|
||||
|
||||
const configMap: V1ConfigMap = {
|
||||
metadata: {
|
||||
name: 'my-configmap',
|
||||
namespace: 'default',
|
||||
},
|
||||
data: {
|
||||
key1: 'value1',
|
||||
key2: 'value2',
|
||||
},
|
||||
binaryData: {
|
||||
key3: 'value3',
|
||||
},
|
||||
};
|
||||
|
||||
const kubeError = 'Error retrieving node details';
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('Confirm renders configmap details summary', async () => {
|
||||
render(ConfigMapDetailsSummary, { configMap });
|
||||
|
||||
expect(screen.getByText('my-configmap')).toBeInTheDocument();
|
||||
expect(screen.getByText('default')).toBeInTheDocument();
|
||||
expect(screen.getByText('key1')).toBeInTheDocument();
|
||||
expect(screen.getByText('value1')).toBeInTheDocument();
|
||||
expect(screen.getByText('key2')).toBeInTheDocument();
|
||||
expect(screen.getByText('value2')).toBeInTheDocument();
|
||||
// binary data just shows the key and size, not the data
|
||||
expect(screen.getByText('key3: 6 bytes')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Expect to show loading if there is no data present', async () => {
|
||||
render(ConfigMapDetailsSummary, {});
|
||||
|
||||
const loadingMessage = screen.getByText('Loading ...');
|
||||
expect(loadingMessage).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Expect to show error message when there is a kube error', async () => {
|
||||
render(ConfigMapDetailsSummary, { configMap, kubeError: kubeError });
|
||||
|
||||
const errorMessage = screen.getByText(kubeError);
|
||||
expect(errorMessage).toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<script lang="ts">
|
||||
import type { V1ConfigMap } from '@kubernetes/client-node';
|
||||
import { ErrorMessage } from '@podman-desktop/ui-svelte';
|
||||
|
||||
import Table from '/@/lib/details/DetailsTable.svelte';
|
||||
|
||||
import KubeConfigMapArtifact from '../kube/details/KubeConfigMapArtifact.svelte';
|
||||
import KubeObjectMetaArtifact from '../kube/details/KubeObjectMetaArtifact.svelte';
|
||||
|
||||
export let configMap: V1ConfigMap | undefined;
|
||||
export let kubeError: string | undefined = undefined;
|
||||
</script>
|
||||
|
||||
<!-- Show the kube error if we're unable to retrieve the data correctly, but we still want to show the
|
||||
basic information -->
|
||||
{#if kubeError}
|
||||
<ErrorMessage error="{kubeError}" />
|
||||
{/if}
|
||||
|
||||
<Table>
|
||||
{#if configMap}
|
||||
<KubeObjectMetaArtifact artifact="{configMap.metadata}" />
|
||||
<KubeConfigMapArtifact artifact="{configMap}" />
|
||||
{:else}
|
||||
<p class="text-purple-500 font-medium">Loading ...</p>
|
||||
{/if}
|
||||
</Table>
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2024 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 { fireEvent, render, screen, waitFor } from '@testing-library/svelte';
|
||||
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import ConfigMapSecretActions from './ConfigMapSecretActions.svelte';
|
||||
import type { ConfigMapSecretUI } from './ConfigMapSecretUI';
|
||||
|
||||
const updateMock = vi.fn();
|
||||
const deleteMock = vi.fn();
|
||||
const showMessageBoxMock = vi.fn();
|
||||
|
||||
const fakeConfigMap: ConfigMapSecretUI = {
|
||||
name: 'my-configmap',
|
||||
namespace: '',
|
||||
selected: false,
|
||||
type: 'ConfigMap',
|
||||
status: '',
|
||||
keys: [],
|
||||
};
|
||||
|
||||
const fakeSecret: ConfigMapSecretUI = {
|
||||
name: 'my-secret',
|
||||
namespace: '',
|
||||
selected: false,
|
||||
type: 'Secret',
|
||||
status: '',
|
||||
keys: [],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
(window as any).showMessageBox = showMessageBoxMock;
|
||||
(window as any).kubernetesDeleteConfigMap = deleteMock;
|
||||
(window as any).kubernetesDeleteSecret = deleteMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('Expect no error when deleting configmap', async () => {
|
||||
showMessageBoxMock.mockResolvedValue({ response: 0 });
|
||||
render(ConfigMapSecretActions, { configMapSecret: fakeConfigMap, onUpdate: updateMock });
|
||||
|
||||
// click on delete button
|
||||
const deleteButton = screen.getByRole('button', { name: 'Delete ConfigMap' });
|
||||
await fireEvent.click(deleteButton);
|
||||
|
||||
// wait for the delete function to be called
|
||||
await waitFor(() => expect(deleteMock).toHaveBeenCalled());
|
||||
});
|
||||
|
||||
test('Expect no error when deleting secret', async () => {
|
||||
showMessageBoxMock.mockResolvedValue({ response: 0 });
|
||||
render(ConfigMapSecretActions, { configMapSecret: fakeSecret, onUpdate: updateMock });
|
||||
|
||||
// click on delete button
|
||||
const deleteButton = screen.getByRole('button', { name: 'Delete Secret' });
|
||||
await fireEvent.click(deleteButton);
|
||||
|
||||
// wait for the delete function to be called
|
||||
await waitFor(() => expect(deleteMock).toHaveBeenCalled());
|
||||
});
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
<script lang="ts">
|
||||
import { faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
import { withConfirmation } from '/@/lib/dialogs/messagebox-utils';
|
||||
|
||||
import ListItemButtonIcon from '../ui/ListItemButtonIcon.svelte';
|
||||
import { ConfigMapSecretUtils } from './configmap-secret-utils';
|
||||
import type { ConfigMapSecretUI } from './ConfigMapSecretUI';
|
||||
|
||||
export let configMapSecret: ConfigMapSecretUI;
|
||||
export let detailed = false;
|
||||
|
||||
const dispatch = createEventDispatcher<{ update: ConfigMapSecretUI }>();
|
||||
export let onUpdate: (update: ConfigMapSecretUI) => void = update => {
|
||||
dispatch('update', update);
|
||||
};
|
||||
|
||||
const configmapSecretUtils = new ConfigMapSecretUtils();
|
||||
|
||||
async function deleteConfigMapSecret(): Promise<void> {
|
||||
configMapSecret.status = 'DELETING';
|
||||
onUpdate(configMapSecret);
|
||||
|
||||
if (configmapSecretUtils.isSecret(configMapSecret)) {
|
||||
await window.kubernetesDeleteSecret(configMapSecret.name);
|
||||
} else {
|
||||
await window.kubernetesDeleteConfigMap((configMapSecret as ConfigMapSecretUI).name);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ListItemButtonIcon
|
||||
title="{`Delete ${configmapSecretUtils.isSecret(configMapSecret) ? 'Secret' : 'ConfigMap'}`}"
|
||||
onClick="{() =>
|
||||
withConfirmation(
|
||||
deleteConfigMapSecret,
|
||||
`delete ${configmapSecretUtils.isSecret(configMapSecret) ? 'secret' : 'configmap'} ${configMapSecret.name}`,
|
||||
)}"
|
||||
detailed="{detailed}"
|
||||
icon="{faTrash}" />
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2024 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 { render, screen } from '@testing-library/svelte';
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
import ConfigMapSecretColumnActions from './ConfigMapSecretColumnActions.svelte';
|
||||
import type { ConfigMapSecretUI } from './ConfigMapSecretUI';
|
||||
|
||||
test('Expect action buttons', async () => {
|
||||
const configMap: ConfigMapSecretUI = {
|
||||
name: 'my-configmap',
|
||||
namespace: '',
|
||||
selected: false,
|
||||
type: 'ConfigMap',
|
||||
status: '',
|
||||
keys: [],
|
||||
};
|
||||
|
||||
render(ConfigMapSecretColumnActions, { object: configMap });
|
||||
|
||||
const buttons = await screen.findAllByRole('button');
|
||||
expect(buttons).toHaveLength(1);
|
||||
});
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<script lang="ts">
|
||||
import ConfigmapSecretActions from './ConfigMapSecretActions.svelte';
|
||||
import type { ConfigMapSecretUI } from './ConfigMapSecretUI';
|
||||
|
||||
export let object: ConfigMapSecretUI;
|
||||
</script>
|
||||
|
||||
<ConfigmapSecretActions configMapSecret="{object}" on:update />
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2024 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 { fireEvent, render, screen } from '@testing-library/svelte';
|
||||
import { router } from 'tinro';
|
||||
import { expect, test, vi } from 'vitest';
|
||||
|
||||
import ConfigMapSecretColumnName from './ConfigMapSecretColumnName.svelte';
|
||||
import type { ConfigMapSecretUI } from './ConfigMapSecretUI';
|
||||
|
||||
const configMap: ConfigMapSecretUI = {
|
||||
name: 'my-configmap',
|
||||
namespace: 'default',
|
||||
selected: false,
|
||||
type: 'ConfigMap',
|
||||
status: '',
|
||||
keys: [],
|
||||
};
|
||||
|
||||
const secret: ConfigMapSecretUI = {
|
||||
name: 'my-secret',
|
||||
namespace: 'default',
|
||||
selected: false,
|
||||
type: 'Secret',
|
||||
status: '',
|
||||
keys: [],
|
||||
};
|
||||
|
||||
test('Expect simple column styling', async () => {
|
||||
render(ConfigMapSecretColumnName, { object: configMap });
|
||||
|
||||
const text = screen.getByText(configMap.name);
|
||||
expect(text).toBeInTheDocument();
|
||||
expect(text).toHaveClass('text-sm');
|
||||
expect(text).toHaveClass('text-[var(--pd-table-body-text-highlight)]');
|
||||
});
|
||||
|
||||
test('Configmap: Expect clicking works', async () => {
|
||||
render(ConfigMapSecretColumnName, { object: configMap });
|
||||
|
||||
const text = screen.getByText(configMap.name);
|
||||
expect(text).toBeInTheDocument();
|
||||
|
||||
// test click
|
||||
const routerGotoSpy = vi.spyOn(router, 'goto');
|
||||
|
||||
fireEvent.click(text);
|
||||
|
||||
expect(routerGotoSpy).toBeCalledWith('/configmapsSecrets/configmap/my-configmap/default/summary');
|
||||
});
|
||||
|
||||
test('Secret: Expect clicking works', async () => {
|
||||
render(ConfigMapSecretColumnName, { object: secret });
|
||||
|
||||
const text = screen.getByText(secret.name);
|
||||
expect(text).toBeInTheDocument();
|
||||
|
||||
// test click
|
||||
const routerGotoSpy = vi.spyOn(router, 'goto');
|
||||
|
||||
fireEvent.click(text);
|
||||
|
||||
expect(routerGotoSpy).toBeCalledWith('/configmapsSecrets/secret/my-secret/default/summary');
|
||||
});
|
||||
|
||||
test('Expect namespace in column', async () => {
|
||||
render(ConfigMapSecretColumnName, { object: configMap });
|
||||
|
||||
const text = screen.getByText(configMap.namespace);
|
||||
expect(text).toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<script lang="ts">
|
||||
import { router } from 'tinro';
|
||||
|
||||
import { ConfigMapSecretUtils } from './configmap-secret-utils';
|
||||
import type { ConfigMapSecretUI } from './ConfigMapSecretUI';
|
||||
|
||||
export let object: ConfigMapSecretUI;
|
||||
|
||||
function openDetails() {
|
||||
const configmapSecretUtils = new ConfigMapSecretUtils();
|
||||
if (configmapSecretUtils.isSecret(object)) {
|
||||
router.goto(`/configmapsSecrets/secret/${encodeURI(object.name)}/${encodeURI(object.namespace)}/summary`);
|
||||
}
|
||||
|
||||
if (configmapSecretUtils.isConfigMap(object)) {
|
||||
router.goto(`/configmapsSecrets/configmap/${encodeURI(object.name)}/${encodeURI(object.namespace)}/summary`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<button class="hover:cursor-pointer flex flex-col max-w-full" on:click="{() => openDetails()}">
|
||||
<div class="text-sm text-[var(--pd-table-body-text-highlight)] max-w-full overflow-hidden text-ellipsis">
|
||||
{object.name}
|
||||
</div>
|
||||
<div class="flex flex-row text-sm gap-1">
|
||||
{#if object.namespace}
|
||||
<div class="font-extra-light text-[var(--pd-table-body-text)]">{object.namespace}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2024 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 { render, screen } from '@testing-library/svelte';
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
import ConfigMapSecretColumnStatus from './ConfigMapSecretColumnStatus.svelte';
|
||||
import type { ConfigMapSecretUI } from './ConfigMapSecretUI';
|
||||
|
||||
test('Expect status styling for running', async () => {
|
||||
const configMap: ConfigMapSecretUI = {
|
||||
name: 'my-configmap',
|
||||
namespace: '',
|
||||
selected: false,
|
||||
type: 'ConfigMap',
|
||||
status: 'RUNNING',
|
||||
keys: [],
|
||||
};
|
||||
|
||||
render(ConfigMapSecretColumnStatus, { object: configMap });
|
||||
|
||||
const text = screen.getByRole('status');
|
||||
expect(text).toBeInTheDocument();
|
||||
expect(text).toHaveClass('bg-[var(--pd-status-running)]');
|
||||
});
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<script lang="ts">
|
||||
import { StatusIcon } from '@podman-desktop/ui-svelte';
|
||||
|
||||
import ConfigMapSecretIcon from '../images/ConfigMapSecretIcon.svelte';
|
||||
import type { ConfigMapSecretUI } from './ConfigMapSecretUI';
|
||||
|
||||
export let object: ConfigMapSecretUI;
|
||||
</script>
|
||||
|
||||
<StatusIcon icon="{ConfigMapSecretIcon}" status="{object.status}" />
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2024 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 { render, screen } from '@testing-library/svelte';
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
import ConfigMapSecretColumnType from './ConfigMapSecretColumnType.svelte';
|
||||
import type { ConfigMapSecretUI } from './ConfigMapSecretUI';
|
||||
|
||||
test('Expect type display for ConfigMap', async () => {
|
||||
const configMap: ConfigMapSecretUI = {
|
||||
name: 'my-configmap',
|
||||
namespace: '',
|
||||
selected: false,
|
||||
type: 'ConfigMap',
|
||||
status: '',
|
||||
keys: [],
|
||||
};
|
||||
|
||||
render(ConfigMapSecretColumnType, { object: configMap });
|
||||
|
||||
const text = screen.getByText('ConfigMap');
|
||||
expect(text).toBeInTheDocument();
|
||||
const svg = text.parentElement?.querySelector('svg');
|
||||
expect(svg).toBeInTheDocument();
|
||||
expect(svg).toHaveClass('text-[var(--pd-status-running)]');
|
||||
});
|
||||
|
||||
test('Expect type display for Secret', async () => {
|
||||
const secret: ConfigMapSecretUI = {
|
||||
name: 'my-secret',
|
||||
namespace: '',
|
||||
selected: false,
|
||||
type: 'Secret',
|
||||
status: '',
|
||||
keys: [],
|
||||
};
|
||||
|
||||
render(ConfigMapSecretColumnType, { object: secret });
|
||||
|
||||
const text = screen.getByText('Secret');
|
||||
expect(text).toBeInTheDocument();
|
||||
const svg = text.parentElement?.querySelector('svg');
|
||||
expect(svg).toBeInTheDocument();
|
||||
expect(svg).toHaveClass('text-[var(--pd-status-running)]');
|
||||
});
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<script lang="ts">
|
||||
import { faFileAlt, faKey } from '@fortawesome/free-solid-svg-icons';
|
||||
import Fa from 'svelte-fa';
|
||||
|
||||
import Label from '../ui/Label.svelte';
|
||||
import type { ConfigMapSecretUI } from './ConfigMapSecretUI';
|
||||
|
||||
export let object: ConfigMapSecretUI;
|
||||
|
||||
// Determine the icon and color based on the type
|
||||
function getTypeAttributes(type: string) {
|
||||
const isConfigMap = type === 'ConfigMap';
|
||||
return {
|
||||
color: 'text-[var(--pd-status-running)]',
|
||||
icon: isConfigMap ? faFileAlt : faKey,
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row gap-1">
|
||||
<Label name="{object.type}" capitalize="{false}">
|
||||
<Fa size="1x" icon="{getTypeAttributes(object.type).icon}" class="{getTypeAttributes(object.type).color}" />
|
||||
</Label>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2024 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/vitest';
|
||||
|
||||
import { render, screen } from '@testing-library/svelte';
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
import ConfigMapSecretEmptyScreen from './ConfigMapSecretEmptyScreen.svelte';
|
||||
|
||||
test('Expect configmap empty screen', async () => {
|
||||
render(ConfigMapSecretEmptyScreen);
|
||||
const noNodes = screen.getByRole('heading', { name: 'No configmaps or secrets' });
|
||||
expect(noNodes).toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<script lang="ts">
|
||||
import { EmptyScreen } from '@podman-desktop/ui-svelte';
|
||||
|
||||
import ConfigMapSecretIcon from '../images/ConfigMapSecretIcon.svelte';
|
||||
</script>
|
||||
|
||||
<EmptyScreen
|
||||
icon="{ConfigMapSecretIcon}"
|
||||
title="No configmaps or secrets"
|
||||
message="Try switching to a different context or namespace" />
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2024 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/vitest';
|
||||
|
||||
import type { V1ConfigMap, V1Secret } from '@kubernetes/client-node';
|
||||
import { render, screen } from '@testing-library/svelte';
|
||||
/* eslint-disable import/no-duplicates */
|
||||
import { tick } from 'svelte';
|
||||
import { get } from 'svelte/store';
|
||||
/* eslint-enable import/no-duplicates */
|
||||
import { beforeAll, beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import { kubernetesCurrentContextConfigMaps } from '/@/stores/kubernetes-contexts-state';
|
||||
|
||||
import ConfigMapSecretList from './ConfigMapSecretList.svelte';
|
||||
|
||||
const kubernetesRegisterGetCurrentContextResourcesMock = vi.fn();
|
||||
|
||||
beforeAll(() => {
|
||||
(window as any).kubernetesRegisterGetCurrentContextResources = kubernetesRegisterGetCurrentContextResourcesMock;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.clearAllMocks();
|
||||
(window as any).kubernetesGetContextsGeneralState = () => Promise.resolve(new Map());
|
||||
(window as any).kubernetesGetCurrentContextGeneralState = () => Promise.resolve({});
|
||||
(window as any).window.kubernetesUnregisterGetCurrentContextResources = () => Promise.resolve(undefined);
|
||||
});
|
||||
|
||||
async function waitRender(customProperties: object): Promise<void> {
|
||||
render(ConfigMapSecretList, { ...customProperties });
|
||||
await tick();
|
||||
}
|
||||
|
||||
test('Expect configmap empty screen', async () => {
|
||||
kubernetesRegisterGetCurrentContextResourcesMock.mockResolvedValue([]);
|
||||
render(ConfigMapSecretList);
|
||||
const noNodes = screen.getByRole('heading', { name: 'No configmaps or secrets' });
|
||||
expect(noNodes).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Expect configmap and secrets list', async () => {
|
||||
const configMap: V1ConfigMap = {
|
||||
metadata: {
|
||||
name: 'my-configmap',
|
||||
namespace: 'my-namespace',
|
||||
},
|
||||
data: {
|
||||
key1: 'value1',
|
||||
key2: 'value2',
|
||||
},
|
||||
};
|
||||
|
||||
const secret: V1Secret = {
|
||||
metadata: {
|
||||
name: 'my-secret',
|
||||
namespace: 'my-namespace',
|
||||
},
|
||||
data: {
|
||||
secretkey1: 'value1',
|
||||
secretkey2: 'value2',
|
||||
},
|
||||
type: 'Opaque',
|
||||
};
|
||||
|
||||
kubernetesRegisterGetCurrentContextResourcesMock.mockResolvedValue([configMap, secret]);
|
||||
|
||||
// wait while store is populated
|
||||
while (get(kubernetesCurrentContextConfigMaps).length === 0) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
await waitRender({});
|
||||
|
||||
const configMapName = screen.getByRole('cell', { name: 'my-configmap my-namespace' });
|
||||
expect(configMapName).toBeInTheDocument();
|
||||
// Expect ConfigMap type
|
||||
const configMapType = screen.getByRole('cell', { name: 'ConfigMap' });
|
||||
expect(configMapType).toBeInTheDocument();
|
||||
|
||||
const secretName = screen.getByRole('cell', { name: 'my-secret my-namespace' });
|
||||
expect(secretName).toBeInTheDocument();
|
||||
// Expect Opaque type
|
||||
const secretType = screen.getByRole('cell', { name: 'Opaque' });
|
||||
expect(secretType).toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
<script lang="ts">
|
||||
import { faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||
import {
|
||||
Button,
|
||||
FilteredEmptyScreen,
|
||||
NavPage,
|
||||
Table,
|
||||
TableColumn,
|
||||
TableDurationColumn,
|
||||
TableRow,
|
||||
TableSimpleColumn,
|
||||
} from '@podman-desktop/ui-svelte';
|
||||
import moment from 'moment';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import type { Unsubscriber } from 'svelte/store';
|
||||
|
||||
import KubernetesCurrentContextConnectionBadge from '/@/lib/ui/KubernetesCurrentContextConnectionBadge.svelte';
|
||||
import {
|
||||
configmapSearchPattern,
|
||||
kubernetesCurrentContextConfigMapsFiltered,
|
||||
kubernetesCurrentContextSecretsFiltered,
|
||||
secretSearchPattern,
|
||||
} from '/@/stores/kubernetes-contexts-state';
|
||||
|
||||
import ConfigMapSecretIcon from '../images/ConfigMapSecretIcon.svelte';
|
||||
import KubeApplyYamlButton from '../kube/KubeApplyYAMLButton.svelte';
|
||||
import { ConfigMapSecretUtils } from './configmap-secret-utils';
|
||||
import ConfigMapSecretColumnActions from './ConfigMapSecretColumnActions.svelte';
|
||||
import ConfigMapSecretColumnName from './ConfigMapSecretColumnName.svelte';
|
||||
import ConfigMapSecretColumnStatus from './ConfigMapSecretColumnStatus.svelte';
|
||||
import ConfigMapSecretColumnType from './ConfigMapSecretColumnType.svelte';
|
||||
import ConfigMapSecretEmptyScreen from './ConfigMapSecretEmptyScreen.svelte';
|
||||
import type { ConfigMapSecretUI } from './ConfigMapSecretUI';
|
||||
|
||||
export let searchTerm = '';
|
||||
$: secretSearchPattern.set(searchTerm);
|
||||
$: configmapSearchPattern.set(searchTerm);
|
||||
|
||||
let configmapsUI: ConfigMapSecretUI[] = [];
|
||||
let secretsUI: ConfigMapSecretUI[] = [];
|
||||
let configmapsSecretsUI: ConfigMapSecretUI[] = [];
|
||||
|
||||
const configmapSecretUtils = new ConfigMapSecretUtils();
|
||||
|
||||
let configmapsUnsubscribe: Unsubscriber;
|
||||
let secretsUnsubscribe: Unsubscriber;
|
||||
onMount(() => {
|
||||
configmapsUnsubscribe = kubernetesCurrentContextConfigMapsFiltered.subscribe(value => {
|
||||
configmapsUI = value.map(configmap => configmapSecretUtils.getConfigMapSecretUI(configmap));
|
||||
configmapsSecretsUI = [...configmapsUI, ...secretsUI];
|
||||
});
|
||||
|
||||
secretsUnsubscribe = kubernetesCurrentContextSecretsFiltered.subscribe(value => {
|
||||
secretsUI = value.map(secret => configmapSecretUtils.getConfigMapSecretUI(secret));
|
||||
configmapsSecretsUI = [...configmapsUI, ...secretsUI];
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
// unsubscribe from the store
|
||||
configmapsUnsubscribe?.();
|
||||
secretsUnsubscribe?.();
|
||||
});
|
||||
|
||||
// delete the items selected in the list
|
||||
let bulkDeleteInProgress = false;
|
||||
async function deleteSelectedConfigMapsSecrets() {
|
||||
const selectedConfigMapsSecrets = configmapsSecretsUI.filter(configmapsSecretsUI => configmapsSecretsUI.selected);
|
||||
if (selectedConfigMapsSecrets.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// mark configmap or secret for deletion
|
||||
bulkDeleteInProgress = true;
|
||||
selectedConfigMapsSecrets.forEach(configmapSecret => (configmapSecret.status = 'DELETING'));
|
||||
configmapsSecretsUI = configmapsSecretsUI;
|
||||
|
||||
if (selectedConfigMapsSecrets.length > 0) {
|
||||
bulkDeleteInProgress = true;
|
||||
await Promise.all(
|
||||
selectedConfigMapsSecrets.map(async configmapSecret => {
|
||||
try {
|
||||
if (configmapSecretUtils.isSecret(configmapSecret)) {
|
||||
await window.kubernetesDeleteSecret(configmapSecret.name);
|
||||
}
|
||||
|
||||
// Separate the delete logic (cannot have in else if) or else you need to infer the type of configmapSecret
|
||||
// using (configmapSecret as ConfigMapSecretUI)
|
||||
if (configmapSecretUtils.isConfigMap(configmapSecret)) {
|
||||
await window.kubernetesDeleteConfigMap(configmapSecret.name);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`error while deleting ${configmapSecretUtils.isSecret(configmapSecret) ? 'secret' : 'configmap'}`,
|
||||
e,
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
bulkDeleteInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
let selectedItemsNumber: number;
|
||||
let table: Table;
|
||||
|
||||
let statusColumn = new TableColumn<ConfigMapSecretUI>('Status', {
|
||||
align: 'center',
|
||||
width: '70px',
|
||||
renderer: ConfigMapSecretColumnStatus,
|
||||
comparator: (a, b) => a.status.localeCompare(b.status),
|
||||
});
|
||||
|
||||
let nameColumn = new TableColumn<ConfigMapSecretUI>('Name', {
|
||||
width: '1.3fr',
|
||||
renderer: ConfigMapSecretColumnName,
|
||||
comparator: (a, b) => a.name.localeCompare(b.name),
|
||||
});
|
||||
|
||||
let ageColumn = new TableColumn<ConfigMapSecretUI, Date | undefined>('Age', {
|
||||
renderMapping: configmapSecret => configmapSecret.created,
|
||||
renderer: TableDurationColumn,
|
||||
comparator: (a, b) => moment(b.created).diff(moment(a.created)),
|
||||
});
|
||||
|
||||
let keysColumn = new TableColumn<ConfigMapSecretUI, string>('Keys', {
|
||||
renderMapping: config => config.keys.length.toString(),
|
||||
renderer: TableSimpleColumn,
|
||||
comparator: (a, b) => a.keys.length - b.keys.length,
|
||||
});
|
||||
|
||||
let typeColumn = new TableColumn<ConfigMapSecretUI>('Type', {
|
||||
overflow: true,
|
||||
width: '2fr',
|
||||
renderer: ConfigMapSecretColumnType,
|
||||
comparator: (a, b) => a.type.localeCompare(b.type),
|
||||
});
|
||||
|
||||
const columns = [
|
||||
statusColumn,
|
||||
nameColumn,
|
||||
typeColumn,
|
||||
keysColumn,
|
||||
ageColumn,
|
||||
new TableColumn<ConfigMapSecretUI>('Actions', { align: 'right', renderer: ConfigMapSecretColumnActions }),
|
||||
];
|
||||
|
||||
const row = new TableRow<ConfigMapSecretUI>({ selectable: _configmapSecret => true });
|
||||
</script>
|
||||
|
||||
<NavPage bind:searchTerm="{searchTerm}" title="configmaps & secrets">
|
||||
<svelte:fragment slot="additional-actions">
|
||||
<KubeApplyYamlButton />
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="bottom-additional-actions">
|
||||
{#if selectedItemsNumber > 0}
|
||||
<Button
|
||||
on:click="{() => deleteSelectedConfigMapsSecrets()}"
|
||||
title="Delete {selectedItemsNumber} selected items"
|
||||
inProgress="{bulkDeleteInProgress}"
|
||||
icon="{faTrash}" />
|
||||
<span>On {selectedItemsNumber} selected items.</span>
|
||||
{/if}
|
||||
<div class="flex grow justify-end">
|
||||
<KubernetesCurrentContextConnectionBadge />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
||||
<div class="flex min-w-full h-full" slot="content">
|
||||
<Table
|
||||
kind="configmap & secret"
|
||||
bind:this="{table}"
|
||||
bind:selectedItemsNumber="{selectedItemsNumber}"
|
||||
data="{configmapsSecretsUI}"
|
||||
columns="{columns}"
|
||||
row="{row}"
|
||||
defaultSortColumn="Name"
|
||||
on:update="{() => (configmapsSecretsUI = configmapsSecretsUI)}">
|
||||
</Table>
|
||||
|
||||
{#if $kubernetesCurrentContextConfigMapsFiltered.length === 0 && $kubernetesCurrentContextSecretsFiltered.length === 0}
|
||||
{#if searchTerm}
|
||||
<FilteredEmptyScreen icon="{ConfigMapSecretIcon}" kind="configmaps or secrets" bind:searchTerm="{searchTerm}" />
|
||||
{:else}
|
||||
<ConfigMapSecretEmptyScreen />
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</NavPage>
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2024 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 ConfigMapSecretUI {
|
||||
name: string;
|
||||
namespace: string;
|
||||
status: string;
|
||||
keys: string[];
|
||||
selected: boolean;
|
||||
type: string;
|
||||
created?: Date;
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2024 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 type { KubernetesObject, V1Secret } from '@kubernetes/client-node';
|
||||
import { render, screen } from '@testing-library/svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import { beforeAll, expect, test, vi } from 'vitest';
|
||||
|
||||
import * as kubeContextStore from '/@/stores/kubernetes-contexts-state';
|
||||
|
||||
import SecretDetails from './SecretDetails.svelte';
|
||||
|
||||
const secret: V1Secret = {
|
||||
metadata: {
|
||||
name: 'my-secret',
|
||||
namespace: 'default',
|
||||
},
|
||||
data: {},
|
||||
};
|
||||
|
||||
vi.mock('/@/stores/kubernetes-contexts-state', async () => {
|
||||
return {
|
||||
kubernetesCurrentContextSecrets: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
(window as any).kubernetesReadNamespacedSecret = vi.fn();
|
||||
});
|
||||
|
||||
test('Confirm renders secret details', async () => {
|
||||
// mock object store
|
||||
const secrets = writable<KubernetesObject[]>([secret]);
|
||||
vi.mocked(kubeContextStore).kubernetesCurrentContextSecrets = secrets;
|
||||
|
||||
render(SecretDetails, { name: 'my-secret', namespace: 'default' });
|
||||
|
||||
expect(screen.getByText('my-secret')).toBeInTheDocument();
|
||||
expect(screen.getByText('default')).toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
<script lang="ts">
|
||||
import type { V1Secret } from '@kubernetes/client-node';
|
||||
import { StatusIcon, Tab } from '@podman-desktop/ui-svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { router } from 'tinro';
|
||||
import { stringify } from 'yaml';
|
||||
|
||||
import { kubernetesCurrentContextSecrets } from '/@/stores/kubernetes-contexts-state';
|
||||
|
||||
import Route from '../../Route.svelte';
|
||||
import MonacoEditor from '../editor/MonacoEditor.svelte';
|
||||
import SecretIcon from '../images/ConfigMapSecretIcon.svelte';
|
||||
import KubeEditYAML from '../kube/KubeEditYAML.svelte';
|
||||
import DetailsPage from '../ui/DetailsPage.svelte';
|
||||
import StateChange from '../ui/StateChange.svelte';
|
||||
import { getTabUrl, isTabSelected } from '../ui/Util';
|
||||
import { ConfigMapSecretUtils } from './configmap-secret-utils';
|
||||
import ConfigMapSecretActions from './ConfigMapSecretActions.svelte';
|
||||
import type { ConfigMapSecretUI } from './ConfigMapSecretUI';
|
||||
import SecretDetailsSummary from './SecretDetailsSummary.svelte';
|
||||
|
||||
export let name: string;
|
||||
export let namespace: string;
|
||||
|
||||
let secret: ConfigMapSecretUI;
|
||||
let detailsPage: DetailsPage;
|
||||
let kubeSecret: V1Secret | undefined;
|
||||
let kubeError: string;
|
||||
|
||||
onMount(() => {
|
||||
const secretUtils = new ConfigMapSecretUtils();
|
||||
// loading secret info
|
||||
return kubernetesCurrentContextSecrets.subscribe(secrets => {
|
||||
const matchingSecret = secrets.find(
|
||||
secret => secret.metadata?.name === name && secret.metadata?.namespace === namespace,
|
||||
);
|
||||
if (matchingSecret) {
|
||||
try {
|
||||
secret = secretUtils.getConfigMapSecretUI(matchingSecret);
|
||||
loadDetails();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
} else if (detailsPage) {
|
||||
// the secret has been deleted
|
||||
detailsPage.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
async function loadDetails() {
|
||||
const getKubeSecret = await window.kubernetesReadNamespacedSecret(secret.name, namespace);
|
||||
if (getKubeSecret) {
|
||||
kubeSecret = getKubeSecret;
|
||||
} else {
|
||||
kubeError = `Unable to retrieve Kubernetes details for ${secret.name}`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if secret}
|
||||
<DetailsPage title="{secret.name}" subtitle="{secret.namespace}" bind:this="{detailsPage}">
|
||||
<StatusIcon slot="icon" icon="{SecretIcon}" size="{24}" status="{secret.status}" />
|
||||
<svelte:fragment slot="actions">
|
||||
<ConfigMapSecretActions configMapSecret="{secret}" detailed="{true}" on:update="{() => (secret = secret)}" />
|
||||
</svelte:fragment>
|
||||
<div slot="detail" class="flex py-2 w-full justify-end text-sm text-gray-700">
|
||||
<StateChange state="{secret.status}" />
|
||||
</div>
|
||||
<svelte:fragment slot="tabs">
|
||||
<Tab
|
||||
title="Summary"
|
||||
selected="{isTabSelected($router.path, 'summary')}"
|
||||
url="{getTabUrl($router.path, 'summary')}" />
|
||||
<Tab
|
||||
title="Inspect"
|
||||
selected="{isTabSelected($router.path, 'inspect')}"
|
||||
url="{getTabUrl($router.path, 'inspect')}" />
|
||||
<Tab title="Kube" selected="{isTabSelected($router.path, 'kube')}" url="{getTabUrl($router.path, 'kube')}" />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content">
|
||||
<Route path="/summary" breadcrumb="Summary" navigationHint="tab">
|
||||
<SecretDetailsSummary secret="{kubeSecret}" kubeError="{kubeError}" />
|
||||
</Route>
|
||||
<Route path="/inspect" breadcrumb="Inspect" navigationHint="tab">
|
||||
<MonacoEditor content="{JSON.stringify(kubeSecret, undefined, 2)}" language="json" />
|
||||
</Route>
|
||||
<Route path="/kube" breadcrumb="Kube" navigationHint="tab">
|
||||
<KubeEditYAML content="{stringify(kubeSecret)}" />
|
||||
</Route>
|
||||
</svelte:fragment>
|
||||
</DetailsPage>
|
||||
{/if}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2024 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 type { V1Secret } from '@kubernetes/client-node';
|
||||
import { render, screen } from '@testing-library/svelte';
|
||||
import { beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import SecretDetailsSummary from './SecretDetailsSummary.svelte';
|
||||
|
||||
const secret: V1Secret = {
|
||||
metadata: {
|
||||
name: 'my-secret',
|
||||
namespace: 'default',
|
||||
},
|
||||
data: {
|
||||
key1: 'value1',
|
||||
key2: 'value2',
|
||||
},
|
||||
type: 'Opaque',
|
||||
};
|
||||
|
||||
const kubeError = 'Error retrieving node details';
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('Confirm renders secret details summary', async () => {
|
||||
render(SecretDetailsSummary, { secret });
|
||||
|
||||
expect(screen.getByText('my-secret')).toBeInTheDocument();
|
||||
expect(screen.getByText('default')).toBeInTheDocument();
|
||||
expect(screen.getByText('key1')).toBeInTheDocument();
|
||||
expect(screen.getByText('value1')).toBeInTheDocument();
|
||||
expect(screen.getByText('key2')).toBeInTheDocument();
|
||||
expect(screen.getByText('value2')).toBeInTheDocument();
|
||||
|
||||
// expect type to be shown
|
||||
expect(screen.getByText('Opaque')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Expect to show loading if there is no data present', async () => {
|
||||
render(SecretDetailsSummary, {});
|
||||
|
||||
const loadingMessage = screen.getByText('Loading ...');
|
||||
expect(loadingMessage).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Expect to show error message when there is a kube error', async () => {
|
||||
render(SecretDetailsSummary, { secret, kubeError: kubeError });
|
||||
|
||||
const errorMessage = screen.getByText(kubeError);
|
||||
expect(errorMessage).toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<script lang="ts">
|
||||
import type { V1Secret } from '@kubernetes/client-node';
|
||||
import { ErrorMessage } from '@podman-desktop/ui-svelte';
|
||||
|
||||
import Table from '/@/lib/details/DetailsTable.svelte';
|
||||
|
||||
import KubeObjectMetaArtifact from '../kube/details/KubeObjectMetaArtifact.svelte';
|
||||
import KubeSecretArtifact from '../kube/details/KubeSecretArtifact.svelte';
|
||||
|
||||
export let secret: V1Secret | undefined;
|
||||
export let kubeError: string | undefined = undefined;
|
||||
</script>
|
||||
|
||||
<!-- Show the kube error if we're unable to retrieve the data correctly, but we still want to show the
|
||||
basic information -->
|
||||
{#if kubeError}
|
||||
<ErrorMessage error="{kubeError}" />
|
||||
{/if}
|
||||
|
||||
<Table>
|
||||
{#if secret}
|
||||
<KubeObjectMetaArtifact artifact="{secret.metadata}" />
|
||||
<KubeSecretArtifact artifact="{secret}" />
|
||||
{:else}
|
||||
<p class="text-purple-500 font-medium">Loading ...</p>
|
||||
{/if}
|
||||
</Table>
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2024 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 type { V1ConfigMap, V1Secret } from '@kubernetes/client-node';
|
||||
import { beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import { ConfigMapSecretUtils } from './configmap-secret-utils';
|
||||
|
||||
let configMapSecretUtils: ConfigMapSecretUtils;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
configMapSecretUtils = new ConfigMapSecretUtils();
|
||||
});
|
||||
|
||||
test('expect configmap UI conversion', async () => {
|
||||
const configMap = {
|
||||
metadata: {
|
||||
name: 'my-configmap',
|
||||
namespace: 'test-namespace',
|
||||
},
|
||||
data: {
|
||||
key1: 'value1',
|
||||
key2: 'value2',
|
||||
},
|
||||
} as V1ConfigMap;
|
||||
const configMapUI = configMapSecretUtils.getConfigMapSecretUI(configMap);
|
||||
expect(configMapUI.name).toEqual('my-configmap');
|
||||
expect(configMapUI.namespace).toEqual('test-namespace');
|
||||
expect(configMapUI.keys).toEqual(['key1', 'key2']);
|
||||
});
|
||||
|
||||
test('expect secret UI conversion', async () => {
|
||||
const secret = {
|
||||
metadata: {
|
||||
name: 'my-secret',
|
||||
namespace: 'test-namespace',
|
||||
},
|
||||
data: {
|
||||
key1: 'value1',
|
||||
key2: 'value2',
|
||||
},
|
||||
type: 'Opaque',
|
||||
} as V1Secret;
|
||||
const secretUI = configMapSecretUtils.getConfigMapSecretUI(secret);
|
||||
expect(secretUI.name).toEqual('my-secret');
|
||||
expect(secretUI.namespace).toEqual('test-namespace');
|
||||
expect(secretUI.keys).toEqual(['key1', 'key2']);
|
||||
expect(secretUI.type).toEqual('Opaque');
|
||||
});
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2024 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 type { V1ConfigMap, V1Secret } from '@kubernetes/client-node';
|
||||
|
||||
import type { ConfigMapSecretUI } from './ConfigMapSecretUI';
|
||||
|
||||
export class ConfigMapSecretUtils {
|
||||
// If it is a secret, then it will have a type property, as well as NO binaryData property
|
||||
isSecret(storage: V1ConfigMap | V1Secret): storage is V1Secret {
|
||||
return 'type' in storage && storage.type !== 'ConfigMap' && !('binaryData' in storage);
|
||||
}
|
||||
|
||||
// If it is a configMap, then the type property will either be undefined, or it will be 'ConfigMap'
|
||||
isConfigMap(storage: V1ConfigMap | V1Secret): storage is V1ConfigMap {
|
||||
return ('type' in storage && storage.type === 'ConfigMap') || 'type' in storage === undefined;
|
||||
}
|
||||
|
||||
getConfigMapSecretUI(storage: V1ConfigMap | V1Secret): ConfigMapSecretUI {
|
||||
const created = storage.metadata?.creationTimestamp;
|
||||
const keys = Object.keys(storage.data ?? {});
|
||||
|
||||
// If storage.type does not exist, it's V1ConfigMap and just set the type as 'ConfigMap'
|
||||
let type = 'ConfigMap';
|
||||
// If storage.type exists, it's V1Secret and set the type as storage.type
|
||||
if ('type' in storage && storage.type) {
|
||||
type = storage.type;
|
||||
}
|
||||
|
||||
return {
|
||||
name: storage.metadata?.name ?? '',
|
||||
namespace: storage.metadata?.namespace ?? '',
|
||||
status: 'RUNNING',
|
||||
keys,
|
||||
selected: false,
|
||||
type,
|
||||
created,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
<script lang="ts">
|
||||
import type { V1ConfigMap } from '@kubernetes/client-node';
|
||||
|
||||
import Cell from '/@/lib/details/DetailsCell.svelte';
|
||||
import Title from '/@/lib/details/DetailsTitle.svelte';
|
||||
|
||||
import Subtitle from '../../details/DetailsSubtitle.svelte';
|
||||
|
||||
export let artifact: V1ConfigMap | undefined;
|
||||
</script>
|
||||
|
||||
{#if artifact}
|
||||
<tr>
|
||||
<Title>Details</Title>
|
||||
</tr>
|
||||
<tr>
|
||||
<Cell>Immutable</Cell>
|
||||
<Cell>{artifact.immutable ? 'Yes' : 'No'}</Cell>
|
||||
</tr>
|
||||
{#if artifact.binaryData}
|
||||
<tr>
|
||||
<Cell>Binary Data</Cell>
|
||||
<Cell>
|
||||
{#each Object.entries(artifact.binaryData) as [key, value]}
|
||||
<div>{key}: {value.length} bytes</div>
|
||||
{/each}
|
||||
</Cell>
|
||||
</tr>
|
||||
{/if}
|
||||
{#if artifact.data}
|
||||
<tr>
|
||||
<Subtitle>Data</Subtitle>
|
||||
</tr>
|
||||
{#each Object.entries(artifact.data) as [key, value]}
|
||||
<tr>
|
||||
<Cell>{key}</Cell>
|
||||
<Cell>{value}</Cell>
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
{/if}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<script lang="ts">
|
||||
import type { V1Secret } from '@kubernetes/client-node';
|
||||
|
||||
import Cell from '/@/lib/details/DetailsCell.svelte';
|
||||
import Title from '/@/lib/details/DetailsTitle.svelte';
|
||||
|
||||
import Subtitle from '../../details/DetailsSubtitle.svelte';
|
||||
|
||||
export let artifact: V1Secret | undefined;
|
||||
</script>
|
||||
|
||||
{#if artifact}
|
||||
<tr>
|
||||
<Title>Details</Title>
|
||||
</tr>
|
||||
<tr>
|
||||
<Cell>Type</Cell>
|
||||
<Cell>{artifact.type}</Cell>
|
||||
</tr>
|
||||
<tr>
|
||||
<Cell>Immutable</Cell>
|
||||
<Cell>{artifact.immutable ? 'Yes' : 'No'}</Cell>
|
||||
</tr>
|
||||
{#if artifact.data}
|
||||
<tr>
|
||||
<Subtitle>Data</Subtitle>
|
||||
</tr>
|
||||
{#each Object.entries(artifact.data) as [key, value]}
|
||||
<tr>
|
||||
<Cell>{key}</Cell>
|
||||
<Cell>{value}</Cell>
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
{/if}
|
||||
|
|
@ -165,6 +165,47 @@ export const kubernetesCurrentContextRoutes = readable<KubernetesObject[]>([], s
|
|||
|
||||
export const routeSearchPattern = writable('');
|
||||
|
||||
// ConfigMaps
|
||||
|
||||
export const kubernetesCurrentContextConfigMaps = readable<KubernetesObject[]>([], set => {
|
||||
window.kubernetesRegisterGetCurrentContextResources('configmaps').then(value => set(value));
|
||||
window.events?.receive('kubernetes-current-context-configmaps-update', (value: unknown) => {
|
||||
set(value as KubernetesObject[]);
|
||||
});
|
||||
return () => {
|
||||
window.kubernetesUnregisterGetCurrentContextResources('configmaps');
|
||||
};
|
||||
});
|
||||
|
||||
export const configmapSearchPattern = writable('');
|
||||
|
||||
// The configmaps in the current context, filtered with `configmapSearchPattern`
|
||||
export const kubernetesCurrentContextConfigMapsFiltered = derived(
|
||||
[configmapSearchPattern, kubernetesCurrentContextConfigMaps],
|
||||
([$searchPattern, $configmaps]) =>
|
||||
$configmaps.filter(configmap => findMatchInLeaves(configmap, $searchPattern.toLowerCase())),
|
||||
);
|
||||
|
||||
// Secrets
|
||||
|
||||
export const kubernetesCurrentContextSecrets = readable<KubernetesObject[]>([], set => {
|
||||
window.kubernetesRegisterGetCurrentContextResources('secrets').then(value => set(value));
|
||||
window.events?.receive('kubernetes-current-context-secrets-update', (value: unknown) => {
|
||||
set(value as KubernetesObject[]);
|
||||
});
|
||||
return () => {
|
||||
window.kubernetesUnregisterGetCurrentContextResources('secrets');
|
||||
};
|
||||
});
|
||||
|
||||
export const secretSearchPattern = writable('');
|
||||
|
||||
// The secrets in the current context, filtered with `secretSearchPattern`
|
||||
export const kubernetesCurrentContextSecretsFiltered = derived(
|
||||
[secretSearchPattern, kubernetesCurrentContextSecrets],
|
||||
([$searchPattern, $secrets]) => $secrets.filter(secret => findMatchInLeaves(secret, $searchPattern.toLowerCase())),
|
||||
);
|
||||
|
||||
// The routes in the current context, filtered with `routeSearchPattern`
|
||||
export const kubernetesCurrentContextRoutesFiltered = derived(
|
||||
[routeSearchPattern, kubernetesCurrentContextRoutes],
|
||||
|
|
|
|||
Loading…
Reference in a new issue