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:
-rw-r--r--.gitlab/ci/build-images.gitlab-ci.yml3
-rw-r--r--.gitlab/ci/qa.gitlab-ci.yml3
-rw-r--r--.gitlab/ci/review.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/rules.gitlab-ci.yml27
-rw-r--r--app/assets/javascripts/environments/components/environments_detail_header.vue174
-rw-r--r--app/assets/javascripts/environments/mixins/environments_mixin.js14
-rw-r--r--app/assets/javascripts/environments/mount_show.js38
-rw-r--r--app/assets/javascripts/pages/admin/runners/index/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/environments/show/index.js4
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/cleanup_status.vue6
-rw-r--r--app/assets/javascripts/runner/admin_runners/admin_runners_app.vue (renamed from app/assets/javascripts/runner/runner_list/runner_list_app.vue)6
-rw-r--r--app/assets/javascripts/runner/admin_runners/index.js (renamed from app/assets/javascripts/runner/runner_list/index.js)6
-rw-r--r--app/assets/javascripts/runner/runner_search_utils.js (renamed from app/assets/javascripts/runner/runner_list/runner_search_utils.js)2
-rw-r--r--app/assets/stylesheets/framework/files.scss18
-rw-r--r--app/helpers/environment_helper.rb27
-rw-r--r--app/presenters/gitlab/blame_presenter.rb7
-rw-r--r--app/views/admin/runners/index.html.haml2
-rw-r--r--app/views/projects/environments/_external_url.html.haml4
-rw-r--r--app/views/projects/environments/_metrics_button.html.haml7
-rw-r--r--app/views/projects/environments/_pin_button.html.haml3
-rw-r--r--app/views/projects/environments/_terminal_button.html.haml3
-rw-r--r--app/views/projects/environments/show.html.haml54
-rw-r--r--config/feature_flags/development/runner_registration_control.yml2
-rw-r--r--doc/administration/monitoring/performance/grafana_configuration.md26
-rw-r--r--doc/subscriptions/quarterly_reconciliation.md20
-rw-r--r--doc/subscriptions/self_managed/index.md253
-rw-r--r--doc/user/admin_area/analytics/dev_ops_report.md2
-rw-r--r--doc/user/group/devops_adoption/index.md2
-rw-r--r--locale/gitlab.pot14
-rw-r--r--qa/qa.rb1
-rw-r--r--qa/qa/page/project/deployments/environments/show.rb23
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/pipeline/pass_dotenv_variables_to_downstream_via_bridge_spec.rb2
-rwxr-xr-xscripts/static-analysis22
-rwxr-xr-xscripts/trigger-build2
-rw-r--r--spec/features/projects/environments/environment_metrics_spec.rb8
-rw-r--r--spec/features/projects/environments/environment_spec.rb47
-rw-r--r--spec/frontend/environments/environments_detail_header_spec.js238
-rw-r--r--spec/frontend/environments/mock_data.js20
-rw-r--r--spec/frontend/registry/explorer/components/list_page/cleanup_status_spec.js4
-rw-r--r--spec/frontend/runner/admin_runners/admin_runners_app_spec.js (renamed from spec/frontend/runner/runner_list/runner_list_app_spec.js)8
-rw-r--r--spec/frontend/runner/runner_search_utils_spec.js (renamed from spec/frontend/runner/runner_list/runner_search_utils_spec.js)2
-rw-r--r--spec/helpers/environment_helper_spec.rb37
42 files changed, 832 insertions, 315 deletions
diff --git a/.gitlab/ci/build-images.gitlab-ci.yml b/.gitlab/ci/build-images.gitlab-ci.yml
index 853f92ed98d..0169f017063 100644
--- a/.gitlab/ci/build-images.gitlab-ci.yml
+++ b/.gitlab/ci/build-images.gitlab-ci.yml
@@ -25,10 +25,9 @@ build-qa-image:
- .build-images:rules:build-qa-image
stage: build-images
needs: []
- variables:
- QA_IMAGE: "${CI_REGISTRY}/${CI_PROJECT_PATH}/gitlab-ee-qa:${CI_COMMIT_REF_SLUG}"
script:
- !reference [.base-image-build, script]
+ - echo $QA_IMAGE
- /kaniko/executor --context=${CI_PROJECT_DIR} --dockerfile=${CI_PROJECT_DIR}/qa/Dockerfile --destination=${QA_IMAGE} --cache=true
# This image is used by:
diff --git a/.gitlab/ci/qa.gitlab-ci.yml b/.gitlab/ci/qa.gitlab-ci.yml
index 8bbd7dbf075..c3e6de76894 100644
--- a/.gitlab/ci/qa.gitlab-ci.yml
+++ b/.gitlab/ci/qa.gitlab-ci.yml
@@ -51,9 +51,10 @@ update-qa-cache:
image: ${GITLAB_DEPENDENCY_PROXY}ruby:2.7-alpine
stage: qa
retry: 0
- script:
+ before_script:
- source scripts/utils.sh
- install_gitlab_gem
+ script:
- ./scripts/trigger-build omnibus
package-and-qa:
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml
index 38d59af5aed..2b8c0dce162 100644
--- a/.gitlab/ci/review.gitlab-ci.yml
+++ b/.gitlab/ci/review.gitlab-ci.yml
@@ -114,7 +114,7 @@ review-stop:
extends:
- .use-docker-in-docker
image:
- name: ${CI_REGISTRY}/${CI_PROJECT_PATH}/gitlab-ee-qa:${CI_COMMIT_REF_SLUG}
+ name: ${QA_IMAGE}
entrypoint: [""]
stage: qa
needs: ["build-qa-image", "review-deploy"]
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index 48a8b9348ab..c74865b9fc5 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -205,6 +205,7 @@
- "{,ee/,jh/}spec/support/helpers/database/**/*"
- "config/prometheus/common_metrics.yml" # Used by Gitlab::DatabaseImporters::CommonMetrics::Importer
- "{,ee/,jh/}app/models/project_statistics.rb" # Used to calculate sizes in migration specs
+ - "GITALY_SERVER_VERSION" # Has interactions with background migrations:https://gitlab.com/gitlab-org/gitlab/-/issues/336538
# CI changes
- ".gitlab-ci.yml"
- ".gitlab/ci/**/*"
@@ -363,11 +364,19 @@
when: never
- <<: *if-dot-com-gitlab-org-and-security-merge-request
changes: *ci-build-images-patterns
+ variables:
+ QA_IMAGE: "${CI_REGISTRY}/${CI_PROJECT_PATH}/gitlab-ee-qa:${CI_MERGE_REQUEST_SOURCE_BRANCH_SHA}"
- <<: *if-dot-com-gitlab-org-and-security-merge-request
changes: *code-qa-patterns
+ variables:
+ QA_IMAGE: "${CI_REGISTRY}/${CI_PROJECT_PATH}/gitlab-ee-qa:${CI_MERGE_REQUEST_SOURCE_BRANCH_SHA}"
- <<: *if-dot-com-gitlab-org-default-branch
changes: *code-qa-patterns
+ variables:
+ QA_IMAGE: "${CI_REGISTRY}/${CI_PROJECT_PATH}/gitlab-ee-qa:${CI_COMMIT_SHA}"
- <<: *if-dot-com-gitlab-org-schedule
+ variables:
+ QA_IMAGE: "${CI_REGISTRY}/${CI_PROJECT_PATH}/gitlab-ee-qa:${CI_COMMIT_SHA}"
.build-images:rules:build-assets-image:
rules:
@@ -579,16 +588,24 @@
when: never
- <<: *if-dot-com-gitlab-org-and-security-merge-request
changes: *ci-qa-patterns
+ variables:
+ QA_IMAGE: "${CI_REGISTRY}/${CI_PROJECT_PATH}/gitlab-ee-qa:${CI_MERGE_REQUEST_SOURCE_BRANCH_SHA}"
allow_failure: true
- <<: *if-dot-com-gitlab-org-and-security-merge-request
changes: *qa-patterns
+ variables:
+ QA_IMAGE: "${CI_REGISTRY}/${CI_PROJECT_PATH}/gitlab-ee-qa:${CI_MERGE_REQUEST_SOURCE_BRANCH_SHA}"
allow_failure: true
- <<: *if-dot-com-gitlab-org-and-security-merge-request
changes: *code-patterns
when: manual
+ variables:
+ QA_IMAGE: "${CI_REGISTRY}/${CI_PROJECT_PATH}/gitlab-ee-qa:${CI_MERGE_REQUEST_SOURCE_BRANCH_SHA}"
allow_failure: true
- <<: *if-dot-com-gitlab-org-schedule
allow_failure: true
+ variables:
+ QA_IMAGE: "${CI_REGISTRY}/${CI_PROJECT_PATH}/gitlab-ee-qa:${CI_COMMIT_SHA}"
###############
# Rails rules #
@@ -1201,11 +1218,17 @@
when: never
- <<: *if-dot-com-gitlab-org-merge-request
changes: *ci-review-patterns
+ variables:
+ QA_IMAGE: "${CI_REGISTRY}/${CI_PROJECT_PATH}/gitlab-ee-qa:${CI_MERGE_REQUEST_SOURCE_BRANCH_SHA}"
- <<: *if-dot-com-gitlab-org-merge-request
changes: *frontend-patterns
+ variables:
+ QA_IMAGE: "${CI_REGISTRY}/${CI_PROJECT_PATH}/gitlab-ee-qa:${CI_MERGE_REQUEST_SOURCE_BRANCH_SHA}"
allow_failure: true
- <<: *if-dot-com-gitlab-org-merge-request
changes: *code-qa-patterns
+ variables:
+ QA_IMAGE: "${CI_REGISTRY}/${CI_PROJECT_PATH}/gitlab-ee-qa:${CI_MERGE_REQUEST_SOURCE_BRANCH_SHA}"
allow_failure: true
# The rule needs to be duplicated between `on_success` and `on_failure`
@@ -1241,9 +1264,13 @@
- <<: *if-dot-com-gitlab-org-merge-request
changes: *code-patterns
when: manual
+ variables:
+ QA_IMAGE: "${CI_REGISTRY}/${CI_PROJECT_PATH}/gitlab-ee-qa:${CI_MERGE_REQUEST_SOURCE_BRANCH_SHA}"
allow_failure: true
- <<: *if-dot-com-gitlab-org-merge-request
changes: *qa-patterns
+ variables:
+ QA_IMAGE: "${CI_REGISTRY}/${CI_PROJECT_PATH}/gitlab-ee-qa:${CI_MERGE_REQUEST_SOURCE_BRANCH_SHA}"
allow_failure: true
# The rule needs to be duplicated between `on_success` and `on_failure`
diff --git a/app/assets/javascripts/environments/components/environments_detail_header.vue b/app/assets/javascripts/environments/components/environments_detail_header.vue
new file mode 100644
index 00000000000..467c89fd8b8
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environments_detail_header.vue
@@ -0,0 +1,174 @@
+<script>
+import { GlButton, GlModalDirective, GlTooltipDirective as GlTooltip, GlSprintf } from '@gitlab/ui';
+import csrf from '~/lib/utils/csrf';
+import { __, s__ } from '~/locale';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
+import DeleteEnvironmentModal from './delete_environment_modal.vue';
+import StopEnvironmentModal from './stop_environment_modal.vue';
+
+export default {
+ name: 'EnvironmentsDetailHeader',
+ csrf,
+ components: {
+ GlButton,
+ GlSprintf,
+ TimeAgo,
+ DeleteEnvironmentModal,
+ StopEnvironmentModal,
+ },
+ directives: {
+ GlModalDirective,
+ GlTooltip,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ environment: {
+ type: Object,
+ required: true,
+ },
+ canReadEnvironment: {
+ type: Boolean,
+ required: true,
+ },
+ canAdminEnvironment: {
+ type: Boolean,
+ required: true,
+ },
+ canUpdateEnvironment: {
+ type: Boolean,
+ required: true,
+ },
+ canDestroyEnvironment: {
+ type: Boolean,
+ required: true,
+ },
+ canStopEnvironment: {
+ type: Boolean,
+ required: true,
+ },
+ cancelAutoStopPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ metricsPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ updatePath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ terminalPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ i18n: {
+ autoStopAtText: s__('Environments|Auto stops %{autoStopAt}'),
+ metricsButtonTitle: __('See metrics'),
+ metricsButtonText: __('Monitoring'),
+ editButtonText: __('Edit'),
+ stopButtonText: s__('Environments|Stop'),
+ deleteButtonText: s__('Environments|Delete'),
+ externalButtonTitle: s__('Environments|Open live environment'),
+ externalButtonText: __('View deployment'),
+ cancelAutoStopButtonTitle: __('Prevent environment from auto-stopping'),
+ },
+ computed: {
+ shouldShowCancelAutoStopButton() {
+ return this.environment.isAvailable && Boolean(this.environment.autoStopAt);
+ },
+ shouldShowExternalUrlButton() {
+ return this.canReadEnvironment && Boolean(this.environment.externalUrl);
+ },
+ shouldShowStopButton() {
+ return this.canStopEnvironment && this.environment.isAvailable;
+ },
+ shouldShowTerminalButton() {
+ return this.canAdminEnvironment && this.environment.hasTerminals;
+ },
+ },
+};
+</script>
+<template>
+ <header class="top-area gl-justify-content-between">
+ <div class="gl-display-flex gl-flex-grow-1 gl-align-items-center">
+ <h3 class="page-title">
+ {{ environment.name }}
+ </h3>
+ <p v-if="shouldShowCancelAutoStopButton" class="gl-mb-0 gl-ml-3" data-testid="auto-stops-at">
+ <gl-sprintf :message="$options.i18n.autoStopAtText">
+ <template #autoStopAt>
+ <time-ago :time="environment.autoStopAt" />
+ </template>
+ </gl-sprintf>
+ </p>
+ </div>
+ <div class="nav-controls gl-my-1">
+ <form method="POST" :action="cancelAutoStopPath" data-testid="cancel-auto-stop-form">
+ <input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
+ <gl-button
+ v-if="shouldShowCancelAutoStopButton"
+ v-gl-tooltip.hover
+ data-testid="cancel-auto-stop-button"
+ :title="$options.i18n.cancelAutoStopButtonTitle"
+ type="submit"
+ icon="thumbtack"
+ />
+ </form>
+ <gl-button
+ v-if="shouldShowTerminalButton"
+ data-testid="terminal-button"
+ :href="terminalPath"
+ icon="terminal"
+ />
+ <gl-button
+ v-if="shouldShowExternalUrlButton"
+ v-gl-tooltip.hover
+ data-testid="external-url-button"
+ :title="$options.i18n.externalButtonTitle"
+ :href="environment.externalUrl"
+ icon="external-link"
+ target="_blank"
+ >{{ $options.i18n.externalButtonText }}</gl-button
+ >
+ <gl-button
+ v-if="canReadEnvironment"
+ data-testid="metrics-button"
+ :href="metricsPath"
+ :title="$options.i18n.metricsButtonTitle"
+ icon="chart"
+ class="gl-mr-2"
+ >
+ {{ $options.i18n.metricsButtonText }}
+ </gl-button>
+ <gl-button v-if="canUpdateEnvironment" data-testid="edit-button" :href="updatePath">
+ {{ $options.i18n.editButtonText }}
+ </gl-button>
+ <gl-button
+ v-if="shouldShowStopButton"
+ v-gl-modal-directive="'stop-environment-modal'"
+ data-testid="stop-button"
+ icon="stop"
+ variant="danger"
+ >
+ {{ $options.i18n.stopButtonText }}
+ </gl-button>
+ <gl-button
+ v-if="canDestroyEnvironment"
+ v-gl-modal-directive="'delete-environment-modal'"
+ data-testid="destroy-button"
+ variant="danger"
+ >
+ {{ $options.i18n.deleteButtonText }}
+ </gl-button>
+ </div>
+ <delete-environment-modal v-if="canDestroyEnvironment" :environment="environment" />
+ <stop-environment-modal v-if="shouldShowStopButton" :environment="environment" />
+ </header>
+</template>
diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js
index 6f701f87261..85cff73cc3e 100644
--- a/app/assets/javascripts/environments/mixins/environments_mixin.js
+++ b/app/assets/javascripts/environments/mixins/environments_mixin.js
@@ -108,7 +108,19 @@ export default {
this.service
.postAction(endpoint)
- .then(() => this.fetchEnvironments())
+ .then(() => {
+ // Originally, the detail page buttons were implemented as <form>s that POSTed
+ // to the server, which would naturally result in a page refresh.
+ // When environment details page was converted to Vue, the buttons were updated to trigger
+ // HTTP requests using `axios`, which did not cause a refresh on completion.
+ // To preserve the original behavior, we manually reload the page when
+ // network requests complete successfully.
+ if (!this.isDetailView) {
+ this.fetchEnvironments();
+ } else {
+ window.location.reload();
+ }
+ })
.catch((err) => {
this.isLoading = false;
createFlash({
diff --git a/app/assets/javascripts/environments/mount_show.js b/app/assets/javascripts/environments/mount_show.js
index d0b68b0c14f..f1c2dfec94b 100644
--- a/app/assets/javascripts/environments/mount_show.js
+++ b/app/assets/javascripts/environments/mount_show.js
@@ -1,30 +1,48 @@
import Vue from 'vue';
-import DeleteEnvironmentModal from './components/delete_environment_modal.vue';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import EnvironmentsDetailHeader from './components/environments_detail_header.vue';
import environmentsMixin from './mixins/environments_mixin';
-export default () => {
- const el = document.getElementById('delete-environment-modal');
+export const initHeader = () => {
+ const el = document.getElementById('environments-detail-view-header');
const container = document.getElementById('environments-detail-view');
+ const dataset = convertObjectPropsToCamelCase(JSON.parse(container.dataset.details));
return new Vue({
el,
- components: {
- DeleteEnvironmentModal,
- },
mixins: [environmentsMixin],
data() {
- const environment = JSON.parse(JSON.stringify(container.dataset));
- environment.delete_path = environment.deletePath;
- environment.onSingleEnvironmentPage = true;
+ const environment = {
+ name: dataset.name,
+ id: Number(dataset.id),
+ externalUrl: dataset.externalUrl,
+ isAvailable: dataset.isEnvironmentAvailable,
+ hasTerminals: dataset.hasTerminals,
+ autoStopAt: dataset.autoStopAt,
+ onSingleEnvironmentPage: true,
+ // TODO: These two props are snake_case because the environments_mixin file uses
+ // them and the mixin is imported in several files. It would be nice to conver them to camelCase.
+ stop_path: dataset.environmentStopPath,
+ delete_path: dataset.environmentDeletePath,
+ };
return {
environment,
};
},
render(createElement) {
- return createElement('delete-environment-modal', {
+ return createElement(EnvironmentsDetailHeader, {
props: {
environment: this.environment,
+ canDestroyEnvironment: dataset.canDestroyEnvironment,
+ canUpdateEnvironment: dataset.canUpdateEnvironment,
+ canReadEnvironment: dataset.canReadEnvironment,
+ canStopEnvironment: dataset.canStopEnvironment,
+ canAdminEnvironment: dataset.canAdminEnvironment,
+ cancelAutoStopPath: dataset.environmentCancelAutoStopPath,
+ terminalPath: dataset.environmentTerminalPath,
+ metricsPath: dataset.environmentMetricsPath,
+ updatePath: dataset.environmentEditPath,
},
});
},
diff --git a/app/assets/javascripts/pages/admin/runners/index/index.js b/app/assets/javascripts/pages/admin/runners/index/index.js
index d5563470394..8e7861c300a 100644
--- a/app/assets/javascripts/pages/admin/runners/index/index.js
+++ b/app/assets/javascripts/pages/admin/runners/index/index.js
@@ -2,7 +2,7 @@ import AdminRunnersFilteredSearchTokenKeys from '~/filtered_search/admin_runners
import { FILTERED_SEARCH } from '~/pages/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import { initInstallRunner } from '~/pages/shared/mount_runner_instructions';
-import { initRunnerList } from '~/runner/runner_list';
+import { initAdminRunners } from '~/runner/admin_runners';
initFilteredSearch({
page: FILTERED_SEARCH.ADMIN_RUNNERS,
@@ -13,5 +13,5 @@ initFilteredSearch({
initInstallRunner();
if (gon.features?.runnerListViewVueUi) {
- initRunnerList();
+ initAdminRunners();
}
diff --git a/app/assets/javascripts/pages/projects/environments/show/index.js b/app/assets/javascripts/pages/projects/environments/show/index.js
index a4960037eaa..cd3a0d62bb6 100644
--- a/app/assets/javascripts/pages/projects/environments/show/index.js
+++ b/app/assets/javascripts/pages/projects/environments/show/index.js
@@ -1,3 +1,3 @@
-import initShowEnvironment from '~/environments/mount_show';
+import { initHeader } from '~/environments/mount_show';
-initShowEnvironment();
+initHeader();
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/cleanup_status.vue b/app/assets/javascripts/registry/explorer/components/list_page/cleanup_status.vue
index 8d9e221af4c..1f52e319ad0 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/cleanup_status.vue
+++ b/app/assets/javascripts/registry/explorer/components/list_page/cleanup_status.vue
@@ -1,7 +1,7 @@
<script>
import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import {
- ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
+ CLEANUP_TIMED_OUT_ERROR_MESSAGE,
CLEANUP_STATUS_SCHEDULED,
CLEANUP_STATUS_ONGOING,
CLEANUP_STATUS_UNFINISHED,
@@ -34,7 +34,7 @@ export default {
CLEANUP_STATUS_SCHEDULED,
CLEANUP_STATUS_ONGOING,
CLEANUP_STATUS_UNFINISHED,
- ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
+ CLEANUP_TIMED_OUT_ERROR_MESSAGE,
},
computed: {
showStatus() {
@@ -61,7 +61,7 @@ export default {
</span>
<gl-icon
v-if="failedDelete"
- v-gl-tooltip="{ title: $options.i18n.ASYNC_DELETE_IMAGE_ERROR_MESSAGE }"
+ v-gl-tooltip="{ title: $options.i18n.CLEANUP_TIMED_OUT_ERROR_MESSAGE }"
:size="14"
class="gl-text-black-normal"
data-testid="extra-info"
diff --git a/app/assets/javascripts/runner/runner_list/runner_list_app.vue b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
index 8d39243d609..23ecee449a4 100644
--- a/app/assets/javascripts/runner/runner_list/runner_list_app.vue
+++ b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
@@ -9,15 +9,15 @@ import RunnerPagination from '../components/runner_pagination.vue';
import RunnerTypeHelp from '../components/runner_type_help.vue';
import { INSTANCE_TYPE, I18N_FETCH_ERROR } from '../constants';
import getRunnersQuery from '../graphql/get_runners.query.graphql';
-import { captureException } from '../sentry_utils';
import {
fromUrlQueryToSearch,
fromSearchToUrl,
fromSearchToVariables,
-} from './runner_search_utils';
+} from '../runner_search_utils';
+import { captureException } from '../sentry_utils';
export default {
- name: 'RunnerListApp',
+ name: 'AdminRunnersApp',
components: {
RunnerFilteredSearchBar,
RunnerList,
diff --git a/app/assets/javascripts/runner/runner_list/index.js b/app/assets/javascripts/runner/admin_runners/index.js
index 16616f00d1e..1eec1019b73 100644
--- a/app/assets/javascripts/runner/runner_list/index.js
+++ b/app/assets/javascripts/runner/admin_runners/index.js
@@ -1,11 +1,11 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
-import RunnerDetailsApp from './runner_list_app.vue';
+import AdminRunnersApp from './admin_runners_app.vue';
Vue.use(VueApollo);
-export const initRunnerList = (selector = '#js-runner-list') => {
+export const initAdminRunners = (selector = '#js-admin-runners') => {
const el = document.querySelector(selector);
if (!el) {
@@ -32,7 +32,7 @@ export const initRunnerList = (selector = '#js-runner-list') => {
runnerInstallHelpPage,
},
render(h) {
- return h(RunnerDetailsApp, {
+ return h(AdminRunnersApp, {
props: {
activeRunnersCount: parseInt(activeRunnersCount, 10),
registrationToken,
diff --git a/app/assets/javascripts/runner/runner_list/runner_search_utils.js b/app/assets/javascripts/runner/runner_search_utils.js
index 9a0dc9c3a32..65f75eb11ac 100644
--- a/app/assets/javascripts/runner/runner_list/runner_search_utils.js
+++ b/app/assets/javascripts/runner/runner_search_utils.js
@@ -16,7 +16,7 @@ import {
PARAM_KEY_BEFORE,
DEFAULT_SORT,
RUNNER_PAGE_SIZE,
-} from '../constants';
+} from './constants';
const getPaginationFromParams = (params) => {
const page = parseInt(params[PARAM_KEY_PAGE], 10);
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 5ad7ceecb2b..465b1a80fa4 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -509,6 +509,24 @@ span.idiff {
}
}
+.version-link {
+ @include gl-display-inline-block;
+ @include gl-align-self-center;
+ @include gl-mt-2;
+ @include gl-w-5;
+ @include gl-h-5;
+ @include gl-float-left;
+ background-color: $gray-400;
+ mask-image: asset_url('icons-stacked.svg#doc-versions');
+ mask-repeat: no-repeat;
+ mask-size: cover;
+ mask-position: center;
+
+ &:hover {
+ background-color: $black;
+ }
+}
+
//
// IMPORTANT PERFORMANCE OPTIMIZATION BELOW
//
diff --git a/app/helpers/environment_helper.rb b/app/helpers/environment_helper.rb
index 491d2731e91..3f23f73eed7 100644
--- a/app/helpers/environment_helper.rb
+++ b/app/helpers/environment_helper.rb
@@ -65,4 +65,31 @@ module EnvironmentHelper
content_tag(:span, text, class: klass)
end
end
+
+ def environments_detail_data(user, project, environment)
+ {
+ name: environment.name,
+ id: environment.id,
+ external_url: environment.external_url,
+ can_update_environment: can?(current_user, :update_environment, environment),
+ can_destroy_environment: can_destroy_environment?(environment),
+ can_read_environment: can?(current_user, :read_environment, environment),
+ can_stop_environment: can?(current_user, :stop_environment, environment),
+ can_admin_environment: can?(current_user, :admin_environment, project),
+ environment_metrics_path: environment_metrics_path(environment),
+ environments_fetch_path: project_environments_path(project, format: :json),
+ environment_edit_path: edit_project_environment_path(project, environment),
+ environment_stop_path: stop_project_environment_path(project, environment),
+ environment_delete_path: environment_delete_path(environment),
+ environment_cancel_auto_stop_path: cancel_auto_stop_project_environment_path(project, environment),
+ environment_terminal_path: terminal_project_environment_path(project, environment),
+ has_terminals: environment.has_terminals?,
+ is_environment_available: environment.available?,
+ auto_stop_at: environment.auto_stop_at
+ }
+ end
+
+ def environments_detail_data_json(user, project, environment)
+ environments_detail_data(user, project, environment).to_json
+ end
end
diff --git a/app/presenters/gitlab/blame_presenter.rb b/app/presenters/gitlab/blame_presenter.rb
index 26c78384144..1f2445b04a1 100644
--- a/app/presenters/gitlab/blame_presenter.rb
+++ b/app/presenters/gitlab/blame_presenter.rb
@@ -66,17 +66,14 @@ module Gitlab
link_to project_blame_path(project, tree_join(previous_commit_id, path)),
title: _('View blame prior to this change'),
aria: { label: _('View blame prior to this change') },
+ class: 'version-link',
data: { toggle: 'tooltip', placement: 'right', container: 'body' } do
- versions_sprite_icon
+ '&nbsp;'.html_safe
end
end
def project_duration
@project_duration ||= age_map_duration(groups, project)
end
-
- def versions_sprite_icon
- @versions_sprite_icon ||= sprite_icon('doc-versions', css_class: 'doc-versions align-text-bottom')
- end
end
end
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index f9c52d9316b..5dce44a787c 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -2,7 +2,7 @@
- page_title _('Runners')
- if Feature.enabled?(:runner_list_view_vue_ui, current_user, default_enabled: :yaml)
- #js-runner-list{ data: { registration_token: Gitlab::CurrentSettings.runners_registration_token, runner_install_help_page: 'https://docs.gitlab.com/runner/install/', active_runners_count: @active_runners_count } }
+ #js-admin-runners{ data: { registration_token: Gitlab::CurrentSettings.runners_registration_token, runner_install_help_page: 'https://docs.gitlab.com/runner/install/', active_runners_count: @active_runners_count } }
- else
.row
.col-sm-6
diff --git a/app/views/projects/environments/_external_url.html.haml b/app/views/projects/environments/_external_url.html.haml
deleted file mode 100644
index b9208969fb3..00000000000
--- a/app/views/projects/environments/_external_url.html.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-- if environment.external_url && can?(current_user, :read_environment, environment)
- = link_to environment.external_url, target: '_blank', rel: 'noopener noreferrer', class: 'gl-button btn external-url has-tooltip qa-view-deployment', title: s_('Environments|Open live environment') do
- = sprite_icon('external-link')
- = _("View deployment")
diff --git a/app/views/projects/environments/_metrics_button.html.haml b/app/views/projects/environments/_metrics_button.html.haml
deleted file mode 100644
index 65abaf44082..00000000000
--- a/app/views/projects/environments/_metrics_button.html.haml
+++ /dev/null
@@ -1,7 +0,0 @@
-- environment = local_assigns.fetch(:environment)
-
-- return unless can?(current_user, :read_environment, environment)
-
-= link_to environment_metrics_path(environment), title: _('See metrics'), class: 'gl-button btn metrics-button' do
- = sprite_icon('chart', css_class: 'gl-mr-2')
- = _("Monitoring")
diff --git a/app/views/projects/environments/_pin_button.html.haml b/app/views/projects/environments/_pin_button.html.haml
deleted file mode 100644
index ec3e7e20365..00000000000
--- a/app/views/projects/environments/_pin_button.html.haml
+++ /dev/null
@@ -1,3 +0,0 @@
-- if environment.auto_stop_at? && environment.available?
- = button_to cancel_auto_stop_project_environment_path(environment.project, environment), class: 'gl-button btn btn-secondary has-tooltip', title: _('Prevent environment from auto-stopping') do
- = sprite_icon('thumbtack')
diff --git a/app/views/projects/environments/_terminal_button.html.haml b/app/views/projects/environments/_terminal_button.html.haml
deleted file mode 100644
index ab3363bbb07..00000000000
--- a/app/views/projects/environments/_terminal_button.html.haml
+++ /dev/null
@@ -1,3 +0,0 @@
-- if environment.has_terminals? && can?(current_user, :admin_environment, @project)
- = link_to terminal_project_environment_path(@project, environment), class: 'gl-button btn terminal-button' do
- = sprite_icon('terminal')
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index b3e4b7a4998..b123b81b89c 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -5,58 +5,8 @@
- add_page_specific_style 'page_bundles/environments'
- add_page_specific_style 'page_bundles/ci_status'
-#environments-detail-view{ data: { name: @environment.name, id: @environment.id, delete_path: environment_delete_path(@environment)} }
- - if @environment.available? && can?(current_user, :stop_environment, @environment)
- #stop-environment-modal.modal.fade{ tabindex: -1 }
- .modal-dialog
- .modal-content
- .modal-header
- %h4.modal-title.d-flex.mw-100
- = s_("Environments|Stopping")
- %span.has-tooltip.text-truncate.ml-1.mr-1.flex-fill{ title: @environment.name, data: { container: '#stop-environment-modal' } }
- #{@environment.name}?
- .modal-body
- %p= s_('Environments|Are you sure you want to stop this environment?')
- - unless @environment.stop_action_available?
- .warning_message
- %p= s_('Environments|Note that this action will stop the environment, but it will %{emphasis_start}not%{emphasis_end} have an effect on any existing deployment due to no “stop environment action” being defined in the %{ci_config_link_start}.gitlab-ci.yml%{ci_config_link_end} file.').html_safe % { emphasis_start: '<strong>'.html_safe,
- emphasis_end: '</strong>'.html_safe,
- ci_config_link_start: '<a href="https://docs.gitlab.com/ee/ci/yaml/" target="_blank" rel="noopener noreferrer">'.html_safe,
- ci_config_link_end: '</a>'.html_safe }
- %a{ href: 'https://docs.gitlab.com/ee/ci/environments/index.html#stopping-an-environment',
- target: '_blank',
- rel: 'noopener noreferrer' }
- = s_('Environments|Learn more about stopping environments')
- .modal-footer
- = button_tag _('Cancel'), type: 'button', class: 'gl-button btn btn-cancel', data: { dismiss: 'modal' }
- = button_to stop_project_environment_path(@project, @environment), class: 'gl-button btn btn-danger has-tooltip', method: :post do
- = s_('Environments|Stop environment')
-
- - if can_destroy_environment?(@environment)
- #delete-environment-modal
-
- .top-area.justify-content-between
- .d-flex
- %h3.page-title= @environment.name
- - if @environment.auto_stop_at?
- %p.align-self-end.gl-ml-3
- = s_('Environments|Auto stops %{auto_stop_time}').html_safe % {auto_stop_time: time_ago_with_tooltip(@environment.auto_stop_at)}
- .nav-controls.my-2
- = render 'projects/environments/pin_button', environment: @environment
- = render 'projects/environments/terminal_button', environment: @environment
- = render 'projects/environments/external_url', environment: @environment
- = render 'projects/environments/metrics_button', environment: @environment
- - if can?(current_user, :update_environment, @environment)
- = link_to _('Edit'), edit_project_environment_path(@project, @environment), class: 'btn'
- - if @environment.available? && can?(current_user, :stop_environment, @environment)
- = button_tag class: 'gl-button btn btn-danger', type: 'button', data: { toggle: 'modal',
- target: '#stop-environment-modal' } do
- = sprite_icon('stop')
- = s_('Environments|Stop')
- - if can_destroy_environment?(@environment)
- = button_tag class: 'gl-button btn btn-danger', type: 'button', data: { toggle: 'modal',
- target: '#delete-environment-modal' } do
- = s_('Environments|Delete')
+#environments-detail-view{ data: { details: environments_detail_data_json(current_user, @project, @environment) } }
+ #environments-detail-view-header
.environments-container
- if @deployments.blank?
diff --git a/config/feature_flags/development/runner_registration_control.yml b/config/feature_flags/development/runner_registration_control.yml
index 21edb90474d..56c01bf36e5 100644
--- a/config/feature_flags/development/runner_registration_control.yml
+++ b/config/feature_flags/development/runner_registration_control.yml
@@ -1,7 +1,7 @@
---
name: runner_registration_control
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61407
-rollout_issue_url:
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/336087
milestone: '14.1'
type: development
group: group::runner
diff --git a/doc/administration/monitoring/performance/grafana_configuration.md b/doc/administration/monitoring/performance/grafana_configuration.md
index 3b82b0e4bb8..6de60162d46 100644
--- a/doc/administration/monitoring/performance/grafana_configuration.md
+++ b/doc/administration/monitoring/performance/grafana_configuration.md
@@ -74,6 +74,32 @@ GitLab sidebar:
GitLab displays your link in the **Menu > Admin > Monitoring > Metrics Dashboard**.
+## Required Scopes
+
+> [Introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5822) in GitLab 13.10.
+
+When setting up Grafana through the process above, no scope shows in the screen at
+**Menu >** **{admin}** **Admin > Applications > GitLab Grafana**. However, the `read_user` scope is
+required and is provided to the application automatically. Note that setting any scope other than
+`read_user` without also including `read_user` leads to this error when you try to log in using
+GitLab as the OAuth provider:
+
+```plaintext
+The requested scope is invalid, unknown, or malformed.
+```
+
+If you see this error, make sure that one of the following is true in the GitLab Grafana
+configuration screen:
+
+- No scopes appear.
+- The `read_user` scope is included.
+
+> Versions of GitLab prior 13.10 use the API scope instead of `read_user`. In versions of GitLab
+> prior to 13.10, the API scope:
+>
+> - Is required to access Grafana through the GitLab OAuth provider.
+> - Is set by enabling the Grafana application as shown in [Integration with GitLab UI](#integration-with-gitlab-ui).
+
## Security Update
Users running GitLab version 12.0 or later should immediately upgrade to one of the
diff --git a/doc/subscriptions/quarterly_reconciliation.md b/doc/subscriptions/quarterly_reconciliation.md
new file mode 100644
index 00000000000..af9fec1ddc4
--- /dev/null
+++ b/doc/subscriptions/quarterly_reconciliation.md
@@ -0,0 +1,20 @@
+---
+stage: Fulfillment
+group: Purchase
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
+---
+
+# The quarterly subscription reconciliation process
+
+GitLab reviews your seat usage every quarter and sends you an invoice for
+any overages.
+
+Annual reviews, also known as the [annual true-up process](self_managed/index.md#users-over-license),
+require you to pay the full annual subscription fee for users added anytime during the year. With quarterly
+reviews, you only pay for the remaining period of your subscription term.
+
+For example, if you add users in the third quarter of your subscription term, you only
+pay 25% of what you would have paid previously. This results in substantial savings.
+
+If it's not possible for you to participate in quarterly reconciliations, you can opt out of the
+process by using a contract amendment. In that case, you default to the annual review.
diff --git a/doc/subscriptions/self_managed/index.md b/doc/subscriptions/self_managed/index.md
index ecaeec35dd1..1ddecc77d2f 100644
--- a/doc/subscriptions/self_managed/index.md
+++ b/doc/subscriptions/self_managed/index.md
@@ -39,9 +39,11 @@ for each tier, see the
A GitLab self-managed subscription uses a hybrid model. You pay for a subscription
according to the maximum number of users enabled during the subscription period.
-For instances that are offline or on a closed network, the maximum number of
-simultaneous users in the GitLab self-managed installation is checked each quarter,
-using [Seat Link](#seat-link).
+For instances that aren't offline or on a closed network, the maximum number of
+simultaneous users in the GitLab self-managed installation is checked each quarter.
+
+If an instance is unable to generate a quarterly usage report, the existing [true-up model](#users-over-license) is used.
+Prorated charges are not possible without a quarterly usage report.
### Billable users
@@ -77,6 +79,140 @@ GitLab has several features which can help you manage the number of users:
users manually.
- View a breakdown of users by role in the [Users statistics](../../user/admin_area/index.md#users-statistics) page.
+## Cloud licensing
+
+> Introduced in GitLab 14.1.
+
+Cloud licensing manages licenses for self-managed GitLab subscription plans. Cloud licensing includes:
+
+- Activation: Unlock plan features and activate your self-managed instance by using an activation code.
+- License sync: Sync subscription data between your self-managed instance and GitLab.
+
+### What cloud licensing includes
+
+#### Auto-renewals
+
+For renewals that occur on or after 2021-08-01, your subscriptions will auto-renew.
+You have the option to manually cancel in the Customers Portal any time until thirty (30) days before renewal.
+
+#### Cloud licensing
+
+You can activate and manage your GitLab licenses by using the Customers Portal.
+This feature was formerly known as Seat Link.
+
+#### Operational data
+
+Usage data helps GitLab improve the product experience and provide proactive support.
+Most data is categorized as optional and can be disabled. Data that is categorized as
+operational, like number of issues, pipelines, merge requests, and version, is not configurable.
+
+Please see our [service usage privacy page](https://about.gitlab.com/handbook/legal/privacy/services-usage-data/)
+for details on what information is collected.
+
+#### Quarterly subscription reconciliation
+
+See the [quarterly subscription reconciliation section](../quarterly_reconciliation.md) for more information.
+
+### How cloud licensing works
+
+#### Activate your license
+
+1. When you purchase a GitLab self-managed plan, an activation code is generated.
+ This activation code is sent to the email address associated with the Customers Portal account.
+1. In GitLab, on the top bar, select **Menu >** **{admin}** **Admin**.
+1. On the left sidebar, select **Subscription** and paste the activation code in the text field.
+1. Select **Activate**.
+
+The page displays the details of the subscription.
+
+#### License sync
+
+Once a day, a job sends license data to the Customers Portal. This information automates activation,
+provisioning, co-terms, and renewals. The data is sent securely through an encrypted HTTPS connection
+to `customers.gitlab.com` on port `443`.
+
+This sync job runs daily around 3AM UTC. If the job fails, it is retried up to 12 times over approximately 17 hours.
+
+The daily job provides **only** the following information to the Customers Portal:
+
+- Date
+- Timestamp
+- License key
+- Historical maximum user count
+- Billable users count
+- GitLab version
+- Hostname
+- Instance ID
+- MD5 hash of license
+
+<details>
+<summary>Click here to view an example of a cloud licensing sync request.</summary>
+<pre><code>
+{
+ "gitlab_version": "14.1.0-pre",
+ "timestamp": "2021-06-14T12:00:09Z",
+ "date": "2021-06-14",
+ "license_key": "eyJkYXRhIjoiYlR2MFBPSEJPSnNOc1plbGtFRGZ6M
+ Ex1mWWhyM1Y3NWFOU0Zj\nak1xTmtLZHU1YzJJUWJzZzVxT3FQRU1PXG5
+ KRzErL2ZNd0JuKzBwZmQ3YnY4\nTkFrTDFsMFZyQi9NcG5DVEdkTXQyNT
+ R3NlR0ZEc0MjBoTTVna2VORlVcbjAz\nbUgrNGl5N0NuenRhZlljd096R
+ nUzd2JIWEZ3NzV2V2lqb3FuQ3RYZWppWVFU\neDdESkgwSUIybFJhZlxu
+ Y2k0Mzl3RWlKYjltMkJoUzExeGIwWjN3Uk90ZGp1\nNXNNT3dtL0Vtc3l
+ zWVowSHE3ekFILzBjZ2FXSXVQXG5ENWJwcHhOZzRlcFhr\neFg0K3d6Zk
+ w3cHRQTTJMTGdGb2Vwai90S0VJL0ZleXhxTEhvaUc2NzVIbHRp\nVlRcb
+ nYzY090bmhsdTMrc0VGZURJQ3VmcXFFUS9ISVBqUXRhL3ZTbW9SeUNh\n
+ SjdDTkU4YVJnQTlBMEF5OFBiZlxuT0VORWY5WENQVkREdUMvTTVCb25Re
+ ENv\nK0FrekFEWWJ6VGZLZ1dBRjgzUXhyelJWUVJGTTErWm9TeTQ4XG5V
+ aWdXV0d4\nQ2graGtoSXQ1eXdTaUFaQzBtZGd2aG1YMnl1KzltcU9WMUx
+ RWXE4a2VSOHVn\nV3BMN1VFNThcbnMvU3BtTk1JZk5YUHhOSmFlVHZqUz
+ lXdjlqMVZ6ODFQQnFx\nL1phaTd6MFBpdG5NREFOVnpPK3h4TE5CQ1xub
+ GtacHNRdUxTZmtWWEZVUnB3\nWTZtWGdhWE5GdXhURjFndWhyVDRlTE92
+ bTR3bW1ac0pCQnBkVWJIRGNyXG5z\nUjVsTWJxZEVUTXJNRXNDdUlWVlZ
+ CTnJZVTA2M2dHblc4eVNXZTc0enFUcW1V\nNDBrMUZpN3RTdzBaZjBcbm
+ 16UGNYV0RoelpkVk02cWR1dTl0Q1VqU05tWWlU\nOXlwRGZFaEhXZWhjb
+ m50RzA5UWVjWEM5em52Y1BjU1xueFU0MDMvVml5R3du\nQXNMTHkyajN5
+ b3hhTkJUSWpWQ1BMUjdGeThRSEVnNGdBd0x6RkRHVWg1M0Qz\nMHFRXG5
+ 5eWtXdHNHN3VBREdCNmhPODFJanNSZnEreDhyb2ZpVU5JVXo4NCtD\nem
+ Z1V1Q0K1l1VndPTngyc1l0TU5cbi9WTzlaaVdPMFhtMkZzM2g1NlVXcGI
+ y\nSUQzRnRlbW5vZHdLOWU4L0tiYWRESVRPQmgzQnIxbDNTS2tHN1xuQ3
+ hpc29D\nNGh4UW5mUmJFSmVoQkh6eHV1dkY5aG11SUsyVmVDQm1zTXZCY
+ nZQNGdDbHZL\ndUExWnBEREpDXG41eEhEclFUd3E1clRYS2VuTjhkd3BU
+ SnVLQXgvUjlQVGpy\ncHJLNEIzdGNMK0xIN2JKcmhDOTlabnAvLzZcblZ
+ HbXk5SzJSZERIcXp3U2c3\nQjFwSmFPcFBFUHhOUFJxOUtnY2hVR0xWMF
+ d0Rk9vPVxuIiwia2V5IjoiUURM\nNU5paUdoRlVwZzkwNC9lQWg5bFY0Q
+ 3pkc2tSQjBDeXJUbG1ZNDE2eEpPUzdM\nVXkrYXRhTFdpb0lTXG5sTWlR
+ WEU3MVY4djFJaENnZHJGTzJsTUpHbUR5VHY0\ndWlSc1FobXZVWEhpL3h
+ vb1J4bW9XbzlxK2Z1OGFcblB6anp1TExhTEdUQVdJ\nUDA5Z28zY3JCcz
+ ZGOEVLV28xVzRGWWtUUVh2TzM0STlOSjVHR1RUeXkzVkRB\nc1xubUdRe
+ jA2eCtNNkFBM1VxTUJLZXRMUXRuNUN2R3l3T1VkbUx0eXZNQ3JX\nSWVQ
+ TElrZkJwZHhPOUN5Z1dCXG44UkpBdjRSQ1dkMlFhWVdKVmxUMllRTXc5\
+ nL29LL2hFNWRQZ1pLdWEyVVZNRWMwRkNlZzg5UFZrQS9mdDVcbmlETWlh
+ YUZz\nakRVTUl5SjZSQjlHT2ovZUdTRTU5NVBBMExKcFFiVzFvZz09XG4
+ iLCJpdiI6\nImRGSjl0YXlZWit2OGlzbGgyS2ZxYWc9PVxuIn0=\n",
+ "max_historical_user_count": 75,
+ "billable_users_count": 75,
+ "hostname": "gitlab.example.com",
+ "instance_id": "9367590b-82ad-48cb-9da7-938134c29088",
+ "license_md5": "002f02470fe45ef6a333a4282aca6222"
+}
+</code></pre>
+</details>
+
+#### Sync subscription details
+
+You can manually sync your subscription details at any time.
+
+1. On the top bar, select **Menu >** **{admin}** **Admin**.
+1. On the left sidebar, select **Subscription**.
+1. In the **Subscription details** section, select **Sync subscription details**.
+
+A job is queued. When the job finishes, the subscription details are updated.
+
+#### Troubleshooting cloud licensing sync
+
+If the sync job is not working, ensure you allow network traffic from your GitLab instance
+to IP address `104.46.106.135:443` (`customers.gitlab.com`).
+
## Obtain a subscription
To subscribe to GitLab through a GitLab self-managed installation:
@@ -201,117 +337,6 @@ We recommend following these steps during renewal:
An invoice is generated for the renewal and available for viewing or download on the [View invoices](https://customers.gitlab.com/receipts) page. If you have difficulty during the renewal process, contact our [support team](https://support.gitlab.com/hc/en-us/requests/new?ticket_form_id=360000071293) for assistance.
-### Seat Link
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/208832) in GitLab 12.9.
-
-Seat Link allows GitLab Inc. to provide our GitLab self-managed customers with prorated charges for user growth throughout the year using a quarterly reconciliation process.
-
-Seat Link daily sends a count of all users in connected GitLab self-managed instances to GitLab. That information is used to automate prorated reconciliations. The data is sent securely through an encrypted HTTPS connection to `customers.gitlab.com` on port `443`.
-
-Seat Link provides **only** the following information to GitLab:
-
-- Date
-- Timestamp
-- License key
-- Historical maximum user count
-- Billable users count
-- GitLab version
-- Hostname
-- Instance ID
-- MD5 hash of license
-
-For offline or closed network customers, the existing [true-up model](#users-over-license) is used. Prorated charges are not possible without user count data.
-
-<details>
-<summary>Click here to view example content of a Seat Link POST request.</summary>
-
-<pre><code>
-{
- gitlab_version: '13.12.0',
- timestamp: '2020-01-29T18:25:57+00:00',
- date: '2020-01-29',
- license_key: 'ZXlKa1lYUmhJam9pWm5WNmVsTjVZekZ2YTJoV2NucDBh
-RXRxTTA5amQxcG1VMVZqDQpXR3RwZEc5SGIyMVhibmxuZDJ0NWFrNXJTVzVH
-UzFCT1hHNVRiVFIyT0ZaUFlVSm1OV1ZGV0VObE1uVk4NCk4xY3ZkM1F4Y2to
-MFFuVklXSFJvUWpSM01VdE9SVE5rYkVjclZrdDJORkpOTlhka01qaE5aalpj
-YmxSMg0KWVd3MFNFTldTRmRtV1ZGSGRDOUhPR05oUVZvNUsxVnRXRUZIZFU1
-U1VqUm5aVFZGZUdwTWIxbDFZV1EyDQphV1JTY1V4c1ZYSjNPVGhrYVZ4dVlu
-TkpWMHRJZUU5dmF6ZEJRVVkxTlVWdFUwMTNSMGRHWm5SNlJFcFYNClQyVkJl
-VXc0UzA0NWFFb3ZlSFJrZW0xbVRqUlZabkZ4U1hWcWNXRnZYRzVaTm5GSmVW
-UnJVR1JQYTJKdA0KU0ZZclRHTmFPRTVhZEVKMUt6UjRkSE15WkRCT1UyNWlS
-MGRJZDFCdmRFWk5Za2h4Tm5sT1VsSktlVlYyDQpXRmhjYmxSeU4wRnRNMU5q
-THpCVWFGTmpTMnh3UWpOWVkyc3pkbXBST1dnelZHY3hUV3hxVDIwdlZYRlQN
-Ck9EWTJSVWx4WlVOT01EQXhVRlZ3ZGs1Rk0xeHVSVEJTTDFkMWJUQTVhV1ZK
-WjBORFdWUktaRXNyVnpsTw0KTldkWWQwWTNZa05VWlZBMmRUVk9kVUpxT1hV
-Mk5VdDFTUzk0TUU5V05XbFJhWGh0WEc1cVkyWnhaeTlXDQpTMEpyZWt0cmVY
-bzBOVGhFVG1oU1oxSm5WRFprY0Uwck0wZEdhVUpEV1d4a1RXZFRjVU5tYTB0
-a2RteEQNCmNWTlFSbFpuWlZWY2JpdFVVbXhIV0d4MFRuUnRWbkJKTkhwSFJt
-TnRaMGsyV0U1MFFUUXJWMUJVTWtOSA0KTVhKUWVGTkxPVTkzV1VsMlVUUldk
-R3hNTWswNU1USlNjRnh1U1UxTGJTdHRRM1l5YTFWaWJtSlBTMkUxDQplRkpL
-SzJSckszaG1hVXB1ZVRWT1UwdHZXV0ZOVG1WamMyVjRPV0pSUlZkUU9UUnpU
-VWh2Wlc5cFhHNUgNClNtRkdVMDUyY1RGMWNGTnhVbU5JUkZkeGVWcHVRMnBh
-VTBSUGR6VnRNVGhvWTFBM00zVkZlVzFOU0djMA0KY1ZFM1FWSlplSFZ5UzFS
-aGIxTmNia3BSUFQxY2JpSXNJbxRsZVNJNkltZFhiVzFGVkRZNWNFWndiV2Rt
-DQpNWEIyY21SbFFrdFNZamxaYURCdVVHcHhiRlV3Tm1WQ2JGSlFaSFJ3Y0Rs
-cFMybGhSMnRPTkZOMWNVNU0NClVGeHVTa3N6TUUxcldVOTVWREl6WVVWdk5U
-ZGhWM1ZvVjJkSFRtZFBZVXRJTkVGcE55dE1NRE5dWnpWeQ0KWlV0aWJsVk9T
-RmRzVVROUGRHVXdWR3hEWEc1MWjWaEtRMGQ2YTAxWFpUZHJURTVET0doV00w
-ODRWM0V2DQphV2M1YWs5cWFFWk9aR3BYTm1aVmJXNUNaazlXVUVRMWRrMXpj
-bTFDV0V4dldtRmNibFpTTWpWU05VeFMNClEwTjRNMWxWCUtSVGEzTTJaV2xE
-V0hKTFRGQmpURXRsZFVaQlNtRnJTbkpPZGtKdlUyUmlNVWxNWWpKaQ0KT0dw
-c05YbE1kVnh1YzFWbk5VZDFhbU56ZUM5Tk16TXZUakZOVW05cVpsVTNObEo0
-TjJ4eVlVUkdkWEJtDQpkSHByYWpreVJrcG9UVlo0Y0hKSU9URndiV2RzVFdO
-VlhHNXRhVmszTkV0SVEzcEpNMWRyZEVoRU4ydHINCmRIRnFRVTlCVUVVM1pV
-SlRORE4xUjFaYVJGb3JlWGM5UFZ4dUlpd2lhWFlpt2lKV00yRnNVbk5RTjJk
-Sg0KU1hNMGExaE9SVGR2V2pKQlBUMWNiaUo5DQo=',
- hostname: 'gitlab.example.com',
- instance_id: 'c1ac02cb-cb3f-4120-b7fe-961bbfa3abb7',
- license_md5: '7cd897fffb3517dddf01b79a0889b515'
-}
-</code></pre>
-
-</details>
-
-You can view the exact JSON payload in the administration panel. To view the payload:
-
-1. On the top bar, select **Menu >** **{admin}** **Admin**.
-1. On the left sidebar, select **Settings > Metrics and profiling** and expand **Seat Link**.
-1. Select **Preview payload**.
-
-#### Disable Seat Link
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/212375) in GitLab 12.10.
-
-Seat Link is enabled by default.
-
-To disable this feature:
-
-1. On the top bar, select **Menu >** **{admin}** **Admin**.
-1. On the left sidebar, select **Settings > Metrics and profiling** and expand **Seat Link**.
-1. Clear the **Enable Seat Link** checkbox.
-1. Select **Save changes**.
-
-To disable Seat Link in an Omnibus GitLab installation, and prevent it from
-being configured in the future through the administration panel, set the following in
-[`gitlab.rb`](https://docs.gitlab.com/omnibus/settings/configuration.html#configuration-options):
-
-```ruby
-gitlab_rails['seat_link_enabled'] = false
-```
-
-To disable Seat Link in a GitLab source installation, and prevent it from
-being configured in the future through the administration panel,
-set the following in `gitlab.yml`:
-
-```yaml
-production: &base
- # ...
- gitlab:
- # ...
- seat_link_enabled: false
-```
-
## Upgrade your subscription tier
To upgrade your [GitLab tier](https://about.gitlab.com/pricing/):
diff --git a/doc/user/admin_area/analytics/dev_ops_report.md b/doc/user/admin_area/analytics/dev_ops_report.md
index 89d8a5ea054..592b93796a6 100644
--- a/doc/user/admin_area/analytics/dev_ops_report.md
+++ b/doc/user/admin_area/analytics/dev_ops_report.md
@@ -46,6 +46,7 @@ feature is available.
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#disable-or-enable-devops-adoption). **(ULTIMATE SELF)**
> - The Overview tab [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/330401) in GitLab 14.1.
> - DAST and SAST metrics [added](https://gitlab.com/gitlab-org/gitlab/-/issues/328033) in GitLab 14.1.
+> - Fuzz Testing metrics [added](https://gitlab.com/gitlab-org/gitlab/-/issues/330398) in GitLab 14.2.
DevOps Adoption shows you which groups in your organization are using the most essential features of GitLab:
@@ -57,6 +58,7 @@ DevOps Adoption shows you which groups in your organization are using the most e
- Sec
- DAST
- SAST
+ - Fuzz Testing
- Ops
- Deployments
- Pipelines
diff --git a/doc/user/group/devops_adoption/index.md b/doc/user/group/devops_adoption/index.md
index 4332f261481..948fca00593 100644
--- a/doc/user/group/devops_adoption/index.md
+++ b/doc/user/group/devops_adoption/index.md
@@ -10,6 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/333556) in GitLab 14.1.
> - The Overview tab [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/330401) in GitLab 14.1.
> - DAST and SAST metrics [added](https://gitlab.com/gitlab-org/gitlab/-/issues/328033) in GitLab 14.1.
+> - Fuzz Testing metrics [added](https://gitlab.com/gitlab-org/gitlab/-/issues/330398) in GitLab 14.2.
Prerequisites:
@@ -27,6 +28,7 @@ Group DevOps Adoption shows you how individual groups and sub-groups within your
- Sec
- DAST
- SAST
+ - Fuzz Testing
- Ops
- Deployments
- Pipelines
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 669900d1bf6..28f10728179 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -11267,6 +11267,12 @@ msgstr ""
msgid "DevopsAdoption|Feature adoption is based on usage in the previous calendar month. Last updated: %{timestamp}."
msgstr ""
+msgid "DevopsAdoption|Fuzz Testing"
+msgstr ""
+
+msgid "DevopsAdoption|Fuzz Testing enabled for at least one project"
+msgstr ""
+
msgid "DevopsAdoption|Issues"
msgstr ""
@@ -12445,7 +12451,7 @@ msgstr ""
msgid "Environments|Auto stop in"
msgstr ""
-msgid "Environments|Auto stops %{auto_stop_time}"
+msgid "Environments|Auto stops %{autoStopAt}"
msgstr ""
msgid "Environments|Commit"
@@ -12526,9 +12532,6 @@ msgstr ""
msgid "Environments|Note that this action will stop the environment, but it will %{emphasisStart}not%{emphasisEnd} have an effect on any existing deployment due to no “stop environment action” being defined in the %{ciConfigLinkStart}.gitlab-ci.yml%{ciConfigLinkEnd} file."
msgstr ""
-msgid "Environments|Note that this action will stop the environment, but it will %{emphasis_start}not%{emphasis_end} have an effect on any existing deployment due to no “stop environment action” being defined in the %{ci_config_link_start}.gitlab-ci.yml%{ci_config_link_end} file."
-msgstr ""
-
msgid "Environments|Open live environment"
msgstr ""
@@ -12571,9 +12574,6 @@ msgstr ""
msgid "Environments|Stop environment"
msgstr ""
-msgid "Environments|Stopping"
-msgstr ""
-
msgid "Environments|Stopping %{environmentName}"
msgstr ""
diff --git a/qa/qa.rb b/qa/qa.rb
index 566effb3cd9..dccb95613f1 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -385,7 +385,6 @@ module QA
module Deployments
module Environments
autoload :Index, 'qa/page/project/deployments/environments/index'
- autoload :Show, 'qa/page/project/deployments/environments/show'
end
end
diff --git a/qa/qa/page/project/deployments/environments/show.rb b/qa/qa/page/project/deployments/environments/show.rb
deleted file mode 100644
index 48e4850d3be..00000000000
--- a/qa/qa/page/project/deployments/environments/show.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-module QA
- module Page
- module Project
- module Deployments
- module Environments
- class Show < Page::Base
- view 'app/views/projects/environments/_external_url.html.haml' do
- element :view_deployment
- end
-
- def view_deployment(&block)
- new_window = window_opened_by { click_element(:view_deployment) }
-
- within_window(new_window, &block) if block
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pass_dotenv_variables_to_downstream_via_bridge_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pass_dotenv_variables_to_downstream_via_bridge_spec.rb
index c1625f1e679..adacedb36ab 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pass_dotenv_variables_to_downstream_via_bridge_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pass_dotenv_variables_to_downstream_via_bridge_spec.rb
@@ -41,7 +41,7 @@ module QA
after do
runner.remove_via_api!
- group.remove_via_api!
+ [upstream_project, downstream_project].each(&:remove_via_api!)
end
it 'runs the pipeline with composed config', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1086' do
diff --git a/scripts/static-analysis b/scripts/static-analysis
index fc917f1b975..a1859254459 100755
--- a/scripts/static-analysis
+++ b/scripts/static-analysis
@@ -20,22 +20,20 @@ class StaticAnalysis
# contain values that a FOSS installation won't find. To work
# around this we will only enable this task on EE installations.
TASKS_BY_DURATIONS_SECONDS_DESC = {
- %w[bin/rake lint:haml] => 488,
- (Gitlab.ee? ? %w[bin/rake gettext:updated_check] : nil) => 410,
- # Most of the time, RuboCop finishes in 30 seconds, but sometimes it can take around 1200 seconds so we set a
- # duration of 300 to lower the likelihood that it will run in the same job as another long task...
- %w[bundle exec rubocop --parallel --except Gitlab/MarkUsedFeatureFlags] => 300,
+ %w[bin/rake lint:haml] => 800,
# We need to disable the cache for this cop since it creates files under tmp/feature_flags/*.used,
# the cache would prevent these files from being created.
%w[bundle exec rubocop --only Gitlab/MarkUsedFeatureFlags --cache false] => 600,
- %w[yarn run lint:eslint:all] => 264,
- %w[yarn run lint:prettier] => 134,
- %w[bin/rake gettext:lint] => 81,
- %w[bundle exec license_finder] => 49,
- %w[bin/rake lint:static_verification] => 24,
- %w[bin/rake gitlab:sidekiq:all_queues_yml:check] => 12,
+ (Gitlab.ee? ? %w[bin/rake gettext:updated_check] : nil) => 360,
+ %w[yarn run lint:eslint:all] => 312,
+ %w[yarn run lint:prettier] => 162,
+ %w[bin/rake gettext:lint] => 65,
+ %w[bundle exec license_finder] => 61,
+ %w[bin/rake lint:static_verification] => 45,
+ %w[bundle exec rubocop --parallel] => 40,
+ %w[bin/rake config_lint] => 26,
+ %w[bin/rake gitlab:sidekiq:all_queues_yml:check] => 15,
(Gitlab.ee? ? %w[bin/rake gitlab:sidekiq:sidekiq_queues_yml:check] : nil) => 11,
- %w[bin/rake config_lint] => 11,
%w[yarn run internal:stylelint] => 8,
%w[scripts/lint-conflicts.sh] => 1,
%w[yarn run block-dependencies] => 1,
diff --git a/scripts/trigger-build b/scripts/trigger-build
index cb235677b5d..c65fb5b5f76 100755
--- a/scripts/trigger-build
+++ b/scripts/trigger-build
@@ -143,7 +143,7 @@ module Trigger
{
'GITLAB_VERSION' => source_sha,
'IMAGE_TAG' => source_sha,
- 'QA_IMAGE' => "#{ENV['CI_REGISTRY']}/#{ENV['CI_PROJECT_PATH']}/gitlab-ee-qa:#{ENV['CI_COMMIT_REF_SLUG']}",
+ 'QA_IMAGE' => ENV['QA_IMAGE'],
'SKIP_QA_DOCKER' => 'true',
'ALTERNATIVE_SOURCES' => 'true',
'SECURITY_SOURCES' => Trigger.security? ? 'true' : 'false',
diff --git a/spec/features/projects/environments/environment_metrics_spec.rb b/spec/features/projects/environments/environment_metrics_spec.rb
index e8f197b67c2..ee5afb22109 100644
--- a/spec/features/projects/environments/environment_metrics_spec.rb
+++ b/spec/features/projects/environments/environment_metrics_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe 'Environment > Metrics' do
shared_examples 'has environment selector' do
it 'has a working environment selector', :js do
- click_link('See metrics')
+ click_link 'Monitoring'
expect(page).to have_current_path(project_metrics_dashboard_path(project, environment: environment.id))
expect(page).to have_css('[data-qa-selector="environments_dropdown"]')
@@ -55,10 +55,10 @@ RSpec.describe 'Environment > Metrics' do
create(:deployment, environment: environment, deployable: build)
end
- it 'shows metrics' do
- click_link('See metrics')
+ it 'shows metrics', :js do
+ click_link 'Monitoring'
- expect(page).to have_css('div#prometheus-graphs')
+ expect(page).to have_css('[data-qa-selector="prometheus_graphs"]')
end
it_behaves_like 'has environment selector'
diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
index fea054de64e..5320f68b525 100644
--- a/spec/features/projects/environments/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -27,20 +27,6 @@ RSpec.describe 'Environment' do
visit_environment(environment)
end
- it 'shows environment name' do
- expect(page).to have_content(environment.name)
- end
-
- context 'without auto-stop' do
- it 'does not show auto-stop text' do
- expect(page).not_to have_content('Auto stops')
- end
-
- it 'does not show auto-stop button' do
- expect(page).not_to have_selector(auto_stop_button_selector)
- end
- end
-
context 'with auto-stop' do
let!(:environment) { create(:environment, :will_auto_stop, name: 'staging', project: project) }
@@ -48,11 +34,11 @@ RSpec.describe 'Environment' do
visit_environment(environment)
end
- it 'shows auto stop info' do
+ it 'shows auto stop info', :js do
expect(page).to have_content('Auto stops')
end
- it 'shows auto stop button' do
+ it 'shows auto stop button', :js do
expect(page).to have_selector(auto_stop_button_selector)
expect(page.find(auto_stop_button_selector).find(:xpath, '..')['action']).to have_content(cancel_auto_stop_project_environment_path(environment.project, environment))
end
@@ -80,7 +66,6 @@ RSpec.describe 'Environment' do
it 'does show deployment SHA' do
expect(page).to have_link(deployment.short_sha)
expect(page).not_to have_link('Re-deploy')
- expect(page).not_to have_terminal_button
end
end
@@ -186,7 +171,7 @@ RSpec.describe 'Environment' do
let(:build) { create(:ci_build, pipeline: pipeline) }
let(:deployment) { create(:deployment, :success, environment: environment, deployable: build) }
- it 'does show an external link button' do
+ it 'does show an external link button', :js do
expect(page).to have_link(nil, href: environment.external_url)
end
end
@@ -200,10 +185,6 @@ RSpec.describe 'Environment' do
context 'for project maintainer' do
let(:role) { :maintainer }
- it 'shows the terminal button' do
- expect(page).to have_terminal_button
- end
-
context 'web terminal', :js do
before do
# Stub #terminals as it causes js-enabled feature specs to
@@ -224,14 +205,6 @@ RSpec.describe 'Environment' do
end
end
end
-
- context 'for developer' do
- let(:role) { :developer }
-
- it 'does not show terminal button' do
- expect(page).not_to have_terminal_button
- end
- end
end
end
@@ -259,7 +232,7 @@ RSpec.describe 'Environment' do
click_button('Stop')
click_button('Stop environment') # confirm modal
wait_for_all_requests
- expect(page).to have_content('close_app')
+ expect(page).to have_button('Delete')
end
end
@@ -269,7 +242,7 @@ RSpec.describe 'Environment' do
name: action.ref, project: project)
end
- it 'does not allow to stop environment' do
+ it 'does not allow to stop environment', :js do
expect(page).not_to have_button('Stop')
end
end
@@ -277,7 +250,7 @@ RSpec.describe 'Environment' do
context 'for reporter' do
let(:role) { :reporter }
- it 'does not show stop button' do
+ it 'does not show stop button', :js do
expect(page).not_to have_button('Stop')
end
end
@@ -287,7 +260,7 @@ RSpec.describe 'Environment' do
context 'when environment is stopped' do
let(:environment) { create(:environment, project: project, state: :stopped) }
- it 'does not show stop button' do
+ it 'does not show stop button', :js do
expect(page).not_to have_button('Stop')
end
end
@@ -323,7 +296,7 @@ RSpec.describe 'Environment' do
ref: 'feature')
end
- it 'user visits environment page' do
+ it 'user visits environment page', :js do
visit_environment(environment)
expect(page).to have_button('Stop')
@@ -380,8 +353,4 @@ RSpec.describe 'Environment' do
def visit_environment(environment)
visit project_environment_path(environment.project, environment)
end
-
- def have_terminal_button
- have_link(nil, href: terminal_project_environment_path(project, environment))
- end
end
diff --git a/spec/frontend/environments/environments_detail_header_spec.js b/spec/frontend/environments/environments_detail_header_spec.js
new file mode 100644
index 00000000000..6334060c736
--- /dev/null
+++ b/spec/frontend/environments/environments_detail_header_spec.js
@@ -0,0 +1,238 @@
+import { GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import DeleteEnvironmentModal from '~/environments/components/delete_environment_modal.vue';
+import EnvironmentsDetailHeader from '~/environments/components/environments_detail_header.vue';
+import StopEnvironmentModal from '~/environments/components/stop_environment_modal.vue';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import { createEnvironment } from './mock_data';
+
+describe('Environments detail header component', () => {
+ const cancelAutoStopPath = '/my-environment/cancel/path';
+ const terminalPath = '/my-environment/terminal/path';
+ const metricsPath = '/my-environment/metrics/path';
+ const updatePath = '/my-environment/edit/path';
+
+ let wrapper;
+
+ const findHeader = () => wrapper.findByRole('heading');
+ const findAutoStopsAt = () => wrapper.findByTestId('auto-stops-at');
+ const findCancelAutoStopAtButton = () => wrapper.findByTestId('cancel-auto-stop-button');
+ const findCancelAutoStopAtForm = () => wrapper.findByTestId('cancel-auto-stop-form');
+ const findTerminalButton = () => wrapper.findByTestId('terminal-button');
+ const findExternalUrlButton = () => wrapper.findByTestId('external-url-button');
+ const findMetricsButton = () => wrapper.findByTestId('metrics-button');
+ const findEditButton = () => wrapper.findByTestId('edit-button');
+ const findStopButton = () => wrapper.findByTestId('stop-button');
+ const findDestroyButton = () => wrapper.findByTestId('destroy-button');
+ const findStopEnvironmentModal = () => wrapper.findComponent(StopEnvironmentModal);
+ const findDeleteEnvironmentModal = () => wrapper.findComponent(DeleteEnvironmentModal);
+
+ const buttons = [
+ ['Cancel Auto Stop At', findCancelAutoStopAtButton],
+ ['Terminal', findTerminalButton],
+ ['External Url', findExternalUrlButton],
+ ['Metrics', findMetricsButton],
+ ['Edit', findEditButton],
+ ['Stop', findStopButton],
+ ['Destroy', findDestroyButton],
+ ];
+
+ const createWrapper = ({ props }) => {
+ wrapper = shallowMountExtended(EnvironmentsDetailHeader, {
+ stubs: {
+ GlSprintf,
+ TimeAgo,
+ },
+ propsData: {
+ canReadEnvironment: false,
+ canAdminEnvironment: false,
+ canUpdateEnvironment: false,
+ canStopEnvironment: false,
+ canDestroyEnvironment: false,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('default state with minimal access', () => {
+ beforeEach(() => {
+ createWrapper({ props: { environment: createEnvironment() } });
+ });
+
+ it('displays the environment name', () => {
+ expect(findHeader().text()).toBe('My environment');
+ });
+
+ it('does not display an auto stops at text', () => {
+ expect(findAutoStopsAt().exists()).toBe(false);
+ });
+
+ it.each(buttons)('does not display button: %s', (_, findSelector) => {
+ expect(findSelector().exists()).toBe(false);
+ });
+
+ it('does not display stop environment modal', () => {
+ expect(findStopEnvironmentModal().exists()).toBe(false);
+ });
+
+ it('does not display delete environment modal', () => {
+ expect(findDeleteEnvironmentModal().exists()).toBe(false);
+ });
+ });
+
+ describe('when auto stops at is enabled and environment is available', () => {
+ beforeEach(() => {
+ const now = new Date();
+ const tomorrow = new Date();
+ tomorrow.setDate(now.getDate() + 1);
+ createWrapper({
+ props: {
+ environment: createEnvironment({ autoStopAt: tomorrow.toISOString() }),
+ cancelAutoStopPath,
+ },
+ });
+ });
+
+ it('displays a text that describes when the environment is going to be stopped', () => {
+ expect(findAutoStopsAt().text()).toBe('Auto stops in 1 day');
+ });
+
+ it('displays a cancel auto stops at button with a form to make a post request', () => {
+ const button = findCancelAutoStopAtButton();
+ const form = findCancelAutoStopAtForm();
+ expect(form.attributes('action')).toBe(cancelAutoStopPath);
+ expect(form.attributes('method')).toBe('POST');
+ expect(button.props('icon')).toBe('thumbtack');
+ expect(button.attributes('type')).toBe('submit');
+ });
+
+ it('includes a csrf token', () => {
+ const input = findCancelAutoStopAtForm().find('input');
+ expect(input.attributes('name')).toBe('authenticity_token');
+ });
+ });
+
+ describe('when auto stops at is enabled and environment is unavailable (already stopped)', () => {
+ beforeEach(() => {
+ const now = new Date();
+ const tomorrow = new Date();
+ tomorrow.setDate(now.getDate() + 1);
+ createWrapper({
+ props: {
+ environment: createEnvironment({
+ autoStopAt: tomorrow.toISOString(),
+ isAvailable: false,
+ }),
+ cancelAutoStopPath,
+ },
+ });
+ });
+
+ it('does not display a text that describes when the environment is going to be stopped', () => {
+ expect(findAutoStopsAt().exists()).toBe(false);
+ });
+
+ it('displays a cancel auto stops at button with correct path', () => {
+ expect(findCancelAutoStopAtButton().exists()).toBe(false);
+ });
+ });
+
+ describe('when has a terminal', () => {
+ beforeEach(() => {
+ createWrapper({
+ props: {
+ environment: createEnvironment({ hasTerminals: true }),
+ canAdminEnvironment: true,
+ terminalPath,
+ },
+ });
+ });
+
+ it('displays the terminal button with correct path', () => {
+ expect(findTerminalButton().attributes('href')).toBe(terminalPath);
+ });
+ });
+
+ describe('when has an external url enabled', () => {
+ const externalUrl = 'https://example.com/my-environment/external/url';
+
+ beforeEach(() => {
+ createWrapper({
+ props: {
+ environment: createEnvironment({ hasTerminals: true, externalUrl }),
+ canReadEnvironment: true,
+ },
+ });
+ });
+
+ it('displays the external url button with correct path', () => {
+ expect(findExternalUrlButton().attributes('href')).toBe(externalUrl);
+ });
+ });
+
+ describe('when metrics are enabled', () => {
+ beforeEach(() => {
+ createWrapper({
+ props: {
+ environment: createEnvironment(),
+ canReadEnvironment: true,
+ metricsPath,
+ },
+ });
+ });
+
+ it('displays the metrics button with correct path', () => {
+ expect(findMetricsButton().attributes('href')).toBe(metricsPath);
+ });
+ });
+
+ describe('when has all admin rights', () => {
+ beforeEach(() => {
+ createWrapper({
+ props: {
+ environment: createEnvironment(),
+ canReadEnvironment: true,
+ canAdminEnvironment: true,
+ canStopEnvironment: true,
+ canUpdateEnvironment: true,
+ updatePath,
+ },
+ });
+ });
+
+ it('displays the edit button with correct path', () => {
+ expect(findEditButton().attributes('href')).toBe(updatePath);
+ });
+
+ it('displays the stop button with correct icon', () => {
+ expect(findStopButton().attributes('icon')).toBe('stop');
+ });
+
+ it('displays stop environment modal', () => {
+ expect(findStopEnvironmentModal().exists()).toBe(true);
+ });
+ });
+
+ describe('when the environment is unavailable and user has destroy permissions', () => {
+ beforeEach(() => {
+ createWrapper({
+ props: {
+ environment: createEnvironment({ isAvailable: false }),
+ canDestroyEnvironment: true,
+ },
+ });
+ });
+
+ it('displays a delete button', () => {
+ expect(findDestroyButton().exists()).toBe(true);
+ });
+
+ it('displays delete environment modal', () => {
+ expect(findDeleteEnvironmentModal().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/environments/mock_data.js b/spec/frontend/environments/mock_data.js
index 9ba71b78c2f..b04d0551b50 100644
--- a/spec/frontend/environments/mock_data.js
+++ b/spec/frontend/environments/mock_data.js
@@ -301,4 +301,22 @@ const tableData = {
},
};
-export { environment, environmentsList, folder, serverData, tableData, deployBoardMockData };
+const createEnvironment = (data = {}) => ({
+ id: 1,
+ name: 'My environment',
+ externalUrl: 'my external url',
+ isAvailable: true,
+ hasTerminals: false,
+ autoStopAt: null,
+ ...data,
+});
+
+export {
+ environment,
+ environmentsList,
+ folder,
+ serverData,
+ tableData,
+ deployBoardMockData,
+ createEnvironment,
+};
diff --git a/spec/frontend/registry/explorer/components/list_page/cleanup_status_spec.js b/spec/frontend/registry/explorer/components/list_page/cleanup_status_spec.js
index c89bb874a7f..8f2c049a357 100644
--- a/spec/frontend/registry/explorer/components/list_page/cleanup_status_spec.js
+++ b/spec/frontend/registry/explorer/components/list_page/cleanup_status_spec.js
@@ -2,7 +2,7 @@ import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import CleanupStatus from '~/registry/explorer/components/list_page/cleanup_status.vue';
import {
- ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
+ CLEANUP_TIMED_OUT_ERROR_MESSAGE,
CLEANUP_STATUS_SCHEDULED,
CLEANUP_STATUS_ONGOING,
CLEANUP_STATUS_UNFINISHED,
@@ -81,7 +81,7 @@ describe('cleanup_status', () => {
const tooltip = getBinding(findExtraInfoIcon().element, 'gl-tooltip');
- expect(tooltip.value.title).toBe(ASYNC_DELETE_IMAGE_ERROR_MESSAGE);
+ expect(tooltip.value.title).toBe(CLEANUP_TIMED_OUT_ERROR_MESSAGE);
});
});
});
diff --git a/spec/frontend/runner/runner_list/runner_list_app_spec.js b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
index 54b7d1f1bdb..2c28f10ca8e 100644
--- a/spec/frontend/runner/runner_list/runner_list_app_spec.js
+++ b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
@@ -6,6 +6,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import { updateHistory } from '~/lib/utils/url_utility';
+import AdminRunnersApp from '~/runner/admin_runners/admin_runners_app.vue';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
import RunnerList from '~/runner/components/runner_list.vue';
import RunnerManualSetupHelp from '~/runner/components/runner_manual_setup_help.vue';
@@ -22,7 +23,6 @@ import {
RUNNER_PAGE_SIZE,
} from '~/runner/constants';
import getRunnersQuery from '~/runner/graphql/get_runners.query.graphql';
-import RunnerListApp from '~/runner/runner_list/runner_list_app.vue';
import { captureException } from '~/runner/sentry_utils';
import { runnersData, runnersDataPaginated } from '../mock_data';
@@ -40,7 +40,7 @@ jest.mock('~/lib/utils/url_utility', () => ({
const localVue = createLocalVue();
localVue.use(VueApollo);
-describe('RunnerListApp', () => {
+describe('AdminRunnersApp', () => {
let wrapper;
let mockRunnersQuery;
let originalLocation;
@@ -54,7 +54,7 @@ describe('RunnerListApp', () => {
const createComponentWithApollo = ({ props = {}, mountFn = shallowMount } = {}) => {
const handlers = [[getRunnersQuery, mockRunnersQuery]];
- wrapper = mountFn(RunnerListApp, {
+ wrapper = mountFn(AdminRunnersApp, {
localVue,
apolloProvider: createMockApollo(handlers),
propsData: {
@@ -197,7 +197,7 @@ describe('RunnerListApp', () => {
it('error is reported to sentry', async () => {
expect(captureException).toHaveBeenCalledWith({
error: new Error('Network error: Error!'),
- component: 'RunnerListApp',
+ component: 'AdminRunnersApp',
});
});
diff --git a/spec/frontend/runner/runner_list/runner_search_utils_spec.js b/spec/frontend/runner/runner_search_utils_spec.js
index e7969676549..3a0c3abe7bd 100644
--- a/spec/frontend/runner/runner_list/runner_search_utils_spec.js
+++ b/spec/frontend/runner/runner_search_utils_spec.js
@@ -3,7 +3,7 @@ import {
fromUrlQueryToSearch,
fromSearchToUrl,
fromSearchToVariables,
-} from '~/runner/runner_list/runner_search_utils';
+} from '~/runner/runner_search_utils';
describe('search_params.js', () => {
const examples = [
diff --git a/spec/helpers/environment_helper_spec.rb b/spec/helpers/environment_helper_spec.rb
index 8c542ca01f4..0eecae32cc1 100644
--- a/spec/helpers/environment_helper_spec.rb
+++ b/spec/helpers/environment_helper_spec.rb
@@ -22,4 +22,41 @@ RSpec.describe EnvironmentHelper do
end
end
end
+
+ describe '#environments_detail_data_json' do
+ subject { helper.environments_detail_data_json(user, project, environment) }
+
+ let_it_be(:auto_stop_at) { Time.now.utc }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project, reload: true) { create(:project, :repository) }
+ let_it_be(:environment) { create(:environment, project: project, auto_stop_at: auto_stop_at) }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ allow(helper).to receive(:can?).and_return(true)
+ end
+
+ it 'returns the correct data' do
+ expect(subject).to eq({
+ name: environment.name,
+ id: environment.id,
+ external_url: environment.external_url,
+ can_update_environment: true,
+ can_destroy_environment: true,
+ can_read_environment: true,
+ can_stop_environment: true,
+ can_admin_environment: true,
+ environment_metrics_path: environment_metrics_path(environment),
+ environments_fetch_path: project_environments_path(project, format: :json),
+ environment_edit_path: edit_project_environment_path(project, environment),
+ environment_stop_path: stop_project_environment_path(project, environment),
+ environment_delete_path: environment_delete_path(environment),
+ environment_cancel_auto_stop_path: cancel_auto_stop_project_environment_path(project, environment),
+ environment_terminal_path: terminal_project_environment_path(project, environment),
+ has_terminals: false,
+ is_environment_available: true,
+ auto_stop_at: auto_stop_at
+ }.to_json)
+ end
+ end
end