diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-10-20 08:43:02 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-10-20 08:43:02 +0000 |
commit | d9ab72d6080f594d0b3cae15f14b3ef2c6c638cb (patch) | |
tree | 2341ef426af70ad1e289c38036737e04b0aa5007 /app/assets/javascripts/clusters_list/components | |
parent | d6e514dd13db8947884cd58fe2a9c2a063400a9b (diff) | |
download | gitlab-ce-d9ab72d6080f594d0b3cae15f14b3ef2c6c638cb.tar.gz |
Add latest changes from gitlab-org/gitlab@14-4-stable-eev14.4.0-rc42
Diffstat (limited to 'app/assets/javascripts/clusters_list/components')
5 files changed, 769 insertions, 0 deletions
diff --git a/app/assets/javascripts/clusters_list/components/agent_empty_state.vue b/app/assets/javascripts/clusters_list/components/agent_empty_state.vue new file mode 100644 index 00000000000..405339b3d36 --- /dev/null +++ b/app/assets/javascripts/clusters_list/components/agent_empty_state.vue @@ -0,0 +1,119 @@ +<script> +import { GlButton, GlEmptyState, GlLink, GlSprintf, GlAlert, GlModalDirective } from '@gitlab/ui'; +import { INSTALL_AGENT_MODAL_ID } from '../constants'; + +export default { + modalId: INSTALL_AGENT_MODAL_ID, + components: { + GlButton, + GlEmptyState, + GlLink, + GlSprintf, + GlAlert, + }, + directives: { + GlModalDirective, + }, + inject: [ + 'emptyStateImage', + 'projectPath', + 'agentDocsUrl', + 'installDocsUrl', + 'getStartedDocsUrl', + 'integrationDocsUrl', + ], + props: { + hasConfigurations: { + type: Boolean, + required: true, + }, + }, + computed: { + repositoryPath() { + return `/${this.projectPath}`; + }, + }, +}; +</script> + +<template> + <gl-empty-state + :svg-path="emptyStateImage" + :title="s__('ClusterAgents|Integrate Kubernetes with a GitLab Agent')" + class="empty-state--agent" + > + <template #description> + <p class="mw-460 gl-mx-auto"> + <gl-sprintf + :message=" + s__( + 'ClusterAgents|The GitLab Kubernetes Agent allows an Infrastructure as Code, GitOps approach to integrating Kubernetes clusters with GitLab. %{linkStart}Learn more.%{linkEnd}', + ) + " + > + <template #link="{ content }"> + <gl-link :href="agentDocsUrl" target="_blank" data-testid="agent-docs-link"> + {{ content }} + </gl-link> + </template> + </gl-sprintf> + </p> + + <p class="mw-460 gl-mx-auto"> + <gl-sprintf + :message=" + s__( + 'ClusterAgents|The GitLab Agent also requires %{linkStart}enabling the Agent Server%{linkEnd}', + ) + " + > + <template #link="{ content }"> + <gl-link :href="installDocsUrl" target="_blank" data-testid="install-docs-link"> + {{ content }} + </gl-link> + </template> + </gl-sprintf> + </p> + + <gl-alert + v-if="!hasConfigurations" + variant="warning" + class="gl-mb-5 text-left" + :dismissible="false" + > + {{ + s__( + 'ClusterAgents|To install an Agent you should create an agent directory in the Repository first. We recommend that you add the Agent configuration to the directory before you start the installation process.', + ) + }} + + <template #actions> + <gl-button + category="primary" + variant="info" + :href="getStartedDocsUrl" + target="_blank" + class="gl-ml-0!" + > + {{ s__('ClusterAgents|Read more about getting started') }} + </gl-button> + <gl-button category="secondary" variant="info" :href="repositoryPath"> + {{ s__('ClusterAgents|Go to the repository') }} + </gl-button> + </template> + </gl-alert> + </template> + + <template #actions> + <gl-button + v-gl-modal-directive="$options.modalId" + :disabled="!hasConfigurations" + data-testid="integration-primary-button" + category="primary" + variant="success" + > + {{ s__('ClusterAgents|Integrate with the GitLab Agent') }} + </gl-button> + </template> + </gl-empty-state> +</template> diff --git a/app/assets/javascripts/clusters_list/components/agent_table.vue b/app/assets/javascripts/clusters_list/components/agent_table.vue new file mode 100644 index 00000000000..487e512c06d --- /dev/null +++ b/app/assets/javascripts/clusters_list/components/agent_table.vue @@ -0,0 +1,152 @@ +<script> +import { + GlButton, + GlLink, + GlModalDirective, + GlTable, + GlIcon, + GlSprintf, + GlTooltip, + GlPopover, +} from '@gitlab/ui'; +import { s__ } from '~/locale'; +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 { + components: { + GlButton, + GlLink, + GlTable, + GlIcon, + GlSprintf, + GlTooltip, + GlPopover, + TimeAgoTooltip, + }, + directives: { + GlModalDirective, + }, + mixins: [timeagoMixin], + inject: ['integrationDocsUrl'], + INSTALL_AGENT_MODAL_ID, + AGENT_STATUSES, + TROUBLESHOOTING_LINK, + props: { + agents: { + required: true, + type: Array, + }, + }, + computed: { + fields() { + return [ + { + 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'), + }, + ]; + }, + }, +}; +</script> + +<template> + <div> + <div class="gl-display-block gl-text-right gl-my-3"> + <gl-button + v-gl-modal-directive="$options.INSTALL_AGENT_MODAL_ID" + variant="confirm" + category="primary" + >{{ s__('ClusterAgents|Install a new GitLab Agent') }} + </gl-button> + </div> + + <gl-table + :items="agents" + :fields="fields" + stacked="md" + head-variant="white" + thead-class="gl-border-b-solid gl-border-b-1 gl-border-b-gray-100" + data-testid="cluster-agent-list-table" + > + <template #cell(name)="{ item }"> + <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.AGENT_STATUSES[item.status].class" class="gl-mr-3"> + <gl-icon :name="$options.AGENT_STATUSES[item.status].icon" :size="12" /></span + >{{ $options.AGENT_STATUSES[item.status].name }} + </span> + <gl-tooltip + v-if="item.status === 'active'" + :target="`connection-status-${item.name}`" + placement="right" + > + <gl-sprintf :message="$options.AGENT_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.AGENT_STATUSES[item.status].tooltip.title" + placement="right" + container="viewport" + > + <p> + <gl-sprintf :message="$options.AGENT_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.TROUBLESHOOTING_LINK" target="_blank" class="gl-font-sm"> + {{ $options.TROUBLESHOOTING_LINK }}</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>{{ s__('ClusterAgents|Never') }}</span> + </span> + </template> + + <template #cell(configuration)="{ item }"> + <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> + + <span v-else>.gitlab/agents/{{ item.name }}</span> + <!-- eslint-enable @gitlab/vue-require-i18n-strings --> + </span> + </template> + </gl-table> + </div> +</template> diff --git a/app/assets/javascripts/clusters_list/components/agents.vue b/app/assets/javascripts/clusters_list/components/agents.vue new file mode 100644 index 00000000000..ed44c1f5fa7 --- /dev/null +++ b/app/assets/javascripts/clusters_list/components/agents.vue @@ -0,0 +1,156 @@ +<script> +import { GlAlert, GlKeysetPagination, GlLoadingIcon } from '@gitlab/ui'; +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'; +import InstallAgentModal from './install_agent_modal.vue'; + +export default { + apollo: { + agents: { + query: getAgentsQuery, + variables() { + return { + defaultBranchName: this.defaultBranchName, + projectPath: this.projectPath, + ...this.cursor, + }; + }, + update(data) { + this.updateTreeList(data); + return data; + }, + }, + }, + components: { + AgentEmptyState, + AgentTable, + InstallAgentModal, + GlAlert, + GlKeysetPagination, + GlLoadingIcon, + }, + inject: ['projectPath'], + props: { + defaultBranchName: { + default: '.noBranch', + required: false, + type: String, + }, + }, + data() { + return { + cursor: { + first: MAX_LIST_COUNT, + last: null, + }, + folderList: {}, + }; + }, + computed: { + agentList() { + let list = this.agents?.project?.clusterAgents?.nodes; + + if (list) { + list = list.map((agent) => { + const configFolder = this.folderList[agent.name]; + const lastContact = this.getLastContact(agent); + const status = this.getStatus(lastContact); + return { ...agent, configFolder, lastContact, status }; + }); + } + + return list; + }, + agentPageInfo() { + return this.agents?.project?.clusterAgents?.pageInfo || {}; + }, + isLoading() { + return this.$apollo.queries.agents.loading; + }, + showPagination() { + return this.agentPageInfo.hasPreviousPage || this.agentPageInfo.hasNextPage; + }, + treePageInfo() { + return this.agents?.project?.repository?.tree?.trees?.pageInfo || {}; + }, + hasConfigurations() { + return Boolean(this.agents?.project?.repository?.tree?.trees?.nodes?.length); + }, + }, + methods: { + reloadAgents() { + this.$apollo.queries.agents.refetch(); + }, + nextPage() { + this.cursor = { + first: MAX_LIST_COUNT, + last: null, + afterAgent: this.agentPageInfo.endCursor, + afterTree: this.treePageInfo.endCursor, + }; + }, + prevPage() { + this.cursor = { + first: null, + last: MAX_LIST_COUNT, + beforeAgent: this.agentPageInfo.startCursor, + beforeTree: this.treePageInfo.endCursor, + }; + }, + updateTreeList(data) { + const configFolders = data?.project?.repository?.tree?.trees?.nodes; + + if (configFolders) { + configFolders.forEach((folder) => { + this.folderList[folder.name] = folder; + }); + } + }, + 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> + +<template> + <gl-loading-icon v-if="isLoading" size="md" class="gl-mt-3" /> + + <section v-else-if="agentList" class="gl-mt-3"> + <div v-if="agentList.length"> + <AgentTable :agents="agentList" /> + + <div v-if="showPagination" class="gl-display-flex gl-justify-content-center gl-mt-5"> + <gl-keyset-pagination v-bind="agentPageInfo" @prev="prevPage" @next="nextPage" /> + </div> + </div> + + <AgentEmptyState v-else :has-configurations="hasConfigurations" /> + <InstallAgentModal @agentRegistered="reloadAgents" /> + </section> + + <gl-alert v-else variant="danger" :dismissible="false"> + {{ s__('ClusterAgents|An error occurred while loading your GitLab Agents') }} + </gl-alert> +</template> diff --git a/app/assets/javascripts/clusters_list/components/available_agents_dropdown.vue b/app/assets/javascripts/clusters_list/components/available_agents_dropdown.vue new file mode 100644 index 00000000000..9fb020d2f4f --- /dev/null +++ b/app/assets/javascripts/clusters_list/components/available_agents_dropdown.vue @@ -0,0 +1,83 @@ +<script> +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { I18N_AVAILABLE_AGENTS_DROPDOWN } from '../constants'; +import agentConfigurations from '../graphql/queries/agent_configurations.query.graphql'; + +export default { + name: 'AvailableAgentsDropdown', + i18n: I18N_AVAILABLE_AGENTS_DROPDOWN, + components: { + GlDropdown, + GlDropdownItem, + }, + inject: ['projectPath'], + props: { + isRegistering: { + required: true, + type: Boolean, + }, + }, + apollo: { + agents: { + query: agentConfigurations, + variables() { + return { + projectPath: this.projectPath, + }; + }, + update(data) { + this.populateAvailableAgents(data); + }, + }, + }, + data() { + return { + availableAgents: [], + selectedAgent: null, + }; + }, + computed: { + isLoading() { + return this.$apollo.queries.agents.loading; + }, + dropdownText() { + if (this.isRegistering) { + return this.$options.i18n.registeringAgent; + } else if (this.selectedAgent === null) { + return this.$options.i18n.selectAgent; + } + + return this.selectedAgent; + }, + }, + methods: { + selectAgent(agent) { + this.$emit('agentSelected', agent); + this.selectedAgent = agent; + }, + isSelected(agent) { + return this.selectedAgent === agent; + }, + populateAvailableAgents(data) { + const installedAgents = data?.project?.clusterAgents?.nodes.map((agent) => agent.name) ?? []; + const configuredAgents = + data?.project?.agentConfigurations?.nodes.map((config) => config.agentName) ?? []; + + this.availableAgents = configuredAgents.filter((agent) => !installedAgents.includes(agent)); + }, + }, +}; +</script> +<template> + <gl-dropdown :text="dropdownText" :loading="isLoading || isRegistering"> + <gl-dropdown-item + v-for="agent in availableAgents" + :key="agent" + :is-checked="isSelected(agent)" + is-check-item + @click="selectAgent(agent)" + > + {{ agent }} + </gl-dropdown-item> + </gl-dropdown> +</template> diff --git a/app/assets/javascripts/clusters_list/components/install_agent_modal.vue b/app/assets/javascripts/clusters_list/components/install_agent_modal.vue new file mode 100644 index 00000000000..5f192fe4d5a --- /dev/null +++ b/app/assets/javascripts/clusters_list/components/install_agent_modal.vue @@ -0,0 +1,259 @@ +<script> +import { + GlAlert, + GlButton, + GlFormGroup, + GlFormInputGroup, + GlLink, + GlModal, + GlSprintf, +} from '@gitlab/ui'; +import { helpPagePath } from '~/helpers/help_page_helper'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import CodeBlock from '~/vue_shared/components/code_block.vue'; +import { generateAgentRegistrationCommand } from '../clusters_util'; +import { INSTALL_AGENT_MODAL_ID, I18N_INSTALL_AGENT_MODAL } from '../constants'; +import createAgent from '../graphql/mutations/create_agent.mutation.graphql'; +import createAgentToken from '../graphql/mutations/create_agent_token.mutation.graphql'; +import AvailableAgentsDropdown from './available_agents_dropdown.vue'; + +export default { + modalId: INSTALL_AGENT_MODAL_ID, + i18n: I18N_INSTALL_AGENT_MODAL, + components: { + AvailableAgentsDropdown, + ClipboardButton, + CodeBlock, + GlAlert, + GlButton, + GlFormGroup, + GlFormInputGroup, + GlLink, + GlModal, + GlSprintf, + }, + inject: ['projectPath', 'kasAddress'], + data() { + return { + registering: false, + agentName: null, + agentToken: null, + error: null, + }; + }, + computed: { + registered() { + return Boolean(this.agentToken); + }, + nextButtonDisabled() { + return !this.registering && this.agentName !== null; + }, + canCancel() { + return !this.registered && !this.registering; + }, + agentRegistrationCommand() { + return generateAgentRegistrationCommand(this.agentToken, this.kasAddress); + }, + basicInstallPath() { + return helpPagePath('user/clusters/agent/index', { + anchor: 'install-the-agent-into-the-cluster', + }); + }, + advancedInstallPath() { + return helpPagePath('user/clusters/agent/index', { anchor: 'advanced-installation' }); + }, + }, + methods: { + setAgentName(name) { + this.agentName = name; + }, + cancelClicked() { + this.$refs.modal.hide(); + }, + doneClicked() { + this.$emit('agentRegistered'); + this.$refs.modal.hide(); + }, + resetModal() { + this.registering = null; + this.agentName = null; + this.agentToken = null; + this.error = null; + }, + createAgentMutation() { + return this.$apollo + .mutate({ + mutation: createAgent, + variables: { + input: { + name: this.agentName, + projectPath: this.projectPath, + }, + }, + }) + .then(({ data: { createClusterAgent } }) => createClusterAgent); + }, + createAgentTokenMutation(agendId) { + return this.$apollo + .mutate({ + mutation: createAgentToken, + variables: { + input: { + clusterAgentId: agendId, + name: this.agentName, + }, + }, + }) + .then(({ data: { clusterAgentTokenCreate } }) => clusterAgentTokenCreate); + }, + async registerAgent() { + this.registering = true; + this.error = null; + + try { + const { errors: agentErrors, clusterAgent } = await this.createAgentMutation(); + + if (agentErrors?.length > 0) { + throw new Error(agentErrors[0]); + } + + const { errors: tokenErrors, secret } = await this.createAgentTokenMutation( + clusterAgent.id, + ); + + if (tokenErrors?.length > 0) { + throw new Error(tokenErrors[0]); + } + + this.agentToken = secret; + } catch (error) { + if (error) { + this.error = error.message; + } else { + this.error = this.$options.i18n.unknownError; + } + } finally { + this.registering = false; + } + }, + }, +}; +</script> + +<template> + <gl-modal + ref="modal" + :modal-id="$options.modalId" + :title="$options.i18n.modalTitle" + static + lazy + @hidden="resetModal" + > + <template v-if="!registered"> + <p> + <strong>{{ $options.i18n.selectAgentTitle }}</strong> + </p> + + <p> + <gl-sprintf :message="$options.i18n.selectAgentBody"> + <template #link="{ content }"> + <gl-link :href="basicInstallPath" target="_blank"> {{ content }}</gl-link> + </template> + </gl-sprintf> + </p> + + <form> + <gl-form-group label-for="agent-name"> + <available-agents-dropdown + class="gl-w-70p" + :is-registering="registering" + @agentSelected="setAgentName" + /> + </gl-form-group> + </form> + + <p v-if="error"> + <gl-alert + :title="$options.i18n.registrationErrorTitle" + variant="danger" + :dismissible="false" + > + {{ error }} + </gl-alert> + </p> + </template> + + <template v-else> + <p> + <strong>{{ $options.i18n.tokenTitle }}</strong> + </p> + + <p> + <gl-sprintf :message="$options.i18n.tokenBody"> + <template #link="{ content }"> + <gl-link :href="basicInstallPath" target="_blank"> {{ content }}</gl-link> + </template> + </gl-sprintf> + </p> + + <p> + <gl-alert + :title="$options.i18n.tokenSingleUseWarningTitle" + variant="warning" + :dismissible="false" + > + {{ $options.i18n.tokenSingleUseWarningBody }} + </gl-alert> + </p> + + <p> + <gl-form-input-group readonly :value="agentToken" :select-on-click="true"> + <template #append> + <clipboard-button :text="agentToken" :title="$options.i18n.copyToken" /> + </template> + </gl-form-input-group> + </p> + + <p> + <strong>{{ $options.i18n.basicInstallTitle }}</strong> + </p> + + <p> + {{ $options.i18n.basicInstallBody }} + </p> + + <p> + <code-block :code="agentRegistrationCommand" /> + </p> + + <p> + <strong>{{ $options.i18n.advancedInstallTitle }}</strong> + </p> + + <p> + <gl-sprintf :message="$options.i18n.advancedInstallBody"> + <template #link="{ content }"> + <gl-link :href="advancedInstallPath" target="_blank"> {{ content }}</gl-link> + </template> + </gl-sprintf> + </p> + </template> + + <template #modal-footer> + <gl-button v-if="canCancel" @click="cancelClicked">{{ $options.i18n.cancel }} </gl-button> + + <gl-button v-if="registered" variant="confirm" category="primary" @click="doneClicked" + >{{ $options.i18n.done }} + </gl-button> + + <gl-button + v-else + :disabled="!nextButtonDisabled" + variant="confirm" + category="primary" + @click="registerAgent" + >{{ $options.i18n.next }} + </gl-button> + </template> + </gl-modal> +</template> |