diff --git a/shell/assets/styles/themes/_dark.scss b/shell/assets/styles/themes/_dark.scss
index 607079ce31..fee1a3a763 100644
--- a/shell/assets/styles/themes/_dark.scss
+++ b/shell/assets/styles/themes/_dark.scss
@@ -165,6 +165,7 @@
--tabbed-container-bg : #{mix($medium, $dark, 20%)};
--yaml-editor-bg : #{$darkest};
+ --drawer-body-bg : #{$darkest};
--diff-border : var(--border);
--diff-header-bg : var(--nav-bg);
diff --git a/shell/assets/styles/themes/_light.scss b/shell/assets/styles/themes/_light.scss
index 9da391e6c1..e12e965201 100644
--- a/shell/assets/styles/themes/_light.scss
+++ b/shell/assets/styles/themes/_light.scss
@@ -483,6 +483,7 @@ BODY, .theme-light {
--tabbed-container-bg : #{mix($light, $lighter, 15%)};
--yaml-editor-bg : #{$lighter};
+ --drawer-body-bg : #{$lighter};
--diff-border : var(--border);
--diff-header-bg : var(--nav-bg);
diff --git a/shell/assets/translations/en-us.yaml b/shell/assets/translations/en-us.yaml
index dcf7b5fbfc..c43b3404d8 100644
--- a/shell/assets/translations/en-us.yaml
+++ b/shell/assets/translations/en-us.yaml
@@ -8505,6 +8505,22 @@ errors:
withUrl: '{msg}: {url}'
withoutUrl: '{msg}'
component:
+ drawer:
+ chrome:
+ ariaLabel:
+ close: Close {target} drawer
+ close: Close
+ resourceDetailDrawer:
+ title: "{resourceName} ({resourceType}) - Configuration"
+ editConfig: Edit Config
+ editYaml: Edit YAML
+ ariaLabel:
+ editConfig: Edit Config
+ editYaml: Edit YAML
+ yamlTab:
+ title: YAML
+ configTab:
+ title: Config
resource:
detail:
card:
diff --git a/shell/components/Drawer/Chrome.vue b/shell/components/Drawer/Chrome.vue
new file mode 100644
index 0000000000..f67c71e70f
--- /dev/null
+++ b/shell/components/Drawer/Chrome.vue
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
diff --git a/shell/components/Drawer/ResourceDetailDrawer/ConfigTab.vue b/shell/components/Drawer/ResourceDetailDrawer/ConfigTab.vue
new file mode 100644
index 0000000000..e8acd394ca
--- /dev/null
+++ b/shell/components/Drawer/ResourceDetailDrawer/ConfigTab.vue
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/shell/components/Drawer/ResourceDetailDrawer/YamlTab.vue b/shell/components/Drawer/ResourceDetailDrawer/YamlTab.vue
new file mode 100644
index 0000000000..609656b712
--- /dev/null
+++ b/shell/components/Drawer/ResourceDetailDrawer/YamlTab.vue
@@ -0,0 +1,48 @@
+
+
+
+ yamlComponent?.refresh()"
+ >
+
+
+
+
+
diff --git a/shell/components/Drawer/ResourceDetailDrawer/__tests__/ConfigTab.test.ts b/shell/components/Drawer/ResourceDetailDrawer/__tests__/ConfigTab.test.ts
new file mode 100644
index 0000000000..6a8fe8c5f5
--- /dev/null
+++ b/shell/components/Drawer/ResourceDetailDrawer/__tests__/ConfigTab.test.ts
@@ -0,0 +1,54 @@
+import { mount } from '@vue/test-utils';
+import ConfigTab from '@shell/components/Drawer/ResourceDetailDrawer/ConfigTab.vue';
+import { createStore } from 'vuex';
+import { defineComponent, markRaw } from 'vue';
+import Tab from '@shell/components/Tabbed/Tab.vue';
+import { _VIEW } from '@shell/config/query-params';
+
+const DynamicComponent = defineComponent({
+ template: '
DynamicComponent
',
+ props: {
+ value: { type: Object, required: true },
+ mode: { type: String, required: true },
+ initialValue: { type: Object, required: true },
+ useTabbedHash: { type: Boolean, required: true }
+ }
+});
+
+describe('component: ResourceDetailDrawer/ConfigTab', () => {
+ const resource = { resource: 'RESOURCE' };
+ const global = {
+ provide: {
+ addTab: jest.fn(), removeTab: jest.fn(), sideTabs: false, store: createStore({})
+ },
+ directives: { 'clean-tooltip': jest.fn() }
+
+ };
+
+ it('should render container with config-tab class and correct label and name', async() => {
+ const wrapper = mount(ConfigTab, {
+ props: { resource, component: markRaw(DynamicComponent) },
+ global
+ });
+
+ const component = wrapper.getComponent(Tab);
+
+ expect(wrapper.classes().includes('config-tab')).toBeTruthy();
+ expect(component.props('label')).toStrictEqual('component.drawer.resourceDetailDrawer.configTab.title');
+ expect(component.props('name')).toStrictEqual('config-tab');
+ });
+
+ it('should render a dynamic component within the .container and pass the correct props', () => {
+ const wrapper = mount(ConfigTab, {
+ props: { resource, component: markRaw(DynamicComponent) },
+ global
+ });
+
+ const component = wrapper.find('.container').getComponent(DynamicComponent);
+
+ expect(component.props('value')).toStrictEqual(resource);
+ expect(component.props('mode')).toStrictEqual(_VIEW);
+ expect(component.props('initialValue')).toStrictEqual(resource);
+ expect(component.props('useTabbedHash')).toStrictEqual(false);
+ });
+});
diff --git a/shell/components/Drawer/ResourceDetailDrawer/__tests__/YamlTab.test.ts b/shell/components/Drawer/ResourceDetailDrawer/__tests__/YamlTab.test.ts
new file mode 100644
index 0000000000..1880ae70ba
--- /dev/null
+++ b/shell/components/Drawer/ResourceDetailDrawer/__tests__/YamlTab.test.ts
@@ -0,0 +1,80 @@
+import { mount } from '@vue/test-utils';
+import YamlTab from '@shell/components/Drawer/ResourceDetailDrawer/YamlTab.vue';
+import { createStore } from 'vuex';
+
+import Tab from '@shell/components/Tabbed/Tab.vue';
+import { _VIEW } from '@shell/config/query-params';
+import ResourceYaml from '@shell/components/ResourceYaml.vue';
+import { nextTick } from 'vue';
+
+jest.mock('@shell/components/ResourceYaml.vue', () => ({
+ template: `ResourceYaml
`,
+ props: {
+ value: {
+ type: Object,
+ required: true
+ },
+ yaml: {
+ type: String,
+ required: true
+ },
+ mode: {
+ type: String,
+ required: true
+ },
+ },
+ methods: { refresh: jest.fn() }
+}));
+
+describe('component: ResourceDetailDrawer/ConfigTab', () => {
+ const resource = { resource: 'RESOURCE' };
+ const yaml = 'YAML';
+ const global = {
+ provide: {
+ addTab: jest.fn(), removeTab: jest.fn(), sideTabs: false, store: createStore({})
+ },
+ directives: { 'clean-tooltip': jest.fn() }
+
+ };
+
+ it('should render container with yaml-tab class and correct label and name', async() => {
+ const wrapper = mount(YamlTab, {
+ props: { resource, yaml },
+ global
+ });
+
+ const component = wrapper.getComponent(Tab);
+
+ expect(wrapper.classes().includes('yaml-tab')).toBeTruthy();
+ expect(component.props('label')).toStrictEqual('component.drawer.resourceDetailDrawer.yamlTab.title');
+ expect(component.props('name')).toStrictEqual('yaml-tab');
+ });
+
+ it('should render a ResourceYaml component and pass the correct props', () => {
+ const wrapper = mount(YamlTab, {
+ props: { resource, yaml },
+ global
+ });
+
+ const component = wrapper.getComponent(ResourceYaml);
+
+ expect(component.props('value')).toStrictEqual(resource);
+ expect(component.props('mode')).toStrictEqual(_VIEW);
+ expect(component.props('yaml')).toStrictEqual(yaml);
+ });
+
+ it('should refresh yaml editor when tab is activated', async() => {
+ const wrapper = mount(YamlTab, {
+ props: { resource, yaml },
+ global
+ });
+
+ const tabComponent = wrapper.getComponent(Tab);
+
+ expect(ResourceYaml.methods?.refresh).toHaveBeenCalledTimes(0);
+ tabComponent.vm.$emit('active');
+ await nextTick();
+
+ expect(ResourceYaml.methods?.refresh).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/shell/components/Drawer/ResourceDetailDrawer/__tests__/composables.test.ts b/shell/components/Drawer/ResourceDetailDrawer/__tests__/composables.test.ts
new file mode 100644
index 0000000000..3cefae9e86
--- /dev/null
+++ b/shell/components/Drawer/ResourceDetailDrawer/__tests__/composables.test.ts
@@ -0,0 +1,81 @@
+import { useDefaultConfigTabProps, useDefaultYamlTabProps, useResourceDetailDrawer } from '@shell/components/Drawer/ResourceDetailDrawer/composables';
+import * as helpers from '@shell/components/Drawer/ResourceDetailDrawer/helpers';
+import * as vuex from 'vuex';
+import * as drawer from '@shell/composables/drawer';
+
+jest.mock('@shell/components/Drawer/ResourceDetailDrawer/helpers');
+jest.mock('vuex');
+jest.mock('@shell/composables/drawer');
+jest.mock('@shell/components/Drawer/ResourceDetailDrawer/index.vue', () => ({ name: 'ResourceDetailDrawer' } as any));
+
+describe('composables: ResourceDetailDrawer', () => {
+ const resource = { type: 'RESOURCE' };
+ const yaml = 'YAML';
+
+ describe('useDefaultYamlTabProps', () => {
+ it('should return the appropriate values based on input', async() => {
+ const getYamlSpy = jest.spyOn(helpers, 'getYaml').mockImplementation(() => Promise.resolve(yaml));
+ const props = await useDefaultYamlTabProps(resource);
+
+ expect(getYamlSpy).toHaveBeenCalledWith(resource);
+ expect(props.yaml).toStrictEqual(yaml);
+ expect(props.resource).toStrictEqual(resource);
+ });
+ });
+
+ describe('useDefaultConfigTabProps', () => {
+ const hasCustomEdit = jest.fn();
+ const importEdit = jest.fn();
+ const editComponent = { component: 'EDIT_COMPONENT' };
+ const store: any = {
+ getters: {
+ 'type-map/hasCustomEdit': hasCustomEdit,
+ 'type-map/importEdit': importEdit
+ }
+ };
+
+ it('should return undefined if it does not have a customEdit', async() => {
+ jest.spyOn(vuex, 'useStore').mockImplementation(() => store);
+ const hasCustomEditSpy = hasCustomEdit.mockImplementation(() => false);
+ const props = useDefaultConfigTabProps(resource);
+
+ expect(hasCustomEditSpy).toHaveBeenCalledWith(resource.type);
+ expect(props).toBeUndefined();
+ });
+
+ it('should return props if it has a customEdit', async() => {
+ jest.spyOn(vuex, 'useStore').mockImplementation(() => store);
+ const hasCustomEditSpy = hasCustomEdit.mockImplementation(() => true);
+ const importEditSpy = importEdit.mockImplementation(() => editComponent);
+ const props = useDefaultConfigTabProps(resource);
+
+ expect(hasCustomEditSpy).toHaveBeenCalledWith(resource.type);
+ expect(importEditSpy).toHaveBeenCalledWith(resource.type);
+ expect(props?.component).toStrictEqual(editComponent);
+ expect(props?.resource).toStrictEqual(resource);
+ });
+ });
+
+ describe('useResourceDetailDrawer', () => {
+ it('should create a wrapper that passes that appropriate properties to the drawer methods', () => {
+ const openSpy = jest.fn();
+ const closeSpy = jest.fn();
+
+ const useDrawerSpy = jest.spyOn(drawer, 'useDrawer').mockImplementation(() => ({ open: openSpy, close: closeSpy }));
+ const resourceDetailDrawer = useResourceDetailDrawer();
+
+ resourceDetailDrawer.openResourceDetailDrawer(resource);
+
+ expect(useDrawerSpy).toHaveBeenCalledTimes(1);
+ expect(openSpy).toHaveBeenCalledWith({ name: 'ResourceDetailDrawer' }, {
+ resource,
+ onClose: closeSpy,
+ width: '73%',
+ // We want this to be full viewport height top to bottom
+ height: '100vh',
+ top: '0',
+ 'z-index': 101 // We want this to be above the main side menu
+ });
+ });
+ });
+});
diff --git a/shell/components/Drawer/ResourceDetailDrawer/__tests__/helpers.test.ts b/shell/components/Drawer/ResourceDetailDrawer/__tests__/helpers.test.ts
new file mode 100644
index 0000000000..bd1e06a495
--- /dev/null
+++ b/shell/components/Drawer/ResourceDetailDrawer/__tests__/helpers.test.ts
@@ -0,0 +1,42 @@
+import { getYaml } from '@shell/components/Drawer/ResourceDetailDrawer/helpers';
+
+describe('helpers: ResourceDetailDrawer', () => {
+ describe('getYaml', () => {
+ const resource = {
+ hasLink: jest.fn(),
+ followLink: jest.fn(),
+ cleanForDownload: jest.fn()
+ };
+
+ const yaml = 'YAML';
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should skip following a link if it does not have a view link', async() => {
+ resource.hasLink.mockImplementation(() => false);
+ resource.cleanForDownload.mockImplementation(() => yaml);
+
+ const response = await getYaml(resource);
+
+ expect(resource.hasLink).toHaveBeenCalledWith('view');
+ expect(resource.followLink).toHaveBeenCalledTimes(0);
+ expect(resource.cleanForDownload).toHaveBeenCalledWith(undefined);
+ expect(response).toStrictEqual(yaml);
+ });
+
+ it('should follow link if it has a view link', async() => {
+ resource.hasLink.mockImplementation(() => true);
+ resource.followLink.mockImplementation(() => Promise.resolve({ data: yaml }));
+ resource.cleanForDownload.mockImplementation(() => yaml);
+
+ const response = await getYaml(resource);
+
+ expect(resource.hasLink).toHaveBeenCalledWith('view');
+ expect(resource.followLink).toHaveBeenCalledWith('view', { headers: { accept: 'application/yaml' } });
+ expect(resource.cleanForDownload).toHaveBeenCalledWith(yaml);
+ expect(response).toStrictEqual(yaml);
+ });
+ });
+});
diff --git a/shell/components/Drawer/ResourceDetailDrawer/composables.ts b/shell/components/Drawer/ResourceDetailDrawer/composables.ts
new file mode 100644
index 0000000000..647756c2f7
--- /dev/null
+++ b/shell/components/Drawer/ResourceDetailDrawer/composables.ts
@@ -0,0 +1,46 @@
+import ResourceDetailDrawer from '@shell/components/Drawer/ResourceDetailDrawer/index.vue';
+import { Props as YamlTabProps } from '@shell/components/Drawer/ResourceDetailDrawer/YamlTab.vue';
+import { Props as ConfigTabProps } from '@shell/components/Drawer/ResourceDetailDrawer/ConfigTab.vue';
+import { useStore } from 'vuex';
+import { useDrawer } from '@shell/composables/drawer';
+import { getYaml } from '@shell/components/Drawer/ResourceDetailDrawer/helpers';
+
+export function useResourceDetailDrawer() {
+ const { open, close } = useDrawer();
+
+ const openResourceDetailDrawer = (resource: any) => {
+ open(ResourceDetailDrawer, {
+ resource,
+ onClose: close,
+ width: '73%',
+ // We want this to be full viewport height top to bottom
+ height: '100vh',
+ top: '0',
+ 'z-index': 101 // We want this to be above the main side menu
+ });
+ };
+
+ return { openResourceDetailDrawer };
+}
+
+export async function useDefaultYamlTabProps(resource: any): Promise {
+ const yaml = await getYaml(resource);
+
+ return {
+ resource,
+ yaml
+ };
+}
+
+export function useDefaultConfigTabProps(resource: any): ConfigTabProps | undefined {
+ const store = useStore();
+
+ if (!store.getters['type-map/hasCustomEdit'](resource.type)) {
+ return;
+ }
+
+ return {
+ resource,
+ component: store.getters['type-map/importEdit'](resource.type)
+ };
+}
diff --git a/shell/components/Drawer/ResourceDetailDrawer/helpers.ts b/shell/components/Drawer/ResourceDetailDrawer/helpers.ts
new file mode 100644
index 0000000000..46073bb719
--- /dev/null
+++ b/shell/components/Drawer/ResourceDetailDrawer/helpers.ts
@@ -0,0 +1,10 @@
+export async function getYaml(resource: any): Promise {
+ let yaml;
+ const opt = { headers: { accept: 'application/yaml' } };
+
+ if (resource.hasLink('view')) {
+ yaml = (await resource.followLink('view', opt)).data;
+ }
+
+ return resource.cleanForDownload(yaml);
+}
diff --git a/shell/components/Drawer/ResourceDetailDrawer/index.vue b/shell/components/Drawer/ResourceDetailDrawer/index.vue
new file mode 100644
index 0000000000..31c5c689bd
--- /dev/null
+++ b/shell/components/Drawer/ResourceDetailDrawer/index.vue
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+ {{ title }}
+
+
+ {activeTab = selectedName;}"
+ >
+
+
+
+
+
+
+ {{ action.label }}
+
+
+
+
+
+
diff --git a/shell/components/Resource/Detail/Additional.vue b/shell/components/Resource/Detail/Additional.vue
new file mode 100644
index 0000000000..dfbe8aaf41
--- /dev/null
+++ b/shell/components/Resource/Detail/Additional.vue
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+ {{ first }}
+
+
+ {{ t('generic.plusMore', {n: items.length-1}) }}
+
+
+
+
+
diff --git a/shell/components/Resource/Detail/Metadata/Annotations/__tests__/index.test.ts b/shell/components/Resource/Detail/Metadata/Annotations/__tests__/index.test.ts
index 1158f428d0..458cd0bdc1 100644
--- a/shell/components/Resource/Detail/Metadata/Annotations/__tests__/index.test.ts
+++ b/shell/components/Resource/Detail/Metadata/Annotations/__tests__/index.test.ts
@@ -3,7 +3,7 @@ import Annotations from '@shell/components/Resource/Detail/Metadata/Annotations/
import { createStore } from 'vuex';
describe('component: Metadata/Annotations', () => {
- it('shoulder render KeyValue with the appropriate props', async() => {
+ it('should render KeyValue with the appropriate props', async() => {
const annotations = [{ key: 'key', value: 'value' }];
const wrapper = mount(Annotations, {
props: { annotations },
diff --git a/shell/components/Resource/Detail/Metadata/Annotations/index.vue b/shell/components/Resource/Detail/Metadata/Annotations/index.vue
index d33d8d9d59..a7f14214e4 100644
--- a/shell/components/Resource/Detail/Metadata/Annotations/index.vue
+++ b/shell/components/Resource/Detail/Metadata/Annotations/index.vue
@@ -7,12 +7,15 @@ export type Annotation = Row;
export interface AnnotationsProps {
annotations: Annotation[];
+
+ onShowConfiguration?: () => void;
}
@@ -22,5 +25,7 @@ const i18n = useI18n(store);
:propertyName="i18n.t('component.resource.detail.metadata.annotations.title')"
:rows="annotations"
:outline="true"
+
+ @show-configuration="() => emit('show-configuration')"
/>
diff --git a/shell/components/Resource/Detail/Metadata/IdentifyingInformation/composable.ts b/shell/components/Resource/Detail/Metadata/IdentifyingInformation/composable.ts
index 4dfff8216b..bfe9cb5da1 100644
--- a/shell/components/Resource/Detail/Metadata/IdentifyingInformation/composable.ts
+++ b/shell/components/Resource/Detail/Metadata/IdentifyingInformation/composable.ts
@@ -1,223 +1,9 @@
import { useI18n } from '@shell/composables/useI18n';
import { computed, ComputedRef, markRaw, toValue } from 'vue';
import LiveDate from '@shell/components/formatter/LiveDate.vue';
-import LinkName from '@shell/components/formatter/LinkName.vue';
-import Additional from '@shell/components/Resource/Detail/Additional.vue';
import { useStore } from 'vuex';
-import CopyToClipboardText from '@shell/components/CopyToClipboardText.vue';
-import IconText from '@shell/components/formatter/IconText.vue';
-import { NODE } from '@shell/config/types';
-import day from 'dayjs';
import { Row } from '@shell/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue';
-export const useContainerRuntime = (resource: any): ComputedRef => {
- const store = useStore();
- const i18n = useI18n(store);
- const resourceValue = toValue(resource);
-
- return computed(() => ({
- label: i18n.t('node.detail.detailTop.containerRuntime'),
- valueOverride: {
- component: markRaw(IconText),
- props: { value: resourceValue.containerRuntimeVersion, iconClass: resourceValue.containerRuntimeIcon }
- },
- value: resourceValue.containerRuntimeVersion
- }));
-};
-
-export const useExternalIp = (resource: any): ComputedRef => {
- const store = useStore();
- const i18n = useI18n(store);
- const resourceValue = toValue(resource);
-
- return computed(() => ({
- label: i18n.t('node.detail.detailTop.externalIP'),
- valueOverride: {
- component: markRaw(CopyToClipboardText),
- props: { text: resourceValue.externalIp }
- },
- value: resourceValue.externalIp
- }));
-};
-
-export const usePodIp = (resource: any): ComputedRef => {
- const store = useStore();
- const i18n = useI18n(store);
- const resourceValue = toValue(resource);
-
- return computed(() => ({
- label: i18n.t('workload.detailTop.podIP'),
- value: resourceValue.status.podIP
- }));
-};
-
-export const useWorkload = (resource: any): ComputedRef => {
- const store = useStore();
- const i18n = useI18n(store);
- const resourceValue = toValue(resource);
-
- return computed(() => ({
- label: i18n.t('workload.detailTop.workload'),
- valueOverride: {
- component: markRaw(LinkName),
- props: {
- type: resourceValue.workloadRef.type,
- value: resourceValue.workloadRef.name,
- namespace: resourceValue.workloadRef.namespace
- }
- },
- value: resourceValue.workloadRef.name
- }));
-};
-
-export const useNode = (resource: any): ComputedRef => {
- const store = useStore();
- const i18n = useI18n(store);
- const resourceValue = toValue(resource);
-
- return computed(() => ({
- label: i18n.t('workload.detailTop.node'),
- valueOverride: {
- component: markRaw(LinkName),
- props: { type: NODE, value: resourceValue.spec.nodeName }
- },
- value: resourceValue.spec.nodeName
- }));
-};
-
-export const useStarted = (resource: any): ComputedRef => {
- const store = useStore();
- const i18n = useI18n(store);
- const resourceValue = toValue(resource);
-
- return computed(() => ({
- label: i18n.t('workload.detailTop.started'),
- valueOverride: {
- component: markRaw(LiveDate),
- props: { addSuffix: true, value: resourceValue.status.startTime }
- },
- value: resourceValue.spec.nodeName
- }));
-};
-
-export const useDuration = (resource: any): ComputedRef => {
- const store = useStore();
- const i18n = useI18n(store);
- const resourceValue = toValue(resource);
-
- const FACTORS = [60, 60, 24];
- const LABELS = ['sec', 'min', 'hour', 'day'];
- const value = computed(() => {
- const { completionTime, startTime } = resourceValue.status;
- const end = day(completionTime);
- const start = day(startTime);
- let diff = end.diff(start) / 1000;
-
- let label: any;
-
- let i = 0;
-
- while (diff >= FACTORS[i] && i < FACTORS.length) {
- diff /= FACTORS[i];
- i++;
- }
-
- if (diff < 5) {
- label = Math.floor(diff * 10) / 10;
- } else {
- label = Math.floor(diff);
- }
-
- label += ` ${ i18n.t(`unit.${ LABELS[i] }`, { count: label }) } `;
- label = label.trim();
-
- return label;
- });
-
- return computed(() => ({
- label: i18n.t('workload.detailTop.duration'),
- valueOverride: {
- component: markRaw(LiveDate),
- props: { value: value.value }
- },
- value: value.value
- }));
-};
-
-export const useImage = (resource: any): ComputedRef => {
- const store = useStore();
- const i18n = useI18n(store);
- const resourceValue = toValue(resource);
-
- return computed(() => ({
- label: i18n.t('component.resource.detail.metadata.identifyingInformation.image'),
- value: resourceValue.imageNames,
- valueOverride: {
- component: markRaw(Additional),
- props: { items: resourceValue.imageNames }
- },
- }));
-};
-
-export const useReady = (resource: any): ComputedRef => {
- const store = useStore();
- const i18n = useI18n(store);
- const resourceValue = toValue(resource);
-
- return computed(() => ({
- label: i18n.t('component.resource.detail.metadata.identifyingInformation.ready'),
- value: resourceValue.ready,
- }));
-};
-
-export const useRestarts = (resource: any): ComputedRef => {
- const store = useStore();
- const i18n = useI18n(store);
- const resourceValue = toValue(resource);
-
- return computed(() => ({
- label: i18n.t('workload.detailTop.podRestarts'),
- value: resourceValue.restartCount
- }));
-};
-
-export const useInternalIp = (resource: any): ComputedRef => {
- const store = useStore();
- const i18n = useI18n(store);
- const resourceValue = toValue(resource);
-
- return computed(() => ({
- label: i18n.t('node.detail.detailTop.internalIP'),
- valueOverride: {
- component: markRaw(CopyToClipboardText),
- props: { text: resourceValue.internalIp }
- },
- value: resourceValue.internalIp
- }));
-};
-
-export const useVersion = (resource: any): ComputedRef => {
- const store = useStore();
- const i18n = useI18n(store);
- const resourceValue = toValue(resource);
-
- return computed(() => ({
- label: i18n.t('node.detail.detailTop.version'),
- value: resourceValue.version
- }));
-};
-
-export const useOs = (resource: any): ComputedRef => {
- const store = useStore();
- const i18n = useI18n(store);
- const resourceValue = toValue(resource);
-
- return computed(() => ({
- label: i18n.t('node.detail.detailTop.os'),
- value: resourceValue.status.nodeInfo.osImage
- }));
-};
-
export const useLiveDate = (resource: any): ComputedRef => {
const store = useStore();
const i18n = useI18n(store);
@@ -233,18 +19,6 @@ export const useLiveDate = (resource: any): ComputedRef => {
}));
};
-export const useProject = (resource: any): ComputedRef => {
- const store = useStore();
- const i18n = useI18n(store);
- const resourceValue = toValue(resource);
-
- return computed(() => ({
- label: i18n.t('component.resource.detail.metadata.identifyingInformation.project'),
- value: resourceValue.project.nameDisplay,
- to: '#'
- }));
-};
-
export const useDefaultIdentifyingInformation = (resource: any): ComputedRef => {
const store = useStore();
const i18n = useI18n(store);
@@ -259,23 +33,3 @@ export const useDefaultIdentifyingInformation = (resource: any): ComputedRef => {
- const store = useStore();
- const i18n = useI18n(store);
-
- const resourceValue = toValue(resource);
-
- return computed(() => [
- useImage(resource).value,
- useReady(resource).value,
- {
- label: i18n.t('component.resource.detail.metadata.identifyingInformation.up-to-date'),
- value: resourceValue.upToDate,
- },
- {
- label: i18n.t('component.resource.detail.metadata.identifyingInformation.available'),
- value: resourceValue.available,
- },
- ]);
-};
diff --git a/shell/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue b/shell/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue
index 14c60973c2..32252b5d81 100644
--- a/shell/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue
+++ b/shell/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue
@@ -50,12 +50,16 @@ const { rows } = defineProps();
:class="['status', row.status]"
/>
{{ row.value }}
- {{ row.value }}
+ {{ row.value }}
+ —
diff --git a/shell/components/Resource/Detail/Metadata/KeyValue.vue b/shell/components/Resource/Detail/Metadata/KeyValue.vue
index 8bd61e444b..9171fab994 100644
--- a/shell/components/Resource/Detail/Metadata/KeyValue.vue
+++ b/shell/components/Resource/Detail/Metadata/KeyValue.vue
@@ -30,6 +30,7 @@ const {
const store = useStore();
const i18n = useI18n(store);
+const emit = defineEmits(['show-configuration']);
// Account for the show all button
const visibleRowsLength = computed(() => (rows.value.length > maxRows.value ? maxRows.value - 1 : rows.value.length));
@@ -50,18 +51,19 @@ const displayValue = (row: Row) => `${ row.key }: ${ row.value }`;
{{ i18n.t('component.resource.detail.metadata.keyValue.noRows', {propertyName: lowercasePropertyName}) }}
- {ev.preventDefault(); emit('show-configuration');}"
>
{{ i18n.t('component.resource.detail.metadata.keyValue.showConfiguration') }}
-
+
`${ row.key }: ${ row.value }`;
{{ displayValue(row) }}
- {ev.preventDefault(); emit('show-configuration');}"
>
{{ showAllLabel }}
-
+
@@ -101,6 +104,8 @@ const displayValue = (row: Row) => `${ row.key }: ${ row.value }`;
}
.row {
+ width: 100%;
+
&:not(:first-of-type) {
margin-top: 4px;
}
@@ -109,13 +114,6 @@ const displayValue = (row: Row) => `${ row.key }: ${ row.value }`;
margin-top: 8px;
}
}
- .row {
- margin-top: 8px;
-
- &:not(:first-of-type) {
- margin-top: 4px;
- }
- }
.show-all {
margin-top: 8px;
}
diff --git a/shell/components/Resource/Detail/Metadata/Labels/__tests__/index.test.ts b/shell/components/Resource/Detail/Metadata/Labels/__tests__/index.test.ts
index 289bebf43b..b1ba052e21 100644
--- a/shell/components/Resource/Detail/Metadata/Labels/__tests__/index.test.ts
+++ b/shell/components/Resource/Detail/Metadata/Labels/__tests__/index.test.ts
@@ -3,7 +3,7 @@ import Labels from '@shell/components/Resource/Detail/Metadata/Labels/index.vue'
import { createStore } from 'vuex';
describe('component: Metadata/Labels', () => {
- it('shoulder render KeyValue with the appropriate props', async() => {
+ it('should render KeyValue with the appropriate props', async() => {
const labels = [{ key: 'key', value: 'value' }];
const wrapper = mount(Labels, {
props: { labels },
diff --git a/shell/components/Resource/Detail/Metadata/Labels/index.vue b/shell/components/Resource/Detail/Metadata/Labels/index.vue
index e738fd9f09..8d06f1bade 100644
--- a/shell/components/Resource/Detail/Metadata/Labels/index.vue
+++ b/shell/components/Resource/Detail/Metadata/Labels/index.vue
@@ -7,6 +7,8 @@ import { useStore } from 'vuex';
export type Label = Row;
export interface LabelsProps {
labels: Label[];
+
+ onShowConfiguration?: () => void;
}
@@ -14,6 +16,7 @@ export interface LabelsProps {
+
+
+
+
+
+
+
+
+
+
+ {{ i18n.t('sortableTable.noRows') }}
+
+
+
+
diff --git a/shell/components/Resource/Detail/TitleBar/__tests__/Title.test.ts b/shell/components/Resource/Detail/TitleBar/__tests__/Title.test.ts
index 0d3a36b051..843c64c5ad 100644
--- a/shell/components/Resource/Detail/TitleBar/__tests__/Title.test.ts
+++ b/shell/components/Resource/Detail/TitleBar/__tests__/Title.test.ts
@@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils';
import Title from '@shell/components/Resource/Detail/TitleBar/Title.vue';
describe('component: TitleBar/Title', () => {
- it('shoulder render container with class title', async() => {
+ it('should render container with class title', async() => {
const wrapper = mount(Title);
expect(wrapper.find('.title').exists()).toBeTruthy();
diff --git a/shell/components/Resource/Detail/TitleBar/__tests__/Top.test.ts b/shell/components/Resource/Detail/TitleBar/__tests__/Top.test.ts
index c0b3f6ccb6..4a6416ed6e 100644
--- a/shell/components/Resource/Detail/TitleBar/__tests__/Top.test.ts
+++ b/shell/components/Resource/Detail/TitleBar/__tests__/Top.test.ts
@@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils';
import Top from '@shell/components/Resource/Detail/TitleBar/Top.vue';
describe('component: TitleBar/Top', () => {
- it('shoulder render container with class top', async() => {
+ it('should render container with class top', async() => {
const wrapper = mount(Top);
expect(wrapper.find('.top').exists()).toBeTruthy();
diff --git a/shell/components/Resource/Detail/TitleBar/__tests__/composables.test.ts b/shell/components/Resource/Detail/TitleBar/__tests__/composables.test.ts
new file mode 100644
index 0000000000..066ad2d80c
--- /dev/null
+++ b/shell/components/Resource/Detail/TitleBar/__tests__/composables.test.ts
@@ -0,0 +1,45 @@
+import { useDefaultTitleBarProps } from '@shell/components/Resource/Detail/TitleBar/composables';
+import { useRoute } from 'vue-router';
+
+const mockStore = { getters: { 'type-map/labelFor': jest.fn() } };
+const mockRoute = { params: { cluster: 'CLUSTER' } };
+const mockDrawer = { openResourceDetailDrawer: jest.fn() };
+
+jest.mock('vuex', () => ({ useStore: () => mockStore }));
+jest.mock('vue-router', () => ({ useRoute: () => mockRoute }));
+jest.mock('@shell/components/Drawer/ResourceDetailDrawer/composables', () => ({ useResourceDetailDrawer: () => mockDrawer }));
+
+describe('composables: TitleBar', () => {
+ const resource = {
+ nameDisplay: 'RESOURCE_NAME',
+ namespace: 'RESOURCE_NAMESPACE',
+ type: 'RESOURCE_TYPE',
+ stateBackground: 'RESOURCE_STATE_BACKGROUND',
+ stateDisplay: 'RESOURCE_STATE_DISPLAY',
+ description: 'RESOURCE_DESCRIPTION',
+ };
+ const labelFor = 'LABEL_FOR';
+
+ it('should return the appropriate values based on input', async() => {
+ const route = useRoute();
+
+ mockStore.getters['type-map/labelFor'].mockImplementation(() => labelFor);
+ const props = useDefaultTitleBarProps(resource);
+
+ expect(props.value.resourceTypeLabel).toStrictEqual(labelFor);
+ expect(mockStore.getters['type-map/labelFor']).toHaveBeenLastCalledWith({ id: resource.type });
+ expect(props.value.resourceTo?.params.product).toStrictEqual('explorer');
+ expect(props.value.resourceTo?.params.cluster).toStrictEqual(route.params.cluster);
+ expect(props.value.resourceTo?.params.namespace).toStrictEqual(resource.namespace);
+ expect(props.value.resourceTo?.params.resource).toStrictEqual(resource.type);
+ expect(props.value.resourceName).toStrictEqual(resource.nameDisplay);
+
+ expect(props.value.actionMenuResource).toStrictEqual(resource);
+ expect(props.value.badge?.color).toStrictEqual(resource.stateBackground);
+ expect(props.value.badge?.label).toStrictEqual(resource.stateDisplay);
+ expect(props.value.description).toStrictEqual(resource.description);
+
+ props.value.onShowConfiguration?.();
+ expect(mockDrawer.openResourceDetailDrawer).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/shell/components/Resource/Detail/TitleBar/__tests__/index.test.ts b/shell/components/Resource/Detail/TitleBar/__tests__/index.test.ts
index bd72e9f5b8..d5fbc18d14 100644
--- a/shell/components/Resource/Detail/TitleBar/__tests__/index.test.ts
+++ b/shell/components/Resource/Detail/TitleBar/__tests__/index.test.ts
@@ -11,7 +11,7 @@ describe('component: TitleBar/index', () => {
const resourceName = 'RESOURCE_NAME';
const store = createStore({});
- it('shoulder render container with class title-bar', async() => {
+ it('should render container with class title-bar', async() => {
const wrapper = mount(TitleBar, {
props: {
resourceTypeLabel,
diff --git a/shell/components/Resource/Detail/TitleBar/composable.ts b/shell/components/Resource/Detail/TitleBar/composables.ts
similarity index 70%
rename from shell/components/Resource/Detail/TitleBar/composable.ts
rename to shell/components/Resource/Detail/TitleBar/composables.ts
index 768b66fdb0..e80196af72 100644
--- a/shell/components/Resource/Detail/TitleBar/composable.ts
+++ b/shell/components/Resource/Detail/TitleBar/composables.ts
@@ -1,12 +1,13 @@
+import { useResourceDetailDrawer } from '@shell/components/Drawer/ResourceDetailDrawer/composables';
import { TitleBarProps } from '@shell/components/Resource/Detail/TitleBar/index.vue';
import { computed, Ref, toValue } from 'vue';
import { useRoute } from 'vue-router';
import { useStore } from 'vuex';
-export const useDefaultTitleBarData = (resource: any): Ref => {
+export const useDefaultTitleBarProps = (resource: any): Ref => {
const route = useRoute();
const store = useStore();
-
+ const { openResourceDetailDrawer } = useResourceDetailDrawer();
const resourceValue = toValue(resource);
return computed(() => ({
@@ -26,6 +27,7 @@ export const useDefaultTitleBarData = (resource: any): Ref => {
color: resourceValue.stateBackground,
label: resourceValue.stateDisplay
},
- onShowConfiguration: () => resourceValue.goToEdit()
+ description: resourceValue.description,
+ onShowConfiguration: () => openResourceDetailDrawer(resourceValue)
}));
};
diff --git a/shell/components/Resource/Detail/TitleBar/index.vue b/shell/components/Resource/Detail/TitleBar/index.vue
index cd34d0c128..fd4d75aed8 100644
--- a/shell/components/Resource/Detail/TitleBar/index.vue
+++ b/shell/components/Resource/Detail/TitleBar/index.vue
@@ -1,12 +1,12 @@
diff --git a/shell/components/SlideInPanelManager.vue b/shell/components/SlideInPanelManager.vue
index 7caba880d1..1d9684da19 100644
--- a/shell/components/SlideInPanelManager.vue
+++ b/shell/components/SlideInPanelManager.vue
@@ -15,6 +15,11 @@ const currentComponent = computed(() => store.getters['slideInPanel/component'])
const currentProps = computed(() => store.getters['slideInPanel/componentProps']);
const panelTop = computed(() => {
+ // Some components like the ResourceDetailDrawer are designed to take up the full height of the viewport so we want to be able to specify the top.
+ if (currentProps?.value?.top) {
+ return currentProps?.value?.top;
+ }
+
const banner = document.getElementById('banner-header');
let height = HEADER_HEIGHT;
@@ -25,7 +30,8 @@ const panelTop = computed(() => {
return `${ height }px`;
});
-const panelHeight = computed(() => `calc(100vh - ${ panelTop?.value })`);
+// Some components like the ResourceDetailDrawer are designed to take up the full height of the viewport so we want to be able to specify the height.
+const panelHeight = computed(() => (currentProps?.value?.height) ? (currentProps?.value?.height) : `calc(100vh - ${ panelTop?.value })`);
const panelWidth = computed(() => currentProps?.value?.width || '33%');
const panelRight = computed(() => (isOpen?.value ? '0' : `-${ panelWidth?.value }`));
const panelZIndex = computed(() => `${ (isOpen?.value ? 1 : 2) * (currentProps?.value?.zIndex ?? 1000) }`);
@@ -100,6 +106,9 @@ function closePanel() {
data-testid="slide-in-glass"
class="slide-in-glass"
:class="{ 'slide-in-glass-open': isOpen }"
+ :style="{
+ ['z-index']: panelZIndex
+ }"
@click="closePanel"
/>
+import { StateColor, stateColorCssVar } from '@shell/utils/style';
+import { computed } from 'vue';
+
+interface Props {
+ color: StateColor;
+ size?: string;
+}
+
+const props = withDefaults(defineProps
(), { size: '8px' });
+const backgroundColor = computed(() => stateColorCssVar(props.color));
+
+
+
+
+
+
+
diff --git a/shell/components/form/ResourceTabs/index.vue b/shell/components/form/ResourceTabs/index.vue
index cf6300b681..b29854b564 100644
--- a/shell/components/form/ResourceTabs/index.vue
+++ b/shell/components/form/ResourceTabs/index.vue
@@ -68,6 +68,10 @@ export default {
extensionParams: {
type: Object,
default: null
+ },
+ useHash: {
+ type: Boolean,
+ default: true
}
},
@@ -233,6 +237,7 @@ export default {
v-bind="$attrs"
:default-tab="defaultTab"
:resource="value"
+ :use-hash="useHash"
@changed="tabChange"
>
diff --git a/shell/composables/drawer.ts b/shell/composables/drawer.ts
new file mode 100644
index 0000000000..b36f33db05
--- /dev/null
+++ b/shell/composables/drawer.ts
@@ -0,0 +1,22 @@
+import { Component } from 'vue';
+import { useStore } from 'vuex';
+
+export const useDrawer = () => {
+ const store = useStore();
+
+ const open = (component: Component, options?: Record) => {
+ store.commit('slideInPanel/open', {
+ component,
+ componentProps: options || {}
+ });
+ };
+
+ const close = () => {
+ store.commit('slideInPanel/close');
+ };
+
+ return {
+ open,
+ close
+ };
+};
diff --git a/shell/composables/resources.ts b/shell/composables/resources.ts
new file mode 100644
index 0000000000..45500849e5
--- /dev/null
+++ b/shell/composables/resources.ts
@@ -0,0 +1,30 @@
+import { computed, Ref, toValue } from 'vue';
+import { useRoute } from 'vue-router';
+import { useStore } from 'vuex';
+
+type ResourceType = string | Ref;
+type IdType = string | Ref;
+
+export const useResourceIdentifiers = (type: ResourceType) => {
+ const route = useRoute();
+ const store = useStore();
+
+ const typeValue = toValue(type);
+
+ const id = computed(() => route.params.namespace ? `${ route.params.namespace }/${ route.params.id }` : `${ route.params.id }`);
+ const schema = computed(() => store.getters['cluster/schemaFor'](typeValue));
+ const clusterId = computed(() => route.params.cluster as string);
+
+ return {
+ clusterId: clusterId.value, id: id.value, schema: schema.value
+ };
+};
+
+export const useFetchResourceWithId = async(type: ResourceType, id: IdType) => {
+ const store = useStore();
+
+ const typeValue = toValue(type);
+ const idValue = toValue(id);
+
+ return await store.dispatch('cluster/find', { type: typeValue, id: idValue });
+};
diff --git a/shell/edit/configmap.vue b/shell/edit/configmap.vue
index eb2d7a79e8..f703bfa0c4 100644
--- a/shell/edit/configmap.vue
+++ b/shell/edit/configmap.vue
@@ -105,7 +105,10 @@ export default {
:register-before-hook="registerBeforeHook"
/>
-
+
import DetailPage from '@shell/components/Resource/Detail/Page.vue';
+import TitleBar from '@shell/components/Resource/Detail/TitleBar/index.vue';
+import { useDefaultTitleBarProps } from '@shell/components/Resource/Detail/TitleBar/composables';
+import Metadata from '@shell/components/Resource/Detail/Metadata/index.vue';
+import { useDefaultMetadataProps } from '@shell/components/Resource/Detail/Metadata/composables';
import { CONFIG_MAP } from '@shell/config/types';
-import { useStore } from 'vuex';
-
-const store = useStore();
-const configMaps = await store.dispatch('cluster/findAll', { type: CONFIG_MAP });
+import { useFetchResourceWithId, useResourceIdentifiers } from '@shell/composables/resources';
+import ResourceTabs from '@shell/components/form/ResourceTabs/index.vue';
+import ConfigMapDataTab from '@shell/components/Resource/Detail/ResourceTabs/ConfigMapDataTab/index.vue';
+import { useGetConfigMapDataTabProps } from '@shell/components/Resource/Detail/ResourceTabs/ConfigMapDataTab/composables';
+const { id, schema } = useResourceIdentifiers(CONFIG_MAP);
+const configMap = await useFetchResourceWithId(CONFIG_MAP, id);
+const titleBarProps = useDefaultTitleBarProps(configMap);
+const metadataProps = useDefaultMetadataProps(configMap);
+const configMapDataTabProps = useGetConfigMapDataTabProps(configMap);
+
- ConfigMap
+
+
-
- {{ configMaps }}
+
+
+
+
diff --git a/shell/plugins/dashboard-store/resource-class.js b/shell/plugins/dashboard-store/resource-class.js
index f927b106f7..a679081204 100644
--- a/shell/plugins/dashboard-store/resource-class.js
+++ b/shell/plugins/dashboard-store/resource-class.js
@@ -736,6 +736,17 @@ export default class Resource {
);
}
+ get stateColorPair() {
+ return {
+ state: this.stateDisplay,
+ color: this.stateSimpleColor
+ };
+ }
+
+ get stateSimpleColor() {
+ return this.stateColor.replace('text-', '');
+ }
+
get stateBackground() {
return this.stateColor.replace('text-', 'bg-');
}