Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/clusters_list')
-rw-r--r--app/assets/javascripts/clusters_list/clusters_util.js4
-rw-r--r--app/assets/javascripts/clusters_list/components/agent_empty_state.vue81
-rw-r--r--app/assets/javascripts/clusters_list/components/agent_table.vue162
-rw-r--r--app/assets/javascripts/clusters_list/components/agents.vue36
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters.vue54
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters_actions.vue44
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters_empty_state.vue76
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters_main_view.vue73
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters_view_all.vue218
-rw-r--r--app/assets/javascripts/clusters_list/components/install_agent_modal.vue52
-rw-r--r--app/assets/javascripts/clusters_list/constants.js94
-rw-r--r--app/assets/javascripts/clusters_list/graphql/cache_update.js29
-rw-r--r--app/assets/javascripts/clusters_list/graphql/fragments/cluster_agent.fragment.graphql10
-rw-r--r--app/assets/javascripts/clusters_list/graphql/mutations/create_agent.mutation.graphql4
-rw-r--r--app/assets/javascripts/clusters_list/graphql/queries/get_agents.query.graphql12
-rw-r--r--app/assets/javascripts/clusters_list/index.js4
-rw-r--r--app/assets/javascripts/clusters_list/load_agents.js44
-rw-r--r--app/assets/javascripts/clusters_list/load_clusters.js7
-rw-r--r--app/assets/javascripts/clusters_list/load_main_view.js50
-rw-r--r--app/assets/javascripts/clusters_list/store/actions.js16
-rw-r--r--app/assets/javascripts/clusters_list/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/clusters_list/store/mutations.js5
-rw-r--r--app/assets/javascripts/clusters_list/store/state.js7
23 files changed, 853 insertions, 230 deletions
diff --git a/app/assets/javascripts/clusters_list/clusters_util.js b/app/assets/javascripts/clusters_list/clusters_util.js
index 9b870134512..c78c93fe1ba 100644
--- a/app/assets/javascripts/clusters_list/clusters_util.js
+++ b/app/assets/javascripts/clusters_list/clusters_util.js
@@ -6,3 +6,7 @@ export function generateAgentRegistrationCommand(agentToken, kasAddress) {
--agent-version stable \\
--namespace gitlab-kubernetes-agent | kubectl apply -f -`;
}
+
+export function getAgentConfigPath(clusterAgentName) {
+ return `.gitlab/agents/${clusterAgentName}`;
+}
diff --git a/app/assets/javascripts/clusters_list/components/agent_empty_state.vue b/app/assets/javascripts/clusters_list/components/agent_empty_state.vue
index 405339b3d36..af44a23b4b3 100644
--- a/app/assets/javascripts/clusters_list/components/agent_empty_state.vue
+++ b/app/assets/javascripts/clusters_list/components/agent_empty_state.vue
@@ -1,9 +1,16 @@
<script>
import { GlButton, GlEmptyState, GlLink, GlSprintf, GlAlert, GlModalDirective } from '@gitlab/ui';
-import { INSTALL_AGENT_MODAL_ID } from '../constants';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { INSTALL_AGENT_MODAL_ID, I18N_AGENTS_EMPTY_STATE } from '../constants';
export default {
+ i18n: I18N_AGENTS_EMPTY_STATE,
modalId: INSTALL_AGENT_MODAL_ID,
+ multipleClustersDocsUrl: helpPagePath('user/project/clusters/multiple_kubernetes_clusters'),
+ installDocsUrl: helpPagePath('administration/clusters/kas'),
+ getStartedDocsUrl: helpPagePath('user/clusters/agent/index', {
+ anchor: 'define-a-configuration-repository',
+ }),
components: {
GlButton,
GlEmptyState,
@@ -14,19 +21,17 @@ export default {
directives: {
GlModalDirective,
},
- inject: [
- 'emptyStateImage',
- 'projectPath',
- 'agentDocsUrl',
- 'installDocsUrl',
- 'getStartedDocsUrl',
- 'integrationDocsUrl',
- ],
+ inject: ['emptyStateImage', 'projectPath'],
props: {
hasConfigurations: {
type: Boolean,
required: true,
},
+ isChildComponent: {
+ default: false,
+ required: false,
+ type: Boolean,
+ },
},
computed: {
repositoryPath() {
@@ -37,22 +42,19 @@ export default {
</script>
<template>
- <gl-empty-state
- :svg-path="emptyStateImage"
- :title="s__('ClusterAgents|Integrate Kubernetes with a GitLab Agent')"
- class="empty-state--agent"
- >
+ <gl-empty-state :svg-path="emptyStateImage" title="" class="agents-empty-state">
<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}',
- )
- "
- >
+ <p class="mw-460 gl-mx-auto gl-text-left">
+ {{ $options.i18n.introText }}
+ </p>
+ <p class="mw-460 gl-mx-auto gl-text-left">
+ <gl-sprintf :message="$options.i18n.multipleClustersText">
<template #link="{ content }">
- <gl-link :href="agentDocsUrl" target="_blank" data-testid="agent-docs-link">
+ <gl-link
+ :href="$options.multipleClustersDocsUrl"
+ target="_blank"
+ data-testid="multiple-clusters-docs-link"
+ >
{{ content }}
</gl-link>
</template>
@@ -60,19 +62,9 @@ export default {
</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>
+ <gl-link :href="$options.installDocsUrl" target="_blank" data-testid="install-docs-link">
+ {{ $options.i18n.learnMoreText }}
+ </gl-link>
</p>
<gl-alert
@@ -81,24 +73,20 @@ export default {
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.',
- )
- }}
+ {{ $options.i18n.warningText }}
<template #actions>
<gl-button
category="primary"
variant="info"
- :href="getStartedDocsUrl"
+ :href="$options.getStartedDocsUrl"
target="_blank"
class="gl-ml-0!"
>
- {{ s__('ClusterAgents|Read more about getting started') }}
+ {{ $options.i18n.readMoreText }}
</gl-button>
<gl-button category="secondary" variant="info" :href="repositoryPath">
- {{ s__('ClusterAgents|Go to the repository') }}
+ {{ $options.i18n.repositoryButtonText }}
</gl-button>
</template>
</gl-alert>
@@ -106,13 +94,14 @@ export default {
<template #actions>
<gl-button
+ v-if="!isChildComponent"
v-gl-modal-directive="$options.modalId"
:disabled="!hasConfigurations"
data-testid="integration-primary-button"
category="primary"
- variant="success"
+ variant="confirm"
>
- {{ s__('ClusterAgents|Integrate with the GitLab Agent') }}
+ {{ $options.i18n.primaryButtonText }}
</gl-button>
</template>
</gl-empty-state>
diff --git a/app/assets/javascripts/clusters_list/components/agent_table.vue b/app/assets/javascripts/clusters_list/components/agent_table.vue
index 487e512c06d..000730ac1ba 100644
--- a/app/assets/javascripts/clusters_list/components/agent_table.vue
+++ b/app/assets/javascripts/clusters_list/components/agent_table.vue
@@ -1,6 +1,5 @@
<script>
import {
- GlButton,
GlLink,
GlModalDirective,
GlTable,
@@ -12,11 +11,12 @@ import {
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';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { INSTALL_AGENT_MODAL_ID, AGENT_STATUSES } from '../constants';
+import { getAgentConfigPath } from '../clusters_util';
export default {
components: {
- GlButton,
GlLink,
GlTable,
GlIcon,
@@ -29,10 +29,12 @@ export default {
GlModalDirective,
},
mixins: [timeagoMixin],
- inject: ['integrationDocsUrl'],
INSTALL_AGENT_MODAL_ID,
AGENT_STATUSES,
- TROUBLESHOOTING_LINK,
+
+ troubleshooting_link: helpPagePath('user/clusters/agent/index', {
+ anchor: 'troubleshooting',
+ }),
props: {
agents: {
required: true,
@@ -41,112 +43,102 @@ export default {
},
computed: {
fields() {
+ const tdClass = 'gl-py-5!';
return [
{
key: 'name',
label: s__('ClusterAgents|Name'),
+ tdClass,
},
{
key: 'status',
label: s__('ClusterAgents|Connection status'),
+ tdClass,
},
{
key: 'lastContact',
label: s__('ClusterAgents|Last contact'),
+ tdClass,
},
{
key: 'configuration',
label: s__('ClusterAgents|Configuration'),
+ tdClass,
},
];
},
},
+ methods: {
+ getCellId(item) {
+ return `connection-status-${item.name}`;
+ },
+ getAgentConfigPath,
+ },
};
</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-2 gl-border-b-gray-100"
+ class="gl-mb-4!"
+ 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>
- <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(status)="{ item }">
+ <span :id="getCellId(item)" class="gl-md-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="getCellId(item)" 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="getCellId(item)"
+ :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">
+ <gl-link :href="$options.troubleshooting_link" target="_blank" class="gl-font-sm">
+ {{ s__('ClusterAgents|Learn how to troubleshoot') }}</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(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>
+ <template #cell(configuration)="{ item }">
+ <span data-testid="cluster-agent-configuration-link">
+ <gl-link v-if="item.configFolder" :href="item.configFolder.webPath">
+ {{ getAgentConfigPath(item.name) }}
+ </gl-link>
- <span v-else>.gitlab/agents/{{ item.name }}</span>
- <!-- eslint-enable @gitlab/vue-require-i18n-strings -->
- </span>
- </template>
- </gl-table>
- </div>
+ <span v-else>{{ getAgentConfigPath(item.name) }}</span>
+ </span>
+ </template>
+ </gl-table>
</template>
diff --git a/app/assets/javascripts/clusters_list/components/agents.vue b/app/assets/javascripts/clusters_list/components/agents.vue
index ed44c1f5fa7..fb5cf7d1206 100644
--- a/app/assets/javascripts/clusters_list/components/agents.vue
+++ b/app/assets/javascripts/clusters_list/components/agents.vue
@@ -4,7 +4,6 @@ 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: {
@@ -21,12 +20,14 @@ export default {
this.updateTreeList(data);
return data;
},
+ result() {
+ this.emitAgentsLoaded();
+ },
},
},
components: {
AgentEmptyState,
AgentTable,
- InstallAgentModal,
GlAlert,
GlKeysetPagination,
GlLoadingIcon,
@@ -38,11 +39,21 @@ export default {
required: false,
type: String,
},
+ isChildComponent: {
+ default: false,
+ required: false,
+ type: Boolean,
+ },
+ limit: {
+ default: null,
+ required: false,
+ type: Number,
+ },
},
data() {
return {
cursor: {
- first: MAX_LIST_COUNT,
+ first: this.limit ? this.limit : MAX_LIST_COUNT,
last: null,
},
folderList: {},
@@ -70,7 +81,7 @@ export default {
return this.$apollo.queries.agents.loading;
},
showPagination() {
- return this.agentPageInfo.hasPreviousPage || this.agentPageInfo.hasNextPage;
+ return !this.limit && (this.agentPageInfo.hasPreviousPage || this.agentPageInfo.hasNextPage);
},
treePageInfo() {
return this.agents?.project?.repository?.tree?.trees?.pageInfo || {};
@@ -130,24 +141,31 @@ export default {
}
return 'unused';
},
+ emitAgentsLoaded() {
+ const count = this.agents?.project?.clusterAgents?.count;
+ this.$emit('onAgentsLoad', count);
+ },
},
};
</script>
<template>
- <gl-loading-icon v-if="isLoading" size="md" class="gl-mt-3" />
+ <gl-loading-icon v-if="isLoading" size="md" />
- <section v-else-if="agentList" class="gl-mt-3">
+ <section v-else-if="agentList">
<div v-if="agentList.length">
- <AgentTable :agents="agentList" />
+ <agent-table :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" />
+ <agent-empty-state
+ v-else
+ :has-configurations="hasConfigurations"
+ :is-child-component="isChildComponent"
+ />
</section>
<gl-alert v-else variant="danger" :dismissible="false">
diff --git a/app/assets/javascripts/clusters_list/components/clusters.vue b/app/assets/javascripts/clusters_list/components/clusters.vue
index 0d1534d20e0..9c330045596 100644
--- a/app/assets/javascripts/clusters_list/components/clusters.vue
+++ b/app/assets/javascripts/clusters_list/components/clusters.vue
@@ -14,6 +14,7 @@ import { __, sprintf } from '~/locale';
import { CLUSTER_TYPES, STATUSES } from '../constants';
import AncestorNotice from './ancestor_notice.vue';
import NodeErrorHelpText from './node_error_help_text.vue';
+import ClustersEmptyState from './clusters_empty_state.vue';
export default {
nodeMemoryText: __('%{totalMemory} (%{freeSpacePercentage}%{percentSymbol} free)'),
@@ -28,10 +29,23 @@ export default {
GlSprintf,
GlTable,
NodeErrorHelpText,
+ ClustersEmptyState,
},
directives: {
GlTooltip: GlTooltipDirective,
},
+ props: {
+ isChildComponent: {
+ default: false,
+ required: false,
+ type: Boolean,
+ },
+ limit: {
+ default: null,
+ required: false,
+ type: Number,
+ },
+ },
computed: {
...mapState([
'clusters',
@@ -40,7 +54,7 @@ export default {
'loadingNodes',
'page',
'providers',
- 'totalCulsters',
+ 'totalClusters',
]),
contentAlignClasses() {
return 'gl-display-flex gl-align-items-center gl-justify-content-end gl-justify-content-md-start';
@@ -55,43 +69,57 @@ export default {
},
},
fields() {
+ const tdClass = 'gl-py-5!';
return [
{
key: 'name',
label: __('Kubernetes cluster'),
+ tdClass,
},
{
key: 'environment_scope',
label: __('Environment scope'),
+ tdClass,
},
{
key: 'node_size',
label: __('Nodes'),
+ tdClass,
},
{
key: 'total_cpu',
label: __('Total cores (CPUs)'),
+ tdClass,
},
{
key: 'total_memory',
label: __('Total memory (GB)'),
+ tdClass,
},
{
key: 'cluster_type',
label: __('Cluster level'),
+ tdClass,
formatter: (value) => CLUSTER_TYPES[value],
},
];
},
- hasClusters() {
+ hasClustersPerPage() {
return this.clustersPerPage > 0;
},
+ hasClusters() {
+ return this.totalClusters > 0;
+ },
},
mounted() {
+ if (this.limit) {
+ this.setClustersPerPage(this.limit);
+ }
+
this.fetchClusters();
},
methods: {
- ...mapActions(['fetchClusters', 'reportSentryError', 'setPage']),
+ ...mapActions(['fetchClusters', 'reportSentryError', 'setPage', 'setClustersPerPage']),
k8sQuantityToGb(quantity) {
if (!quantity) {
return 0;
@@ -196,18 +224,20 @@ export default {
</script>
<template>
- <gl-loading-icon v-if="loadingClusters" size="md" class="gl-mt-3" />
+ <gl-loading-icon v-if="loadingClusters" size="md" />
<section v-else>
<ancestor-notice />
<gl-table
+ v-if="hasClusters"
:items="clusters"
:fields="fields"
+ fixed
stacked="md"
head-variant="white"
- thead-class="gl-border-b-solid gl-border-b-1 gl-border-b-gray-100"
- class="qa-clusters-table"
+ thead-class="gl-border-b-solid gl-border-b-2 gl-border-b-gray-100"
+ class="qa-clusters-table gl-mb-4!"
data-testid="cluster_list_table"
>
<template #cell(name)="{ item }">
@@ -241,7 +271,7 @@ export default {
<gl-skeleton-loading v-else-if="loadingNodes" :lines="1" :class="contentAlignClasses" />
- <NodeErrorHelpText
+ <node-error-help-text
v-else-if="item.kubernetes_errors"
:class="contentAlignClasses"
:error-type="item.kubernetes_errors.connection_error"
@@ -262,7 +292,7 @@ export default {
<gl-skeleton-loading v-else-if="loadingNodes" :lines="1" :class="contentAlignClasses" />
- <NodeErrorHelpText
+ <node-error-help-text
v-else-if="item.kubernetes_errors"
:class="contentAlignClasses"
:error-type="item.kubernetes_errors.node_connection_error"
@@ -283,7 +313,7 @@ export default {
<gl-skeleton-loading v-else-if="loadingNodes" :lines="1" :class="contentAlignClasses" />
- <NodeErrorHelpText
+ <node-error-help-text
v-else-if="item.kubernetes_errors"
:class="contentAlignClasses"
:error-type="item.kubernetes_errors.metrics_connection_error"
@@ -298,11 +328,13 @@ export default {
</template>
</gl-table>
+ <clusters-empty-state v-else :is-child-component="isChildComponent" />
+
<gl-pagination
- v-if="hasClusters"
+ v-if="hasClustersPerPage && !limit"
v-model="currentPage"
:per-page="clustersPerPage"
- :total-items="totalCulsters"
+ :total-items="totalClusters"
:prev-text="__('Prev')"
:next-text="__('Next')"
align="center"
diff --git a/app/assets/javascripts/clusters_list/components/clusters_actions.vue b/app/assets/javascripts/clusters_list/components/clusters_actions.vue
new file mode 100644
index 00000000000..25f67462223
--- /dev/null
+++ b/app/assets/javascripts/clusters_list/components/clusters_actions.vue
@@ -0,0 +1,44 @@
+<script>
+import { GlDropdown, GlDropdownItem, GlModalDirective } from '@gitlab/ui';
+import { INSTALL_AGENT_MODAL_ID, CLUSTERS_ACTIONS } from '../constants';
+
+export default {
+ i18n: CLUSTERS_ACTIONS,
+ INSTALL_AGENT_MODAL_ID,
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ },
+ directives: {
+ GlModalDirective,
+ },
+ inject: ['newClusterPath', 'addClusterPath'],
+};
+</script>
+
+<template>
+ <div class="nav-controls gl-ml-auto">
+ <gl-dropdown
+ ref="dropdown"
+ v-gl-modal-directive="$options.INSTALL_AGENT_MODAL_ID"
+ category="primary"
+ variant="confirm"
+ :text="$options.i18n.actionsButton"
+ split
+ right
+ >
+ <gl-dropdown-item :href="newClusterPath" data-testid="new-cluster-link" @click.stop>
+ {{ $options.i18n.createNewCluster }}
+ </gl-dropdown-item>
+ <gl-dropdown-item
+ v-gl-modal-directive="$options.INSTALL_AGENT_MODAL_ID"
+ data-testid="connect-new-agent-link"
+ >
+ {{ $options.i18n.connectWithAgent }}
+ </gl-dropdown-item>
+ <gl-dropdown-item :href="addClusterPath" data-testid="connect-cluster-link" @click.stop>
+ {{ $options.i18n.connectExistingCluster }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </div>
+</template>
diff --git a/app/assets/javascripts/clusters_list/components/clusters_empty_state.vue b/app/assets/javascripts/clusters_list/components/clusters_empty_state.vue
new file mode 100644
index 00000000000..3879af6e9cb
--- /dev/null
+++ b/app/assets/javascripts/clusters_list/components/clusters_empty_state.vue
@@ -0,0 +1,76 @@
+<script>
+import { GlEmptyState, GlButton, GlLink, GlSprintf } from '@gitlab/ui';
+import { mapState } from 'vuex';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { I18N_CLUSTERS_EMPTY_STATE } from '../constants';
+
+export default {
+ i18n: I18N_CLUSTERS_EMPTY_STATE,
+ components: {
+ GlEmptyState,
+ GlButton,
+ GlLink,
+ GlSprintf,
+ },
+ inject: ['emptyStateHelpText', 'clustersEmptyStateImage', 'newClusterPath'],
+ props: {
+ isChildComponent: {
+ default: false,
+ required: false,
+ type: Boolean,
+ },
+ },
+ learnMoreHelpUrl: helpPagePath('user/project/clusters/index'),
+ multipleClustersHelpUrl: helpPagePath('user/project/clusters/multiple_kubernetes_clusters'),
+ computed: {
+ ...mapState(['canAddCluster']),
+ },
+};
+</script>
+
+<template>
+ <gl-empty-state :svg-path="clustersEmptyStateImage" title="">
+ <template #description>
+ <p class="gl-text-left">
+ {{ $options.i18n.description }}
+ </p>
+ <p class="gl-text-left">
+ <gl-sprintf :message="$options.i18n.multipleClustersText">
+ <template #link="{ content }">
+ <gl-link
+ :href="$options.multipleClustersHelpUrl"
+ target="_blank"
+ data-testid="multiple-clusters-docs-link"
+ >
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+
+ <p v-if="emptyStateHelpText" data-testid="clusters-empty-state-text">
+ {{ emptyStateHelpText }}
+ </p>
+
+ <p>
+ <gl-link :href="$options.learnMoreHelpUrl" target="_blank" data-testid="clusters-docs-link">
+ {{ $options.i18n.learnMoreLinkText }}
+ </gl-link>
+ </p>
+ </template>
+
+ <template #actions>
+ <gl-button
+ v-if="!isChildComponent"
+ data-testid="integration-primary-button"
+ data-qa-selector="add_kubernetes_cluster_link"
+ category="primary"
+ variant="confirm"
+ :disabled="!canAddCluster"
+ :href="newClusterPath"
+ >
+ {{ $options.i18n.buttonText }}
+ </gl-button>
+ </template>
+ </gl-empty-state>
+</template>
diff --git a/app/assets/javascripts/clusters_list/components/clusters_main_view.vue b/app/assets/javascripts/clusters_list/components/clusters_main_view.vue
new file mode 100644
index 00000000000..9e03093aa67
--- /dev/null
+++ b/app/assets/javascripts/clusters_list/components/clusters_main_view.vue
@@ -0,0 +1,73 @@
+<script>
+import { GlTabs, GlTab } from '@gitlab/ui';
+import { CLUSTERS_TABS, MAX_CLUSTERS_LIST, MAX_LIST_COUNT, AGENT } from '../constants';
+import Agents from './agents.vue';
+import InstallAgentModal from './install_agent_modal.vue';
+import ClustersActions from './clusters_actions.vue';
+import Clusters from './clusters.vue';
+import ClustersViewAll from './clusters_view_all.vue';
+
+export default {
+ components: {
+ GlTabs,
+ GlTab,
+ ClustersActions,
+ ClustersViewAll,
+ Clusters,
+ Agents,
+ InstallAgentModal,
+ },
+ CLUSTERS_TABS,
+ props: {
+ defaultBranchName: {
+ default: '.noBranch',
+ required: false,
+ type: String,
+ },
+ },
+ data() {
+ return {
+ selectedTabIndex: 0,
+ maxAgents: MAX_CLUSTERS_LIST,
+ };
+ },
+ methods: {
+ onTabChange(tabName) {
+ this.selectedTabIndex = CLUSTERS_TABS.findIndex((tab) => tab.queryParamValue === tabName);
+
+ this.maxAgents = tabName === AGENT ? MAX_LIST_COUNT : MAX_CLUSTERS_LIST;
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-tabs
+ v-model="selectedTabIndex"
+ sync-active-tab-with-query-params
+ nav-class="gl-flex-grow-1 gl-align-items-center"
+ lazy
+ >
+ <gl-tab
+ v-for="(tab, idx) in $options.CLUSTERS_TABS"
+ :key="idx"
+ :title="tab.title"
+ :query-param-value="tab.queryParamValue"
+ class="gl-line-height-20 gl-mt-5"
+ >
+ <component
+ :is="tab.component"
+ :default-branch-name="defaultBranchName"
+ data-testid="clusters-tab-component"
+ @changeTab="onTabChange"
+ />
+ </gl-tab>
+
+ <template #tabs-end>
+ <clusters-actions />
+ </template>
+ </gl-tabs>
+
+ <install-agent-modal :default-branch-name="defaultBranchName" :max-agents="maxAgents" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/clusters_list/components/clusters_view_all.vue b/app/assets/javascripts/clusters_list/components/clusters_view_all.vue
new file mode 100644
index 00000000000..285876e57d8
--- /dev/null
+++ b/app/assets/javascripts/clusters_list/components/clusters_view_all.vue
@@ -0,0 +1,218 @@
+<script>
+import {
+ GlCard,
+ GlSprintf,
+ GlPopover,
+ GlLink,
+ GlButton,
+ GlBadge,
+ GlLoadingIcon,
+ GlModalDirective,
+} from '@gitlab/ui';
+import { mapState } from 'vuex';
+import {
+ AGENT_CARD_INFO,
+ CERTIFICATE_BASED_CARD_INFO,
+ MAX_CLUSTERS_LIST,
+ INSTALL_AGENT_MODAL_ID,
+} from '../constants';
+import Clusters from './clusters.vue';
+import Agents from './agents.vue';
+
+export default {
+ components: {
+ GlCard,
+ GlSprintf,
+ GlPopover,
+ GlLink,
+ GlButton,
+ GlBadge,
+ GlLoadingIcon,
+ Clusters,
+ Agents,
+ },
+ directives: {
+ GlModalDirective,
+ },
+ AGENT_CARD_INFO,
+ CERTIFICATE_BASED_CARD_INFO,
+ MAX_CLUSTERS_LIST,
+ INSTALL_AGENT_MODAL_ID,
+ inject: ['addClusterPath'],
+ props: {
+ defaultBranchName: {
+ default: '.noBranch',
+ required: false,
+ type: String,
+ },
+ },
+ data() {
+ return {
+ loadingAgents: true,
+ totalAgents: null,
+ };
+ },
+ computed: {
+ ...mapState(['loadingClusters', 'totalClusters']),
+ isLoading() {
+ return this.loadingAgents || this.loadingClusters;
+ },
+ agentsCardTitle() {
+ let cardTitle;
+ if (this.totalAgents > 0) {
+ cardTitle = {
+ message: AGENT_CARD_INFO.title,
+ number: this.totalAgents < MAX_CLUSTERS_LIST ? this.totalAgents : MAX_CLUSTERS_LIST,
+ total: this.totalAgents,
+ };
+ } else {
+ cardTitle = {
+ message: AGENT_CARD_INFO.emptyTitle,
+ };
+ }
+
+ return cardTitle;
+ },
+ clustersCardTitle() {
+ let cardTitle;
+ if (this.totalClusters > 0) {
+ cardTitle = {
+ message: CERTIFICATE_BASED_CARD_INFO.title,
+ number: this.totalClusters < MAX_CLUSTERS_LIST ? this.totalClusters : MAX_CLUSTERS_LIST,
+ total: this.totalClusters,
+ };
+ } else {
+ cardTitle = {
+ message: CERTIFICATE_BASED_CARD_INFO.emptyTitle,
+ };
+ }
+
+ return cardTitle;
+ },
+ },
+ methods: {
+ cardFooterNumber(number) {
+ return number > MAX_CLUSTERS_LIST ? number : '';
+ },
+ onAgentsLoad(number) {
+ this.totalAgents = number;
+ this.loadingAgents = false;
+ },
+ changeTab($event, tab) {
+ $event.preventDefault();
+ this.$emit('changeTab', tab);
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-loading-icon v-if="isLoading" size="md" />
+ <div v-show="!isLoading" data-testid="clusters-cards-container">
+ <gl-card
+ header-class="gl-bg-white gl-display-flex gl-align-items-center gl-justify-content-space-between gl-py-4"
+ body-class="gl-pb-0"
+ footer-class="gl-text-right"
+ >
+ <template #header>
+ <h3 data-testid="agent-card-title" class="gl-my-0 gl-font-weight-normal gl-font-size-h2">
+ <gl-sprintf :message="agentsCardTitle.message"
+ ><template #number>{{ agentsCardTitle.number }}</template>
+ <template #total>{{ agentsCardTitle.total }}</template>
+ </gl-sprintf>
+ </h3>
+
+ <gl-badge id="clusters-recommended-badge" size="md" variant="info">{{
+ $options.AGENT_CARD_INFO.tooltip.label
+ }}</gl-badge>
+
+ <gl-popover
+ target="clusters-recommended-badge"
+ container="viewport"
+ placement="bottom"
+ :title="$options.AGENT_CARD_INFO.tooltip.title"
+ >
+ <p class="gl-mb-0">
+ <gl-sprintf :message="$options.AGENT_CARD_INFO.tooltip.text">
+ <template #link="{ content }">
+ <gl-link
+ :href="$options.AGENT_CARD_INFO.tooltip.link"
+ target="_blank"
+ class="gl-font-sm"
+ >
+ {{ content }}</gl-link
+ >
+ </template>
+ </gl-sprintf>
+ </p>
+ </gl-popover>
+ </template>
+
+ <agents
+ :limit="$options.MAX_CLUSTERS_LIST"
+ :default-branch-name="defaultBranchName"
+ :is-child-component="true"
+ @onAgentsLoad="onAgentsLoad"
+ />
+
+ <template #footer>
+ <gl-link
+ v-if="totalAgents"
+ data-testid="agents-tab-footer-link"
+ :href="`?tab=${$options.AGENT_CARD_INFO.tabName}`"
+ @click="changeTab($event, $options.AGENT_CARD_INFO.tabName)"
+ ><gl-sprintf :message="$options.AGENT_CARD_INFO.footerText"
+ ><template #number>{{ cardFooterNumber(totalAgents) }}</template></gl-sprintf
+ ></gl-link
+ ><gl-button
+ v-gl-modal-directive="$options.INSTALL_AGENT_MODAL_ID"
+ class="gl-ml-4"
+ category="secondary"
+ variant="confirm"
+ >{{ $options.AGENT_CARD_INFO.actionText }}</gl-button
+ >
+ </template>
+ </gl-card>
+
+ <gl-card
+ class="gl-mt-6"
+ header-class="gl-bg-white gl-display-flex gl-align-items-center gl-justify-content-space-between"
+ body-class="gl-pb-0"
+ footer-class="gl-text-right"
+ >
+ <template #header>
+ <h3
+ class="gl-my-1 gl-font-weight-normal gl-font-size-h2"
+ data-testid="clusters-card-title"
+ >
+ <gl-sprintf :message="clustersCardTitle.message"
+ ><template #number>{{ clustersCardTitle.number }}</template>
+ <template #total>{{ clustersCardTitle.total }}</template>
+ </gl-sprintf>
+ </h3>
+ </template>
+
+ <clusters :limit="$options.MAX_CLUSTERS_LIST" :is-child-component="true" />
+
+ <template #footer>
+ <gl-link
+ v-if="totalClusters"
+ data-testid="clusters-tab-footer-link"
+ :href="`?tab=${$options.CERTIFICATE_BASED_CARD_INFO.tabName}`"
+ @click="changeTab($event, $options.CERTIFICATE_BASED_CARD_INFO.tabName)"
+ ><gl-sprintf :message="$options.CERTIFICATE_BASED_CARD_INFO.footerText"
+ ><template #number>{{ cardFooterNumber(totalClusters) }}</template></gl-sprintf
+ ></gl-link
+ ><gl-button
+ category="secondary"
+ data-qa-selector="connect_existing_cluster_button"
+ variant="confirm"
+ class="gl-ml-4"
+ :href="addClusterPath"
+ >{{ $options.CERTIFICATE_BASED_CARD_INFO.actionText }}</gl-button
+ >
+ </template>
+ </gl-card>
+ </div>
+ </div>
+</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
index 5f192fe4d5a..6eb2e85ecea 100644
--- a/app/assets/javascripts/clusters_list/components/install_agent_modal.vue
+++ b/app/assets/javascripts/clusters_list/components/install_agent_modal.vue
@@ -13,8 +13,10 @@ 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 { addAgentToStore } from '../graphql/cache_update';
import createAgent from '../graphql/mutations/create_agent.mutation.graphql';
import createAgentToken from '../graphql/mutations/create_agent_token.mutation.graphql';
+import getAgentsQuery from '../graphql/queries/get_agents.query.graphql';
import AvailableAgentsDropdown from './available_agents_dropdown.vue';
export default {
@@ -33,12 +35,24 @@ export default {
GlSprintf,
},
inject: ['projectPath', 'kasAddress'],
+ props: {
+ defaultBranchName: {
+ default: '.noBranch',
+ required: false,
+ type: String,
+ },
+ maxAgents: {
+ required: true,
+ type: Number,
+ },
+ },
data() {
return {
registering: false,
agentName: null,
agentToken: null,
error: null,
+ clusterAgent: null,
};
},
computed: {
@@ -55,27 +69,31 @@ export default {
return generateAgentRegistrationCommand(this.agentToken, this.kasAddress);
},
basicInstallPath() {
- return helpPagePath('user/clusters/agent/index', {
+ return helpPagePath('user/clusters/agent/install/index', {
anchor: 'install-the-agent-into-the-cluster',
});
},
advancedInstallPath() {
- return helpPagePath('user/clusters/agent/index', { anchor: 'advanced-installation' });
+ return helpPagePath('user/clusters/agent/install/index', { anchor: 'advanced-installation' });
+ },
+ getAgentsQueryVariables() {
+ return {
+ defaultBranchName: this.defaultBranchName,
+ first: this.maxAgents,
+ last: null,
+ projectPath: this.projectPath,
+ };
},
},
methods: {
setAgentName(name) {
this.agentName = name;
},
- cancelClicked() {
- this.$refs.modal.hide();
- },
- doneClicked() {
- this.$emit('agentRegistered');
+ closeModal() {
this.$refs.modal.hide();
},
resetModal() {
- this.registering = null;
+ this.registering = false;
this.agentName = null;
this.agentToken = null;
this.error = null;
@@ -90,6 +108,14 @@ export default {
projectPath: this.projectPath,
},
},
+ update: (store, { data: { createClusterAgent } }) => {
+ addAgentToStore(
+ store,
+ createClusterAgent,
+ getAgentsQuery,
+ this.getAgentsQueryVariables,
+ );
+ },
})
.then(({ data: { createClusterAgent } }) => createClusterAgent);
},
@@ -117,6 +143,8 @@ export default {
throw new Error(agentErrors[0]);
}
+ this.clusterAgent = clusterAgent;
+
const { errors: tokenErrors, secret } = await this.createAgentTokenMutation(
clusterAgent.id,
);
@@ -240,10 +268,10 @@ export default {
</template>
<template #modal-footer>
- <gl-button v-if="canCancel" @click="cancelClicked">{{ $options.i18n.cancel }} </gl-button>
+ <gl-button v-if="canCancel" @click="closeModal">{{ $options.i18n.cancel }} </gl-button>
- <gl-button v-if="registered" variant="confirm" category="primary" @click="doneClicked"
- >{{ $options.i18n.done }}
+ <gl-button v-if="registered" variant="confirm" category="primary" @click="closeModal"
+ >{{ $options.i18n.close }}
</gl-button>
<gl-button
@@ -252,7 +280,7 @@ export default {
variant="confirm"
category="primary"
@click="registerAgent"
- >{{ $options.i18n.next }}
+ >{{ $options.i18n.registerAgentButton }}
</gl-button>
</template>
</gl-modal>
diff --git a/app/assets/javascripts/clusters_list/constants.js b/app/assets/javascripts/clusters_list/constants.js
index 0bade1fc281..9fefdf450c4 100644
--- a/app/assets/javascripts/clusters_list/constants.js
+++ b/app/assets/javascripts/clusters_list/constants.js
@@ -1,10 +1,9 @@
import { __, s__, sprintf } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
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 CLUSTER_ERRORS = {
default: {
@@ -66,8 +65,8 @@ export const STATUSES = {
};
export const I18N_INSTALL_AGENT_MODAL = {
- next: __('Next'),
- done: __('Done'),
+ registerAgentButton: s__('ClusterAgents|Register Agent'),
+ close: __('Close'),
cancel: __('Cancel'),
modalTitle: s__('ClusterAgents|Install new Agent'),
@@ -91,7 +90,7 @@ export const I18N_INSTALL_AGENT_MODAL = {
),
basicInstallTitle: s__('ClusterAgents|Recommended installation method'),
- basicInstallBody: s__(
+ basicInstallBody: __(
`Open a CLI and connect to the cluster you want to install the Agent in. Use this installation method to minimize any manual steps. The token is already included in the command.`,
),
@@ -100,7 +99,7 @@ export const I18N_INSTALL_AGENT_MODAL = {
'ClusterAgents|For alternative installation methods %{linkStart}go to the documentation%{linkEnd}.',
),
- registrationErrorTitle: s__('Failed to register Agent'),
+ registrationErrorTitle: __('Failed to register Agent'),
unknownError: s__('ClusterAgents|An unknown error occurred. Please try again.'),
};
@@ -141,3 +140,86 @@ export const AGENT_STATUSES = {
},
},
};
+
+export const I18N_AGENTS_EMPTY_STATE = {
+ introText: s__(
+ 'ClusterAgents|Use GitLab Agents to more securely integrate with your clusters to deploy your applications, run your pipelines, use review apps and much more.',
+ ),
+ multipleClustersText: s__(
+ 'ClusterAgents|If you are setting up multiple clusters and are using Auto DevOps, %{linkStart}read about using multiple Kubernetes clusters first.%{linkEnd}',
+ ),
+ learnMoreText: s__('ClusterAgents|Learn more about the GitLab Kubernetes Agent.'),
+ warningText: 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.',
+ ),
+ readMoreText: s__('ClusterAgents|Read more about getting started'),
+ repositoryButtonText: s__('ClusterAgents|Go to the repository'),
+ primaryButtonText: s__('ClusterAgents|Connect with a GitLab Agent'),
+};
+
+export const I18N_CLUSTERS_EMPTY_STATE = {
+ description: s__(
+ 'ClusterIntegration|Use certificates to integrate with your clusters to deploy your applications, run your pipelines, use review apps and much more in an easy way.',
+ ),
+ multipleClustersText: s__(
+ 'ClusterIntegration|If you are setting up multiple clusters and are using Auto DevOps, %{linkStart}read about using multiple Kubernetes clusters first.%{linkEnd}',
+ ),
+ learnMoreLinkText: s__('ClusterIntegration|Learn more about the GitLab managed clusters'),
+ buttonText: s__('ClusterIntegration|Connect with a certificate'),
+};
+
+export const AGENT_CARD_INFO = {
+ tabName: 'agent',
+ title: sprintf(s__('ClusterAgents|%{number} of %{total} Agent based integrations')),
+ emptyTitle: s__('ClusterAgents|No Agent based integrations'),
+ tooltip: {
+ label: s__('ClusterAgents|Recommended'),
+ title: s__('ClusterAgents|GitLab Agents'),
+ text: sprintf(
+ s__(
+ 'ClusterAgents|GitLab Agents provide an increased level of security when integrating with clusters. %{linkStart}Learn more about the GitLab Kubernetes Agent.%{linkEnd}',
+ ),
+ ),
+ link: helpPagePath('user/clusters/agent/index'),
+ },
+ actionText: s__('ClusterAgents|Install new Agent'),
+ footerText: sprintf(s__('ClusterAgents|View all %{number} Agent based integrations')),
+};
+
+export const CERTIFICATE_BASED_CARD_INFO = {
+ tabName: 'certificate_based',
+ title: sprintf(s__('ClusterAgents|%{number} of %{total} Certificate based integrations')),
+ emptyTitle: s__('ClusterAgents|No Certificate based integrations'),
+ actionText: s__('ClusterAgents|Connect existing cluster'),
+ footerText: sprintf(s__('ClusterAgents|View all %{number} Certificate based integrations')),
+};
+
+export const MAX_CLUSTERS_LIST = 6;
+
+export const CLUSTERS_TABS = [
+ {
+ title: s__('ClusterAgents|All'),
+ component: 'ClustersViewAll',
+ queryParamValue: 'all',
+ },
+ {
+ title: s__('ClusterAgents|Agent'),
+ component: 'agents',
+ queryParamValue: 'agent',
+ },
+ {
+ title: s__('ClusterAgents|Certificate based'),
+ component: 'clusters',
+ queryParamValue: 'certificate_based',
+ },
+];
+
+export const CLUSTERS_ACTIONS = {
+ actionsButton: s__('ClusterAgents|Actions'),
+ createNewCluster: s__('ClusterAgents|Create new cluster'),
+ connectWithAgent: s__('ClusterAgents|Connect with Agent'),
+ connectExistingCluster: s__('ClusterAgents|Connect with certificate'),
+};
+
+export const AGENT = 'agent';
+export const CERTIFICATE_BASED = 'certificate_based';
diff --git a/app/assets/javascripts/clusters_list/graphql/cache_update.js b/app/assets/javascripts/clusters_list/graphql/cache_update.js
new file mode 100644
index 00000000000..dd633820952
--- /dev/null
+++ b/app/assets/javascripts/clusters_list/graphql/cache_update.js
@@ -0,0 +1,29 @@
+import produce from 'immer';
+import { getAgentConfigPath } from '../clusters_util';
+
+export function addAgentToStore(store, createClusterAgent, query, variables) {
+ const { clusterAgent } = createClusterAgent;
+ const sourceData = store.readQuery({
+ query,
+ variables,
+ });
+
+ const data = produce(sourceData, (draftData) => {
+ const configuration = {
+ name: clusterAgent.name,
+ path: getAgentConfigPath(clusterAgent.name),
+ webPath: clusterAgent.webPath,
+ __typename: 'TreeEntry',
+ };
+
+ draftData.project.clusterAgents.nodes.push(clusterAgent);
+ draftData.project.clusterAgents.count += 1;
+ draftData.project.repository.tree.trees.nodes.push(configuration);
+ });
+
+ store.writeQuery({
+ query,
+ variables,
+ data,
+ });
+}
diff --git a/app/assets/javascripts/clusters_list/graphql/fragments/cluster_agent.fragment.graphql b/app/assets/javascripts/clusters_list/graphql/fragments/cluster_agent.fragment.graphql
new file mode 100644
index 00000000000..9b40260471c
--- /dev/null
+++ b/app/assets/javascripts/clusters_list/graphql/fragments/cluster_agent.fragment.graphql
@@ -0,0 +1,10 @@
+fragment ClusterAgentFragment on ClusterAgent {
+ id
+ name
+ webPath
+ tokens {
+ nodes {
+ lastUsedAt
+ }
+ }
+}
diff --git a/app/assets/javascripts/clusters_list/graphql/mutations/create_agent.mutation.graphql b/app/assets/javascripts/clusters_list/graphql/mutations/create_agent.mutation.graphql
index c29756159f5..996b388089b 100644
--- a/app/assets/javascripts/clusters_list/graphql/mutations/create_agent.mutation.graphql
+++ b/app/assets/javascripts/clusters_list/graphql/mutations/create_agent.mutation.graphql
@@ -1,7 +1,9 @@
+#import "../fragments/cluster_agent.fragment.graphql"
+
mutation createClusterAgent($input: CreateClusterAgentInput!) {
createClusterAgent(input: $input) {
clusterAgent {
- id
+ ...ClusterAgentFragment
}
errors
}
diff --git a/app/assets/javascripts/clusters_list/graphql/queries/get_agents.query.graphql b/app/assets/javascripts/clusters_list/graphql/queries/get_agents.query.graphql
index 61989e00d9e..47b25988877 100644
--- a/app/assets/javascripts/clusters_list/graphql/queries/get_agents.query.graphql
+++ b/app/assets/javascripts/clusters_list/graphql/queries/get_agents.query.graphql
@@ -1,4 +1,5 @@
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
+#import "../fragments/cluster_agent.fragment.graphql"
query getAgents(
$defaultBranchName: String!
@@ -13,19 +14,14 @@ query getAgents(
project(fullPath: $projectPath) {
clusterAgents(first: $first, last: $last, before: $beforeAgent, after: $afterAgent) {
nodes {
- id
- name
- webPath
- tokens {
- nodes {
- lastUsedAt
- }
- }
+ ...ClusterAgentFragment
}
pageInfo {
...PageInfo
}
+
+ count
}
repository {
diff --git a/app/assets/javascripts/clusters_list/index.js b/app/assets/javascripts/clusters_list/index.js
index de18965abbd..7f1ef37814b 100644
--- a/app/assets/javascripts/clusters_list/index.js
+++ b/app/assets/javascripts/clusters_list/index.js
@@ -1,11 +1,11 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import loadClusters from './load_clusters';
-import loadAgents from './load_agents';
+import loadMainView from './load_main_view';
Vue.use(VueApollo);
export default () => {
loadClusters(Vue);
- loadAgents(Vue, VueApollo);
+ loadMainView(Vue, VueApollo);
};
diff --git a/app/assets/javascripts/clusters_list/load_agents.js b/app/assets/javascripts/clusters_list/load_agents.js
deleted file mode 100644
index b77d386df20..00000000000
--- a/app/assets/javascripts/clusters_list/load_agents.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import createDefaultClient from '~/lib/graphql';
-import Agents from './components/agents.vue';
-
-export default (Vue, VueApollo) => {
- const el = document.querySelector('#js-cluster-agents-list');
-
- if (!el) {
- return null;
- }
-
- const defaultClient = createDefaultClient({}, { assumeImmutableResults: true });
-
- const {
- emptyStateImage,
- defaultBranchName,
- projectPath,
- agentDocsUrl,
- installDocsUrl,
- getStartedDocsUrl,
- integrationDocsUrl,
- kasAddress,
- } = el.dataset;
-
- return new Vue({
- el,
- apolloProvider: new VueApollo({ defaultClient }),
- provide: {
- emptyStateImage,
- projectPath,
- agentDocsUrl,
- installDocsUrl,
- getStartedDocsUrl,
- integrationDocsUrl,
- kasAddress,
- },
- render(createElement) {
- return createElement(Agents, {
- props: {
- defaultBranchName,
- },
- });
- },
- });
-};
diff --git a/app/assets/javascripts/clusters_list/load_clusters.js b/app/assets/javascripts/clusters_list/load_clusters.js
index 01430230879..1bb3ea546b2 100644
--- a/app/assets/javascripts/clusters_list/load_clusters.js
+++ b/app/assets/javascripts/clusters_list/load_clusters.js
@@ -8,8 +8,15 @@ export default (Vue) => {
return null;
}
+ const { emptyStateHelpText, newClusterPath, clustersEmptyStateImage } = el.dataset;
+
return new Vue({
el,
+ provide: {
+ emptyStateHelpText,
+ newClusterPath,
+ clustersEmptyStateImage,
+ },
store: createStore(el.dataset),
render(createElement) {
return createElement(Clusters);
diff --git a/app/assets/javascripts/clusters_list/load_main_view.js b/app/assets/javascripts/clusters_list/load_main_view.js
new file mode 100644
index 00000000000..08c99b46e16
--- /dev/null
+++ b/app/assets/javascripts/clusters_list/load_main_view.js
@@ -0,0 +1,50 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import ClustersMainView from './components/clusters_main_view.vue';
+import { createStore } from './store';
+
+Vue.use(VueApollo);
+
+export default () => {
+ const el = document.querySelector('.js-clusters-main-view');
+
+ if (!el) {
+ return null;
+ }
+
+ const defaultClient = createDefaultClient();
+
+ const {
+ emptyStateImage,
+ defaultBranchName,
+ projectPath,
+ kasAddress,
+ newClusterPath,
+ addClusterPath,
+ emptyStateHelpText,
+ clustersEmptyStateImage,
+ } = el.dataset;
+
+ return new Vue({
+ el,
+ apolloProvider: new VueApollo({ defaultClient }),
+ provide: {
+ emptyStateImage,
+ projectPath,
+ kasAddress,
+ newClusterPath,
+ addClusterPath,
+ emptyStateHelpText,
+ clustersEmptyStateImage,
+ },
+ store: createStore(el.dataset),
+ render(createElement) {
+ return createElement(ClustersMainView, {
+ props: {
+ defaultBranchName,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/clusters_list/store/actions.js b/app/assets/javascripts/clusters_list/store/actions.js
index 5f35a0b26f3..d70b36e63bc 100644
--- a/app/assets/javascripts/clusters_list/store/actions.js
+++ b/app/assets/javascripts/clusters_list/store/actions.js
@@ -3,7 +3,7 @@ import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import Poll from '~/lib/utils/poll';
-import { __ } from '~/locale';
+import { s__ } from '~/locale';
import { MAX_REQUESTS } from '../constants';
import * as types from './mutation_types';
@@ -30,7 +30,13 @@ export const fetchClusters = ({ state, commit, dispatch }) => {
const poll = new Poll({
resource: {
- fetchClusters: (paginatedEndPoint) => axios.get(paginatedEndPoint),
+ fetchClusters: (paginatedEndPoint) =>
+ axios.get(paginatedEndPoint, {
+ params: {
+ page: state.page,
+ per_page: state.clustersPerPage,
+ },
+ }),
},
data: `${state.endpoint}?page=${state.page}`,
method: 'fetchClusters',
@@ -65,7 +71,7 @@ export const fetchClusters = ({ state, commit, dispatch }) => {
commit(types.SET_LOADING_CLUSTERS, false);
commit(types.SET_LOADING_NODES, false);
createFlash({
- message: __('Clusters|An error occurred while loading clusters'),
+ message: s__('Clusters|An error occurred while loading clusters'),
});
dispatch('reportSentryError', { error: response, tag: 'fetchClustersErrorCallback' });
@@ -78,3 +84,7 @@ export const fetchClusters = ({ state, commit, dispatch }) => {
export const setPage = ({ commit }, page) => {
commit(types.SET_PAGE, page);
};
+
+export const setClustersPerPage = ({ commit }, limit) => {
+ commit(types.SET_CLUSTERS_PER_PAGE, limit);
+};
diff --git a/app/assets/javascripts/clusters_list/store/mutation_types.js b/app/assets/javascripts/clusters_list/store/mutation_types.js
index beb4388c93e..e88d4c74761 100644
--- a/app/assets/javascripts/clusters_list/store/mutation_types.js
+++ b/app/assets/javascripts/clusters_list/store/mutation_types.js
@@ -2,3 +2,4 @@ export const SET_CLUSTERS_DATA = 'SET_CLUSTERS_DATA';
export const SET_LOADING_CLUSTERS = 'SET_LOADING_CLUSTERS';
export const SET_LOADING_NODES = 'SET_LOADING_NODES';
export const SET_PAGE = 'SET_PAGE';
+export const SET_CLUSTERS_PER_PAGE = 'SET_CLUSTERS_PER_PAGE';
diff --git a/app/assets/javascripts/clusters_list/store/mutations.js b/app/assets/javascripts/clusters_list/store/mutations.js
index 5b462928518..93156c9200f 100644
--- a/app/assets/javascripts/clusters_list/store/mutations.js
+++ b/app/assets/javascripts/clusters_list/store/mutations.js
@@ -12,10 +12,13 @@ export default {
clusters: data.clusters,
clustersPerPage: paginationInformation.perPage,
hasAncestorClusters: data.has_ancestor_clusters,
- totalCulsters: paginationInformation.total,
+ totalClusters: paginationInformation.total,
});
},
[types.SET_PAGE](state, value) {
state.page = Number(value) || 1;
},
+ [types.SET_CLUSTERS_PER_PAGE](state, value) {
+ state.clustersPerPage = Number(value) || 1;
+ },
};
diff --git a/app/assets/javascripts/clusters_list/store/state.js b/app/assets/javascripts/clusters_list/store/state.js
index 51fafd49479..763d7389d0f 100644
--- a/app/assets/javascripts/clusters_list/store/state.js
+++ b/app/assets/javascripts/clusters_list/store/state.js
@@ -1,9 +1,11 @@
+import { parseBoolean } from '~/lib/utils/common_utils';
+
export default (initialState = {}) => ({
ancestorHelperPath: initialState.ancestorHelpPath,
endpoint: initialState.endpoint,
hasAncestorClusters: false,
clusters: [],
- clustersPerPage: 0,
+ clustersPerPage: 20,
loadingClusters: true,
loadingNodes: true,
page: 1,
@@ -12,5 +14,6 @@ export default (initialState = {}) => ({
default: { path: initialState.imgTagsDefaultPath, text: initialState.imgTagsDefaultText },
gcp: { path: initialState.imgTagsGcpPath, text: initialState.imgTagsGcpText },
},
- totalCulsters: 0,
+ totalClusters: 0,
+ canAddCluster: parseBoolean(initialState.canAddCluster),
});