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
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-03-25 12:08:11 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-03-25 12:08:11 +0300
commit5064bf8c5647d4c4430cbb4d097cf1592416de29 (patch)
treed051bf2abe2cc7061b3a7facb6669a56ccb9cf54 /app
parent9c83aadd2604e7e6cb1f84683f951e6b12872618 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue19
-rw-r--r--app/assets/javascripts/blob_edit/blob_bundle.js11
-rw-r--r--app/assets/javascripts/environments/components/delete_environment_modal.vue66
-rw-r--r--app/assets/javascripts/environments/components/environment_delete.vue70
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue16
-rw-r--r--app/assets/javascripts/environments/components/environments_app.vue3
-rw-r--r--app/assets/javascripts/environments/components/stop_environment_modal.vue7
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.vue3
-rw-r--r--app/assets/javascripts/environments/mixins/environments_mixin.js86
-rw-r--r--app/assets/javascripts/environments/mount_show.js32
-rw-r--r--app/assets/javascripts/environments/services/environments_service.js5
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js8
-rw-r--r--app/assets/javascripts/pages/projects/environments/show/index.js3
-rw-r--r--app/controllers/projects/settings/operations_controller.rb19
-rw-r--r--app/helpers/environments_helper.rb4
-rw-r--r--app/helpers/gitlab_routing_helper.rb5
-rw-r--r--app/models/commit_status.rb9
-rw-r--r--app/models/concerns/noteable.rb6
-rw-r--r--app/models/project_services/prometheus_service.rb9
-rw-r--r--app/models/snippet.rb2
-rw-r--r--app/policies/environment_policy.rb6
-rw-r--r--app/policies/project_policy.rb2
-rw-r--r--app/serializers/environment_entity.rb8
-rw-r--r--app/services/projects/operations/update_service.rb18
-rw-r--r--app/views/projects/blob/_editor.html.haml2
-rw-r--r--app/views/projects/blob/new.html.haml2
-rw-r--r--app/views/projects/environments/show.html.haml141
-rw-r--r--app/views/shared/icons/_dev_ops_score_no_data.svg1
-rw-r--r--app/views/shared/icons/_dev_ops_score_no_index.svg3
29 files changed, 455 insertions, 111 deletions
diff --git a/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue b/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue
index 7f0c232eea8..7418ca9edfc 100644
--- a/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue
+++ b/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue
@@ -1,7 +1,6 @@
<script>
import { GlPopover, GlSprintf, GlButton, GlIcon } from '@gitlab/ui';
-import Cookies from 'js-cookie';
-import { parseBoolean, scrollToElement } from '~/lib/utils/common_utils';
+import { parseBoolean, scrollToElement, setCookie, getCookie } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import { glEmojiTag } from '~/emoji';
import Tracking from '~/tracking';
@@ -51,7 +50,7 @@ export default {
},
data() {
return {
- popoverDismissed: parseBoolean(Cookies.get(this.dismissKey)),
+ popoverDismissed: parseBoolean(getCookie(`${this.trackLabel}_${this.dismissKey}`)),
tracking: {
label: this.trackLabel,
property: this.humanAccess,
@@ -68,17 +67,27 @@ export default {
emoji() {
return popoverStates[this.trackLabel].emoji || '';
},
+ dismissCookieName() {
+ return `${this.trackLabel}_${this.dismissKey}`;
+ },
+ commitCookieName() {
+ return `suggest_gitlab_ci_yml_commit_${this.dismissKey}`;
+ },
},
mounted() {
- if (this.trackLabel === 'suggest_commit_first_project_gitlab_ci_yml' && !this.popoverDismissed)
+ if (
+ this.trackLabel === 'suggest_commit_first_project_gitlab_ci_yml' &&
+ !this.popoverDismissed
+ ) {
scrollToElement(document.querySelector(this.target));
+ }
this.trackOnShow();
},
methods: {
onDismiss() {
this.popoverDismissed = true;
- Cookies.set(this.dismissKey, this.popoverDismissed, { expires: 365 });
+ setCookie(this.dismissCookieName, this.popoverDismissed);
},
trackOnShow() {
if (!this.popoverDismissed) this.track();
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js
index f4ce98037c8..5a77896f5ef 100644
--- a/app/assets/javascripts/blob_edit/blob_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_bundle.js
@@ -5,6 +5,7 @@ import NewCommitForm from '../new_commit_form';
import EditBlob from './edit_blob';
import BlobFileDropzone from '../blob/blob_file_dropzone';
import initPopover from '~/blob/suggest_gitlab_ci_yml';
+import { setCookie } from '~/lib/utils/common_utils';
export default () => {
const editBlobForm = $('.js-edit-blob-form');
@@ -60,6 +61,16 @@ export default () => {
}
if (suggestEl) {
+ const commitButton = document.querySelector('#commit-changes');
+
initPopover(suggestEl);
+
+ if (commitButton) {
+ const commitCookieName = `suggest_gitlab_ci_yml_commit_${suggestEl.dataset.dismissKey}`;
+
+ commitButton.addEventListener('click', () => {
+ setCookie(commitCookieName, true);
+ });
+ }
}
};
diff --git a/app/assets/javascripts/environments/components/delete_environment_modal.vue b/app/assets/javascripts/environments/components/delete_environment_modal.vue
new file mode 100644
index 00000000000..f731dc49a5b
--- /dev/null
+++ b/app/assets/javascripts/environments/components/delete_environment_modal.vue
@@ -0,0 +1,66 @@
+<script>
+import { GlTooltipDirective } from '@gitlab/ui';
+import GlModal from '~/vue_shared/components/gl_modal.vue';
+import { s__, sprintf } from '~/locale';
+import eventHub from '../event_hub';
+
+export default {
+ id: 'delete-environment-modal',
+ name: 'DeleteEnvironmentModal',
+
+ components: {
+ GlModal,
+ },
+
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+
+ props: {
+ environment: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ computed: {
+ confirmDeleteMessage() {
+ return sprintf(
+ s__(
+ `Environments|Deleting the '%{environmentName}' environment cannot be undone. Do you want to delete it anyway?`,
+ ),
+ {
+ environmentName: this.environment.name,
+ },
+ false,
+ );
+ },
+ },
+
+ methods: {
+ onSubmit() {
+ eventHub.$emit('deleteEnvironment', this.environment);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-modal
+ :id="$options.id"
+ :footer-primary-button-text="s__('Environments|Delete environment')"
+ footer-primary-button-variant="danger"
+ @submit="onSubmit"
+ >
+ <template slot="header">
+ <h4 class="modal-title d-flex mw-100">
+ {{ __('Delete') }}
+ <span v-gl-tooltip :title="environment.name" class="text-truncate mx-1 flex-fill">
+ {{ environment.name }}?
+ </span>
+ </h4>
+ </template>
+
+ <p>{{ confirmDeleteMessage }}</p>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/environments/components/environment_delete.vue b/app/assets/javascripts/environments/components/environment_delete.vue
new file mode 100644
index 00000000000..b53c5fa6583
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_delete.vue
@@ -0,0 +1,70 @@
+<script>
+/**
+ * Renders the delete button that allows deleting a stopped environment.
+ * Used in the environments table and the environment detail view.
+ */
+
+import $ from 'jquery';
+import { GlTooltipDirective } from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
+import { s__ } from '~/locale';
+import eventHub from '../event_hub';
+import LoadingButton from '../../vue_shared/components/loading_button.vue';
+
+export default {
+ components: {
+ Icon,
+ LoadingButton,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ environment: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isLoading: false,
+ };
+ },
+ computed: {
+ title() {
+ return s__('Environments|Delete environment');
+ },
+ },
+ mounted() {
+ eventHub.$on('deleteEnvironment', this.onDeleteEnvironment);
+ },
+ beforeDestroy() {
+ eventHub.$off('deleteEnvironment', this.onDeleteEnvironment);
+ },
+ methods: {
+ onClick() {
+ $(this.$el).tooltip('dispose');
+ eventHub.$emit('requestDeleteEnvironment', this.environment);
+ },
+ onDeleteEnvironment(environment) {
+ if (this.environment.id === environment.id) {
+ this.isLoading = true;
+ }
+ },
+ },
+};
+</script>
+<template>
+ <loading-button
+ v-gl-tooltip
+ :loading="isLoading"
+ :title="title"
+ :aria-label="title"
+ container-class="btn btn-danger d-none d-sm-none d-md-block"
+ data-toggle="modal"
+ data-target="#delete-environment-modal"
+ @click="onClick"
+ >
+ <icon name="remove" />
+ </loading-button>
+</template>
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index dc489c804e9..ec5b1092c14 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -15,8 +15,9 @@ import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue';
import MonitoringButtonComponent from './environment_monitoring.vue';
import PinComponent from './environment_pin.vue';
-import RollbackComponent from './environment_rollback.vue';
+import DeleteComponent from './environment_delete.vue';
import StopComponent from './environment_stop.vue';
+import RollbackComponent from './environment_rollback.vue';
import TerminalButtonComponent from './environment_terminal_button.vue';
/**
@@ -33,6 +34,7 @@ export default {
Icon,
MonitoringButtonComponent,
PinComponent,
+ DeleteComponent,
RollbackComponent,
StopComponent,
TerminalButtonComponent,
@@ -113,6 +115,15 @@ export default {
},
/**
+ * Returns whether the environment can be deleted.
+ *
+ * @returns {Boolean}
+ */
+ canDeleteEnvironment() {
+ return Boolean(this.model && this.model.can_delete && this.model.delete_path);
+ },
+
+ /**
* Verifies if the `deployable` key is present in `last_deployment` key.
* Used to verify whether we should or not render the rollback partial.
*
@@ -485,6 +496,7 @@ export default {
this.externalURL ||
this.monitoringUrl ||
this.canStopEnvironment ||
+ this.canDeleteEnvironment ||
this.canRetry
);
},
@@ -680,6 +692,8 @@ export default {
/>
<stop-component v-if="canStopEnvironment" :environment="model" />
+
+ <delete-component v-if="canDeleteEnvironment" :environment="model" />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue
index 07b8d20fde0..cc1d86d06ed 100644
--- a/app/assets/javascripts/environments/components/environments_app.vue
+++ b/app/assets/javascripts/environments/components/environments_app.vue
@@ -9,6 +9,7 @@ import environmentsMixin from '../mixins/environments_mixin';
import CIPaginationMixin from '~/vue_shared/mixins/ci_pagination_api_mixin';
import EnableReviewAppButton from './enable_review_app_button.vue';
import StopEnvironmentModal from './stop_environment_modal.vue';
+import DeleteEnvironmentModal from './delete_environment_modal.vue';
import ConfirmRollbackModal from './confirm_rollback_modal.vue';
export default {
@@ -18,6 +19,7 @@ export default {
EnableReviewAppButton,
GlButton,
StopEnvironmentModal,
+ DeleteEnvironmentModal,
},
mixins: [CIPaginationMixin, environmentsMixin, envrionmentsAppMixin],
@@ -95,6 +97,7 @@ export default {
<template>
<div>
<stop-environment-modal :environment="environmentInStopModal" />
+ <delete-environment-modal :environment="environmentInDeleteModal" />
<confirm-rollback-modal :environment="environmentInRollbackModal" />
<div class="top-area">
diff --git a/app/assets/javascripts/environments/components/stop_environment_modal.vue b/app/assets/javascripts/environments/components/stop_environment_modal.vue
index 3caf723442e..d3e8fb7ff08 100644
--- a/app/assets/javascripts/environments/components/stop_environment_modal.vue
+++ b/app/assets/javascripts/environments/components/stop_environment_modal.vue
@@ -63,10 +63,9 @@ export default {
<template slot="header">
<h4 class="modal-title d-flex mw-100">
Stopping
- <span v-gl-tooltip :title="environment.name" class="text-truncate ml-1 mr-1 flex-fill">{{
- environment.name
- }}</span>
- ?
+ <span v-gl-tooltip :title="environment.name" class="text-truncate ml-1 mr-1 flex-fill">
+ {{ environment.name }}?
+ </span>
</h4>
</template>
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue
index d60c2efd618..30b02585692 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_view.vue
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue
@@ -3,10 +3,12 @@ import folderMixin from 'ee_else_ce/environments/mixins/environments_folder_view
import environmentsMixin from '../mixins/environments_mixin';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
import StopEnvironmentModal from '../components/stop_environment_modal.vue';
+import DeleteEnvironmentModal from '../components/delete_environment_modal.vue';
export default {
components: {
StopEnvironmentModal,
+ DeleteEnvironmentModal,
},
mixins: [environmentsMixin, CIPaginationMixin, folderMixin],
@@ -39,6 +41,7 @@ export default {
<template>
<div :class="cssContainerClass">
<stop-environment-modal :environment="environmentInStopModal" />
+ <delete-environment-modal :environment="environmentInDeleteModal" />
<h4 class="js-folder-name environments-folder-name">
{{ s__('Environments|Environments') }} /
diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js
index 1c5884b541c..4fadecdd3e9 100644
--- a/app/assets/javascripts/environments/mixins/environments_mixin.js
+++ b/app/assets/javascripts/environments/mixins/environments_mixin.js
@@ -27,6 +27,10 @@ export default {
data() {
const store = new EnvironmentsStore();
+ const isDetailView = document.body.contains(
+ document.getElementById('environments-detail-view'),
+ );
+
return {
store,
state: store.state,
@@ -36,7 +40,9 @@ export default {
page: getParameterByName('page') || '1',
requestData: {},
environmentInStopModal: {},
+ environmentInDeleteModal: {},
environmentInRollbackModal: {},
+ isDetailView,
};
},
@@ -121,6 +127,10 @@ export default {
this.environmentInStopModal = environment;
},
+ updateDeleteModal(environment) {
+ this.environmentInDeleteModal = environment;
+ },
+
updateRollbackModal(environment) {
this.environmentInRollbackModal = environment;
},
@@ -133,6 +143,30 @@ export default {
this.postAction({ endpoint, errorMessage });
},
+ deleteEnvironment(environment) {
+ const endpoint = environment.delete_path;
+ const mountedToShow = environment.mounted_to_show;
+ const errorMessage = s__(
+ 'Environments|An error occurred while deleting the environment. Check if the environment stopped; if not, stop it and try again.',
+ );
+
+ this.service
+ .deleteAction(endpoint)
+ .then(() => {
+ if (!mountedToShow) {
+ // Reload as a first solution to bust the ETag cache
+ window.location.reload();
+ return;
+ }
+ const url = window.location.href.split('/');
+ url.pop();
+ window.location.href = url.join('/');
+ })
+ .catch(() => {
+ Flash(errorMessage);
+ });
+ },
+
rollbackEnvironment(environment) {
const { retryUrl, isLastDeployment } = environment;
const errorMessage = isLastDeployment
@@ -178,36 +212,42 @@ export default {
this.service = new EnvironmentsService(this.endpoint);
this.requestData = { page: this.page, scope: this.scope, nested: true };
- this.poll = new Poll({
- resource: this.service,
- method: 'fetchEnvironments',
- data: this.requestData,
- successCallback: this.successCallback,
- errorCallback: this.errorCallback,
- notificationCallback: isMakingRequest => {
- this.isMakingRequest = isMakingRequest;
- },
- });
-
- if (!Visibility.hidden()) {
- this.isLoading = true;
- this.poll.makeRequest();
- } else {
- this.fetchEnvironments();
- }
+ if (!this.isDetailView) {
+ this.poll = new Poll({
+ resource: this.service,
+ method: 'fetchEnvironments',
+ data: this.requestData,
+ successCallback: this.successCallback,
+ errorCallback: this.errorCallback,
+ notificationCallback: isMakingRequest => {
+ this.isMakingRequest = isMakingRequest;
+ },
+ });
- Visibility.change(() => {
if (!Visibility.hidden()) {
- this.poll.restart();
+ this.isLoading = true;
+ this.poll.makeRequest();
} else {
- this.poll.stop();
+ this.fetchEnvironments();
}
- });
+
+ Visibility.change(() => {
+ if (!Visibility.hidden()) {
+ this.poll.restart();
+ } else {
+ this.poll.stop();
+ }
+ });
+ }
eventHub.$on('postAction', this.postAction);
+
eventHub.$on('requestStopEnvironment', this.updateStopModal);
eventHub.$on('stopEnvironment', this.stopEnvironment);
+ eventHub.$on('requestDeleteEnvironment', this.updateDeleteModal);
+ eventHub.$on('deleteEnvironment', this.deleteEnvironment);
+
eventHub.$on('requestRollbackEnvironment', this.updateRollbackModal);
eventHub.$on('rollbackEnvironment', this.rollbackEnvironment);
@@ -216,9 +256,13 @@ export default {
beforeDestroy() {
eventHub.$off('postAction', this.postAction);
+
eventHub.$off('requestStopEnvironment', this.updateStopModal);
eventHub.$off('stopEnvironment', this.stopEnvironment);
+ eventHub.$off('requestDeleteEnvironment', this.updateDeleteModal);
+ eventHub.$off('deleteEnvironment', this.deleteEnvironment);
+
eventHub.$off('requestRollbackEnvironment', this.updateRollbackModal);
eventHub.$off('rollbackEnvironment', this.rollbackEnvironment);
diff --git a/app/assets/javascripts/environments/mount_show.js b/app/assets/javascripts/environments/mount_show.js
new file mode 100644
index 00000000000..1929ed080a1
--- /dev/null
+++ b/app/assets/javascripts/environments/mount_show.js
@@ -0,0 +1,32 @@
+import Vue from 'vue';
+import DeleteEnvironmentModal from './components/delete_environment_modal.vue';
+import environmentsMixin from './mixins/environments_mixin';
+
+export default () => {
+ const el = document.getElementById('delete-environment-modal');
+ const container = document.getElementById('environments-detail-view');
+
+ return new Vue({
+ el,
+ components: {
+ DeleteEnvironmentModal,
+ },
+ mixins: [environmentsMixin],
+ data() {
+ const environment = JSON.parse(JSON.stringify(container.dataset));
+ environment.delete_path = environment.deletePath;
+ environment.mounted_to_show = true;
+
+ return {
+ environment,
+ };
+ },
+ render(createElement) {
+ return createElement('delete-environment-modal', {
+ props: {
+ environment: this.environment,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/environments/services/environments_service.js b/app/assets/javascripts/environments/services/environments_service.js
index cb4ff6856db..122c8f84a2c 100644
--- a/app/assets/javascripts/environments/services/environments_service.js
+++ b/app/assets/javascripts/environments/services/environments_service.js
@@ -16,6 +16,11 @@ export default class EnvironmentsService {
return axios.post(endpoint, {});
}
+ // eslint-disable-next-line class-methods-use-this
+ deleteAction(endpoint) {
+ return axios.delete(endpoint, {});
+ }
+
getFolderContent(folderUrl) {
return axios.get(`${folderUrl}.json?per_page=${this.folderResults}`);
}
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index abecfba5718..9b0ee40a30a 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -9,6 +9,7 @@ import { getLocationHash } from './url_utility';
import { convertToCamelCase, convertToSnakeCase } from './text_utility';
import { isObject } from './type_utility';
import { isFunction } from 'lodash';
+import Cookies from 'js-cookie';
export const getPagePath = (index = 0) => {
const page = $('body').attr('data-page') || '';
@@ -902,3 +903,10 @@ window.gl.utils = {
spriteIcon,
imagePath,
};
+
+// Methods to set and get Cookie
+export const setCookie = (name, value) => Cookies.set(name, value, { expires: 365 });
+
+export const getCookie = name => Cookies.get(name);
+
+export const removeCookie = name => Cookies.remove(name);
diff --git a/app/assets/javascripts/pages/projects/environments/show/index.js b/app/assets/javascripts/pages/projects/environments/show/index.js
new file mode 100644
index 00000000000..10e3e28f024
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/environments/show/index.js
@@ -0,0 +1,3 @@
+import initShowEnvironment from '~/environments/mount_show';
+
+document.addEventListener('DOMContentLoaded', () => initShowEnvironment());
diff --git a/app/controllers/projects/settings/operations_controller.rb b/app/controllers/projects/settings/operations_controller.rb
index 164cd5b9384..a9d1dc0759d 100644
--- a/app/controllers/projects/settings/operations_controller.rb
+++ b/app/controllers/projects/settings/operations_controller.rb
@@ -4,6 +4,9 @@ module Projects
module Settings
class OperationsController < Projects::ApplicationController
before_action :authorize_admin_operations!
+ before_action :authorize_read_prometheus_alerts!, only: [:reset_alerting_token]
+
+ respond_to :json, only: [:reset_alerting_token]
helper_method :error_tracking_setting
@@ -27,8 +30,24 @@ module Projects
end
end
+ def reset_alerting_token
+ result = ::Projects::Operations::UpdateService
+ .new(project, current_user, alerting_params)
+ .execute
+
+ if result[:status] == :success
+ render json: { token: project.alerting_setting.token }
+ else
+ render json: {}, status: :unprocessable_entity
+ end
+ end
+
private
+ def alerting_params
+ { alerting_setting_attributes: { regenerate_token: true } }
+ end
+
def prometheus_service
project.find_or_initialize_service(::PrometheusService.to_param)
end
diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb
index 6bf920448a5..68d78959407 100644
--- a/app/helpers/environments_helper.rb
+++ b/app/helpers/environments_helper.rb
@@ -50,4 +50,8 @@ module EnvironmentsHelper
"cluster-applications-documentation-path" => help_page_path('user/clusters/applications.md', anchor: 'elastic-stack')
}
end
+
+ def can_destroy_environment?(environment)
+ can?(current_user, :destroy_environment, environment)
+ end
end
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index 1fb0b83b010..4474534045b 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -4,6 +4,7 @@
module GitlabRoutingHelper
extend ActiveSupport::Concern
+ include API::Helpers::RelatedResourcesHelpers
included do
Gitlab::Routing.includes_helpers(self)
end
@@ -29,6 +30,10 @@ module GitlabRoutingHelper
metrics_project_environment_path(environment.project, environment, *args)
end
+ def environment_delete_path(environment, *args)
+ expose_path(api_v4_projects_environments_path(id: environment.project.id, environment_id: environment.id))
+ end
+
def issue_path(entity, *args)
project_issue_path(entity.project, entity, *args)
end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 35b727720ba..03260b28335 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -62,13 +62,16 @@ class CommitStatus < ApplicationRecord
preload(project: :namespace)
end
- scope :match_id_and_lock_version, -> (slice) do
+ scope :match_id_and_lock_version, -> (items) do
# it expects that items are an array of attributes to match
# each hash needs to have `id` and `lock_version`
- slice.inject(self) do |relation, item|
- match = CommitStatus.where(item.slice(:id, :lock_version))
+ or_conditions = items.inject(none) do |relation, item|
+ match = CommitStatus.default_scoped.where(item.slice(:id, :lock_version))
+
relation.or(match)
end
+
+ merge(or_conditions)
end
# We use `CommitStatusEnums.failure_reasons` here so that EE can more easily
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index 19f2daa1b01..a7f1fb66a88 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -79,6 +79,12 @@ module Noteable
.discussions(self)
end
+ def discussion_ids_relation
+ notes.select(:discussion_id)
+ .group(:discussion_id)
+ .order('MIN(created_at), MIN(id)')
+ end
+
def capped_notes_count(max)
notes.limit(max).count
end
diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb
index 75dfad4f3df..fd4ee069041 100644
--- a/app/models/project_services/prometheus_service.rb
+++ b/app/models/project_services/prometheus_service.rb
@@ -81,7 +81,7 @@ class PrometheusService < MonitoringService
def prometheus_client
return unless should_return_client?
- Gitlab::PrometheusClient.new(api_url)
+ Gitlab::PrometheusClient.new(api_url, allow_local_requests: allow_local_api_url?)
end
def prometheus_available?
@@ -94,7 +94,8 @@ class PrometheusService < MonitoringService
end
def allow_local_api_url?
- self_monitoring_project? && internal_prometheus_url?
+ allow_local_requests_from_web_hooks_and_services? ||
+ (self_monitoring_project? && internal_prometheus_url?)
end
def configured?
@@ -111,6 +112,10 @@ class PrometheusService < MonitoringService
api_url.present? && api_url == ::Gitlab::Prometheus::Internal.uri
end
+ def allow_local_requests_from_web_hooks_and_services?
+ current_settings.allow_local_requests_from_web_hooks_and_services?
+ end
+
def should_return_client?
api_url.present? && manual_configuration? && active? && valid?
end
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index b6127baca90..c7b5d7c8278 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -19,8 +19,6 @@ class Snippet < ApplicationRecord
MAX_FILE_COUNT = 1
- ignore_column :repository_storage, remove_with: '12.10', remove_after: '2020-03-22'
-
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description
cache_markdown_field :content
diff --git a/app/policies/environment_policy.rb b/app/policies/environment_policy.rb
index be512dd3b94..f0187a39687 100644
--- a/app/policies/environment_policy.rb
+++ b/app/policies/environment_policy.rb
@@ -12,7 +12,13 @@ class EnvironmentPolicy < BasePolicy
!@subject.stop_action_available? && can?(:update_environment, @subject)
end
+ condition(:stopped) do
+ @subject.stopped?
+ end
+
rule { stop_with_deployment_allowed | stop_with_update_allowed }.enable :stop_environment
+
+ rule { ~stopped }.prevent(:destroy_environment)
end
EnvironmentPolicy.prepend_if_ee('EE::EnvironmentPolicy')
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index aecefcc89ab..99aeca17699 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -271,6 +271,7 @@ class ProjectPolicy < BasePolicy
enable :destroy_container_image
enable :create_environment
enable :update_environment
+ enable :destroy_environment
enable :create_deployment
enable :update_deployment
enable :create_release
@@ -316,6 +317,7 @@ class ProjectPolicy < BasePolicy
enable :create_deploy_token
enable :read_pod_logs
enable :destroy_deploy_token
+ enable :read_prometheus_alerts
end
rule { (mirror_available & can?(:admin_project)) | admin }.enable :admin_remote_mirror
diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb
index d9af7af8a8b..7da5910a75b 100644
--- a/app/serializers/environment_entity.rb
+++ b/app/serializers/environment_entity.rb
@@ -28,6 +28,10 @@ class EnvironmentEntity < Grape::Entity
cancel_auto_stop_project_environment_path(environment.project, environment)
end
+ expose :delete_path do |environment|
+ environment_delete_path(environment)
+ end
+
expose :cluster_type, if: ->(environment, _) { cluster_platform_kubernetes? } do |environment|
cluster.cluster_type
end
@@ -63,6 +67,10 @@ class EnvironmentEntity < Grape::Entity
environment.elastic_stack_available?
end
+ expose :can_delete do |environment|
+ can?(current_user, :destroy_environment, environment)
+ end
+
private
alias_method :environment, :object
diff --git a/app/services/projects/operations/update_service.rb b/app/services/projects/operations/update_service.rb
index 27bbf5c6e57..c06f572b52f 100644
--- a/app/services/projects/operations/update_service.rb
+++ b/app/services/projects/operations/update_service.rb
@@ -13,12 +13,30 @@ module Projects
def project_update_params
error_tracking_params
+ .merge(alerting_setting_params)
.merge(metrics_setting_params)
.merge(grafana_integration_params)
.merge(prometheus_integration_params)
.merge(incident_management_setting_params)
end
+ def alerting_setting_params
+ return {} unless can?(current_user, :read_prometheus_alerts, project)
+
+ attr = params[:alerting_setting_attributes]
+ return {} unless attr
+
+ regenerate_token = attr.delete(:regenerate_token)
+
+ if regenerate_token
+ attr[:token] = nil
+ else
+ attr = attr.except(:token)
+ end
+
+ { alerting_setting_attributes: attr }
+ end
+
def metrics_setting_params
attribs = params[:metrics_setting_attributes]
return {} unless attribs
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index 51b0b2722d1..b67f9d0cd08 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -23,7 +23,7 @@
.js-suggest-gitlab-ci-yml{ data: { toggle: 'popover',
target: '#gitlab-ci-yml-selector',
track_label: 'suggest_gitlab_ci_yml',
- dismiss_key: "suggest_gitlab_ci_yml_#{@project.id}",
+ dismiss_key: @project.id,
human_access: human_access } }
.file-buttons
diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml
index 1afbe1fe24e..8f166e9aa16 100644
--- a/app/views/projects/blob/new.html.haml
+++ b/app/views/projects/blob/new.html.haml
@@ -17,5 +17,5 @@
.js-suggest-gitlab-ci-yml-commit-changes{ data: { toggle: 'popover',
target: '#commit-changes',
track_label: 'suggest_commit_first_project_gitlab_ci_yml',
- dismiss_key: "suggest_commit_first_project_gitlab_ci_yml_#{@project.id}",
+ dismiss_key: @project.id,
human_access: human_access } }
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index ff78abfddf4..3a7a93dc4e6 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -5,74 +5,81 @@
- content_for :page_specific_javascripts do
= stylesheet_link_tag 'page_bundles/xterm'
-- 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.html#stopping-an-environment',
- target: '_blank',
- rel: 'noopener noreferrer' }
- = s_('Environments|Learn more about stopping environments')
- .modal-footer
- = button_tag _('Cancel'), type: 'button', class: 'btn btn-cancel', data: { dismiss: 'modal' }
- = button_to stop_project_environment_path(@project, @environment), class: 'btn btn-danger has-tooltip', method: :post do
- = s_('Environments|Stop environment')
+#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.html#stopping-an-environment',
+ target: '_blank',
+ rel: 'noopener noreferrer' }
+ = s_('Environments|Learn more about stopping environments')
+ .modal-footer
+ = button_tag _('Cancel'), type: 'button', class: 'btn btn-cancel', data: { dismiss: 'modal' }
+ = button_to stop_project_environment_path(@project, @environment), class: 'btn btn-danger has-tooltip', method: :post do
+ = s_('Environments|Stop environment')
-.top-area.justify-content-between
- .d-flex
- %h3.page-title= @environment.name
- - if @environment.auto_stop_at?
- %p.align-self-end.prepend-left-8
- = 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: 'btn btn-danger', type: 'button', data: { toggle: 'modal',
- target: '#stop-environment-modal' } do
- = sprite_icon('stop')
- = s_('Environments|Stop')
+ - if can_destroy_environment?(@environment)
+ #delete-environment-modal
-.environments-container
- - if @deployments.blank?
- .empty-state
- .text-content
- %h4.state-title
- = _("You don't have any deployments right now.")
- %p.blank-state-text
- = _("Define environments in the deploy stage(s) in <code>.gitlab-ci.yml</code> to track deployments here.").html_safe
- .text-center
- = link_to _("Read more"), help_page_path("ci/environments"), class: "btn btn-success"
- - else
- .table-holder
- .ci-table.environments{ role: 'grid' }
- .gl-responsive-table-row.table-row-header{ role: 'row' }
- .table-section.section-15{ role: 'columnheader' }= _('Status')
- .table-section.section-10{ role: 'columnheader' }= _('ID')
- .table-section.section-10{ role: 'columnheader' }= _('Triggerer')
- .table-section.section-25{ role: 'columnheader' }= _('Commit')
- .table-section.section-10{ role: 'columnheader' }= _('Job')
- .table-section.section-10{ role: 'columnheader' }= _('Created')
- .table-section.section-10{ role: 'columnheader' }= _('Deployed')
+ .top-area.justify-content-between
+ .d-flex
+ %h3.page-title= @environment.name
+ - if @environment.auto_stop_at?
+ %p.align-self-end.prepend-left-8
+ = 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: '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: 'btn btn-danger', type: 'button', data: { toggle: 'modal',
+ target: '#delete-environment-modal' } do
+ = s_('Environments|Delete')
- = render @deployments
+ .environments-container
+ - if @deployments.blank?
+ .empty-state
+ .text-content
+ %h4.state-title
+ = _("You don't have any deployments right now.")
+ %p.blank-state-text
+ = _("Define environments in the deploy stage(s) in <code>.gitlab-ci.yml</code> to track deployments here.").html_safe
+ .text-center
+ = link_to _("Read more"), help_page_path("ci/environments"), class: "btn btn-success"
+ - else
+ .table-holder
+ .ci-table.environments{ role: 'grid' }
+ .gl-responsive-table-row.table-row-header{ role: 'row' }
+ .table-section.section-15{ role: 'columnheader' }= _('Status')
+ .table-section.section-10{ role: 'columnheader' }= _('ID')
+ .table-section.section-10{ role: 'columnheader' }= _('Triggerer')
+ .table-section.section-25{ role: 'columnheader' }= _('Commit')
+ .table-section.section-10{ role: 'columnheader' }= _('Job')
+ .table-section.section-10{ role: 'columnheader' }= _('Created')
+ .table-section.section-10{ role: 'columnheader' }= _('Deployed')
- = paginate @deployments, theme: 'gitlab'
+ = render @deployments
+
+ = paginate @deployments, theme: 'gitlab'
diff --git a/app/views/shared/icons/_dev_ops_score_no_data.svg b/app/views/shared/icons/_dev_ops_score_no_data.svg
index ed32b2333e7..5de929859ae 100644
--- a/app/views/shared/icons/_dev_ops_score_no_data.svg
+++ b/app/views/shared/icons/_dev_ops_score_no_data.svg
@@ -34,7 +34,6 @@
<rect width="38" height="4" y="12" fill="#FB722E" rx="2"/>
</g>
<path fill="#EEE" d="M4 14h106v4H4z"/>
- <path fill="#333" d="M35.724 138h9.696v-2.856h-2.856V122.76h-2.592c-1.08.648-2.136 1.08-3.792 1.392v2.184h2.856v8.808h-3.312V138zm17.736.288c-2.952 0-5.76-2.208-5.76-7.56 0-5.688 2.952-8.256 6.168-8.256 2.016 0 3.48.84 4.44 1.824l-1.848 2.112c-.528-.576-1.488-1.08-2.376-1.08-1.68 0-3.024 1.2-3.144 4.752.792-1.008 2.112-1.608 3.048-1.608 2.616 0 4.536 1.488 4.536 4.704 0 3.168-2.304 5.112-5.064 5.112zm-.072-2.64c1.056 0 1.92-.744 1.92-2.472 0-1.608-.84-2.208-1.992-2.208-.792 0-1.68.432-2.304 1.512.312 2.4 1.32 3.168 2.376 3.168zM63.9 132c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024zm.528 8.256l8.448-16.224h2.04l-8.448 16.224h-2.04zm11.016 0c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024z"/>
</g>
</g>
</svg>
diff --git a/app/views/shared/icons/_dev_ops_score_no_index.svg b/app/views/shared/icons/_dev_ops_score_no_index.svg
index 95c00e81d10..0577efca93f 100644
--- a/app/views/shared/icons/_dev_ops_score_no_index.svg
+++ b/app/views/shared/icons/_dev_ops_score_no_index.svg
@@ -17,7 +17,6 @@
<rect width="38" height="4" y="12" fill="#FB722E" rx="2"/>
</g>
<path fill="#EEE" d="M2 12h106v4H2z"/>
- <path fill="#333" d="M38.048 127.792c.792 0 1.68-.432 2.28-1.512-.312-2.4-1.296-3.168-2.376-3.168-1.032 0-1.92.744-1.92 2.472 0 1.608.864 2.208 2.016 2.208zm-.552 8.496c-2.016 0-3.504-.864-4.464-1.824l1.872-2.112c.504.576 1.464 1.08 2.352 1.08 1.704 0 3.024-1.2 3.144-4.752-.792 1.008-2.112 1.608-3.048 1.608-2.592 0-4.536-1.488-4.536-4.704 0-3.168 2.304-5.112 5.064-5.112 2.952 0 5.784 2.208 5.784 7.56 0 5.688-2.976 8.256-6.168 8.256zm13.488 0c-3.048 0-5.304-1.704-5.304-4.176 0-1.848 1.152-2.976 2.592-3.744v-.096c-1.176-.888-2.04-1.992-2.04-3.6 0-2.592 2.04-4.2 4.872-4.2 2.784 0 4.632 1.656 4.632 4.176 0 1.464-.936 2.64-1.992 3.336v.096c1.464.792 2.64 1.968 2.64 3.984 0 2.4-2.16 4.224-5.4 4.224zm.96-9.168c.6-.696.936-1.44.936-2.232 0-1.176-.696-1.968-1.848-1.968-.936 0-1.704.576-1.704 1.752 0 1.248 1.056 1.848 2.616 2.448zm-.888 6.72c1.176 0 2.04-.624 2.04-1.896 0-1.344-1.296-1.848-3.216-2.664-.672.624-1.176 1.488-1.176 2.424 0 1.344 1.08 2.136 2.352 2.136zm10.8-3.84c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024zm.528 8.256l8.448-16.224h2.04l-8.448 16.224h-2.04zm11.016 0c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024z"/>
</g>
<g transform="translate(122)">
<rect width="110" height="168" x="2" y="2" fill="#FFF" rx="10"/>
@@ -39,7 +38,6 @@
<rect width="8" height="4" x="73" y="14" fill="#EEE" rx="2"/>
<rect width="8" height="4" x="86" y="14" fill="#EEE" rx="2"/>
<rect width="8" height="4" x="99" y="14" fill="#EEE" rx="2"/>
- <path fill="#EEE" d="M46.716 138.288c-3.264 0-5.448-2.784-5.448-7.968s2.184-7.848 5.448-7.848c3.264 0 5.448 2.664 5.448 7.848 0 5.184-2.184 7.968-5.448 7.968zm0-2.736c1.2 0 2.112-1.08 2.112-5.232 0-4.176-.912-5.112-2.112-5.112-1.176 0-2.112.936-2.112 5.112 0 4.152.936 5.232 2.112 5.232zM57.564 132c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024zm.528 8.256l8.448-16.224h2.04l-8.448 16.224h-2.04zm11.016 0c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024z"/>
</g>
<g transform="translate(243)">
<rect width="110" height="168" x="2" y="2" fill="#FFF" rx="10"/>
@@ -61,7 +59,6 @@
<rect width="8" height="4" x="73" y="14" fill="#EEE" rx="2"/>
<rect width="8" height="4" x="86" y="14" fill="#EEE" rx="2"/>
<rect width="8" height="4" x="99" y="14" fill="#EEE" rx="2"/>
- <path fill="#EEE" d="M46.716 138.288c-3.264 0-5.448-2.784-5.448-7.968s2.184-7.848 5.448-7.848c3.264 0 5.448 2.664 5.448 7.848 0 5.184-2.184 7.968-5.448 7.968zm0-2.736c1.2 0 2.112-1.08 2.112-5.232 0-4.176-.912-5.112-2.112-5.112-1.176 0-2.112.936-2.112 5.112 0 4.152.936 5.232 2.112 5.232zM57.564 132c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024zm.528 8.256l8.448-16.224h2.04l-8.448 16.224h-2.04zm11.016 0c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024z"/>
</g>
</g>
</svg>