Commit 9a15f565 authored by anna_vovchenko's avatar anna_vovchenko

Added connectivity status to Kubernetes Agents

Connectivity status is shown based on the most recently connected token.

Changelog: added
EE: true
parent 1e0dcf3a
<script>
import { GlButton, GlLink, GlModalDirective, GlTable } from '@gitlab/ui';
import {
GlButton,
GlLink,
GlModalDirective,
GlTable,
GlIcon,
GlSprintf,
GlTooltip,
GlPopover,
} from '@gitlab/ui';
import { s__ } from '~/locale';
import { INSTALL_AGENT_MODAL_ID } from '../constants';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { INSTALL_AGENT_MODAL_ID, AGENT_STATUSES, TROUBLESHOOTING_LINK } from '../constants';
export default {
modalId: INSTALL_AGENT_MODAL_ID,
statuses: AGENT_STATUSES,
troubleshootingLink: TROUBLESHOOTING_LINK,
components: {
GlButton,
GlLink,
GlTable,
GlIcon,
GlSprintf,
GlTooltip,
GlPopover,
TimeAgoTooltip,
},
directives: {
GlModalDirective,
},
mixins: [timeagoMixin],
inject: ['integrationDocsUrl'],
props: {
agents: {
......@@ -27,6 +46,14 @@ export default {
key: 'name',
label: s__('ClusterAgents|Name'),
},
{
key: 'status',
label: s__('ClusterAgents|Connection status'),
},
{
key: 'lastContact',
label: s__('ClusterAgents|Last contact'),
},
{
key: 'configuration',
label: s__('ClusterAgents|Configuration'),
......@@ -47,18 +74,67 @@ export default {
<gl-table :items="agents" :fields="fields" stacked="md" data-testid="cluster-agent-list-table">
<template #cell(name)="{ item }">
<gl-link :href="item.webPath">
<gl-link :href="item.webPath" data-testid="cluster-agent-name-link">
{{ item.name }}
</gl-link>
</template>
<template #cell(status)="{ item }">
<span
:id="`connection-status-${item.name}`"
class="gl-pr-5"
data-testid="cluster-agent-connection-status"
>
<span :class="$options.statuses[item.status].class" class="gl-mr-3">
<gl-icon :name="$options.statuses[item.status].icon" :size="12" /></span
>{{ $options.statuses[item.status].name }}
</span>
<gl-tooltip
v-if="item.status === 'active'"
:target="`connection-status-${item.name}`"
placement="right"
>
<gl-sprintf :message="$options.statuses[item.status].tooltip.title"
><template #timeAgo>{{ timeFormatted(item.lastContact) }}</template>
</gl-sprintf>
</gl-tooltip>
<gl-popover
v-else
:target="`connection-status-${item.name}`"
:title="$options.statuses[item.status].tooltip.title"
placement="right"
container="viewport"
>
<p>
<gl-sprintf :message="$options.statuses[item.status].tooltip.body"
><template #timeAgo>{{ timeFormatted(item.lastContact) }}</template></gl-sprintf
>
</p>
<p class="gl-mb-0">
{{ s__('ClusterAgents|For more troubleshooting information go to') }}
<gl-link :href="$options.troubleshootingLink" target="_blank" class="gl-font-sm">
{{ $options.troubleshootingLink }}</gl-link
>
</p>
</gl-popover>
</template>
<template #cell(lastContact)="{ item }">
<span data-testid="cluster-agent-last-contact">
<time-ago-tooltip v-if="item.lastContact" :time="item.lastContact" />
<span v-else>{{ __('Never') }}</span>
</span>
</template>
<template #cell(configuration)="{ item }">
<!-- eslint-disable @gitlab/vue-require-i18n-strings -->
<gl-link v-if="item.configFolder" :href="item.configFolder.webPath">
.gitlab/agents/{{ item.name }}
</gl-link>
<span data-testid="cluster-agent-configuration-link">
<!-- eslint-disable @gitlab/vue-require-i18n-strings -->
<gl-link v-if="item.configFolder" :href="item.configFolder.webPath">
.gitlab/agents/{{ item.name }}
</gl-link>
<p v-else>.gitlab/agents/{{ item.name }}</p>
<span v-else>.gitlab/agents/{{ item.name }}</span>
</span>
</template>
</gl-table>
</div>
......
<script>
import { GlAlert, GlKeysetPagination, GlLoadingIcon } from '@gitlab/ui';
import { MAX_LIST_COUNT } from '../constants';
import { MAX_LIST_COUNT, ACTIVE_CONNECTION_TIME } from '../constants';
import getAgentsQuery from '../graphql/queries/get_agents.query.graphql';
import AgentEmptyState from './agent_empty_state.vue';
import AgentTable from './agent_table.vue';
......@@ -55,7 +55,9 @@ export default {
if (list) {
list = list.map((agent) => {
const configFolder = this.folderList[agent.name];
return { ...agent, configFolder };
const lastContact = this.getLastContact(agent);
const status = this.getStatus(lastContact);
return { ...agent, configFolder, lastContact, status };
});
}
......@@ -106,6 +108,28 @@ export default {
});
}
},
getLastContact(agent) {
const tokens = agent?.tokens?.nodes;
let lastContact = null;
if (tokens?.length) {
tokens.forEach((token) => {
const lastContactToDate = new Date(token.lastUsedAt).getTime();
if (lastContactToDate > lastContact) {
lastContact = lastContactToDate;
}
});
}
return lastContact;
},
getStatus(lastContact) {
if (lastContact) {
const now = new Date().getTime();
const diff = now - lastContact;
return diff > ACTIVE_CONNECTION_TIME ? 'inactive' : 'active';
}
return 'unused';
},
},
};
</script>
......
import { __, s__ } from '~/locale';
import { __, s__, sprintf } from '~/locale';
export const MAX_LIST_COUNT = 25;
export const INSTALL_AGENT_MODAL_ID = 'install-agent';
export const ACTIVE_CONNECTION_TIME = 480000;
export const TROUBLESHOOTING_LINK =
'https://docs.gitlab.com/ee/user/clusters/agent/#troubleshooting';
export const I18N_INSTALL_AGENT_MODAL = {
next: __('Next'),
......@@ -46,3 +49,36 @@ export const I18N_AVAILABLE_AGENTS_DROPDOWN = {
selectAgent: s__('ClusterAgents|Select an Agent'),
registeringAgent: s__('ClusterAgents|Registering Agent'),
};
export const AGENT_STATUSES = {
active: {
name: s__('ClusterAgents|Connected'),
icon: 'status-success',
class: 'text-success-500',
tooltip: {
title: sprintf(s__('ClusterAgents|Last connected %{timeAgo}.')),
},
},
inactive: {
name: s__('ClusterAgents|Not connected'),
icon: 'severity-critical',
class: 'text-danger-800',
tooltip: {
title: s__('ClusterAgents|Agent might not be connected to GitLab'),
body: sprintf(
s__(
'ClusterAgents|The Agent has not been connected in a long time. There might be a connectivity issue. Last contact was %{timeAgo}.',
),
),
},
},
unused: {
name: s__('ClusterAgents|Never connected'),
icon: 'status-neutral',
class: 'text-secondary-400',
tooltip: {
title: s__('ClusterAgents|Agent never connected to GitLab'),
body: s__('ClusterAgents|Make sure you are using a valid token.'),
},
},
};
......@@ -16,6 +16,11 @@ query getAgents(
id
name
webPath
tokens {
nodes {
lastUsedAt
}
}
}
pageInfo {
......
import { GlButton, GlLink } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { GlButton, GlLink, GlIcon } from '@gitlab/ui';
import AgentTable from 'ee/clusters_list/components/agent_table.vue';
import { ACTIVE_CONNECTION_TIME } from 'ee/clusters_list/constants';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import timeagoMixin from '~/vue_shared/mixins/timeago';
const connectedTimeNow = new Date();
const connectedTimeInactive = new Date(connectedTimeNow.getTime() - ACTIVE_CONNECTION_TIME);
const propsData = {
agents: [
......@@ -10,10 +15,35 @@ const propsData = {
webPath: '/agent/full/path',
},
webPath: '/agent-1',
status: 'unused',
lastContact: null,
tokens: null,
},
{
name: 'agent-2',
webPath: '/agent-2',
status: 'active',
lastContact: connectedTimeNow.getTime(),
tokens: {
nodes: [
{
lastUsedAt: connectedTimeNow,
},
],
},
},
{
name: 'agent-3',
webPath: '/agent-3',
status: 'inactive',
lastContact: connectedTimeInactive.getTime(),
tokens: {
nodes: [
{
lastUsedAt: connectedTimeInactive,
},
],
},
},
],
};
......@@ -22,8 +52,15 @@ const provideData = { integrationDocsUrl: 'path/to/integrationDocs' };
describe('AgentTable', () => {
let wrapper;
const findAgentLink = (at) => wrapper.findAllByTestId('cluster-agent-name-link').at(at);
const findStatusIcon = (at) => wrapper.findAllComponents(GlIcon).at(at);
const findStatusText = (at) => wrapper.findAllByTestId('cluster-agent-connection-status').at(at);
const findLastContactText = (at) => wrapper.findAllByTestId('cluster-agent-last-contact').at(at);
const findConfiguration = (at) =>
wrapper.findAllByTestId('cluster-agent-configuration-link').at(at);
beforeEach(() => {
wrapper = mount(AgentTable, { propsData, provide: provideData });
wrapper = mountExtended(AgentTable, { propsData, provide: provideData });
});
afterEach(() => {
......@@ -43,13 +80,27 @@ describe('AgentTable', () => {
${'agent-1'} | ${'/agent-1'} | ${0}
${'agent-2'} | ${'/agent-2'} | ${1}
`('displays agent link', ({ agentName, link, lineNumber }) => {
const agents = wrapper.findAll(
'[data-testid="cluster-agent-list-table"] tbody tr > td:first-child',
);
const agent = agents.at(lineNumber).find(GlLink);
expect(findAgentLink(lineNumber).text()).toBe(agentName);
expect(findAgentLink(lineNumber).attributes('href')).toBe(link);
});
expect(agent.text()).toBe(agentName);
expect(agent.attributes('href')).toBe(link);
it.each`
status | iconName | lineNumber
${'Never connected'} | ${'status-neutral'} | ${0}
${'Connected'} | ${'status-success'} | ${1}
${'Not connected'} | ${'severity-critical'} | ${2}
`('displays agent connection status', ({ status, iconName, lineNumber }) => {
expect(findStatusText(lineNumber).text()).toBe(status);
expect(findStatusIcon(lineNumber).props('name')).toBe(iconName);
});
it.each`
lastContact | lineNumber
${'Never'} | ${0}
${timeagoMixin.methods.timeFormatted(connectedTimeNow)} | ${1}
${timeagoMixin.methods.timeFormatted(connectedTimeInactive)} | ${2}
`('displays agent last contact time', ({ lastContact, lineNumber }) => {
expect(findLastContactText(lineNumber).text()).toBe(lastContact);
});
it.each`
......@@ -57,13 +108,10 @@ describe('AgentTable', () => {
${'.gitlab/agents/agent-1'} | ${true} | ${0}
${'.gitlab/agents/agent-2'} | ${false} | ${1}
`('displays config file path', ({ agentPath, hasLink, lineNumber }) => {
const agents = wrapper.findAll(
'[data-testid="cluster-agent-list-table"] tbody tr > td:nth-child(2)',
);
const agent = agents.at(lineNumber);
const findLink = findConfiguration(lineNumber).find(GlLink);
expect(agent.find(GlLink).exists()).toBe(hasLink);
expect(agent.text()).toBe(agentPath);
expect(findLink.exists()).toBe(hasLink);
expect(findConfiguration(lineNumber).text()).toBe(agentPath);
});
});
});
......@@ -4,6 +4,7 @@ import VueApollo from 'vue-apollo';
import AgentEmptyState from 'ee/clusters_list/components/agent_empty_state.vue';
import AgentTable from 'ee/clusters_list/components/agent_table.vue';
import Agents from 'ee/clusters_list/components/agents.vue';
import { ACTIVE_CONNECTION_TIME } from 'ee/clusters_list/constants';
import getAgentsQuery from 'ee/clusters_list/graphql/queries/get_agents.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
......@@ -26,7 +27,7 @@ describe('Agents', () => {
const apolloQueryResponse = {
data: {
project: {
clusterAgents: { nodes: agents, pageInfo },
clusterAgents: { nodes: agents, pageInfo, tokens: { nodes: [] } },
repository: { tree: { trees: { nodes: trees, pageInfo } } },
},
},
......@@ -58,16 +59,25 @@ describe('Agents', () => {
});
describe('when there is a list of agents', () => {
let testDate = new Date();
const agents = [
{
id: '1',
name: 'agent-1',
webPath: '/agent-1',
tokens: null,
},
{
id: '2',
name: 'agent-2',
webPath: '/agent-2',
tokens: {
nodes: [
{
lastUsedAt: testDate,
},
],
},
},
];
......@@ -79,6 +89,37 @@ describe('Agents', () => {
},
];
const expectedAgentsList = [
{
id: '1',
name: 'agent-1',
webPath: '/agent-1',
configFolder: undefined,
status: 'unused',
lastContact: null,
tokens: null,
},
{
id: '2',
name: 'agent-2',
configFolder: {
name: 'agent-2',
path: '.gitlab/agents/agent-2',
webPath: '/project/path/.gitlab/agents/agent-2',
},
webPath: '/agent-2',
status: 'active',
lastContact: new Date(testDate).getTime(),
tokens: {
nodes: [
{
lastUsedAt: testDate,
},
],
},
},
];
beforeEach(() => {
return createWrapper({ agents, trees });
});
......@@ -89,19 +130,28 @@ describe('Agents', () => {
});
it('should pass agent and folder info to table component', () => {
expect(findAgentTable().props('agents')).toEqual([
{ id: '1', name: 'agent-1', webPath: '/agent-1', configFolder: undefined },
{
id: '2',
name: 'agent-2',
configFolder: {
name: 'agent-2',
path: '.gitlab/agents/agent-2',
webPath: '/project/path/.gitlab/agents/agent-2',
},
webPath: '/agent-2',
},
]);
expect(findAgentTable().props('agents')).toMatchObject(expectedAgentsList);
});
describe('when the agent has recently connected tokens', () => {
it('should set agent status to active', () => {
expect(findAgentTable().props('agents')).toMatchObject(expectedAgentsList);
});
});
describe('when the agent has tokens connected more then 8 minutes ago', () => {
const now = new Date();
testDate = new Date(now.getTime() - ACTIVE_CONNECTION_TIME);
it('should set agent status to inactive', () => {
expect(findAgentTable().props('agents')).toMatchObject(expectedAgentsList);
});
});
describe('when the agent has no connected tokens', () => {
testDate = null;
it('should set agent status to unused', () => {
expect(findAgentTable().props('agents')).toMatchObject(expectedAgentsList);
});
});
it('should not render pagination buttons when there are no additional pages', () => {
......
......@@ -7139,6 +7139,12 @@ msgstr ""
msgid "ClusterAgents|Access tokens"
msgstr ""
msgid "ClusterAgents|Agent might not be connected to GitLab"
msgstr ""
msgid "ClusterAgents|Agent never connected to GitLab"
msgstr ""
msgid "ClusterAgents|Alternative installation methods"
msgstr ""
......@@ -7154,6 +7160,12 @@ msgstr ""
msgid "ClusterAgents|Configuration"
msgstr ""
msgid "ClusterAgents|Connected"
msgstr ""
msgid "ClusterAgents|Connection status"
msgstr ""
msgid "ClusterAgents|Copy token"
msgstr ""
......@@ -7172,6 +7184,9 @@ msgstr ""
msgid "ClusterAgents|For alternative installation methods %{linkStart}go to the documentation%{linkEnd}."
msgstr ""
msgid "ClusterAgents|For more troubleshooting information go to"
msgstr ""
msgid "ClusterAgents|Go to the repository"
msgstr ""
......@@ -7187,18 +7202,33 @@ msgstr ""
msgid "ClusterAgents|Integrate with the GitLab Agent"
msgstr ""
msgid "ClusterAgents|Last connected %{timeAgo}."
msgstr ""
msgid "ClusterAgents|Last contact"
msgstr ""
msgid "ClusterAgents|Last used"
msgstr ""
msgid "ClusterAgents|Learn how to create an agent access token"
msgstr ""
msgid "ClusterAgents|Make sure you are using a valid token."
msgstr ""
msgid "ClusterAgents|Name"
msgstr ""
msgid "ClusterAgents|Never"
msgstr ""
msgid "ClusterAgents|Never connected"
msgstr ""
msgid "ClusterAgents|Not connected"
msgstr ""
msgid "ClusterAgents|Read more about getting started"
msgstr ""
......@@ -7220,6 +7250,9 @@ msgstr ""
msgid "ClusterAgents|Select which Agent you want to install"
msgstr ""
msgid "ClusterAgents|The Agent has not been connected in a long time. There might be a connectivity issue. Last contact was %{timeAgo}."
msgstr ""
msgid "ClusterAgents|The GitLab Agent also requires %{linkStart}enabling the Agent Server%{linkEnd}"
msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment