summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue93
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue17
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue1
-rw-r--r--app/assets/javascripts/ci_variable_list/constants.js2
-rw-r--r--app/assets/javascripts/ci_variable_list/store/actions.js19
-rw-r--r--app/assets/javascripts/ci_variable_list/store/getters.js9
-rw-r--r--app/assets/javascripts/ci_variable_list/store/index.js2
-rw-r--r--app/assets/javascripts/ci_variable_list/store/mutation_types.js4
-rw-r--r--app/assets/javascripts/ci_variable_list/store/mutations.js21
-rw-r--r--app/assets/javascripts/ci_variable_list/store/state.js1
-rw-r--r--app/assets/javascripts/droplab/utils.js2
-rw-r--r--app/assets/javascripts/monitoring/components/panel_type.vue5
-rw-r--r--app/assets/javascripts/monitoring/constants.js5
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js2
-rw-r--r--app/assets/javascripts/monitoring/stores/getters.js25
-rw-r--r--app/assets/javascripts/monitoring/stores/utils.js10
-rw-r--r--app/uploaders/file_uploader.rb2
-rw-r--r--app/views/admin/serverless/domains/_form.html.haml31
-rw-r--r--changelogs/unreleased/209940-geo-fails-to-sync-file-uploads-with-improper-formatted-path.yml5
-rw-r--r--changelogs/unreleased/rc-use_metric_step.yml5
-rw-r--r--locale/gitlab.pot18
-rw-r--r--spec/features/admin/admin_serverless_domains_spec.rb28
-rw-r--r--spec/frontend/ci_variable_list/components/ci_enviroments_dropdown_spec.js103
-rw-r--r--spec/frontend/ci_variable_list/services/mock_data.js69
-rw-r--r--spec/frontend/ci_variable_list/store/getters_spec.js21
-rw-r--r--spec/frontend/ci_variable_list/store/mutations_spec.js58
-rw-r--r--spec/frontend/monitoring/mock_data.js22
-rw-r--r--spec/frontend/monitoring/store/actions_spec.js80
-rw-r--r--spec/frontend/monitoring/store/getters_spec.js53
-rw-r--r--spec/frontend/monitoring/store/mutations_spec.js8
-rw-r--r--spec/frontend/monitoring/store/utils_spec.js2
-rw-r--r--spec/uploaders/file_uploader_spec.rb36
32 files changed, 711 insertions, 48 deletions
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue b/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue
new file mode 100644
index 00000000000..175e89a454b
--- /dev/null
+++ b/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue
@@ -0,0 +1,93 @@
+<script>
+import {
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
+ GlSearchBoxByType,
+ GlIcon,
+} from '@gitlab/ui';
+import { __, sprintf } from '~/locale';
+import { mapGetters } from 'vuex';
+
+export default {
+ name: 'CiEnvironmentsDropdown',
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
+ GlSearchBoxByType,
+ GlIcon,
+ },
+ props: {
+ value: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ searchTerm: this.value || '',
+ };
+ },
+ computed: {
+ ...mapGetters(['joinedEnvironments']),
+ composedCreateButtonLabel() {
+ return sprintf(__('Create wildcard: %{searchTerm}'), { searchTerm: this.searchTerm });
+ },
+ shouldRenderCreateButton() {
+ return this.searchTerm && !this.joinedEnvironments.includes(this.searchTerm);
+ },
+ filteredResults() {
+ const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
+ return this.joinedEnvironments.filter(resultString =>
+ resultString.toLowerCase().includes(lowerCasedSearchTerm),
+ );
+ },
+ },
+ watch: {
+ value(newVal) {
+ this.searchTerm = newVal;
+ },
+ },
+ methods: {
+ selectEnvironment(selected) {
+ this.$emit('selectEnvironment', selected);
+ this.searchTerm = '';
+ },
+ createClicked() {
+ this.$emit('createClicked', this.searchTerm);
+ this.searchTerm = '';
+ },
+ isSelected(env) {
+ return this.value === env;
+ },
+ },
+};
+</script>
+<template>
+ <gl-dropdown :text="value">
+ <gl-search-box-by-type v-model.trim="searchTerm" class="m-2" />
+ <gl-dropdown-item
+ v-for="environment in filteredResults"
+ :key="environment"
+ @click="selectEnvironment(environment)"
+ >
+ <gl-icon
+ :class="{ invisible: !isSelected(environment) }"
+ name="mobile-issue-close"
+ class="vertical-align-middle"
+ />
+ {{ environment }}
+ </gl-dropdown-item>
+ <gl-dropdown-item v-if="!filteredResults.length" ref="noMatchingResults">{{
+ __('No matching results')
+ }}</gl-dropdown-item>
+ <template v-if="shouldRenderCreateButton">
+ <gl-dropdown-divider />
+ <gl-dropdown-item @click="createClicked">
+ {{ composedCreateButtonLabel }}
+ </gl-dropdown-item>
+ </template>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
index 0ccc58ec2da..0460181558b 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
@@ -2,6 +2,7 @@
import { __ } from '~/locale';
import { mapActions, mapState } from 'vuex';
import { ADD_CI_VARIABLE_MODAL_ID } from '../constants';
+import CiEnvironmentsDropdown from './ci_environments_dropdown.vue';
import {
GlButton,
GlModal,
@@ -17,6 +18,7 @@ import {
export default {
modalId: ADD_CI_VARIABLE_MODAL_ID,
components: {
+ CiEnvironmentsDropdown,
GlButton,
GlModal,
GlFormSelect,
@@ -36,6 +38,7 @@ export default {
'variableBeingEdited',
'isGroup',
'maskableRegex',
+ 'selectedEnvironment',
]),
canSubmit() {
if (this.variableData.masked && this.maskedState === false) {
@@ -80,6 +83,10 @@ export default {
'displayInputValue',
'clearModal',
'deleteVariable',
+ 'setEnvironmentScope',
+ 'addWildCardScope',
+ 'resetSelectedEnvironment',
+ 'setSelectedEnvironment',
]),
updateOrAddVariable() {
if (this.variableBeingEdited) {
@@ -95,6 +102,7 @@ export default {
} else {
this.clearModal();
}
+ this.resetSelectedEnvironment();
},
hideModal() {
this.$refs.modal.hide();
@@ -158,10 +166,11 @@ export default {
label-for="ci-variable-env"
class="w-50"
>
- <gl-form-select
- id="ci-variable-env"
- v-model="variableData.environment_scope"
- :options="environments"
+ <ci-environments-dropdown
+ class="w-100"
+ :value="variableData.environment_scope"
+ @selectEnvironment="setEnvironmentScope"
+ @createClicked="addWildCardScope"
/>
</gl-form-group>
</div>
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue
index 3f2f89ada6f..806fa3e1191 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue
@@ -92,6 +92,7 @@ export default {
sort-by="key"
sort-direction="asc"
stacked="lg"
+ table-class="text-secondary"
fixed
show-empty
sort-icon-left
diff --git a/app/assets/javascripts/ci_variable_list/constants.js b/app/assets/javascripts/ci_variable_list/constants.js
index b2fa980c546..d22138db102 100644
--- a/app/assets/javascripts/ci_variable_list/constants.js
+++ b/app/assets/javascripts/ci_variable_list/constants.js
@@ -6,7 +6,7 @@ export const ADD_CI_VARIABLE_MODAL_ID = 'add-ci-variable';
export const displayText = {
variableText: __('Var'),
fileText: __('File'),
- allEnvironmentsText: __('All'),
+ allEnvironmentsText: __('All (default)'),
};
export const types = {
diff --git a/app/assets/javascripts/ci_variable_list/store/actions.js b/app/assets/javascripts/ci_variable_list/store/actions.js
index f3a629b84ee..a22fa03e16d 100644
--- a/app/assets/javascripts/ci_variable_list/store/actions.js
+++ b/app/assets/javascripts/ci_variable_list/store/actions.js
@@ -153,3 +153,22 @@ export const fetchEnvironments = ({ dispatch, state }) => {
createFlash(__('There was an error fetching the environments information.'));
});
};
+
+export const setEnvironmentScope = ({ commit, dispatch }, environment) => {
+ commit(types.SET_ENVIRONMENT_SCOPE, environment);
+ dispatch('setSelectedEnvironment', environment);
+};
+
+export const addWildCardScope = ({ commit, dispatch }, environment) => {
+ commit(types.ADD_WILD_CARD_SCOPE, environment);
+ commit(types.SET_ENVIRONMENT_SCOPE, environment);
+ dispatch('setSelectedEnvironment', environment);
+};
+
+export const resetSelectedEnvironment = ({ commit }) => {
+ commit(types.RESET_SELECTED_ENVIRONMENT);
+};
+
+export const setSelectedEnvironment = ({ commit }, environment) => {
+ commit(types.SET_SELECTED_ENVIRONMENT, environment);
+};
diff --git a/app/assets/javascripts/ci_variable_list/store/getters.js b/app/assets/javascripts/ci_variable_list/store/getters.js
new file mode 100644
index 00000000000..14b728302f9
--- /dev/null
+++ b/app/assets/javascripts/ci_variable_list/store/getters.js
@@ -0,0 +1,9 @@
+/* eslint-disable import/prefer-default-export */
+// Disabling import/prefer-default-export can be
+// removed once a second getter is added to this file
+import { uniq } from 'lodash';
+
+export const joinedEnvironments = state => {
+ const scopesFromVariables = (state.variables || []).map(variable => variable.environment_scope);
+ return uniq(state.environments.concat(scopesFromVariables)).sort();
+};
diff --git a/app/assets/javascripts/ci_variable_list/store/index.js b/app/assets/javascripts/ci_variable_list/store/index.js
index db4ba95b3c2..83802f6a36f 100644
--- a/app/assets/javascripts/ci_variable_list/store/index.js
+++ b/app/assets/javascripts/ci_variable_list/store/index.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
+import * as getters from './getters';
import mutations from './mutations';
import state from './state';
@@ -10,6 +11,7 @@ export default (initialState = {}) =>
new Vuex.Store({
actions,
mutations,
+ getters,
state: {
...state(),
...initialState,
diff --git a/app/assets/javascripts/ci_variable_list/store/mutation_types.js b/app/assets/javascripts/ci_variable_list/store/mutation_types.js
index 240066d0f22..0b41c20bce7 100644
--- a/app/assets/javascripts/ci_variable_list/store/mutation_types.js
+++ b/app/assets/javascripts/ci_variable_list/store/mutation_types.js
@@ -20,3 +20,7 @@ export const RECEIVE_UPDATE_VARIABLE_ERROR = 'RECEIVE_UPDATE_VARIABLE_ERROR';
export const REQUEST_ENVIRONMENTS = 'REQUEST_ENVIRONMENTS';
export const RECEIVE_ENVIRONMENTS_SUCCESS = 'RECEIVE_ENVIRONMENTS_SUCCESS';
+export const SET_ENVIRONMENT_SCOPE = 'SET_ENVIRONMENT_SCOPE';
+export const ADD_WILD_CARD_SCOPE = 'ADD_WILD_CARD_SCOPE';
+export const RESET_SELECTED_ENVIRONMENT = 'RESET_SELECTED_ENVIRONMENT';
+export const SET_SELECTED_ENVIRONMENT = 'SET_SELECTED_ENVIRONMENT';
diff --git a/app/assets/javascripts/ci_variable_list/store/mutations.js b/app/assets/javascripts/ci_variable_list/store/mutations.js
index c75eb4a91fd..7ee7d7bdc26 100644
--- a/app/assets/javascripts/ci_variable_list/store/mutations.js
+++ b/app/assets/javascripts/ci_variable_list/store/mutations.js
@@ -83,4 +83,25 @@ export default {
state.variableBeingEdited = null;
state.showInputValue = false;
},
+
+ [types.SET_ENVIRONMENT_SCOPE](state, environment) {
+ if (state.variableBeingEdited) {
+ state.variableBeingEdited.environment_scope = environment;
+ } else {
+ state.variable.environment_scope = environment;
+ }
+ },
+
+ [types.ADD_WILD_CARD_SCOPE](state, environment) {
+ state.environments.push(environment);
+ state.environments.sort();
+ },
+
+ [types.RESET_SELECTED_ENVIRONMENT](state) {
+ state.selectedEnvironment = '';
+ },
+
+ [types.SET_SELECTED_ENVIRONMENT](state, environment) {
+ state.selectedEnvironment = environment;
+ },
};
diff --git a/app/assets/javascripts/ci_variable_list/store/state.js b/app/assets/javascripts/ci_variable_list/store/state.js
index 5166321d6a7..8c0b9c6966f 100644
--- a/app/assets/javascripts/ci_variable_list/store/state.js
+++ b/app/assets/javascripts/ci_variable_list/store/state.js
@@ -21,4 +21,5 @@ export default () => ({
environments: [],
typeOptions: [displayText.variableText, displayText.fileText],
variableBeingEdited: null,
+ selectedEnvironment: '',
});
diff --git a/app/assets/javascripts/droplab/utils.js b/app/assets/javascripts/droplab/utils.js
index 5272778ce2d..df3c5c2132a 100644
--- a/app/assets/javascripts/droplab/utils.js
+++ b/app/assets/javascripts/droplab/utils.js
@@ -1,6 +1,6 @@
/* eslint-disable */
-import { template as _template } from 'underscore';
+import { template as _template } from 'lodash';
import { DATA_TRIGGER, DATA_DROPDOWN, TEMPLATE_REGEX } from './constants';
const utils = {
diff --git a/app/assets/javascripts/monitoring/components/panel_type.vue b/app/assets/javascripts/monitoring/components/panel_type.vue
index 77ba17b6e68..44e38089da8 100644
--- a/app/assets/javascripts/monitoring/components/panel_type.vue
+++ b/app/assets/javascripts/monitoring/components/panel_type.vue
@@ -101,7 +101,8 @@ export default {
return this.graphData.title || '';
},
alertWidgetAvailable() {
- return IS_EE && this.prometheusAlertsAvailable && this.alertsEndpoint && this.graphData;
+ // This method is extended by ee functionality
+ return false;
},
graphDataHasMetrics() {
return (
@@ -209,7 +210,7 @@ export default {
>
<div class="d-flex align-items-center">
<alert-widget
- v-if="alertWidgetAvailable && graphData"
+ v-if="alertWidgetAvailable"
:modal-id="`alert-modal-${index}`"
:alerts-endpoint="alertsEndpoint"
:relevant-queries="graphData.metrics"
diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js
index 3990a8d1f61..6609946e02e 100644
--- a/app/assets/javascripts/monitoring/constants.js
+++ b/app/assets/javascripts/monitoring/constants.js
@@ -104,3 +104,8 @@ export const endpointKeys = [
* as Vue props.
*/
export const initialStateKeys = [...endpointKeys, 'currentEnvironmentName'];
+
+/**
+ * Constant to indicate if a metric exists in the database
+ */
+export const NOT_IN_DB_PREFIX = 'NO_DB';
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index 86f416240c8..2e4987b7349 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -144,7 +144,7 @@ export const fetchPrometheusMetric = ({ commit }, { metric, params }) => {
const minStep = 60;
const queryDataPoints = 600;
- const step = Math.max(minStep, Math.ceil(timeDiff / queryDataPoints));
+ const step = metric.step ? metric.step : Math.max(minStep, Math.ceil(timeDiff / queryDataPoints));
const queryParams = {
start_time,
diff --git a/app/assets/javascripts/monitoring/stores/getters.js b/app/assets/javascripts/monitoring/stores/getters.js
index 1affc6f0a76..a6d80c5063e 100644
--- a/app/assets/javascripts/monitoring/stores/getters.js
+++ b/app/assets/javascripts/monitoring/stores/getters.js
@@ -1,3 +1,5 @@
+import { NOT_IN_DB_PREFIX } from '../constants';
+
const metricsIdsInPanel = panel =>
panel.metrics.filter(metric => metric.metricId && metric.result).map(metric => metric.metricId);
@@ -59,6 +61,29 @@ export const metricsWithData = state => groupKey => {
};
/**
+ * Metrics loaded from project-defined dashboards do not have a metric_id.
+ * This getter checks which metrics are stored in the db (have a metric id)
+ * This is hopefully a temporary solution until BE processes metrics before passing to FE
+ *
+ * Related:
+ * https://gitlab.com/gitlab-org/gitlab/-/issues/28241
+ * https://gitlab.com/gitlab-org/gitlab/-/merge_requests/27447
+ */
+export const metricsSavedToDb = state => {
+ const metricIds = [];
+ state.dashboard.panelGroups.forEach(({ panels }) => {
+ panels.forEach(({ metrics }) => {
+ const metricIdsInDb = metrics
+ .filter(({ metricId }) => !metricId.startsWith(NOT_IN_DB_PREFIX))
+ .map(({ metricId }) => metricId);
+
+ metricIds.push(...metricIdsInDb);
+ });
+ });
+ return metricIds;
+};
+
+/**
* Filter environments by names.
*
* This is used in the environments dropdown with searchable input.
diff --git a/app/assets/javascripts/monitoring/stores/utils.js b/app/assets/javascripts/monitoring/stores/utils.js
index b5938fb1205..5e620d6c2f5 100644
--- a/app/assets/javascripts/monitoring/stores/utils.js
+++ b/app/assets/javascripts/monitoring/stores/utils.js
@@ -2,6 +2,7 @@ import { slugify } from '~/lib/utils/text_utility';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
import { SUPPORTED_FORMATS } from '~/lib/utils/unit_format';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { NOT_IN_DB_PREFIX } from '../constants';
export const gqClient = createGqClient(
{},
@@ -14,11 +15,18 @@ export const gqClient = createGqClient(
* Metrics loaded from project-defined dashboards do not have a metric_id.
* This method creates a unique ID combining metric_id and id, if either is present.
* This is hopefully a temporary solution until BE processes metrics before passing to FE
+ *
+ * Related:
+ * https://gitlab.com/gitlab-org/gitlab/-/issues/28241
+ * https://gitlab.com/gitlab-org/gitlab/-/merge_requests/27447
+ *
* @param {Object} metric - metric
+ * @param {Number} metric.metric_id - Database metric id
+ * @param {String} metric.id - User-defined identifier
* @returns {Object} - normalized metric with a uniqueID
*/
// eslint-disable-next-line babel/camelcase
-export const uniqMetricsId = ({ metric_id, id }) => `${metric_id}_${id}`;
+export const uniqMetricsId = ({ metric_id, id }) => `${metric_id || NOT_IN_DB_PREFIX}_${id}`;
/**
* Project path has a leading slash that doesn't work well
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index 505b51c2006..517c22e2334 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -15,7 +15,7 @@ class FileUploader < GitlabUploader
prepend ObjectStorage::Extension::RecordsUploads
MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)}.freeze
- DYNAMIC_PATH_PATTERN = %r{.*(?<secret>\h{32})/(?<identifier>.*)}.freeze
+ DYNAMIC_PATH_PATTERN = %r{.*/(?<secret>\h{10,32})/(?<identifier>.*)}.freeze
VALID_SECRET_PATTERN = %r{\A\h{10,32}\z}.freeze
InvalidSecret = Class.new(StandardError)
diff --git a/app/views/admin/serverless/domains/_form.html.haml b/app/views/admin/serverless/domains/_form.html.haml
index 8c1c1d41caa..9e7990ef8ca 100644
--- a/app/views/admin/serverless/domains/_form.html.haml
+++ b/app/views/admin/serverless/domains/_form.html.haml
@@ -66,3 +66,34 @@
= _("Upload a private key for your certificate")
= f.submit @domain.persisted? ? _('Save changes') : _('Add domain'), class: "btn btn-success js-serverless-domain-submit", disabled: @domain.persisted?
+ - if @domain.persisted?
+ %button.btn.btn-remove{ type: 'button', data: { toggle: 'modal', target: "#modal-delete-domain" } }
+ = _('Delete domain')
+
+-# haml-lint:disable NoPlainNodes
+- if @domain.persisted?
+ - domain_attached = @domain.serverless_domain_clusters.count > 0
+ .modal{ id: "modal-delete-domain", tabindex: -1 }
+ .modal-dialog
+ .modal-content
+ .modal-header
+ %h3.page-title= _('Delete serverless domain?')
+ %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
+ %span{ "aria-hidden": true } &times;
+
+ .modal-body
+ - if domain_attached
+ = _("You must disassociate %{domain} from all clusters it is attached to before deletion.").html_safe % { domain: "<code>#{@domain.domain}</code>".html_safe }
+ - else
+ = _("You are about to delete %{domain} from your instance. This domain will no longer be available to any Knative application.").html_safe % { domain: "<code>#{@domain.domain}</code>".html_safe }
+
+ .modal-footer
+ %a{ href: '#', data: { dismiss: 'modal' }, class: 'btn btn-default' }
+ = _('Cancel')
+
+ = link_to _('Delete domain'),
+ admin_serverless_domain_path(@domain.id),
+ title: _('Delete'),
+ method: :delete,
+ class: "btn btn-remove",
+ disabled: domain_attached
diff --git a/changelogs/unreleased/209940-geo-fails-to-sync-file-uploads-with-improper-formatted-path.yml b/changelogs/unreleased/209940-geo-fails-to-sync-file-uploads-with-improper-formatted-path.yml
new file mode 100644
index 00000000000..521fc9f83b7
--- /dev/null
+++ b/changelogs/unreleased/209940-geo-fails-to-sync-file-uploads-with-improper-formatted-path.yml
@@ -0,0 +1,5 @@
+---
+title: Fix incorrect regex used in FileUploader#extract_dynamic_path
+merge_request: 28683
+author:
+type: fixed
diff --git a/changelogs/unreleased/rc-use_metric_step.yml b/changelogs/unreleased/rc-use_metric_step.yml
new file mode 100644
index 00000000000..f697523997c
--- /dev/null
+++ b/changelogs/unreleased/rc-use_metric_step.yml
@@ -0,0 +1,5 @@
+---
+title: Allow defining of metric step in dashboard yml
+merge_request: 28247
+author:
+type: added
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 97e224c0fe9..7dbdf40d831 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1631,6 +1631,9 @@ msgstr ""
msgid "All %{replicableType} are being scheduled for %{action}"
msgstr ""
+msgid "All (default)"
+msgstr ""
+
msgid "All Members"
msgstr ""
@@ -5904,6 +5907,9 @@ msgstr ""
msgid "Create project label"
msgstr ""
+msgid "Create wildcard: %{searchTerm}"
+msgstr ""
+
msgid "Create your first page"
msgstr ""
@@ -6446,6 +6452,9 @@ msgstr ""
msgid "Delete comment"
msgstr ""
+msgid "Delete domain"
+msgstr ""
+
msgid "Delete license"
msgstr ""
@@ -6458,6 +6467,9 @@ msgstr ""
msgid "Delete project"
msgstr ""
+msgid "Delete serverless domain?"
+msgstr ""
+
msgid "Delete snippet"
msgstr ""
@@ -23079,6 +23091,9 @@ msgstr ""
msgid "You"
msgstr ""
+msgid "You are about to delete %{domain} from your instance. This domain will no longer be available to any Knative application."
+msgstr ""
+
msgid "You are about to transfer the control of your account to %{group_name} group. This action is NOT reversible, you won't be able to access any of your groups and projects outside of %{group_name} once this transfer is complete."
msgstr ""
@@ -23355,6 +23370,9 @@ msgstr ""
msgid "You must accept our Terms of Service and privacy policy in order to register an account"
msgstr ""
+msgid "You must disassociate %{domain} from all clusters it is attached to before deletion."
+msgstr ""
+
msgid "You must have maintainer access to force delete a lock"
msgstr ""
diff --git a/spec/features/admin/admin_serverless_domains_spec.rb b/spec/features/admin/admin_serverless_domains_spec.rb
index 85fe67004da..48f6af8d4bd 100644
--- a/spec/features/admin/admin_serverless_domains_spec.rb
+++ b/spec/features/admin/admin_serverless_domains_spec.rb
@@ -56,4 +56,32 @@ describe 'Admin Serverless Domains', :js do
expect(page).to have_content 'Domain was successfully updated'
expect(page).to have_content '/CN=test-certificate'
end
+
+ context 'when domain exists' do
+ let!(:domain) { create(:pages_domain, :instance_serverless) }
+
+ it 'Displays a modal when attempting to delete a domain' do
+ visit admin_serverless_domains_path
+
+ click_button 'Delete domain'
+
+ page.within '#modal-delete-domain' do
+ expect(page).to have_content "You are about to delete #{domain.domain} from your instance."
+ expect(page).to have_link('Delete domain')
+ end
+ end
+
+ it 'Displays a modal with disabled button if unable to delete a domain' do
+ create(:serverless_domain_cluster, pages_domain: domain)
+
+ visit admin_serverless_domains_path
+
+ click_button 'Delete domain'
+
+ page.within '#modal-delete-domain' do
+ expect(page).to have_content "You must disassociate #{domain.domain} from all clusters it is attached to before deletion."
+ expect(page).to have_link('Delete domain')
+ end
+ end
+ end
end
diff --git a/spec/frontend/ci_variable_list/components/ci_enviroments_dropdown_spec.js b/spec/frontend/ci_variable_list/components/ci_enviroments_dropdown_spec.js
new file mode 100644
index 00000000000..a52b38599f7
--- /dev/null
+++ b/spec/frontend/ci_variable_list/components/ci_enviroments_dropdown_spec.js
@@ -0,0 +1,103 @@
+import Vuex from 'vuex';
+import CiEnvironmentsDropdown from '~/ci_variable_list/components/ci_environments_dropdown.vue';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { GlDropdownItem, GlIcon } from '@gitlab/ui';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('Ci environments dropdown', () => {
+ let wrapper;
+ let store;
+
+ const createComponent = term => {
+ store = new Vuex.Store({
+ getters: {
+ joinedEnvironments: () => ['dev', 'prod', 'staging'],
+ },
+ });
+
+ wrapper = shallowMount(CiEnvironmentsDropdown, {
+ store,
+ localVue,
+ propsData: {
+ value: term,
+ },
+ });
+ };
+
+ const findAllDropdownItems = () => wrapper.findAll(GlDropdownItem);
+ const findDropdownItemByIndex = index => wrapper.findAll(GlDropdownItem).at(index);
+ const findActiveIconByIndex = index => wrapper.findAll(GlIcon).at(index);
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('No enviroments found', () => {
+ beforeEach(() => {
+ createComponent('stable');
+ });
+
+ it('renders create button with search term if enviroments do not contain search term', () => {
+ expect(findAllDropdownItems()).toHaveLength(2);
+ expect(findDropdownItemByIndex(1).text()).toBe('Create wildcard: stable');
+ });
+
+ it('renders empty results message', () => {
+ expect(findDropdownItemByIndex(0).text()).toBe('No matching results');
+ });
+ });
+
+ describe('Search term is empty', () => {
+ beforeEach(() => {
+ createComponent('');
+ });
+
+ it('renders all enviroments when search term is empty', () => {
+ expect(findAllDropdownItems()).toHaveLength(3);
+ expect(findDropdownItemByIndex(0).text()).toBe('dev');
+ expect(findDropdownItemByIndex(1).text()).toBe('prod');
+ expect(findDropdownItemByIndex(2).text()).toBe('staging');
+ });
+ });
+
+ describe('Enviroments found', () => {
+ beforeEach(() => {
+ createComponent('prod');
+ });
+
+ it('renders only the enviroment searched for', () => {
+ expect(findAllDropdownItems()).toHaveLength(1);
+ expect(findDropdownItemByIndex(0).text()).toBe('prod');
+ });
+
+ it('should not display create button', () => {
+ const enviroments = findAllDropdownItems().filter(env => env.text().startsWith('Create'));
+ expect(enviroments).toHaveLength(0);
+ expect(findAllDropdownItems()).toHaveLength(1);
+ });
+
+ it('should not display empty results message', () => {
+ expect(wrapper.find({ ref: 'noMatchingResults' }).exists()).toBe(false);
+ });
+
+ it('should display active checkmark if active', () => {
+ expect(findActiveIconByIndex(0).classes('invisible')).toBe(false);
+ });
+
+ describe('Custom events', () => {
+ it('should emit selectEnvironment if an environment is clicked', () => {
+ findDropdownItemByIndex(0).vm.$emit('click');
+ expect(wrapper.emitted('selectEnvironment')).toEqual([['prod']]);
+ });
+
+ it('should emit createClicked if an environment is clicked', () => {
+ createComponent('newscope');
+ findDropdownItemByIndex(1).vm.$emit('click');
+ expect(wrapper.emitted('createClicked')).toEqual([['newscope']]);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci_variable_list/services/mock_data.js b/spec/frontend/ci_variable_list/services/mock_data.js
index 5e0fa55a20c..09c6cd9de21 100644
--- a/spec/frontend/ci_variable_list/services/mock_data.js
+++ b/spec/frontend/ci_variable_list/services/mock_data.js
@@ -1,7 +1,7 @@
export default {
mockVariables: [
{
- environment_scope: 'All environments',
+ environment_scope: 'All (default)',
id: 113,
key: 'test_var',
masked: false,
@@ -37,7 +37,7 @@ export default {
mockVariablesDisplay: [
{
- environment_scope: 'All',
+ environment_scope: 'All (default)',
id: 113,
key: 'test_var',
masked: false,
@@ -47,7 +47,7 @@ export default {
variable_type: 'Var',
},
{
- environment_scope: 'All',
+ environment_scope: 'All (default)',
id: 114,
key: 'test_var_2',
masked: false,
@@ -88,4 +88,67 @@ export default {
ozakE+8p06BpxegR4BR3FMHf6p+0jQxUEAkAyb/mVgm66TyghDGC6/YkiKoZptXQ
98TwDIK/39WEB/V607As+KoYazQG8drorw==
-----END CERTIFICATE REQUEST-----`,
+
+ mockVariableScopes: [
+ {
+ id: 13,
+ key: 'test_var_1',
+ value: 'test_val_1',
+ variable_type: 'File',
+ protected: true,
+ masked: true,
+ environment_scope: 'All (default)',
+ secret_value: 'test_val_1',
+ },
+ {
+ id: 28,
+ key: 'goku_var',
+ value: 'goku_val',
+ variable_type: 'Var',
+ protected: true,
+ masked: true,
+ environment_scope: 'staging',
+ secret_value: 'goku_val',
+ },
+ {
+ id: 25,
+ key: 'test_var_4',
+ value: 'test_val_4',
+ variable_type: 'Var',
+ protected: false,
+ masked: false,
+ environment_scope: 'production',
+ secret_value: 'test_val_4',
+ },
+ {
+ id: 14,
+ key: 'test_var_2',
+ value: 'test_val_2',
+ variable_type: 'File',
+ protected: false,
+ masked: false,
+ environment_scope: 'staging',
+ secret_value: 'test_val_2',
+ },
+ {
+ id: 24,
+ key: 'test_var_3',
+ value: 'test_val_3',
+ variable_type: 'Var',
+ protected: false,
+ masked: false,
+ environment_scope: 'All (default)',
+ secret_value: 'test_val_3',
+ },
+ {
+ id: 26,
+ key: 'test_var_5',
+ value: 'test_val_5',
+ variable_type: 'Var',
+ protected: false,
+ masked: false,
+ environment_scope: 'production',
+ secret_value: 'test_val_5',
+ },
+ ],
};
diff --git a/spec/frontend/ci_variable_list/store/getters_spec.js b/spec/frontend/ci_variable_list/store/getters_spec.js
new file mode 100644
index 00000000000..7ad96545652
--- /dev/null
+++ b/spec/frontend/ci_variable_list/store/getters_spec.js
@@ -0,0 +1,21 @@
+import * as getters from '~/ci_variable_list/store/getters';
+import mockData from '../services/mock_data';
+
+describe('Ci variable getters', () => {
+ describe('joinedEnvironments', () => {
+ it('should join fetched enviroments with variable environment scopes', () => {
+ const state = {
+ environments: ['All (default)', 'staging', 'deployment', 'prod'],
+ variables: mockData.mockVariableScopes,
+ };
+
+ expect(getters.joinedEnvironments(state)).toEqual([
+ 'All (default)',
+ 'deployment',
+ 'prod',
+ 'production',
+ 'staging',
+ ]);
+ });
+ });
+});
diff --git a/spec/frontend/ci_variable_list/store/mutations_spec.js b/spec/frontend/ci_variable_list/store/mutations_spec.js
index 05513edff7b..8652359f3df 100644
--- a/spec/frontend/ci_variable_list/store/mutations_spec.js
+++ b/spec/frontend/ci_variable_list/store/mutations_spec.js
@@ -4,6 +4,15 @@ import * as types from '~/ci_variable_list/store/mutation_types';
describe('CI variable list mutations', () => {
let stateCopy;
+ const variableBeingEdited = {
+ environment_scope: '*',
+ id: 63,
+ key: 'test_var',
+ masked: false,
+ protected: false,
+ value: 'test_val',
+ variable_type: 'env_var',
+ };
beforeEach(() => {
stateCopy = state();
@@ -21,16 +30,6 @@ describe('CI variable list mutations', () => {
describe('VARIABLE_BEING_EDITED', () => {
it('should set variable that is being edited', () => {
- const variableBeingEdited = {
- environment_scope: '*',
- id: 63,
- key: 'test_var',
- masked: false,
- protected: false,
- value: 'test_val',
- variable_type: 'env_var',
- };
-
mutations[types.VARIABLE_BEING_EDITED](stateCopy, variableBeingEdited);
expect(stateCopy.variableBeingEdited).toEqual(variableBeingEdited);
@@ -53,7 +52,7 @@ describe('CI variable list mutations', () => {
secret_value: '',
protected: false,
masked: false,
- environment_scope: 'All',
+ environment_scope: 'All (default)',
};
mutations[types.CLEAR_MODAL](stateCopy);
@@ -61,4 +60,41 @@ describe('CI variable list mutations', () => {
expect(stateCopy.variable).toEqual(modalState);
});
});
+
+ describe('RECEIVE_ENVIRONMENTS_SUCCESS', () => {
+ it('should set environments', () => {
+ const environments = ['env1', 'env2'];
+
+ mutations[types.RECEIVE_ENVIRONMENTS_SUCCESS](stateCopy, environments);
+
+ expect(stateCopy.environments).toEqual(['All (default)', 'env1', 'env2']);
+ });
+ });
+
+ describe('SET_ENVIRONMENT_SCOPE', () => {
+ const environment = 'production';
+
+ it('should set scope to variable being updated if updating variable', () => {
+ stateCopy.variableBeingEdited = variableBeingEdited;
+
+ mutations[types.SET_ENVIRONMENT_SCOPE](stateCopy, environment);
+
+ expect(stateCopy.variableBeingEdited.environment_scope).toBe('production');
+ });
+
+ it('should set scope to variable if adding new variable', () => {
+ mutations[types.SET_ENVIRONMENT_SCOPE](stateCopy, environment);
+
+ expect(stateCopy.variable.environment_scope).toBe('production');
+ });
+ });
+
+ describe('ADD_WILD_CARD_SCOPE', () => {
+ it('should add wild card scope to enviroments array and sort', () => {
+ stateCopy.environments = ['dev', 'staging'];
+ mutations[types.ADD_WILD_CARD_SCOPE](stateCopy, 'production');
+
+ expect(stateCopy.environments).toEqual(['dev', 'production', 'staging']);
+ });
+ });
});
diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js
index 58693723624..dde47178c1d 100644
--- a/spec/frontend/monitoring/mock_data.js
+++ b/spec/frontend/monitoring/mock_data.js
@@ -288,7 +288,7 @@ export const mockedEmptyResult = {
};
export const mockedEmptyThroughputResult = {
- metricId: 'undefined_response_metrics_nginx_ingress_16_throughput_status_code',
+ metricId: 'NO_DB_response_metrics_nginx_ingress_16_throughput_status_code',
result: [],
};
@@ -304,12 +304,12 @@ export const mockedQueryResultPayloadCoresTotal = {
export const mockedQueryResultFixture = {
// First metric in fixture `metrics_dashboard/environment_metrics_dashboard.json`
- metricId: 'undefined_response_metrics_nginx_ingress_throughput_status_code',
+ metricId: 'NO_DB_response_metrics_nginx_ingress_throughput_status_code',
result: metricsResult,
};
export const mockedQueryResultFixtureStatusCode = {
- metricId: 'undefined_response_metrics_nginx_ingress_latency_pod_average',
+ metricId: 'NO_DB_response_metrics_nginx_ingress_latency_pod_average',
result: metricsResult,
};
@@ -560,13 +560,11 @@ export const graphDataPrometheusQueryRange = {
weight: 2,
metrics: [
{
- id: 'metric_a1',
- metricId: '2',
+ metricId: '2_metric_a',
query_range:
'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1024/1024/1024',
unit: 'MB',
label: 'Total Consumption',
- metric_id: 2,
prometheus_endpoint_path:
'/root/kubernetes-gke-project/environments/35/prometheus/api/v1/query?query=max%28go_memstats_alloc_bytes%7Bjob%3D%22prometheus%22%7D%29+by+%28job%29+%2F1024%2F1024',
result: [
@@ -587,13 +585,12 @@ export const graphDataPrometheusQueryRangeMultiTrack = {
y_label: 'Time',
metrics: [
{
- metricId: '1',
+ metricId: '1_metric_b',
id: 'response_metrics_nginx_ingress_throughput_status_code',
query_range:
'sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[60m])) by (status_code)',
unit: 'req / sec',
label: 'Status Code',
- metric_id: 1,
prometheus_endpoint_path:
'/root/rails_nodb/environments/3/prometheus/api/v1/query_range?query=sum%28rate%28nginx_upstream_responses_total%7Bupstream%3D~%22%25%7Bkube_namespace%7D-%25%7Bci_environment_slug%7D-.%2A%22%7D%5B2m%5D%29%29+by+%28status_code%29',
result: [
@@ -669,8 +666,7 @@ export const stackedColumnMockedData = {
series_name: 'group 1',
prometheus_endpoint_path:
'/root/autodevops-deploy-6/-/environments/24/prometheus/api/v1/query_range?query=avg%28sum%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%25%7Bci_environment_slug%7D-%28%5B%5Ec%5D.%2A%7Cc%28%5B%5Ea%5D%7Ca%28%5B%5En%5D%7Cn%28%5B%5Ea%5D%7Ca%28%5B%5Er%5D%7Cr%5B%5Ey%5D%29%29%29%29.%2A%7C%29-%28.%2A%29%22%2Cnamespace%3D%22%25%7Bkube_namespace%7D%22%7D%29+by+%28job%29%29+without+%28job%29+%2F+count%28avg%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%25%7Bci_environment_slug%7D-%28%5B%5Ec%5D.%2A%7Cc%28%5B%5Ea%5D%7Ca%28%5B%5En%5D%7Cn%28%5B%5Ea%5D%7Ca%28%5B%5Er%5D%7Cr%5B%5Ey%5D%29%29%29%29.%2A%7C%29-%28.%2A%29%22%2Cnamespace%3D%22%25%7Bkube_namespace%7D%22%7D%29+without+%28job%29%29+%2F1024%2F1024',
- metric_id: 'undefined_metric_of_ages_1024',
- metricId: 'undefined_metric_of_ages_1024',
+ metricId: 'NO_DB_metric_of_ages_1024',
result: [
{
metric: {},
@@ -688,8 +684,7 @@ export const stackedColumnMockedData = {
series_name: 'group 2',
prometheus_endpoint_path:
'/root/autodevops-deploy-6/-/environments/24/prometheus/api/v1/query_range?query=avg%28sum%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%25%7Bci_environment_slug%7D-%28%5B%5Ec%5D.%2A%7Cc%28%5B%5Ea%5D%7Ca%28%5B%5En%5D%7Cn%28%5B%5Ea%5D%7Ca%28%5B%5Er%5D%7Cr%5B%5Ey%5D%29%29%29%29.%2A%7C%29-%28.%2A%29%22%2Cnamespace%3D%22%25%7Bkube_namespace%7D%22%7D%29+by+%28job%29%29+without+%28job%29+%2F+count%28avg%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%25%7Bci_environment_slug%7D-%28%5B%5Ec%5D.%2A%7Cc%28%5B%5Ea%5D%7Ca%28%5B%5En%5D%7Cn%28%5B%5Ea%5D%7Ca%28%5B%5Er%5D%7Cr%5B%5Ey%5D%29%29%29%29.%2A%7C%29-%28.%2A%29%22%2Cnamespace%3D%22%25%7Bkube_namespace%7D%22%7D%29+without+%28job%29%29+%2F1024%2F1024',
- metric_id: 'undefined_metric_of_ages_1000',
- metricId: 'undefined_metric_of_ages_1000',
+ metricId: 'NO_DB_metric_of_ages_1000',
result: [
{
metric: {},
@@ -713,8 +708,7 @@ export const barMockData = {
{
id: 'sla_trends_primary_services',
series_name: 'group 1',
- metric_id: 'undefined_sla_trends_primary_services',
- metricId: 'undefined_sla_trends_primary_services',
+ metricId: 'NO_DB_sla_trends_primary_services',
query_range:
'avg(avg_over_time(slo_observation_status{environment="gprd", stage=~"main|", type=~"api|web|git|registry|sidekiq|ci-runners"}[1d])) by (type)',
unit: 'Percentile',
diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js
index e9f9aa0ba18..9f0b4d16fc1 100644
--- a/spec/frontend/monitoring/store/actions_spec.js
+++ b/spec/frontend/monitoring/store/actions_spec.js
@@ -557,6 +557,86 @@ describe('Monitoring store actions', () => {
).catch(done.fail);
});
+ describe('without metric defined step', () => {
+ const expectedParams = {
+ start_time: '2019-08-06T12:40:02.184Z',
+ end_time: '2019-08-06T20:40:02.184Z',
+ step: 60,
+ };
+
+ it('uses calculated step', done => {
+ mock.onGet('http://test').reply(200, { data }); // One attempt
+
+ testAction(
+ fetchPrometheusMetric,
+ { metric, params },
+ state,
+ [
+ {
+ type: types.REQUEST_METRIC_RESULT,
+ payload: {
+ metricId: metric.metricId,
+ },
+ },
+ {
+ type: types.RECEIVE_METRIC_RESULT_SUCCESS,
+ payload: {
+ metricId: metric.metricId,
+ result: data.result,
+ },
+ },
+ ],
+ [],
+ () => {
+ expect(mock.history.get[0].params).toEqual(expectedParams);
+ done();
+ },
+ ).catch(done.fail);
+ });
+ });
+
+ describe('with metric defined step', () => {
+ beforeEach(() => {
+ metric.step = 7;
+ });
+
+ const expectedParams = {
+ start_time: '2019-08-06T12:40:02.184Z',
+ end_time: '2019-08-06T20:40:02.184Z',
+ step: 7,
+ };
+
+ it('uses metric step', done => {
+ mock.onGet('http://test').reply(200, { data }); // One attempt
+
+ testAction(
+ fetchPrometheusMetric,
+ { metric, params },
+ state,
+ [
+ {
+ type: types.REQUEST_METRIC_RESULT,
+ payload: {
+ metricId: metric.metricId,
+ },
+ },
+ {
+ type: types.RECEIVE_METRIC_RESULT_SUCCESS,
+ payload: {
+ metricId: metric.metricId,
+ result: data.result,
+ },
+ },
+ ],
+ [],
+ () => {
+ expect(mock.history.get[0].params).toEqual(expectedParams);
+ done();
+ },
+ ).catch(done.fail);
+ });
+ });
+
it('commits result, when waiting for results', done => {
// Mock multiple attempts while the cache is filling up
mock.onGet('http://test').replyOnce(statusCodes.NO_CONTENT);
diff --git a/spec/frontend/monitoring/store/getters_spec.js b/spec/frontend/monitoring/store/getters_spec.js
index 5a14ffc03f2..bc62ada1034 100644
--- a/spec/frontend/monitoring/store/getters_spec.js
+++ b/spec/frontend/monitoring/store/getters_spec.js
@@ -1,3 +1,4 @@
+import _ from 'lodash';
import * as getters from '~/monitoring/stores/getters';
import mutations from '~/monitoring/stores/mutations';
import * as types from '~/monitoring/stores/mutation_types';
@@ -274,4 +275,56 @@ describe('Monitoring store Getters', () => {
});
});
});
+
+ describe('metricsSavedToDb', () => {
+ let metricsSavedToDb;
+ let state;
+ let mockData;
+
+ beforeEach(() => {
+ mockData = _.cloneDeep(metricsDashboardPayload);
+ state = {
+ dashboard: {
+ panelGroups: [],
+ },
+ };
+ });
+
+ it('return no metrics when dashboard is not persisted', () => {
+ mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, mockData);
+ metricsSavedToDb = getters.metricsSavedToDb(state);
+
+ expect(metricsSavedToDb).toEqual([]);
+ });
+
+ it('return a metric id when one metric is persisted', () => {
+ const id = 99;
+
+ const [metric] = mockData.panel_groups[0].panels[0].metrics;
+
+ metric.metric_id = id;
+
+ mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, mockData);
+ metricsSavedToDb = getters.metricsSavedToDb(state);
+
+ expect(metricsSavedToDb).toEqual([`${id}_${metric.id}`]);
+ });
+
+ it('return a metric id when two metrics are persisted', () => {
+ const id1 = 101;
+ const id2 = 102;
+
+ const [metric1] = mockData.panel_groups[0].panels[0].metrics;
+ const [metric2] = mockData.panel_groups[0].panels[1].metrics;
+
+ // database persisted 2 metrics
+ metric1.metric_id = id1;
+ metric2.metric_id = id2;
+
+ mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, mockData);
+ metricsSavedToDb = getters.metricsSavedToDb(state);
+
+ expect(metricsSavedToDb).toEqual([`${id1}_${metric1.id}`, `${id2}_${metric2.id}`]);
+ });
+ });
});
diff --git a/spec/frontend/monitoring/store/mutations_spec.js b/spec/frontend/monitoring/store/mutations_spec.js
index 0310c7e9510..6f1a81782f3 100644
--- a/spec/frontend/monitoring/store/mutations_spec.js
+++ b/spec/frontend/monitoring/store/mutations_spec.js
@@ -66,13 +66,13 @@ describe('Monitoring mutations', () => {
const groups = getGroups();
expect(groups[0].panels[0].metrics[0].metricId).toEqual(
- 'undefined_system_metrics_kubernetes_container_memory_total',
+ 'NO_DB_system_metrics_kubernetes_container_memory_total',
);
expect(groups[1].panels[0].metrics[0].metricId).toEqual(
- 'undefined_response_metrics_nginx_ingress_throughput_status_code',
+ 'NO_DB_response_metrics_nginx_ingress_throughput_status_code',
);
expect(groups[2].panels[0].metrics[0].metricId).toEqual(
- 'undefined_response_metrics_nginx_ingress_16_throughput_status_code',
+ 'NO_DB_response_metrics_nginx_ingress_16_throughput_status_code',
);
});
});
@@ -184,7 +184,7 @@ describe('Monitoring mutations', () => {
});
describe('Individual panel/metric results', () => {
- const metricId = 'undefined_response_metrics_nginx_ingress_throughput_status_code';
+ const metricId = 'NO_DB_response_metrics_nginx_ingress_throughput_status_code';
const result = [
{
values: [[0, 1], [1, 1], [1, 3]],
diff --git a/spec/frontend/monitoring/store/utils_spec.js b/spec/frontend/monitoring/store/utils_spec.js
index 3e83ba2858e..e1c8e694122 100644
--- a/spec/frontend/monitoring/store/utils_spec.js
+++ b/spec/frontend/monitoring/store/utils_spec.js
@@ -307,7 +307,7 @@ describe('mapToDashboardViewModel', () => {
describe('uniqMetricsId', () => {
[
- { input: { id: 1 }, expected: 'undefined_1' },
+ { input: { id: 1 }, expected: 'NO_DB_1' },
{ input: { metric_id: 2 }, expected: '2_undefined' },
{ input: { metric_id: 2, id: 21 }, expected: '2_21' },
{ input: { metric_id: 22, id: 1 }, expected: '22_1' },
diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb
index efdbea85d4a..5fd64da6328 100644
--- a/spec/uploaders/file_uploader_spec.rb
+++ b/spec/uploaders/file_uploader_spec.rb
@@ -145,11 +145,39 @@ describe FileUploader do
end
describe '.extract_dynamic_path' do
- it 'works with hashed storage' do
- path = 'export/4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a/test/uploads/72a497a02fe3ee09edae2ed06d390038/dummy.txt'
+ context 'with a 32-byte hexadecimal secret in the path' do
+ let(:secret) { SecureRandom.hex }
+ let(:path) { "export/4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a/test/uploads/#{secret}/dummy.txt" }
- expect(described_class.extract_dynamic_path(path)[:identifier]).to eq('dummy.txt')
- expect(described_class.extract_dynamic_path(path)[:secret]).to eq('72a497a02fe3ee09edae2ed06d390038')
+ it 'extracts the secret' do
+ expect(described_class.extract_dynamic_path(path)[:secret]).to eq(secret)
+ end
+
+ it 'extracts the identifier' do
+ expect(described_class.extract_dynamic_path(path)[:identifier]).to eq('dummy.txt')
+ end
+ end
+
+ context 'with a 10-byte hexadecimal secret in the path' do
+ let(:secret) { SecureRandom.hex(10) }
+ let(:path) { "export/4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a/test/uploads/#{secret}/dummy.txt" }
+
+ it 'extracts the secret' do
+ expect(described_class.extract_dynamic_path(path)[:secret]).to eq(secret)
+ end
+
+ it 'extracts the identifier' do
+ expect(described_class.extract_dynamic_path(path)[:identifier]).to eq('dummy.txt')
+ end
+ end
+
+ context 'with an invalid secret in the path' do
+ let(:secret) { 'foo' }
+ let(:path) { "export/4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a/test/uploads/#{secret}/dummy.txt" }
+
+ it 'returns nil' do
+ expect(described_class.extract_dynamic_path(path)).to be_nil
+ end
end
end