diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-12-15 09:13:34 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-12-15 09:13:34 +0000 |
commit | 5cad106998d7826cead879f1122701040cb7bb38 (patch) | |
tree | 78ae3b440ca2c8949abf839accbfa5ce6e083544 /app/assets/javascripts/clusters/agents | |
parent | 908db9a2900d58d368592ad89b7ceefbdeba8c7f (diff) | |
download | gitlab-ce-5cad106998d7826cead879f1122701040cb7bb38.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/clusters/agents')
7 files changed, 325 insertions, 6 deletions
diff --git a/app/assets/javascripts/clusters/agents/components/activity_events_list.vue b/app/assets/javascripts/clusters/agents/components/activity_events_list.vue new file mode 100644 index 00000000000..146b647cb46 --- /dev/null +++ b/app/assets/javascripts/clusters/agents/components/activity_events_list.vue @@ -0,0 +1,162 @@ +<script> +import { GlLoadingIcon, GlEmptyState, GlLink, GlIcon, GlAlert } from '@gitlab/ui'; +import { helpPagePath } from '~/helpers/help_page_helper'; +import { n__, s__, __ } from '~/locale'; +import { formatDate, getDayDifference, isToday } from '~/lib/utils/datetime_utility'; +import { EVENTS_STORED_DAYS } from '../constants'; +import getAgentActivityEventsQuery from '../graphql/queries/get_agent_activity_events.query.graphql'; +import ActivityHistoryItem from './activity_history_item.vue'; + +export default { + components: { + GlLoadingIcon, + GlEmptyState, + GlAlert, + GlLink, + GlIcon, + ActivityHistoryItem, + }, + i18n: { + emptyText: s__( + 'ClusterAgents|See Agent activity updates such as tokens created or revoked and clusters connected or not connected.', + ), + error: s__( + 'ClusterAgents|An error occurred while retrieving GitLab Agent activity. Reload the page to try again.', + ), + today: __('Today'), + yesterday: __('Yesterday'), + }, + emptyHelpLink: helpPagePath('user/clusters/agent/install/index', { + anchor: 'view-agent-activity', + }), + borderClasses: 'gl-border-b-1 gl-border-b-solid gl-border-b-gray-100', + apollo: { + agentEvents: { + query: getAgentActivityEventsQuery, + variables() { + return { + agentName: this.agentName, + projectPath: this.projectPath, + }; + }, + update: (data) => data?.project?.clusterAgent?.activityEvents?.nodes, + error() { + this.isError = true; + }, + }, + }, + inject: ['agentName', 'projectPath', 'activityEmptyStateImage'], + data() { + return { + isError: false, + }; + }, + computed: { + isLoading() { + return this.$apollo.queries.agentEvents?.loading; + }, + emptyStateTitle() { + return n__( + "ClusterAgents|There's no activity from the past day", + "ClusterAgents|There's no activity from the past %d days", + EVENTS_STORED_DAYS, + ); + }, + eventsList() { + const list = this.agentEvents; + const listByDates = {}; + + if (!list?.length) { + return listByDates; + } + + list.forEach((event) => { + const dateName = this.getFormattedDate(event.recordedAt); + if (!listByDates[dateName]) { + listByDates[dateName] = []; + } + listByDates[dateName].push(event); + }); + + return listByDates; + }, + hasEvents() { + return Object.keys(this.eventsList).length; + }, + }, + methods: { + isYesterday(date) { + const today = new Date(); + return getDayDifference(today, date) === -1; + }, + getFormattedDate(dateString) { + const date = new Date(dateString); + let dateName; + if (isToday(date)) { + dateName = this.$options.i18n.today; + } else if (this.isYesterday(date)) { + dateName = this.$options.i18n.yesterday; + } else { + dateName = formatDate(date, 'yyyy-mm-dd'); + } + return dateName; + }, + isLast(dateEvents, idx) { + return idx === dateEvents.length - 1; + }, + getBodyClasses(dateEvents, idx) { + return !this.isLast(dateEvents, idx) ? this.$options.borderClasses : ''; + }, + }, +}; +</script> + +<template> + <div> + <gl-loading-icon v-if="isLoading" size="md" /> + + <div v-else-if="hasEvents"> + <div + v-for="(dateEvents, key) in eventsList" + :key="key" + class="agent-activity-list issuable-discussion" + > + <h4 + class="gl-pb-4 gl-ml-5" + :class="$options.borderClasses" + data-testid="activity-section-title" + > + {{ key }} + </h4> + + <ul class="notes main-notes-list timeline"> + <activity-history-item + v-for="(event, idx) in dateEvents" + :key="idx" + :event="event" + :body-class="getBodyClasses(dateEvents, idx)" + /> + </ul> + </div> + </div> + + <gl-alert v-else-if="isError" variant="danger" :dismissible="false" class="gl-mt-3"> + {{ $options.i18n.error }} + </gl-alert> + + <gl-empty-state + v-else + :title="emptyStateTitle" + :svg-path="activityEmptyStateImage" + :svg-height="150" + > + <template #description> + <div> + <span>{{ $options.i18n.emptyText }}</span> + + <gl-link :href="$options.emptyHelpLink"><gl-icon name="question" :size="14" /></gl-link> + </div> + </template> + </gl-empty-state> + </div> +</template> diff --git a/app/assets/javascripts/clusters/agents/components/activity_history_item.vue b/app/assets/javascripts/clusters/agents/components/activity_history_item.vue new file mode 100644 index 00000000000..39389c3b04b --- /dev/null +++ b/app/assets/javascripts/clusters/agents/components/activity_history_item.vue @@ -0,0 +1,79 @@ +<script> +import { GlLink, GlIcon, GlSprintf } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import HistoryItem from '~/vue_shared/components/registry/history_item.vue'; +import { EVENT_DETAILS, DEFAULT_ICON } from '../constants'; + +export default { + i18n: { + defaultBodyText: s__('ClusterAgents|Event occurred'), + }, + components: { + GlLink, + GlIcon, + GlSprintf, + TimeAgoTooltip, + HistoryItem, + }, + props: { + event: { + required: true, + type: Object, + }, + bodyClass: { + required: false, + default: '', + type: String, + }, + }, + computed: { + eventDetails() { + const defaultEvent = { + eventTypeIcon: DEFAULT_ICON, + title: this.event.kind, + body: this.$options.i18n.defaultBodyText, + }; + + const eventDetails = EVENT_DETAILS[this.event.kind] || defaultEvent; + const { eventTypeIcon, title, body, titleIcon } = eventDetails; + const resultEvent = { ...this.event, eventTypeIcon, title, body, titleIcon }; + + return resultEvent; + }, + }, +}; +</script> +<template> + <history-item :icon="eventDetails.eventTypeIcon" class="gl-my-0! gl-pr-0!"> + <strong> + <gl-sprintf :message="eventDetails.title" + ><template v-if="eventDetails.titleIcon" #titleIcon + ><gl-icon + class="gl-mr-2" + :name="eventDetails.titleIcon.name" + :size="12" + :class="eventDetails.titleIcon.class" + /> + </template> + <template #tokenName>{{ eventDetails.agentToken.name }}</template></gl-sprintf + > + </strong> + + <template #body> + <p class="gl-mt-2 gl-mb-0 gl-pb-2" :class="bodyClass"> + <gl-sprintf :message="eventDetails.body"> + <template #userName> + <strong>{{ eventDetails.user.name }}</strong> + <gl-link :href="eventDetails.user.webUrl">@{{ eventDetails.user.username }}</gl-link> + </template> + + <template #strong="{ content }"> + <strong> {{ content }} </strong> + </template> + </gl-sprintf> + <time-ago-tooltip :time="eventDetails.recordedAt" /> + </p> + </template> + </history-item> +</template> diff --git a/app/assets/javascripts/clusters/agents/components/show.vue b/app/assets/javascripts/clusters/agents/components/show.vue index afbba9d1f7c..9109c010500 100644 --- a/app/assets/javascripts/clusters/agents/components/show.vue +++ b/app/assets/javascripts/clusters/agents/components/show.vue @@ -8,11 +8,12 @@ import { GlTab, GlTabs, } from '@gitlab/ui'; -import { s__ } from '~/locale'; +import { s__, __ } from '~/locale'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import { MAX_LIST_COUNT } from '../constants'; import getClusterAgentQuery from '../graphql/queries/get_cluster_agent.query.graphql'; import TokenTable from './token_table.vue'; +import ActivityEvents from './activity_events_list.vue'; export default { i18n: { @@ -20,6 +21,7 @@ export default { loadingError: s__('ClusterAgents|An error occurred while loading your agent'), tokens: s__('ClusterAgents|Access tokens'), unknownUser: s__('ClusterAgents|Unknown user'), + activity: __('Activity'), }, apollo: { clusterAgent: { @@ -47,6 +49,7 @@ export default { GlTabs, TimeAgoTooltip, TokenTable, + ActivityEvents, }, props: { agentName: { @@ -127,9 +130,14 @@ export default { </gl-sprintf> </p> - <gl-tabs> + <gl-tabs sync-active-tab-with-query-params lazy> + <gl-tab :title="$options.i18n.activity" query-param-value="activity"> + <activity-events :agent-name="agentName" :project-path="projectPath" /> + </gl-tab> + <slot name="ee-security-tab"></slot> - <gl-tab> + + <gl-tab query-param-value="tokens"> <template #title> <span data-testid="cluster-agent-token-count"> {{ $options.i18n.tokens }} @@ -143,7 +151,7 @@ export default { <gl-loading-icon v-if="isLoading" size="md" class="gl-m-3" /> <div v-else> - <TokenTable :tokens="tokens" /> + <token-table :tokens="tokens" /> <div v-if="showPagination" class="gl-display-flex gl-justify-content-center gl-mt-5"> <gl-keyset-pagination v-bind="tokenPageInfo" @prev="prevPage" @next="nextPage" /> diff --git a/app/assets/javascripts/clusters/agents/components/token_table.vue b/app/assets/javascripts/clusters/agents/components/token_table.vue index 70ed2566134..232a0ea1513 100644 --- a/app/assets/javascripts/clusters/agents/components/token_table.vue +++ b/app/assets/javascripts/clusters/agents/components/token_table.vue @@ -83,7 +83,14 @@ export default { </gl-link> </div> - <gl-table :items="tokens" :fields="fields" fixed stacked="md"> + <gl-table + :items="tokens" + :fields="fields" + fixed + stacked="md" + head-variant="white" + thead-class="gl-border-b-solid gl-border-b-2 gl-border-b-gray-100" + > <template #cell(lastUsed)="{ item }"> <time-ago-tooltip v-if="item.lastUsedAt" :time="item.lastUsedAt" /> <span v-else>{{ $options.i18n.neverUsed }}</span> diff --git a/app/assets/javascripts/clusters/agents/constants.js b/app/assets/javascripts/clusters/agents/constants.js index bbc4630f83b..315c7662755 100644 --- a/app/assets/javascripts/clusters/agents/constants.js +++ b/app/assets/javascripts/clusters/agents/constants.js @@ -1 +1,38 @@ +import { s__ } from '~/locale'; + export const MAX_LIST_COUNT = 25; + +export const EVENTS_STORED_DAYS = 7; + +export const EVENT_DETAILS = { + token_created: { + eventTypeIcon: 'token', + title: s__('ClusterAgents|%{tokenName} created'), + body: s__('ClusterAgents|Token created by %{userName}'), + }, + token_revoked: { + eventTypeIcon: 'token', + title: s__('ClusterAgents|%{tokenName} revoked'), + body: s__('ClusterAgents|Token revoked by %{userName}'), + }, + agent_connected: { + eventTypeIcon: 'connected', + title: s__('ClusterAgents|%{titleIcon}Connected'), + body: s__('ClusterAgents|Agent %{strongStart}connected%{strongEnd}'), + titleIcon: { + name: 'status-success', + class: 'text-success-500', + }, + }, + agent_disconnected: { + eventTypeIcon: 'connected', + title: s__('ClusterAgents|%{titleIcon}Not connected'), + body: s__('ClusterAgents|Agent %{strongStart}disconnected%{strongEnd}'), + titleIcon: { + name: 'severity-critical', + class: 'text-danger-800', + }, + }, +}; + +export const DEFAULT_ICON = 'token'; diff --git a/app/assets/javascripts/clusters/agents/graphql/queries/get_agent_activity_events.query.graphql b/app/assets/javascripts/clusters/agents/graphql/queries/get_agent_activity_events.query.graphql new file mode 100644 index 00000000000..0d7ff029387 --- /dev/null +++ b/app/assets/javascripts/clusters/agents/graphql/queries/get_agent_activity_events.query.graphql @@ -0,0 +1,25 @@ +query getAgentActivityEvents($projectPath: ID!, $agentName: String!) { + project(fullPath: $projectPath) { + id + clusterAgent(name: $agentName) { + id + activityEvents { + nodes { + kind + level + recordedAt + agentToken { + id + name + } + user { + id + name + username + webUrl + } + } + } + } + } +} diff --git a/app/assets/javascripts/clusters/agents/index.js b/app/assets/javascripts/clusters/agents/index.js index 426d8d83847..5796c9e308d 100644 --- a/app/assets/javascripts/clusters/agents/index.js +++ b/app/assets/javascripts/clusters/agents/index.js @@ -13,11 +13,12 @@ export default () => { } const defaultClient = createDefaultClient(); - const { agentName, projectPath } = el.dataset; + const { agentName, projectPath, activityEmptyStateImage } = el.dataset; return new Vue({ el, apolloProvider: new VueApollo({ defaultClient }), + provide: { agentName, projectPath, activityEmptyStateImage }, render(createElement) { return createElement(AgentShowPage, { props: { |