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/environments')
-rw-r--r--app/assets/javascripts/environments/environment_details/components/deployment_job.vue24
-rw-r--r--app/assets/javascripts/environments/environment_details/components/deployment_status_link.vue26
-rw-r--r--app/assets/javascripts/environments/environment_details/components/deployment_triggerer.vue25
-rw-r--r--app/assets/javascripts/environments/environment_details/constants.js17
-rw-r--r--app/assets/javascripts/environments/environment_details/deployments_table.vue55
-rw-r--r--app/assets/javascripts/environments/environment_details/empty_state.vue34
-rw-r--r--app/assets/javascripts/environments/environment_details/index.vue169
-rw-r--r--app/assets/javascripts/environments/environment_details/pagination.vue74
-rw-r--r--app/assets/javascripts/environments/graphql/queries/environment_details.query.graphql23
-rw-r--r--app/assets/javascripts/environments/mount_show.js37
10 files changed, 402 insertions, 82 deletions
diff --git a/app/assets/javascripts/environments/environment_details/components/deployment_job.vue b/app/assets/javascripts/environments/environment_details/components/deployment_job.vue
new file mode 100644
index 00000000000..dbe25a81550
--- /dev/null
+++ b/app/assets/javascripts/environments/environment_details/components/deployment_job.vue
@@ -0,0 +1,24 @@
+<script>
+import { GlTruncate, GlLink, GlBadge } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlBadge,
+ GlTruncate,
+ GlLink,
+ },
+ props: {
+ job: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ },
+};
+</script>
+<template>
+ <gl-link v-if="job" :href="job.webPath">
+ <gl-truncate :text="job.label" />
+ </gl-link>
+ <gl-badge v-else variant="info">{{ __('API') }}</gl-badge>
+</template>
diff --git a/app/assets/javascripts/environments/environment_details/components/deployment_status_link.vue b/app/assets/javascripts/environments/environment_details/components/deployment_status_link.vue
new file mode 100644
index 00000000000..82926e2e596
--- /dev/null
+++ b/app/assets/javascripts/environments/environment_details/components/deployment_status_link.vue
@@ -0,0 +1,26 @@
+<script>
+import DeploymentStatusBadge from '~/environments/components/deployment_status_badge.vue';
+
+export default {
+ components: {
+ DeploymentStatusBadge,
+ },
+ props: {
+ status: {
+ type: String,
+ required: true,
+ },
+ deploymentJob: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ },
+};
+</script>
+<template>
+ <a v-if="deploymentJob" data-testid="deployment-status-job-link" :href="deploymentJob.webPath">
+ <deployment-status-badge :status="status" />
+ </a>
+ <deployment-status-badge v-else :status="status" />
+</template>
diff --git a/app/assets/javascripts/environments/environment_details/components/deployment_triggerer.vue b/app/assets/javascripts/environments/environment_details/components/deployment_triggerer.vue
new file mode 100644
index 00000000000..18ff31f9b0f
--- /dev/null
+++ b/app/assets/javascripts/environments/environment_details/components/deployment_triggerer.vue
@@ -0,0 +1,25 @@
+<script>
+import { GlAvatar, GlAvatarLink, GlTooltipDirective } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlAvatar,
+ GlAvatarLink,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ triggerer: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ },
+};
+</script>
+<template>
+ <gl-avatar-link v-if="triggerer" :href="triggerer.webUrl">
+ <gl-avatar v-gl-tooltip :title="triggerer.name" :src="triggerer.avatarUrl" :size="24" />
+ </gl-avatar-link>
+</template>
diff --git a/app/assets/javascripts/environments/environment_details/constants.js b/app/assets/javascripts/environments/environment_details/constants.js
index 56c70c354b7..bf690ffedeb 100644
--- a/app/assets/javascripts/environments/environment_details/constants.js
+++ b/app/assets/javascripts/environments/environment_details/constants.js
@@ -1,4 +1,5 @@
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
export const ENVIRONMENT_DETAILS_PAGE_SIZE = 20;
export const ENVIRONMENT_DETAILS_TABLE_FIELDS = [
@@ -45,3 +46,17 @@ export const ENVIRONMENT_DETAILS_TABLE_FIELDS = [
tdClass: 'gl-vertical-align-middle! gl-white-space-nowrap',
},
];
+
+export const translations = {
+ emptyStateTitle: s__("Deployments|You don't have any deployments right now."),
+ emptyStatePrimaryButton: __('Read more'),
+ emptyStateDescription: s__(
+ 'Deployments|Define environments in the deploy stage(s) in %{code_open}.gitlab-ci.yml%{code_close} to track deployments here.',
+ ),
+ nextPageButtonLabel: __('Next'),
+ previousPageButtonLabel: __('Prev'),
+};
+
+export const codeBlockPlaceholders = { code: ['code_open', 'code_close'] };
+
+export const environmentsHelpPagePath = helpPagePath('ci/environments/index.md');
diff --git a/app/assets/javascripts/environments/environment_details/deployments_table.vue b/app/assets/javascripts/environments/environment_details/deployments_table.vue
new file mode 100644
index 00000000000..41570ee44c0
--- /dev/null
+++ b/app/assets/javascripts/environments/environment_details/deployments_table.vue
@@ -0,0 +1,55 @@
+<script>
+import { GlTableLite } from '@gitlab/ui';
+import Commit from '~/vue_shared/components/commit.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import DeploymentStatusLink from './components/deployment_status_link.vue';
+import DeploymentJob from './components/deployment_job.vue';
+import DeploymentTriggerer from './components/deployment_triggerer.vue';
+import { ENVIRONMENT_DETAILS_TABLE_FIELDS } from './constants';
+
+export default {
+ components: {
+ DeploymentTriggerer,
+ DeploymentJob,
+ Commit,
+ TimeAgoTooltip,
+ DeploymentStatusLink,
+ GlTableLite,
+ },
+ props: {
+ deployments: {
+ type: Array,
+ required: true,
+ },
+ },
+ tableFields: ENVIRONMENT_DETAILS_TABLE_FIELDS,
+};
+</script>
+<template>
+ <gl-table-lite :items="deployments" :fields="$options.tableFields" fixed stacked="lg">
+ <template #table-colgroup="{ fields }">
+ <col v-for="field in fields" :key="field.key" :class="field.columnClass" />
+ </template>
+ <template #cell(status)="{ item }">
+ <deployment-status-link :deployment-job="item.job" :status="item.status" />
+ </template>
+ <template #cell(id)="{ item }">
+ <strong>{{ item.id }}</strong>
+ </template>
+ <template #cell(triggerer)="{ item }">
+ <deployment-triggerer :triggerer="item.triggerer" />
+ </template>
+ <template #cell(commit)="{ item }">
+ <commit v-bind="item.commit" />
+ </template>
+ <template #cell(job)="{ item }">
+ <deployment-job :job="item.job" />
+ </template>
+ <template #cell(created)="{ item }">
+ <time-ago-tooltip :time="item.created" />
+ </template>
+ <template #cell(deployed)="{ item }">
+ <time-ago-tooltip :time="item.deployed" />
+ </template>
+ </gl-table-lite>
+</template>
diff --git a/app/assets/javascripts/environments/environment_details/empty_state.vue b/app/assets/javascripts/environments/environment_details/empty_state.vue
new file mode 100644
index 00000000000..6f08b319408
--- /dev/null
+++ b/app/assets/javascripts/environments/environment_details/empty_state.vue
@@ -0,0 +1,34 @@
+<script>
+import { GlEmptyState, GlSprintf } from '@gitlab/ui';
+import { translations, codeBlockPlaceholders, environmentsHelpPagePath } from './constants';
+
+export default {
+ components: {
+ GlSprintf,
+ GlEmptyState,
+ },
+ translations,
+ actionButtonUrl: environmentsHelpPagePath,
+ placeholders: {
+ code: codeBlockPlaceholders,
+ },
+};
+</script>
+<template>
+ <gl-empty-state
+ :title="$options.translations.emptyStateTitle"
+ :primary-button-text="$options.translations.emptyStatePrimaryButton"
+ :primary-button-link="$options.actionButtonUrl"
+ >
+ <template #description>
+ <gl-sprintf
+ :message="$options.translations.emptyStateDescription"
+ :placeholders="$options.placeholders.code"
+ >
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ </template>
+ </gl-empty-state>
+</template>
diff --git a/app/assets/javascripts/environments/environment_details/index.vue b/app/assets/javascripts/environments/environment_details/index.vue
index 435d3fd820e..b43f4233b9c 100644
--- a/app/assets/javascripts/environments/environment_details/index.vue
+++ b/app/assets/javascripts/environments/environment_details/index.vue
@@ -1,36 +1,19 @@
<script>
-import {
- GlTableLite,
- GlAvatarLink,
- GlAvatar,
- GlLink,
- GlTooltipDirective,
- GlTruncate,
- GlBadge,
- GlLoadingIcon,
-} from '@gitlab/ui';
-import Commit from '~/vue_shared/components/commit.vue';
-import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { logError } from '~/lib/logger';
import environmentDetailsQuery from '../graphql/queries/environment_details.query.graphql';
import { convertToDeploymentTableRow } from '../helpers/deployment_data_transformation_helper';
-import DeploymentStatusBadge from '../components/deployment_status_badge.vue';
-import { ENVIRONMENT_DETAILS_PAGE_SIZE, ENVIRONMENT_DETAILS_TABLE_FIELDS } from './constants';
+import EmptyState from './empty_state.vue';
+import DeploymentsTable from './deployments_table.vue';
+import Pagination from './pagination.vue';
+import { ENVIRONMENT_DETAILS_PAGE_SIZE } from './constants';
export default {
components: {
+ Pagination,
+ DeploymentsTable,
+ EmptyState,
GlLoadingIcon,
- GlBadge,
- DeploymentStatusBadge,
- TimeAgoTooltip,
- GlTableLite,
- GlAvatarLink,
- GlAvatar,
- GlLink,
- GlTruncate,
- Commit,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
},
props: {
projectFullPath: {
@@ -41,6 +24,16 @@ export default {
type: String,
required: true,
},
+ after: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ before: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
apollo: {
project: {
@@ -49,18 +42,19 @@ export default {
return {
projectFullPath: this.projectFullPath,
environmentName: this.environmentName,
- pageSize: ENVIRONMENT_DETAILS_PAGE_SIZE,
+ first: this.before ? null : ENVIRONMENT_DETAILS_PAGE_SIZE,
+ last: this.before ? ENVIRONMENT_DETAILS_PAGE_SIZE : null,
+ after: this.after,
+ before: this.before,
};
},
},
},
data() {
return {
- project: {
- loading: true,
- },
- loading: 0,
- tableFields: ENVIRONMENT_DETAILS_TABLE_FIELDS,
+ project: {},
+ isInitialPageDataReceived: false,
+ isPrefetchingPages: false,
};
},
computed: {
@@ -70,49 +64,80 @@ export default {
isLoading() {
return this.$apollo.queries.project.loading;
},
+ isDeploymentTableShown() {
+ return this.isInitialPageDataReceived === true && this.deployments.length > 0;
+ },
+ pageInfo() {
+ return this.project.environment?.deployments.pageInfo || {};
+ },
+ isPaginationDisabled() {
+ return this.isLoading || this.isPrefetchingPages;
+ },
+ },
+ watch: {
+ async project(newProject) {
+ this.isInitialPageDataReceived = true;
+ this.isPrefetchingPages = true;
+
+ try {
+ // TLDR: when we load a page, if there's next and/or previous pages existing, we'll load their data as well to improve percepted performance.
+ const {
+ endCursor,
+ hasPreviousPage,
+ hasNextPage,
+ startCursor,
+ } = newProject.environment.deployments.pageInfo;
+
+ // At the moment we have a limit of deployments being requested only from a signle environment entity per query,
+ // and apparently two batched queries count as one on server-side
+ // to load both next and previous page data, we have to query them sequentially
+ if (hasNextPage) {
+ await this.$apollo.query({
+ query: environmentDetailsQuery,
+ variables: {
+ projectFullPath: this.projectFullPath,
+ environmentName: this.environmentName,
+ first: ENVIRONMENT_DETAILS_PAGE_SIZE,
+ after: endCursor,
+ before: null,
+ last: null,
+ },
+ });
+ }
+
+ if (hasPreviousPage) {
+ await this.$apollo.query({
+ query: environmentDetailsQuery,
+ variables: {
+ projectFullPath: this.projectFullPath,
+ environmentName: this.environmentName,
+ first: null,
+ after: null,
+ before: startCursor,
+ last: ENVIRONMENT_DETAILS_PAGE_SIZE,
+ },
+ });
+ }
+ } catch (error) {
+ logError(error);
+ }
+
+ this.isPrefetchingPages = false;
+ },
},
};
</script>
<template>
- <div>
- <gl-loading-icon v-if="isLoading" size="lg" class="mt-3" />
- <gl-table-lite v-else :items="deployments" :fields="tableFields" fixed stacked="lg">
- <template #table-colgroup="{ fields }">
- <col v-for="field in fields" :key="field.key" :class="field.columnClass" />
- </template>
- <template #cell(status)="{ item }">
- <div>
- <deployment-status-badge :status="item.status" />
- </div>
- </template>
- <template #cell(id)="{ item }">
- <strong>{{ item.id }}</strong>
- </template>
- <template #cell(triggerer)="{ item }">
- <gl-avatar-link :href="item.triggerer.webUrl">
- <gl-avatar
- v-gl-tooltip
- :title="item.triggerer.name"
- :src="item.triggerer.avatarUrl"
- :size="24"
- />
- </gl-avatar-link>
- </template>
- <template #cell(commit)="{ item }">
- <commit v-bind="item.commit" />
- </template>
- <template #cell(job)="{ item }">
- <gl-link v-if="item.job" :href="item.job.webPath">
- <gl-truncate :text="item.job.label" />
- </gl-link>
- <gl-badge v-else variant="info">{{ __('API') }}</gl-badge>
- </template>
- <template #cell(created)="{ item }">
- <time-ago-tooltip :time="item.created" />
- </template>
- <template #cell(deployed)="{ item }">
- <time-ago-tooltip :time="item.deployed" />
- </template>
- </gl-table-lite>
+ <div class="gl-relative gl-min-h-6">
+ <div
+ v-if="isLoading"
+ class="gl-absolute gl-top-0 gl-left-0 gl-w-full gl-h-full gl-z-index-200 gl-bg-gray-10 gl-opacity-3"
+ ></div>
+ <gl-loading-icon v-if="isLoading" size="lg" class="gl-absolute gl-top-half gl-left-50p" />
+ <div v-if="isDeploymentTableShown">
+ <deployments-table :deployments="deployments" />
+ <pagination :page-info="pageInfo" :disabled="isPaginationDisabled" />
+ </div>
+ <empty-state v-if="!isDeploymentTableShown && !isLoading" />
</div>
</template>
diff --git a/app/assets/javascripts/environments/environment_details/pagination.vue b/app/assets/javascripts/environments/environment_details/pagination.vue
new file mode 100644
index 00000000000..414610b306a
--- /dev/null
+++ b/app/assets/javascripts/environments/environment_details/pagination.vue
@@ -0,0 +1,74 @@
+<script>
+import { GlKeysetPagination } from '@gitlab/ui';
+import { setUrlParams } from '~/lib/utils/url_utility';
+import { translations } from './constants';
+
+export default {
+ components: {
+ GlKeysetPagination,
+ },
+ props: {
+ pageInfo: {
+ type: Object,
+ required: true,
+ },
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ translations,
+ computed: {
+ previousLink() {
+ if (!this.pageInfo || !this.pageInfo.hasPreviousPage) {
+ return '';
+ }
+ return setUrlParams({ before: this.pageInfo.startCursor }, window.location.href, true);
+ },
+ nextLink() {
+ if (!this.pageInfo || !this.pageInfo.hasNextPage) {
+ return '';
+ }
+ return setUrlParams({ after: this.pageInfo.endCursor }, window.location.href, true);
+ },
+ isPaginationVisible() {
+ if (!this.pageInfo) {
+ return false;
+ }
+
+ return this.pageInfo.hasNextPage || this.pageInfo.hasPreviousPage;
+ },
+ },
+ methods: {
+ onPrev(previousCursor) {
+ this.$router.push({ query: { before: previousCursor } });
+ },
+ onNext(nextCursor) {
+ this.$router.push({ query: { after: nextCursor } });
+ },
+ onPaginationClick(event) {
+ // this check here is to ensure the proper default behvaior when a user ctrl/cmd + clicks the link
+ if (event.shiftKey || event.ctrlKey || event.altKey || event.metaKey) {
+ return;
+ }
+ event.preventDefault();
+ },
+ },
+};
+</script>
+<template>
+ <div v-if="isPaginationVisible" class="gl--flex-center">
+ <gl-keyset-pagination
+ v-bind="pageInfo"
+ :prev-text="$options.translations.previousPageButtonLabel"
+ :next-text="$options.translations.nextPageButtonLabel"
+ :prev-button-link="previousLink"
+ :next-button-link="nextLink"
+ :disabled="disabled"
+ @prev="onPrev"
+ @next="onNext"
+ @click="onPaginationClick"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/environments/graphql/queries/environment_details.query.graphql b/app/assets/javascripts/environments/graphql/queries/environment_details.query.graphql
index e8f2a2cdf7f..c6c2024c840 100644
--- a/app/assets/javascripts/environments/graphql/queries/environment_details.query.graphql
+++ b/app/assets/javascripts/environments/graphql/queries/environment_details.query.graphql
@@ -1,4 +1,11 @@
-query getEnvironmentDetails($projectFullPath: ID!, $environmentName: String, $pageSize: Int) {
+query getEnvironmentDetails(
+ $projectFullPath: ID!
+ $environmentName: String
+ $first: Int
+ $last: Int
+ $after: String
+ $before: String
+) {
project(fullPath: $projectFullPath) {
id
name
@@ -6,7 +13,19 @@ query getEnvironmentDetails($projectFullPath: ID!, $environmentName: String, $pa
environment(name: $environmentName) {
id
name
- deployments(orderBy: { createdAt: DESC }, first: $pageSize) {
+ deployments(
+ orderBy: { createdAt: DESC }
+ first: $first
+ last: $last
+ after: $after
+ before: $before
+ ) {
+ pageInfo {
+ startCursor
+ endCursor
+ hasNextPage
+ hasPreviousPage
+ }
nodes {
id
iid
diff --git a/app/assets/javascripts/environments/mount_show.js b/app/assets/javascripts/environments/mount_show.js
index ba816599ac2..afce2b7f237 100644
--- a/app/assets/javascripts/environments/mount_show.js
+++ b/app/assets/javascripts/environments/mount_show.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import VueRouter from 'vue-router';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import EnvironmentsDetailHeader from './components/environments_detail_header.vue';
import { apolloProvider } from './graphql/client';
@@ -43,7 +44,7 @@ export const initHeader = () => {
cancelAutoStopPath: dataset.environmentCancelAutoStopPath,
terminalPath: dataset.environmentTerminalPath,
metricsPath: dataset.environmentMetricsPath,
- updatePath: dataset.tnvironmentEditPath,
+ updatePath: dataset.environmentEditPath,
},
});
},
@@ -60,18 +61,40 @@ export const initPage = async () => {
const dataSet = convertObjectPropsToCamelCase(JSON.parse(dataElement.dataset.details));
Vue.use(VueApollo);
+ Vue.use(VueRouter);
const el = document.getElementById('environment_details_page');
+
+ const router = new VueRouter({
+ mode: 'history',
+ base: window.location.pathname,
+ routes: [
+ {
+ path: '/',
+ name: 'environment_details',
+ component: EnvironmentsDetailPage,
+ props: (route) => ({
+ after: route.query.after,
+ before: route.query.before,
+ projectFullPath: dataSet.projectFullPath,
+ environmentName: dataSet.name,
+ }),
+ },
+ ],
+ scrollBehavior(to, from, savedPosition) {
+ if (savedPosition) {
+ return savedPosition;
+ }
+ return { top: 0 };
+ },
+ });
+
return new Vue({
el,
apolloProvider: apolloProvider(),
+ router,
provide: {},
render(createElement) {
- return createElement(EnvironmentsDetailPage, {
- props: {
- projectFullPath: dataSet.projectFullPath,
- environmentName: dataSet.name,
- },
- });
+ return createElement('router-view');
},
});
};