Skip to content
代码片段 群组 项目
未验证 提交 dc3c4a76 编辑于 作者: Coung Ngo's avatar Coung Ngo 提交者: GitLab
浏览文件

Merge branch '463331-add-a-new-tab-for-kubernetes-tree-view-2' into 'master'

Add a Tree item component for Kubernetes tree view

See merge request https://gitlab.com/gitlab-org/gitlab/-/merge_requests/154597



Merged-by: default avatarCoung Ngo <cngo@gitlab.com>
Approved-by: default avatarCoung Ngo <cngo@gitlab.com>
Co-authored-by: default avatarAnna Vovchenko <avovchenko@gitlab.com>
No related branches found
No related tags found
无相关合并请求
显示 286 个添加60 个删除
...@@ -191,3 +191,38 @@ export const KUSTOMIZATIONS_RESOURCE_TYPE = 'kustomizations'; ...@@ -191,3 +191,38 @@ export const KUSTOMIZATIONS_RESOURCE_TYPE = 'kustomizations';
export const KUSTOMIZATION = 'Kustomization'; export const KUSTOMIZATION = 'Kustomization';
export const HELM_RELEASE = 'HelmRelease'; export const HELM_RELEASE = 'HelmRelease';
export const TREE_ITEM_KIND_ICONS = {
[KUSTOMIZATION]: 'overview',
};
export const TREE_ITEM_STATUS_ICONS = {
reconciled: {
icon: 'status-success',
class: 'gl-text-green-500',
},
reconciling: {
icon: 'status-running',
class: 'gl-text-blue-500',
},
reconcilingWithBadConfig: {
icon: 'status-running',
class: 'gl-text-blue-500',
},
stalled: {
icon: 'status-paused',
class: 'gl-text-orange-500',
},
failed: {
icon: 'status-failed',
class: 'gl-text-red-500',
},
unknown: {
icon: 'status-waiting',
class: 'gl-text-gray-500',
},
unavailable: {
icon: 'status-waiting',
class: 'gl-text-gray-500',
},
};
...@@ -7,13 +7,10 @@ import { ...@@ -7,13 +7,10 @@ import {
CLUSTER_HEALTH_ERROR, CLUSTER_HEALTH_ERROR,
HEALTH_BADGES, HEALTH_BADGES,
SYNC_STATUS_BADGES, SYNC_STATUS_BADGES,
STATUS_TRUE,
STATUS_FALSE,
STATUS_UNKNOWN,
REASON_PROGRESSING,
HELM_RELEASES_RESOURCE_TYPE, HELM_RELEASES_RESOURCE_TYPE,
KUSTOMIZATIONS_RESOURCE_TYPE, KUSTOMIZATIONS_RESOURCE_TYPE,
} from '~/environments/constants'; } from '~/environments/constants';
import { fluxSyncStatus } from '~/environments/helpers/k8s_integration_helper';
import KubernetesConnectionStatus from '~/environments/environment_details/components/kubernetes/kubernetes_connection_status.vue'; import KubernetesConnectionStatus from '~/environments/environment_details/components/kubernetes/kubernetes_connection_status.vue';
import KubernetesConnectionStatusBadge from '~/environments/environment_details/components/kubernetes/kubernetes_connection_status_badge.vue'; import KubernetesConnectionStatusBadge from '~/environments/environment_details/components/kubernetes/kubernetes_connection_status_badge.vue';
import { import {
...@@ -131,35 +128,6 @@ export default { ...@@ -131,35 +128,6 @@ export default {
fluxBadgeId() { fluxBadgeId() {
return `${this.environmentName}-flux-sync-badge`; return `${this.environmentName}-flux-sync-badge`;
}, },
fluxAnyStalled() {
return this.fluxResourceStatus.find((condition) => {
return condition.status === STATUS_TRUE && condition.type === 'Stalled';
});
},
fluxAnyReconcilingWithBadConfig() {
return this.fluxResourceStatus.find((condition) => {
return (
condition.status === STATUS_UNKNOWN &&
condition.type === 'Ready' &&
condition.reason === REASON_PROGRESSING
);
});
},
fluxAnyReconciling() {
return this.fluxResourceStatus.find((condition) => {
return condition.status === STATUS_TRUE && condition.type === 'Reconciling';
});
},
fluxAnyReconciled() {
return this.fluxResourceStatus.find((condition) => {
return condition.status === STATUS_TRUE && condition.type === 'Ready';
});
},
fluxAnyFailed() {
return this.fluxResourceStatus.find((condition) => {
return condition.status === STATUS_FALSE && condition.type === 'Ready';
});
},
syncStatusBadge() { syncStatusBadge() {
if (!this.fluxResourceStatus.length && this.fluxApiError) { if (!this.fluxResourceStatus.length && this.fluxApiError) {
return { ...SYNC_STATUS_BADGES.unavailable, popoverText: this.fluxApiError }; return { ...SYNC_STATUS_BADGES.unavailable, popoverText: this.fluxApiError };
...@@ -167,25 +135,32 @@ export default { ...@@ -167,25 +135,32 @@ export default {
if (!this.fluxResourceStatus.length) { if (!this.fluxResourceStatus.length) {
return SYNC_STATUS_BADGES.unavailable; return SYNC_STATUS_BADGES.unavailable;
} }
if (this.fluxAnyFailed) {
return { ...SYNC_STATUS_BADGES.failed, popoverText: this.fluxAnyFailed.message }; const fluxStatus = fluxSyncStatus(this.fluxResourceStatus);
}
if (this.fluxAnyStalled) { switch (fluxStatus.status) {
return { ...SYNC_STATUS_BADGES.stalled, popoverText: this.fluxAnyStalled.message }; case 'failed':
} return {
if (this.fluxAnyReconcilingWithBadConfig) { ...SYNC_STATUS_BADGES.failed,
return { popoverText: fluxStatus.message,
...SYNC_STATUS_BADGES.reconciling, };
popoverText: this.fluxAnyReconcilingWithBadConfig.message, case 'stalled':
}; return {
} ...SYNC_STATUS_BADGES.stalled,
if (this.fluxAnyReconciling) { popoverText: fluxStatus.message,
return SYNC_STATUS_BADGES.reconciling; };
} case 'reconcilingWithBadConfig':
if (this.fluxAnyReconciled) { return {
return SYNC_STATUS_BADGES.reconciled; ...SYNC_STATUS_BADGES.reconciling,
popoverText: fluxStatus.message,
};
case 'reconciling':
return SYNC_STATUS_BADGES.reconciling;
case 'reconciled':
return SYNC_STATUS_BADGES.reconciled;
default:
return SYNC_STATUS_BADGES.unknown;
} }
return SYNC_STATUS_BADGES.unknown;
}, },
isFluxConnectionStatus() { isFluxConnectionStatus() {
return Boolean(this.fluxConnectionParams.resourceType); return Boolean(this.fluxConnectionParams.resourceType);
......
...@@ -2,10 +2,13 @@ ...@@ -2,10 +2,13 @@
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { GlTab } from '@gitlab/ui'; import { GlTab } from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { fluxSyncStatus } from '~/environments/helpers/k8s_integration_helper';
import KubernetesTreeItem from './kubernetes_tree_item.vue';
export default { export default {
components: { components: {
GlTab, GlTab,
KubernetesTreeItem,
}, },
i18n: { i18n: {
summaryTitle: s__('Environment|Summary'), summaryTitle: s__('Environment|Summary'),
...@@ -21,6 +24,11 @@ export default { ...@@ -21,6 +24,11 @@ export default {
hasFluxKustomization() { hasFluxKustomization() {
return !isEmpty(this.fluxKustomization); return !isEmpty(this.fluxKustomization);
}, },
fluxKustomizationStatus() {
if (!this.fluxKustomization.conditions?.length) return '';
return fluxSyncStatus(this.fluxKustomization.conditions).status;
},
}, },
}; };
</script> </script>
...@@ -28,8 +36,11 @@ export default { ...@@ -28,8 +36,11 @@ export default {
<gl-tab :title="$options.i18n.summaryTitle"> <gl-tab :title="$options.i18n.summaryTitle">
<p class="gl-mt-3 gl-text-secondary">{{ $options.i18n.treeView }}</p> <p class="gl-mt-3 gl-text-secondary">{{ $options.i18n.treeView }}</p>
<p v-if="hasFluxKustomization"> <kubernetes-tree-item
{{ fluxKustomization.kind }}: {{ fluxKustomization.metadata.name }} v-if="hasFluxKustomization"
</p></gl-tab :kind="fluxKustomization.kind"
> :name="fluxKustomization.metadata.name"
:status="fluxKustomizationStatus"
/>
</gl-tab>
</template> </template>
<script>
import { GlIcon } from '@gitlab/ui';
import { TREE_ITEM_KIND_ICONS, TREE_ITEM_STATUS_ICONS } from '~/environments/constants';
export default {
components: {
GlIcon,
},
props: {
kind: {
required: true,
type: String,
},
name: {
required: true,
type: String,
},
status: {
required: false,
type: String,
default: '',
},
},
computed: {
statusBadge() {
return TREE_ITEM_STATUS_ICONS[this.status] || TREE_ITEM_STATUS_ICONS.unknown;
},
kindIcon() {
return TREE_ITEM_KIND_ICONS[this.kind];
},
},
};
</script>
<template>
<div
class="gl-border-1 gl-border-solid gl-border-gray-200 gl-rounded-base gl-p-3 gl-bg-white gl-flex gl-w-28"
>
<gl-icon :name="kindIcon" data-testid="resource-kind-icon" />
<div class="gl-ml-4">
<span class="gl-text-secondary gl-block gl-mb-2">{{ kind }}:</span>{{ name }}
<gl-icon
v-if="status"
:name="statusBadge.icon"
:size="12"
:class="statusBadge.class"
data-testid="resource-status-icon"
/>
</div>
</div>
</template>
import csrf from '~/lib/utils/csrf'; import csrf from '~/lib/utils/csrf';
import { CLUSTER_AGENT_ERROR_MESSAGES } from '../constants'; import {
CLUSTER_AGENT_ERROR_MESSAGES,
STATUS_TRUE,
STATUS_FALSE,
STATUS_UNKNOWN,
REASON_PROGRESSING,
} from '../constants';
export function humanizeClusterErrors(reason) { export function humanizeClusterErrors(reason) {
const errorReason = String(reason).toLowerCase(); const errorReason = String(reason).toLowerCase();
...@@ -19,3 +25,55 @@ export function createK8sAccessConfiguration({ kasTunnelUrl, gitlabAgentId }) { ...@@ -19,3 +25,55 @@ export function createK8sAccessConfiguration({ kasTunnelUrl, gitlabAgentId }) {
credentials: 'include', credentials: 'include',
}; };
} }
const fluxAnyStalled = (fluxConditions) => {
return fluxConditions.find((condition) => {
return condition.status === STATUS_TRUE && condition.type === 'Stalled';
});
};
const fluxAnyReconcilingWithBadConfig = (fluxConditions) => {
return fluxConditions.find((condition) => {
return (
condition.status === STATUS_UNKNOWN &&
condition.type === 'Ready' &&
condition.reason === REASON_PROGRESSING
);
});
};
const fluxAnyReconciling = (fluxConditions) => {
return fluxConditions.find((condition) => {
return condition.status === STATUS_TRUE && condition.type === 'Reconciling';
});
};
const fluxAnyReconciled = (fluxConditions) => {
return fluxConditions.find((condition) => {
return condition.status === STATUS_TRUE && condition.type === 'Ready';
});
};
const fluxAnyFailed = (fluxConditions) => {
return fluxConditions.find((condition) => {
return condition.status === STATUS_FALSE && condition.type === 'Ready';
});
};
export const fluxSyncStatus = (fluxConditions) => {
if (fluxAnyFailed(fluxConditions)) {
return { status: 'failed', message: fluxAnyFailed(fluxConditions).message };
}
if (fluxAnyStalled(fluxConditions)) {
return { status: 'stalled', message: fluxAnyStalled(fluxConditions).message };
}
if (fluxAnyReconcilingWithBadConfig(fluxConditions)) {
return {
status: 'reconcilingWithBadConfig',
message: fluxAnyReconcilingWithBadConfig(fluxConditions).message,
};
}
if (fluxAnyReconciling(fluxConditions)) {
return { status: 'reconciling' };
}
if (fluxAnyReconciled(fluxConditions)) {
return { status: 'reconciled' };
}
return { status: 'unknown' };
};
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlTab } from '@gitlab/ui'; import { GlTab } from '@gitlab/ui';
import KubernetesSummary from '~/environments/environment_details/components/kubernetes/kubernetes_summary.vue'; import KubernetesSummary from '~/environments/environment_details/components/kubernetes/kubernetes_summary.vue';
import KubernetesTreeItem from '~/environments/environment_details/components/kubernetes/kubernetes_tree_item.vue';
import { fluxKustomization } from '../../../mock_data'; import { fluxKustomization } from '../../../mock_data';
describe('~/environments/environment_details/components/kubernetes/kubernetes_summary.vue', () => { describe('~/environments/environment_details/components/kubernetes/kubernetes_summary.vue', () => {
let wrapper; let wrapper;
const findTab = () => wrapper.findComponent(GlTab); const findTab = () => wrapper.findComponent(GlTab);
const findTreeItem = () => wrapper.findComponent(KubernetesTreeItem);
const createWrapper = () => { const createWrapper = () => {
wrapper = shallowMount(KubernetesSummary, { wrapper = shallowMount(KubernetesSummary, {
...@@ -27,11 +29,15 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_su ...@@ -27,11 +29,15 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_su
}); });
it('renders tree view title', () => { it('renders tree view title', () => {
expect(findTab().text()).toContain('Tree view'); expect(findTab().text()).toBe('Tree view');
}); });
it('renders kustomization resource data', () => { it('renders tree item with kustomization resource data', () => {
expect(findTab().text()).toContain('Kustomization: my-kustomization'); expect(findTreeItem().props()).toEqual({
kind: 'Kustomization',
name: 'my-kustomization',
status: 'reconciled',
});
}); });
}); });
}); });
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import KubernetesTreeItem from '~/environments/environment_details/components/kubernetes/kubernetes_tree_item.vue';
import { TREE_ITEM_KIND_ICONS, TREE_ITEM_STATUS_ICONS } from '~/environments/constants';
describe('~/environments/environment_details/components/kubernetes/kubernetes_tree_item.vue', () => {
let wrapper;
const kind = 'Kustomization';
const name = 'my-kustomization';
const findKindIcon = () => wrapper.findByTestId('resource-kind-icon');
const findStatusIcon = () => wrapper.findByTestId('resource-status-icon');
const createWrapper = ({ status = 'reconciled' } = {}) => {
wrapper = shallowMountExtended(KubernetesTreeItem, {
propsData: {
kind,
name,
status,
},
});
};
describe('mounted', () => {
it('renders correct kind icon', () => {
createWrapper();
expect(findKindIcon().props('name')).toBe(TREE_ITEM_KIND_ICONS[kind]);
});
it('renders correct kind and name', () => {
createWrapper();
expect(wrapper.text()).toBe(`${kind}:${name}`);
});
it('renders correct status icon when the status is provided', () => {
createWrapper();
const iconData = TREE_ITEM_STATUS_ICONS.reconciled;
expect(findStatusIcon().props('name')).toBe(iconData.icon);
expect(findStatusIcon().attributes('class')).toBe(iconData.class);
});
it("doesn't render status icon when the status is not provided", () => {
createWrapper({ status: '' });
expect(findStatusIcon().exists()).toBe(false);
});
});
});
import { import {
humanizeClusterErrors, humanizeClusterErrors,
createK8sAccessConfiguration, createK8sAccessConfiguration,
fluxSyncStatus,
} from '~/environments/helpers/k8s_integration_helper'; } from '~/environments/helpers/k8s_integration_helper';
import { CLUSTER_AGENT_ERROR_MESSAGES } from '~/environments/constants'; import {
CLUSTER_AGENT_ERROR_MESSAGES,
STATUS_TRUE,
STATUS_FALSE,
STATUS_UNKNOWN,
REASON_PROGRESSING,
} from '~/environments/constants';
jest.mock('~/lib/utils/csrf', () => ({ headers: { token: 'mock-csrf-token' } })); jest.mock('~/lib/utils/csrf', () => ({ headers: { token: 'mock-csrf-token' } }));
...@@ -53,4 +60,36 @@ describe('k8s_integration_helper', () => { ...@@ -53,4 +60,36 @@ describe('k8s_integration_helper', () => {
}); });
}); });
}); });
describe('fluxSyncStatus', () => {
const message = 'message from Flux';
let fluxConditions;
it.each`
status | type | reason | statusText | statusMessage
${STATUS_TRUE} | ${'Stalled'} | ${''} | ${'stalled'} | ${{ message }}
${STATUS_TRUE} | ${'Reconciling'} | ${''} | ${'reconciling'} | ${''}
${STATUS_UNKNOWN} | ${'Ready'} | ${REASON_PROGRESSING} | ${'reconcilingWithBadConfig'} | ${{ message }}
${STATUS_TRUE} | ${'Ready'} | ${''} | ${'reconciled'} | ${''}
${STATUS_FALSE} | ${'Ready'} | ${''} | ${'failed'} | ${{ message }}
${STATUS_UNKNOWN} | ${'Ready'} | ${''} | ${'unknown'} | ${''}
`(
'renders sync status as $statusText when status is $status, type is $type, and reason is $reason',
({ status, type, reason, statusText, statusMessage }) => {
fluxConditions = [
{
status,
type,
reason,
message,
},
];
expect(fluxSyncStatus(fluxConditions)).toMatchObject({
status: statusText,
...statusMessage,
});
},
);
});
}); });
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册