307 lines
9.1 KiB
Vue
307 lines
9.1 KiB
Vue
<!-- eslint-disable vue/multi-word-component-names -->
|
|
<script>
|
|
import { GlAlert, GlLoadingIcon, GlBanner, GlTabs, GlTab } from '@gitlab/ui';
|
|
import feedbackBannerIllustration from '@gitlab/svgs/dist/illustrations/chat-sm.svg?url';
|
|
import { s__ } from '~/locale';
|
|
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
|
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
|
|
import getAgentsQuery from 'ee_else_ce/clusters_list/graphql/queries/get_agents.query.graphql';
|
|
import getSharedAgentsQuery from 'ee_else_ce/clusters_list/graphql/queries/get_shared_agents.query.graphql';
|
|
import { AGENT_FEEDBACK_ISSUE, AGENT_FEEDBACK_KEY, KAS_DISABLED_ERROR } from '../constants';
|
|
import getTreeList from '../graphql/queries/get_tree_list.query.graphql';
|
|
import { getAgentLastContact, getAgentStatus } from '../clusters_util';
|
|
import AgentEmptyState from './agent_empty_state.vue';
|
|
import AgentTable from './agent_table.vue';
|
|
import AgentConfigsTable from './agents_configs_table.vue';
|
|
|
|
export default {
|
|
i18n: {
|
|
feedbackBannerTitle: s__('ClusterAgents|Tell us what you think'),
|
|
feedbackBannerText: s__(
|
|
'ClusterAgents|We would love to learn more about your experience with the GitLab Agent.',
|
|
),
|
|
feedbackBannerButton: s__('ClusterAgents|Give feedback'),
|
|
error: s__('ClusterAgents|An error occurred while loading your agents'),
|
|
availableConfigs: s__('ClusterAgents|Available configurations'),
|
|
},
|
|
AGENT_FEEDBACK_ISSUE,
|
|
AGENT_FEEDBACK_KEY,
|
|
apollo: {
|
|
// eslint-disable-next-line @gitlab/vue-no-undef-apollo-properties
|
|
agents: {
|
|
query: getAgentsQuery,
|
|
variables() {
|
|
return {
|
|
fullPath: this.fullPath,
|
|
isGroup: this.isGroup,
|
|
};
|
|
},
|
|
update(data) {
|
|
this.updateAgentsList(data);
|
|
},
|
|
result({ data }) {
|
|
const agentsData = data?.project?.clusterAgents || data?.group?.clusterAgents;
|
|
this.agentsCount = agentsData?.count;
|
|
this.$emit('onAgentsLoad', this.agentsCount);
|
|
},
|
|
error(error) {
|
|
this.queryErrored = true;
|
|
|
|
if (error?.message?.indexOf(KAS_DISABLED_ERROR) >= 0) {
|
|
this.$emit('kasDisabled', true);
|
|
}
|
|
},
|
|
},
|
|
sharedAgents: {
|
|
query: getSharedAgentsQuery,
|
|
variables() {
|
|
return {
|
|
fullPath: this.fullPath,
|
|
};
|
|
},
|
|
update(data) {
|
|
return data;
|
|
},
|
|
skip() {
|
|
return this.isGroup;
|
|
},
|
|
error() {
|
|
this.sharedAgentsQueryErrored = true;
|
|
},
|
|
},
|
|
// eslint-disable-next-line @gitlab/vue-no-undef-apollo-properties
|
|
treeList: {
|
|
query: getTreeList,
|
|
variables() {
|
|
return {
|
|
defaultBranchName: this.defaultBranchName,
|
|
projectPath: this.fullPath,
|
|
};
|
|
},
|
|
update(data) {
|
|
this.updateTreeList(data);
|
|
return data;
|
|
},
|
|
skip() {
|
|
return this.isGroup;
|
|
},
|
|
},
|
|
},
|
|
components: {
|
|
AgentEmptyState,
|
|
AgentTable,
|
|
AgentConfigsTable,
|
|
GlAlert,
|
|
GlLoadingIcon,
|
|
GlBanner,
|
|
GlTabs,
|
|
GlTab,
|
|
LocalStorageSync,
|
|
},
|
|
mixins: [glFeatureFlagMixin()],
|
|
inject: ['fullPath', 'isGroup'],
|
|
props: {
|
|
defaultBranchName: {
|
|
default: '.noBranch',
|
|
required: false,
|
|
type: String,
|
|
},
|
|
isChildComponent: {
|
|
default: false,
|
|
required: false,
|
|
type: Boolean,
|
|
},
|
|
limit: {
|
|
default: null,
|
|
required: false,
|
|
type: Number,
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
feedbackBannerDismissed: false,
|
|
queryErrored: false,
|
|
sharedAgentsQueryErrored: false,
|
|
sharedAgents: [],
|
|
agentList: [],
|
|
agentsCount: null,
|
|
availableConfigs: [],
|
|
configFolders: [],
|
|
currentTab: 0,
|
|
};
|
|
},
|
|
computed: {
|
|
sharedAgentsList() {
|
|
const sharedAgents = [
|
|
...(this.sharedAgents?.project?.ciAccessAuthorizedAgents?.nodes || []),
|
|
...(this.sharedAgents?.project?.userAccessAuthorizedAgents?.nodes || []),
|
|
];
|
|
|
|
const filteredList = sharedAgents.filter((node, index, list) => {
|
|
if (!node?.agent) return false;
|
|
const isDuplicate = index !== list.findIndex((agent) => agent.agent.id === node.agent.id);
|
|
const isSameProject = node.agent.project?.fullPath === this.fullPath;
|
|
return !isDuplicate && !isSameProject;
|
|
});
|
|
|
|
return filteredList
|
|
.map(({ agent }) => {
|
|
const lastContact = getAgentLastContact(agent?.tokens?.nodes);
|
|
const status = getAgentStatus(lastContact);
|
|
return { ...agent, lastContact, status, isShared: true };
|
|
})
|
|
.sort((a, b) => b.lastContact - a.lastContact);
|
|
},
|
|
agentListLoading() {
|
|
return this.$apollo.queries.agents.loading;
|
|
},
|
|
sharedAgentsLoading() {
|
|
return this.$apollo.queries.sharedAgents.loading;
|
|
},
|
|
feedbackBannerEnabled() {
|
|
return this.glFeatures.showGitlabAgentFeedback;
|
|
},
|
|
feedbackBannerClasses() {
|
|
return this.isChildComponent ? 'gl-my-2' : 'gl-mb-4';
|
|
},
|
|
feedbackBannerIllustration() {
|
|
return feedbackBannerIllustration;
|
|
},
|
|
isLoading() {
|
|
return this.agentListLoading && this.sharedAgentsLoading;
|
|
},
|
|
showEmptyState() {
|
|
return (
|
|
!this.queryErrored &&
|
|
!this.agentList.length &&
|
|
!this.sharedAgentsQueryErrored &&
|
|
!this.sharedAgentsList.length
|
|
);
|
|
},
|
|
showFeedbackBanner() {
|
|
return this.feedbackBannerEnabled && (this.agentList.length || this.sharedAgentsList.length);
|
|
},
|
|
agentTabs() {
|
|
const tabs = [];
|
|
|
|
const projectAgents = {
|
|
name: this.isGroup
|
|
? s__('ClusterAgents|Group agents')
|
|
: s__('ClusterAgents|Project agents'),
|
|
count: this.agentsCount,
|
|
agents: this.agentList,
|
|
error: this.queryErrored,
|
|
loading: this.agentListLoading,
|
|
};
|
|
|
|
const sharedAgents = {
|
|
name: s__('ClusterAgents|Shared agents'),
|
|
count: this.sharedAgentsList.length,
|
|
agents: this.sharedAgentsList,
|
|
error: this.sharedAgentsQueryErrored,
|
|
loading: this.sharedAgentsLoading,
|
|
};
|
|
|
|
if (this.agentList.length > 0 || this.queryErrored) {
|
|
tabs.push(projectAgents);
|
|
}
|
|
|
|
if (this.sharedAgentsList.length > 0 || this.sharedAgentsQueryErrored) {
|
|
tabs.push(sharedAgents);
|
|
}
|
|
|
|
return tabs;
|
|
},
|
|
},
|
|
methods: {
|
|
findConfigFolder(agentName) {
|
|
return this.configFolders?.find((folder) => folder.name === agentName);
|
|
},
|
|
updateTreeList(data) {
|
|
this.configFolders = data?.project?.repository?.tree?.trees?.nodes;
|
|
|
|
if (this.configFolders) {
|
|
this.agentList = this.agentList.map((agent) => {
|
|
const configFolder = this.findConfigFolder(agent.name);
|
|
return { ...agent, configFolder };
|
|
});
|
|
|
|
this.updateConfigFolders();
|
|
}
|
|
},
|
|
updateAgentsList({ project, group }) {
|
|
const agentsData = project?.clusterAgents || group?.clusterAgents;
|
|
const agents = agentsData?.nodes || [];
|
|
this.agentList = agents
|
|
.map((agent) => {
|
|
const lastContact = getAgentLastContact(agent?.tokens?.nodes);
|
|
const status = getAgentStatus(lastContact);
|
|
const configFolder = this.findConfigFolder(agent.name);
|
|
return { ...agent, lastContact, status, configFolder };
|
|
})
|
|
.sort((a, b) => {
|
|
return b.lastContact - a.lastContact;
|
|
});
|
|
|
|
this.updateConfigFolders();
|
|
this.currentTab = 0;
|
|
},
|
|
updateConfigFolders() {
|
|
this.availableConfigs = this.configFolders.filter(
|
|
(folder) => !this.agentList.find((agent) => agent.name === folder.name),
|
|
);
|
|
},
|
|
|
|
handleBannerClose() {
|
|
this.feedbackBannerDismissed = true;
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<gl-loading-icon v-if="isLoading" size="lg" />
|
|
<agent-empty-state v-else-if="showEmptyState" />
|
|
|
|
<div v-else>
|
|
<local-storage-sync
|
|
v-if="showFeedbackBanner"
|
|
v-model="feedbackBannerDismissed"
|
|
:storage-key="$options.AGENT_FEEDBACK_KEY"
|
|
>
|
|
<gl-banner
|
|
v-if="!feedbackBannerDismissed"
|
|
:class="feedbackBannerClasses"
|
|
:title="$options.i18n.feedbackBannerTitle"
|
|
:button-text="$options.i18n.feedbackBannerButton"
|
|
:button-link="$options.AGENT_FEEDBACK_ISSUE"
|
|
:svg-path="feedbackBannerIllustration"
|
|
@close="handleBannerClose"
|
|
>
|
|
<p>{{ $options.i18n.feedbackBannerText }}</p>
|
|
</gl-banner>
|
|
</local-storage-sync>
|
|
|
|
<slot name="alerts"></slot>
|
|
|
|
<gl-tabs v-model="currentTab">
|
|
<gl-tab v-for="tab in agentTabs" :key="tab.name" :title="tab.name">
|
|
<gl-loading-icon v-if="tab.loading" size="lg" />
|
|
<gl-alert v-else-if="tab.error" variant="danger" :dismissible="false">
|
|
{{ $options.i18n.error }}
|
|
</gl-alert>
|
|
|
|
<agent-table v-else :agents="tab.agents" :max-agents="limit" />
|
|
</gl-tab>
|
|
|
|
<gl-tab v-if="availableConfigs.length" :title="$options.i18n.availableConfigs">
|
|
<agent-configs-table
|
|
:configs="availableConfigs"
|
|
:max-configs="limit"
|
|
@registerAgent="$emit('registerAgent', $event)"
|
|
/>
|
|
</gl-tab>
|
|
</gl-tabs>
|
|
</div>
|
|
</template>
|