mirror of https://github.com/rancher/dashboard.git
Merge pull request #14538 from codyrancher/detail-page-config-map
Adding the new configmap detail page
This commit is contained in:
commit
26859160ff
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,114 @@
|
|||
<script lang="ts">
|
||||
import { useI18n } from '@shell/composables/useI18n';
|
||||
import { useStore } from 'vuex';
|
||||
import { computed } from 'vue';
|
||||
export interface Props {
|
||||
ariaTarget: string;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const store = useStore();
|
||||
const i18n = useI18n(store);
|
||||
|
||||
const ariaLabel = computed(() => i18n.t('component.drawer.chrome.ariaLabel.close', { target: props.ariaTarget }));
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<div class="chrome">
|
||||
<div class="header pp-4">
|
||||
<slot name="header">
|
||||
<div class="title">
|
||||
<slot name="title" />
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button
|
||||
class="btn role-link"
|
||||
:aria-label="ariaLabel"
|
||||
@click="emit('close')"
|
||||
>
|
||||
<i class="icon icon-close" />
|
||||
</button>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="body pp-4">
|
||||
<slot name="body" />
|
||||
</div>
|
||||
<div class="footer pp-4">
|
||||
<slot name="footer">
|
||||
<div class="actions">
|
||||
<button
|
||||
class="btn role-secondary"
|
||||
:aria-label="ariaLabel"
|
||||
@click="emit('close')"
|
||||
>
|
||||
{{ i18n.t('component.drawer.chrome.close') }}
|
||||
</button>
|
||||
<slot name="additional-actions" />
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.chrome {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
& >.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
background-color: var(--body-bg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
height: var(--header-height);
|
||||
|
||||
& > .title {
|
||||
flex: 1;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
& > .actions {
|
||||
button {
|
||||
display: inline-block;
|
||||
$size: 24px;
|
||||
width: $size;
|
||||
height: $size;
|
||||
color: var(--body-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
background-color: var(--drawer-body-bg);
|
||||
flex: 1;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
|
||||
background-color: var(--body-bg);
|
||||
border-top: 1px solid var(--border);
|
||||
height: 72px;
|
||||
|
||||
.actions > * {
|
||||
margin-left: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<script lang="ts">
|
||||
import { useI18n } from '@shell/composables/useI18n';
|
||||
import { _VIEW } from '@shell/config/query-params';
|
||||
import { useStore } from 'vuex';
|
||||
import Tab from '@shell/components/Tabbed/Tab.vue';
|
||||
|
||||
export interface Props {
|
||||
resource: any;
|
||||
component: any;
|
||||
}
|
||||
</script>
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<Props>();
|
||||
const store = useStore();
|
||||
const i18n = useI18n(store);
|
||||
</script>
|
||||
<template>
|
||||
<Tab
|
||||
class="config-tab"
|
||||
name="config-tab"
|
||||
:label="i18n.t('component.drawer.resourceDetailDrawer.configTab.title')"
|
||||
>
|
||||
<div class="container">
|
||||
<component
|
||||
:is="props.component"
|
||||
v-model:value="props.resource"
|
||||
:mode="_VIEW"
|
||||
:initial-value="props.resource"
|
||||
:use-tabbed-hash="false /* Have to disable hashing on child components or it modifies the url and closes the drawer */"
|
||||
/>
|
||||
</div>
|
||||
</Tab>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.config-tab {
|
||||
.container {
|
||||
background-color: var(--body-bg);
|
||||
border-radius: var(--border-radius-md);
|
||||
padding: 16px;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep() .cru-resource-footer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
<script lang="ts">
|
||||
import ResourceYaml from '@shell/components/ResourceYaml.vue';
|
||||
import { useI18n } from '@shell/composables/useI18n';
|
||||
import { _VIEW } from '@shell/config/query-params';
|
||||
import { useStore } from 'vuex';
|
||||
import Tab from '@shell/components/Tabbed/Tab.vue';
|
||||
import { useTemplateRef } from 'vue';
|
||||
|
||||
export interface Props {
|
||||
resource: any;
|
||||
yaml: string;
|
||||
}
|
||||
</script>
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<Props>();
|
||||
const store = useStore();
|
||||
const i18n = useI18n(store);
|
||||
const yamlComponent: any = useTemplateRef('yaml');
|
||||
</script>
|
||||
<template>
|
||||
<Tab
|
||||
class="yaml-tab"
|
||||
name="yaml-tab"
|
||||
:label="i18n.t('component.drawer.resourceDetailDrawer.yamlTab.title')"
|
||||
@active="() => yamlComponent?.refresh()"
|
||||
>
|
||||
<ResourceYaml
|
||||
ref="yaml"
|
||||
:value="props.resource"
|
||||
:yaml="props.yaml"
|
||||
:mode="_VIEW"
|
||||
/>
|
||||
</Tab>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.yaml-tab {
|
||||
:deep() .yaml-editor .codemirror-container {
|
||||
background-color: var(--body-bg);
|
||||
border-radius: var(--border-radius-md);
|
||||
padding: 16px;
|
||||
|
||||
.CodeMirror, .CodeMirror-gutter {
|
||||
background-color: var(--body-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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: '<div>DynamicComponent</div>',
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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: `<div>ResourceYaml</div>`,
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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<YamlTabProps> {
|
||||
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)
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
export async function getYaml(resource: any): Promise<string> {
|
||||
let yaml;
|
||||
const opt = { headers: { accept: 'application/yaml' } };
|
||||
|
||||
if (resource.hasLink('view')) {
|
||||
yaml = (await resource.followLink('view', opt)).data;
|
||||
}
|
||||
|
||||
return resource.cleanForDownload(yaml);
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
<script lang="ts">
|
||||
import Drawer from '@shell/components/Drawer/Chrome.vue';
|
||||
import { useI18n } from '@shell/composables/useI18n';
|
||||
import { useStore } from 'vuex';
|
||||
import Tabbed from '@shell/components/Tabbed/index.vue';
|
||||
import YamlTab, { Props as YamlProps } from '@shell/components/Drawer/ResourceDetailDrawer/YamlTab.vue';
|
||||
import { useDefaultConfigTabProps, useDefaultYamlTabProps } from '@shell/components/Drawer/ResourceDetailDrawer/composables';
|
||||
import ConfigTab from '@shell/components/Drawer/ResourceDetailDrawer/ConfigTab.vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import RcButton from '@components/RcButton/RcButton.vue';
|
||||
import StateDot from '@shell/components/StateDot/index.vue';
|
||||
|
||||
export interface Props {
|
||||
resource: any;
|
||||
|
||||
onClose?: () => void;
|
||||
}
|
||||
</script>
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits(['close']);
|
||||
const store = useStore();
|
||||
const i18n = useI18n(store);
|
||||
|
||||
const yamlTabProps = ref<YamlProps | null>(null);
|
||||
const configTabProps = useDefaultConfigTabProps(props.resource);
|
||||
|
||||
useDefaultYamlTabProps(props.resource).then((props) => {
|
||||
yamlTabProps.value = props;
|
||||
});
|
||||
|
||||
const title = computed(() => {
|
||||
const resourceType = store.getters['type-map/labelFor']({ id: props.resource.type });
|
||||
const resourceName = props.resource.nameDisplay;
|
||||
|
||||
return i18n.t('component.drawer.resourceDetailDrawer.title', { resourceType, resourceName });
|
||||
});
|
||||
|
||||
const activeTab = ref<string>(configTabProps ? 'config-tab' : 'yaml-tab');
|
||||
|
||||
const action = computed(() => {
|
||||
const isConfig = activeTab.value === 'config-tab';
|
||||
const ariaLabel = isConfig ? i18n.t('component.drawer.resourceDetailDrawer.ariaLabel.editConfig') : i18n.t('component.drawer.resourceDetailDrawer.ariaLabel.editYaml');
|
||||
const label = isConfig ? i18n.t('component.drawer.resourceDetailDrawer.ariaLabel.editConfig') : i18n.t('component.drawer.resourceDetailDrawer.ariaLabel.editYaml');
|
||||
const action = isConfig ? () => props.resource.goToEdit() : () => props.resource.goToEditYaml();
|
||||
|
||||
return {
|
||||
ariaLabel,
|
||||
label,
|
||||
action
|
||||
};
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Drawer
|
||||
class="resource-detail-drawer"
|
||||
:ariaTarget="title"
|
||||
@close="emit('close')"
|
||||
>
|
||||
<template #title>
|
||||
<StateDot
|
||||
:color="resource.stateSimpleColor"
|
||||
class="mmr-3"
|
||||
/>
|
||||
{{ title }}
|
||||
</template>
|
||||
<template #body>
|
||||
<Tabbed
|
||||
class="tabbed"
|
||||
:useHash="false"
|
||||
@changed="({selectedName}) => {activeTab = selectedName;}"
|
||||
>
|
||||
<ConfigTab
|
||||
v-if="configTabProps"
|
||||
v-bind="configTabProps"
|
||||
/>
|
||||
<YamlTab
|
||||
v-if="yamlTabProps"
|
||||
v-bind="yamlTabProps"
|
||||
/>
|
||||
</Tabbed>
|
||||
</template>
|
||||
<template #additional-actions>
|
||||
<RcButton
|
||||
:primary="true"
|
||||
:aria-label="action.ariaLabel"
|
||||
@click="action.action"
|
||||
>
|
||||
{{ action.label }}
|
||||
</RcButton>
|
||||
</template>
|
||||
</Drawer>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.resource-detail-drawer {
|
||||
:deep() .tabbed {
|
||||
& > .tabs {
|
||||
border: none;
|
||||
}
|
||||
|
||||
& > .tab-container {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 0;
|
||||
padding-top: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
<script lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
export interface Props {
|
||||
items: string[];
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
const { items } = defineProps<Props>();
|
||||
|
||||
const first = computed(() => items[0]);
|
||||
const tooltipValue = computed(() => {
|
||||
let rows = '';
|
||||
|
||||
items.forEach((item) => {
|
||||
rows += `• ${ item }<br>`;
|
||||
});
|
||||
|
||||
return rows;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="additional">
|
||||
<div class="initial">
|
||||
{{ first }}
|
||||
</div>
|
||||
<div
|
||||
v-if="items.length > 1"
|
||||
v-clean-tooltip.bottom="tooltipValue"
|
||||
class="more text-muted"
|
||||
>
|
||||
{{ t('generic.plusMore', {n: items.length-1}) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.more {
|
||||
margin-top: 4px;
|
||||
cursor: help;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -7,12 +7,15 @@ export type Annotation = Row;
|
|||
|
||||
export interface AnnotationsProps {
|
||||
annotations: Annotation[];
|
||||
|
||||
onShowConfiguration?: () => void;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { annotations } = defineProps<AnnotationsProps>();
|
||||
const emit = defineEmits(['show-configuration']);
|
||||
const store = useStore();
|
||||
const i18n = useI18n(store);
|
||||
</script>
|
||||
|
|
@ -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')"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -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<Row> => {
|
||||
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<Row> => {
|
||||
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<Row> => {
|
||||
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<Row> => {
|
||||
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<Row> => {
|
||||
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<Row> => {
|
||||
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<Row> => {
|
||||
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<Row> => {
|
||||
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<Row> => {
|
||||
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<Row> => {
|
||||
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<Row> => {
|
||||
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<Row> => {
|
||||
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<Row> => {
|
||||
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<Row> => {
|
||||
const store = useStore();
|
||||
const i18n = useI18n(store);
|
||||
|
|
@ -233,18 +19,6 @@ export const useLiveDate = (resource: any): ComputedRef<Row> => {
|
|||
}));
|
||||
};
|
||||
|
||||
export const useProject = (resource: any): ComputedRef<Row> => {
|
||||
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<Row[]> => {
|
||||
const store = useStore();
|
||||
const i18n = useI18n(store);
|
||||
|
|
@ -259,23 +33,3 @@ export const useDefaultIdentifyingInformation = (resource: any): ComputedRef<Row
|
|||
liveDate.value
|
||||
]);
|
||||
};
|
||||
|
||||
export const useDefaultWorkloadIdentifyingInformation = (resource: any): ComputedRef<Row[]> => {
|
||||
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,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -50,12 +50,16 @@ const { rows } = defineProps<MetadataProps>();
|
|||
:class="['status', row.status]"
|
||||
/>
|
||||
<router-link
|
||||
v-if="row.to"
|
||||
v-if="row.value && row.to"
|
||||
:to="row.to"
|
||||
>
|
||||
{{ row.value }}
|
||||
</router-link>
|
||||
<span v-else>{{ row.value }}</span>
|
||||
<span v-else-if="row.value">{{ row.value }}</span>
|
||||
<span
|
||||
v-else
|
||||
class="text-muted"
|
||||
>—</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 }`;
|
|||
</div>
|
||||
<div
|
||||
v-if="visibleRows.length === 0"
|
||||
class="empty mmt-2"
|
||||
class="empty mmt-2 text-muted"
|
||||
>
|
||||
<div class="no-rows">
|
||||
{{ i18n.t('component.resource.detail.metadata.keyValue.noRows', {propertyName: lowercasePropertyName}) }}
|
||||
</div>
|
||||
<div class="show-configuration mmt-1">
|
||||
<router-link
|
||||
class="secondary"
|
||||
to="#"
|
||||
<a
|
||||
class="secondary text-muted"
|
||||
href="#"
|
||||
@click="(ev: MouseEvent) => {ev.preventDefault(); emit('show-configuration');}"
|
||||
>
|
||||
{{ i18n.t('component.resource.detail.metadata.keyValue.showConfiguration') }}
|
||||
</router-link>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -76,13 +78,14 @@ const displayValue = (row: Row) => `${ row.key }: ${ row.value }`;
|
|||
{{ displayValue(row) }}
|
||||
</Rectangle>
|
||||
</div>
|
||||
<router-link
|
||||
<a
|
||||
v-if="showShowAllButton"
|
||||
to="#"
|
||||
class="show-all"
|
||||
href="#"
|
||||
class="show-all secondary"
|
||||
@click="(ev: MouseEvent) => {ev.preventDefault(); emit('show-configuration');}"
|
||||
>
|
||||
{{ showAllLabel }}
|
||||
</router-link>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import { useStore } from 'vuex';
|
|||
export type Label = Row;
|
||||
export interface LabelsProps {
|
||||
labels: Label[];
|
||||
|
||||
onShowConfiguration?: () => void;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
|
@ -14,6 +16,7 @@ export interface LabelsProps {
|
|||
<script setup lang="ts">
|
||||
const props = defineProps<LabelsProps>();
|
||||
const { labels } = toRefs(props);
|
||||
const emit = defineEmits(['show-configuration']);
|
||||
|
||||
const store = useStore();
|
||||
const i18n = useI18n(store);
|
||||
|
|
@ -23,5 +26,6 @@ const i18n = useI18n(store);
|
|||
<KeyValue
|
||||
:propertyName="i18n.t('component.resource.detail.metadata.labels.title')"
|
||||
:rows="labels"
|
||||
@show-configuration="() => emit('show-configuration')"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ describe('component: Metadata/IdentifyingInformation', () => {
|
|||
});
|
||||
|
||||
expect(wrapper.find('.empty .no-rows').element.innerHTML.trim()).toStrictEqual(`component.resource.detail.metadata.keyValue.noRows-{"propertyName":"${ propertyName.toLowerCase() }"}`);
|
||||
expect(wrapper.find('.empty .show-configuration').findComponent(RouterLinkStub).element.innerHTML).toStrictEqual('component.resource.detail.metadata.keyValue.showConfiguration');
|
||||
expect(wrapper.find('.empty .show-configuration a').text()).toStrictEqual('component.resource.detail.metadata.keyValue.showConfiguration');
|
||||
});
|
||||
|
||||
it('should render show all button if rows length exceeds max', async() => {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils';
|
|||
import Rectangle from '@shell/components/Resource/Detail/Metadata/Rectangle.vue';
|
||||
|
||||
describe('component: Rectangle', () => {
|
||||
it('shoulder render container with class title and missing outline when passed outline:false', async() => {
|
||||
it('should render container with class title and missing outline when passed outline:false', async() => {
|
||||
const wrapper = mount(Rectangle, { props: { outline: false } });
|
||||
|
||||
expect(wrapper.find('.rectangle').exists()).toBeTruthy();
|
||||
|
|
|
|||
|
|
@ -3,27 +3,36 @@ import { useDefaultIdentifyingInformation } from '@shell/components/Resource/Det
|
|||
import { useDefaultLabels } from '@shell/components/Resource/Detail/Metadata/Labels/composable';
|
||||
import { useDefaultAnnotations } from '@shell/components/Resource/Detail/Metadata/Annotations/composable';
|
||||
import { computed, toValue, Ref } from 'vue';
|
||||
import { useResourceDetailDrawer } from '@shell/components/Drawer/ResourceDetailDrawer/composables';
|
||||
|
||||
export const useBasicMetadata = (resource: any) => {
|
||||
const labels = useDefaultLabels(resource);
|
||||
const annotations = useDefaultAnnotations(resource);
|
||||
const { openResourceDetailDrawer } = useResourceDetailDrawer();
|
||||
|
||||
return computed(() => {
|
||||
return {
|
||||
labels,
|
||||
annotations
|
||||
labels: labels.value,
|
||||
annotations: annotations.value,
|
||||
onShowConfiguration: () => openResourceDetailDrawer(resource)
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const useDefaultMetadata = (resource: any, additionalIdentifyingInformation?: (IdentifyingInformationRow[] | Ref<IdentifyingInformationRow[]>)) => {
|
||||
export const useDefaultMetadataProps = (resource: any, additionalIdentifyingInformation?: (IdentifyingInformationRow[] | Ref<IdentifyingInformationRow[]>)) => {
|
||||
const defaultIdentifyingInformation = useDefaultIdentifyingInformation(resource);
|
||||
const additionalIdentifyingInformationValue = toValue(additionalIdentifyingInformation);
|
||||
|
||||
const identifyingInformation = computed(() => [...defaultIdentifyingInformation.value, ...(additionalIdentifyingInformationValue || [])]);
|
||||
const { labels, annotations } = useBasicMetadata(resource);
|
||||
const basicMetaData = useBasicMetadata(resource);
|
||||
const { openResourceDetailDrawer } = useResourceDetailDrawer();
|
||||
|
||||
return computed(() => {
|
||||
return {
|
||||
identifyingInformation,
|
||||
labels,
|
||||
annotations
|
||||
identifyingInformation: identifyingInformation.value,
|
||||
labels: basicMetaData.value.labels,
|
||||
annotations: basicMetaData.value.annotations,
|
||||
onShowConfiguration: () => openResourceDetailDrawer(resource)
|
||||
};
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,10 +11,12 @@ import { useStore } from 'vuex';
|
|||
export interface MetadataProps {
|
||||
identifyingInformation: IdentifyingInformationRow[],
|
||||
labels: Label[],
|
||||
annotations: Annotation[]
|
||||
annotations: Annotation[],
|
||||
onShowConfiguration?: () => void;
|
||||
}
|
||||
|
||||
const { identifyingInformation, labels, annotations } = defineProps<MetadataProps>();
|
||||
const emit = defineEmits(['show-configuration']);
|
||||
|
||||
const store = useStore();
|
||||
const i18n = useI18n(store);
|
||||
|
|
@ -37,6 +39,7 @@ const showBothEmpty = computed(() => labels.length === 0 && annotations.length =
|
|||
<KeyValue
|
||||
:rows="[]"
|
||||
:propertyName="i18n.t('component.resource.detail.metadata.labelsAndAnnotations')"
|
||||
@show-configuration="() => emit('show-configuration')"
|
||||
/>
|
||||
</div>
|
||||
<!-- I'm not using v-else here so I can maintain the spacing correctly with the other columns in other rows. -->
|
||||
|
|
@ -44,13 +47,19 @@ const showBothEmpty = computed(() => labels.length === 0 && annotations.length =
|
|||
v-if="!showBothEmpty"
|
||||
class="labels"
|
||||
>
|
||||
<Labels :labels="labels" />
|
||||
<Labels
|
||||
:labels="labels"
|
||||
@show-configuration="() => emit('show-configuration')"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="!showBothEmpty"
|
||||
class="annotations"
|
||||
>
|
||||
<Annotations :annotations="annotations" />
|
||||
<Annotations
|
||||
:annotations="annotations"
|
||||
@show-configuration="() => emit('show-configuration')"
|
||||
/>
|
||||
</div>
|
||||
</SpacedRow>
|
||||
</template>
|
||||
|
|
@ -60,7 +69,5 @@ const showBothEmpty = computed(() => labels.length === 0 && annotations.length =
|
|||
.labels-and-annotations-empty {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -20,3 +20,18 @@
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.resource-detail-page {
|
||||
padding: 24px;
|
||||
|
||||
:deep() .tabs.horizontal {
|
||||
border: none;
|
||||
}
|
||||
:deep() .tabs.horizontal + .tab-container {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 0;
|
||||
padding-top: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import SubtleLink from '@shell/components/SubtleLink.vue';
|
||||
import { StateColor, stateColorCssVar } from '@shell/utils/style';
|
||||
import StateDot from '@shell/components/StateDot/index.vue';
|
||||
import { StateColor } from '@shell/utils/style';
|
||||
import { sortBy } from 'lodash';
|
||||
import { RouteLocationRaw } from 'vue-router';
|
||||
|
||||
|
|
@ -64,13 +65,10 @@ const {
|
|||
v-else
|
||||
class="counts"
|
||||
>
|
||||
<span
|
||||
<StateDot
|
||||
v-if="color"
|
||||
class="dot"
|
||||
:style="{backgroundColor: stateColorCssVar(color)}"
|
||||
>
|
||||
|
||||
</span>
|
||||
:color="color"
|
||||
/>
|
||||
<span
|
||||
v-for="count in counts"
|
||||
:key="count.label"
|
||||
|
|
@ -105,14 +103,7 @@ const {
|
|||
display: none;
|
||||
}
|
||||
|
||||
.dot {
|
||||
display: inline-block;
|
||||
|
||||
$size: 6px;
|
||||
width: $size;
|
||||
height: $size;
|
||||
|
||||
border-radius: 50%;
|
||||
.state-dot {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
import { useGetConfigMapDataTabProps } from '@shell/components/Resource/Detail/ResourceTabs/ConfigMapDataTab/composables';
|
||||
import { base64Encode } from '@shell/utils/crypto';
|
||||
|
||||
describe('composables: ConfigMapDataTab', () => {
|
||||
const textData = 'This is textData';
|
||||
const binaryData = 'This is binaryData';
|
||||
const binaryDataBase64 = base64Encode(binaryData);
|
||||
|
||||
it('should handle no data', () => {
|
||||
const props = useGetConfigMapDataTabProps({});
|
||||
|
||||
expect(props.rows).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle text and binary data at the same time', () => {
|
||||
const data = { text: textData };
|
||||
const bData = { binary: binaryDataBase64 };
|
||||
|
||||
const props = useGetConfigMapDataTabProps({ data, binaryData: bData });
|
||||
|
||||
expect(props.rows[0].key).toStrictEqual('text');
|
||||
expect(props.rows[0].value).toStrictEqual(textData);
|
||||
expect(props.rows[0].binary).toStrictEqual(false);
|
||||
|
||||
expect(props.rows[1].key).toStrictEqual('binary');
|
||||
expect(props.rows[1].value).toStrictEqual(binaryData);
|
||||
expect(props.rows[1].binary).toStrictEqual(false);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import ConfigMapDataTab from '@shell/components/Resource/Detail/ResourceTabs/ConfigMapDataTab/index.vue';
|
||||
|
||||
jest.mock('clipboard-polyfill', () => {});
|
||||
jest.mock('vuex');
|
||||
jest.mock('@shell/components/DetailText.vue', () => ({
|
||||
name: 'DetailText',
|
||||
template: '<div>DetailText</div>',
|
||||
props: {
|
||||
value: { type: String }, label: { type: String }, binary: { type: Boolean }
|
||||
}
|
||||
}));
|
||||
|
||||
describe('component: ConfigMapDataTab/index', () => {
|
||||
const global = {
|
||||
provide: {
|
||||
addTab: jest.fn(), removeTab: jest.fn(), sideTabs: false,
|
||||
},
|
||||
directives: { 'clean-tooltip': () => { } }
|
||||
};
|
||||
|
||||
it('should render the now rows message', async() => {
|
||||
const wrapper = mount(ConfigMapDataTab, {
|
||||
props: { rows: [] },
|
||||
global
|
||||
});
|
||||
|
||||
expect(wrapper.find('.no-rows').text()).toStrictEqual('sortableTable.noRows');
|
||||
});
|
||||
|
||||
it('should render DetailText with the appropriate props from row', async() => {
|
||||
const row = {
|
||||
key: 'ROW',
|
||||
value: 'VALUE',
|
||||
binary: false
|
||||
};
|
||||
const wrapper = mount(ConfigMapDataTab, {
|
||||
props: { rows: [row] },
|
||||
global
|
||||
});
|
||||
|
||||
const component = wrapper.getComponent({ name: 'DetailText' });
|
||||
|
||||
expect(component.props('value')).toStrictEqual(row.value);
|
||||
expect(component.props('label')).toStrictEqual(row.key);
|
||||
expect(component.props('binary')).toStrictEqual(row.binary);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { Props } from '@shell/components/Resource/Detail/ResourceTabs/ConfigMapDataTab/index.vue';
|
||||
import { computed } from 'vue';
|
||||
import { base64Decode } from '@shell/utils/crypto';
|
||||
|
||||
export const useGetConfigMapDataTabProps = (configMap: any): Props => {
|
||||
const rows = computed(() => {
|
||||
const rows: any[] = [];
|
||||
const { data = {}, binaryData = {} } = configMap;
|
||||
|
||||
Object.keys(data).forEach((key) => {
|
||||
rows.push({
|
||||
key,
|
||||
value: data[key],
|
||||
binary: false
|
||||
});
|
||||
});
|
||||
|
||||
// we define the binary as false so that the ui doesn't display the size of the binary instead of the actual data...
|
||||
Object.keys(binaryData).forEach((key) => {
|
||||
rows.push({
|
||||
key,
|
||||
value: base64Decode(binaryData[key]),
|
||||
binary: false
|
||||
});
|
||||
});
|
||||
|
||||
return rows;
|
||||
});
|
||||
|
||||
return { rows: rows.value };
|
||||
};
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
<script lang="ts">
|
||||
import { useStore } from 'vuex';
|
||||
import Tab from '@shell/components/Tabbed/Tab.vue';
|
||||
import { useI18n } from '@shell/composables/useI18n';
|
||||
|
||||
import DetailText from '@shell/components/DetailText.vue';
|
||||
|
||||
export interface Row {
|
||||
key: string;
|
||||
value: any;
|
||||
binary: boolean;
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
rows: Row[];
|
||||
weight?: number;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const { weight, rows } = defineProps<Props>();
|
||||
|
||||
const store = useStore();
|
||||
const i18n = useI18n(store);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Tab
|
||||
name="data"
|
||||
label-key="secret.data"
|
||||
:weight="weight"
|
||||
>
|
||||
<div
|
||||
v-for="(row,idx) in rows"
|
||||
:key="idx"
|
||||
class="mb-20"
|
||||
>
|
||||
<DetailText
|
||||
:value="row.value"
|
||||
:label="row.key"
|
||||
:binary="row.binary"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="!rows.length">
|
||||
<div class="m-20 text-center no-rows">
|
||||
{{ i18n.t('sortableTable.noRows') }}
|
||||
</div>
|
||||
</div>
|
||||
</Tab>
|
||||
</template>
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<TitleBarProps> => {
|
||||
export const useDefaultTitleBarProps = (resource: any): Ref<TitleBarProps> => {
|
||||
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<TitleBarProps> => {
|
|||
color: resourceValue.stateBackground,
|
||||
label: resourceValue.stateDisplay
|
||||
},
|
||||
onShowConfiguration: () => resourceValue.goToEdit()
|
||||
description: resourceValue.description,
|
||||
onShowConfiguration: () => openResourceDetailDrawer(resourceValue)
|
||||
}));
|
||||
};
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
<script lang="ts">
|
||||
import BadgeState from '@pkg/rancher-components/src/components/BadgeState/BadgeState.vue';
|
||||
import BadgeState from '@components/BadgeState/BadgeState.vue';
|
||||
import { RouteLocationRaw } from 'vue-router';
|
||||
import Title from '@shell/components/Resource/Detail/TitleBar/Title.vue';
|
||||
import Top from '@shell/components/Resource/Detail/TitleBar/Top.vue';
|
||||
import ActionMenu from '@shell/components/ActionMenuShell.vue';
|
||||
import { useStore } from 'vuex';
|
||||
import { useI18n } from '@shell/composables/useI18n';
|
||||
import RcButton from '~/pkg/rancher-components/src/components/RcButton/RcButton.vue';
|
||||
import RcButton from '@components/RcButton/RcButton.vue';
|
||||
|
||||
export interface Badge {
|
||||
color: 'bg-success' | 'bg-error' | 'bg-warning' | 'bg-info';
|
||||
|
|
@ -84,10 +84,10 @@ const emit = defineEmits(['show-configuration']);
|
|||
</RcButton>
|
||||
<ActionMenu
|
||||
v-if="actionMenuResource"
|
||||
class="title-bar-action-menu"
|
||||
button-role="multiAction"
|
||||
:resource="actionMenuResource"
|
||||
data-testid="masthead-action-menu"
|
||||
:button-aria-label="i18n.t('component.resource.detail.titleBar.ariaLabel.actionMenu', { resource: resourceName })"
|
||||
/>
|
||||
</div>
|
||||
</Top>
|
||||
|
|
@ -120,5 +120,9 @@ const emit = defineEmits(['show-configuration']);
|
|||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.description {
|
||||
max-width: 60%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -289,6 +289,9 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
refresh() {
|
||||
this.$refs.yamleditor.refresh();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
<script setup lang="ts">
|
||||
import { StateColor, stateColorCssVar } from '@shell/utils/style';
|
||||
import { computed } from 'vue';
|
||||
|
||||
interface Props {
|
||||
color: StateColor;
|
||||
size?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), { size: '8px' });
|
||||
const backgroundColor = computed(() => stateColorCssVar(props.color));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="state-dot" />
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.state-dot {
|
||||
display: inline-block;
|
||||
|
||||
width: v-bind('props.size');
|
||||
height: v-bind('props.size');
|
||||
|
||||
border-radius: 50%;
|
||||
background-color: v-bind('backgroundColor');
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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"
|
||||
>
|
||||
<slot />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
import { Component } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
export const useDrawer = () => {
|
||||
const store = useStore();
|
||||
|
||||
const open = (component: Component, options?: Record<string, any>) => {
|
||||
store.commit('slideInPanel/open', {
|
||||
component,
|
||||
componentProps: options || {}
|
||||
});
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
store.commit('slideInPanel/close');
|
||||
};
|
||||
|
||||
return {
|
||||
open,
|
||||
close
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { computed, Ref, toValue } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
type ResourceType = string | Ref<string>;
|
||||
type IdType = string | Ref<string>;
|
||||
|
||||
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 });
|
||||
};
|
||||
|
|
@ -105,7 +105,10 @@ export default {
|
|||
:register-before-hook="registerBeforeHook"
|
||||
/>
|
||||
|
||||
<Tabbed :side-tabs="true">
|
||||
<Tabbed
|
||||
:side-tabs="true"
|
||||
:use-hash="useTabbedHash"
|
||||
>
|
||||
<Tab
|
||||
name="data"
|
||||
:label="t('configmap.tabs.data.label')"
|
||||
|
|
|
|||
|
|
@ -44,5 +44,10 @@ export default defineComponent({
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
useTabbedHash: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,19 +1,42 @@
|
|||
<script setup lang="ts">
|
||||
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);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DetailPage>
|
||||
<template #top-area>
|
||||
ConfigMap
|
||||
<TitleBar
|
||||
v-bind="titleBarProps"
|
||||
/>
|
||||
<Metadata
|
||||
class="mmt-6"
|
||||
v-bind="metadataProps"
|
||||
/>
|
||||
</template>
|
||||
<template #middle-area>
|
||||
{{ configMaps }}
|
||||
<template #bottom-area>
|
||||
<ResourceTabs
|
||||
:value="configMap"
|
||||
:schema="schema"
|
||||
>
|
||||
<ConfigMapDataTab
|
||||
v-bind="configMapDataTabProps"
|
||||
/>
|
||||
</ResourceTabs>
|
||||
</template>
|
||||
</DetailPage>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -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-');
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue