diff options
Diffstat (limited to 'app/assets/javascripts/clusters_list')
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), }); |