summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_list.vue2
-rw-r--r--app/assets/javascripts/lib/utils/unit_format/formatter_factory.js20
-rw-r--r--app/assets/javascripts/lib/utils/unit_format/index.js106
-rw-r--r--app/assets/javascripts/notes/services/notes_service.js41
-rw-r--r--app/assets/javascripts/notes/stores/actions.js73
-rw-r--r--app/assets/javascripts/prometheus_metrics/prometheus_metrics.js6
-rw-r--r--app/assets/stylesheets/framework/typography.scss2
-rw-r--r--app/policies/project_policy.rb1
-rw-r--r--app/views/projects/_wiki.html.haml2
-rw-r--r--app/views/projects/blob/preview.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_markup.html.haml2
-rw-r--r--app/views/projects/wikis/show.html.haml2
-rw-r--r--app/views/search/results/_snippet_blob.html.haml2
-rw-r--r--changelogs/unreleased/197960-package-detail-activity.yml5
-rw-r--r--changelogs/unreleased/205596-empty-state-for-code-review-analytics.yml5
-rw-r--r--changelogs/unreleased/21811-project-list-deploy-tokens.yml5
-rw-r--r--changelogs/unreleased/update-dast-ado-image-to-0-10-0.yml5
-rw-r--r--doc/api/deploy_tokens.md51
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql6
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json36
-rw-r--r--doc/user/project/repository/forking_workflow.md20
-rw-r--r--lib/api/deploy_tokens.rb22
-rw-r--r--lib/api/entities/deploy_token.rb3
-rw-r--r--lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml2
-rw-r--r--locale/gitlab.pot18
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/deploy_token.json31
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/deploy_tokens.json6
-rw-r--r--spec/frontend/frequent_items/components/frequent_items_list_item_spec.js4
-rw-r--r--spec/frontend/frequent_items/components/frequent_items_list_spec.js101
-rw-r--r--spec/frontend/frequent_items/mock_data.js52
-rw-r--r--spec/frontend/lib/utils/unit_format/formatter_factory_spec.js282
-rw-r--r--spec/frontend/lib/utils/unit_format/index_spec.js158
-rw-r--r--spec/frontend/notes/components/note_app_spec.js11
-rw-r--r--spec/javascripts/frequent_items/components/frequent_items_list_spec.js90
-rw-r--r--spec/models/issue_spec.rb68
-rw-r--r--spec/policies/project_policy_spec.rb2
-rw-r--r--spec/requests/api/deploy_tokens_spec.rb69
37 files changed, 869 insertions, 444 deletions
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list.vue
index 0ece64692ae..9d898d1a1a1 100644
--- a/app/assets/javascripts/frequent_items/components/frequent_items_list.vue
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_list.vue
@@ -58,7 +58,7 @@ export default {
<template>
<div class="frequent-items-list-container">
- <ul class="list-unstyled">
+ <ul ref="frequentItemsList" class="list-unstyled">
<li v-if="isListEmpty" :class="{ 'section-failure': isFetchFailed }" class="section-empty">
{{ listEmptyMessage }}
</li>
diff --git a/app/assets/javascripts/lib/utils/unit_format/formatter_factory.js b/app/assets/javascripts/lib/utils/unit_format/formatter_factory.js
index 432a9254558..98bcb8348e2 100644
--- a/app/assets/javascripts/lib/utils/unit_format/formatter_factory.js
+++ b/app/assets/javascripts/lib/utils/unit_format/formatter_factory.js
@@ -117,3 +117,23 @@ export const scaledSIFormatter = (unit = '', prefixOffset = 0) => {
return scaledFormatter(units);
};
+
+/**
+ * Returns a function that formats a number scaled using SI units notation.
+ */
+export const scaledBinaryFormatter = (unit = '', prefixOffset = 0) => {
+ // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
+ const multiplicative = ['Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'];
+ const symbols = ['', ...multiplicative];
+
+ const units = symbols.slice(prefixOffset).map(prefix => {
+ return `${prefix}${unit}`;
+ });
+
+ if (!units.length) {
+ // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
+ throw new RangeError('The unit cannot be converted, please try a different scale');
+ }
+
+ return scaledFormatter(units, 1024);
+};
diff --git a/app/assets/javascripts/lib/utils/unit_format/index.js b/app/assets/javascripts/lib/utils/unit_format/index.js
index daf70ebb5d7..d3aea37e677 100644
--- a/app/assets/javascripts/lib/utils/unit_format/index.js
+++ b/app/assets/javascripts/lib/utils/unit_format/index.js
@@ -1,9 +1,18 @@
import { s__ } from '~/locale';
-import { suffixFormatter, scaledSIFormatter, numberFormatter } from './formatter_factory';
+import {
+ suffixFormatter,
+ scaledSIFormatter,
+ scaledBinaryFormatter,
+ numberFormatter,
+} from './formatter_factory';
/**
* Supported formats
+ *
+ * Based on:
+ *
+ * https://tc39.es/proposal-unified-intl-numberformat/section6/locales-currencies-tz_proposed_out.html#sec-issanctionedsimpleunitidentifier
*/
export const SUPPORTED_FORMATS = {
// Number
@@ -13,15 +22,23 @@ export const SUPPORTED_FORMATS = {
// Duration
seconds: 'seconds',
- miliseconds: 'miliseconds',
+ milliseconds: 'milliseconds',
- // Digital
- bytes: 'bytes',
+ // Digital (Metric)
+ decimalBytes: 'decimalBytes',
kilobytes: 'kilobytes',
megabytes: 'megabytes',
gigabytes: 'gigabytes',
terabytes: 'terabytes',
petabytes: 'petabytes',
+
+ // Digital (IEC)
+ bytes: 'bytes',
+ kibibytes: 'kibibytes',
+ mebibytes: 'mebibytes',
+ gibibytes: 'gibibytes',
+ tebibytes: 'tebibytes',
+ pebibytes: 'pebibytes',
};
/**
@@ -32,6 +49,7 @@ export const SUPPORTED_FORMATS = {
*/
export const getFormatter = (format = SUPPORTED_FORMATS.number) => {
// Number
+
if (format === SUPPORTED_FORMATS.number) {
/**
* Formats a number
@@ -70,6 +88,7 @@ export const getFormatter = (format = SUPPORTED_FORMATS.number) => {
}
// Durations
+
if (format === SUPPORTED_FORMATS.seconds) {
/**
* Formats a number of seconds
@@ -82,9 +101,9 @@ export const getFormatter = (format = SUPPORTED_FORMATS.number) => {
*/
return suffixFormatter(s__('Units|s'));
}
- if (format === SUPPORTED_FORMATS.miliseconds) {
+ if (format === SUPPORTED_FORMATS.milliseconds) {
/**
- * Formats a number of miliseconds with ms as units
+ * Formats a number of milliseconds with ms as units
*
* @function
* @param {Number} value - Number to format, `1` is formatted as `1ms`
@@ -95,8 +114,9 @@ export const getFormatter = (format = SUPPORTED_FORMATS.number) => {
return suffixFormatter(s__('Units|ms'));
}
- // Digital
- if (format === SUPPORTED_FORMATS.bytes) {
+ // Digital (Metric)
+
+ if (format === SUPPORTED_FORMATS.decimalBytes) {
/**
* Formats a number of bytes scaled up to larger digital
* units for larger numbers.
@@ -162,6 +182,76 @@ export const getFormatter = (format = SUPPORTED_FORMATS.number) => {
*/
return scaledSIFormatter('B', 5);
}
+
+ // Digital (IEC)
+
+ if (format === SUPPORTED_FORMATS.bytes) {
+ /**
+ * Formats a number of bytes scaled up to larger digital
+ * units for larger numbers.
+ *
+ * @function
+ * @param {Number} value - Number to format, `1` is formatted as `1B`
+ * @param {Number} fractionDigits - number of precision decimals
+ */
+ return scaledBinaryFormatter('B');
+ }
+ if (format === SUPPORTED_FORMATS.kibibytes) {
+ /**
+ * Formats a number of kilobytes scaled up to larger digital
+ * units for larger numbers.
+ *
+ * @function
+ * @param {Number} value - Number to format, `1` is formatted as `1kB`
+ * @param {Number} fractionDigits - number of precision decimals
+ */
+ return scaledBinaryFormatter('B', 1);
+ }
+ if (format === SUPPORTED_FORMATS.mebibytes) {
+ /**
+ * Formats a number of megabytes scaled up to larger digital
+ * units for larger numbers.
+ *
+ * @function
+ * @param {Number} value - Number to format, `1` is formatted as `1MB`
+ * @param {Number} fractionDigits - number of precision decimals
+ */
+ return scaledBinaryFormatter('B', 2);
+ }
+ if (format === SUPPORTED_FORMATS.gibibytes) {
+ /**
+ * Formats a number of gigabytes scaled up to larger digital
+ * units for larger numbers.
+ *
+ * @function
+ * @param {Number} value - Number to format, `1` is formatted as `1GB`
+ * @param {Number} fractionDigits - number of precision decimals
+ */
+ return scaledBinaryFormatter('B', 3);
+ }
+ if (format === SUPPORTED_FORMATS.tebibytes) {
+ /**
+ * Formats a number of terabytes scaled up to larger digital
+ * units for larger numbers.
+ *
+ * @function
+ * @param {Number} value - Number to format, `1` is formatted as `1GB`
+ * @param {Number} fractionDigits - number of precision decimals
+ */
+ return scaledBinaryFormatter('B', 4);
+ }
+ if (format === SUPPORTED_FORMATS.pebibytes) {
+ /**
+ * Formats a number of petabytes scaled up to larger digital
+ * units for larger numbers.
+ *
+ * @function
+ * @param {Number} value - Number to format, `1` is formatted as `1PB`
+ * @param {Number} fractionDigits - number of precision decimals
+ */
+ return scaledBinaryFormatter('B', 5);
+ }
+
// Fail so client library addresses issue
throw TypeError(`${format} is not a valid number format`);
};
diff --git a/app/assets/javascripts/notes/services/notes_service.js b/app/assets/javascripts/notes/services/notes_service.js
deleted file mode 100644
index 4d3dbec435f..00000000000
--- a/app/assets/javascripts/notes/services/notes_service.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import axios from '~/lib/utils/axios_utils';
-import * as constants from '../constants';
-
-export default {
- fetchDiscussions(endpoint, filter, persistFilter = true) {
- const config =
- filter !== undefined
- ? { params: { notes_filter: filter, persist_filter: persistFilter } }
- : null;
- return axios.get(endpoint, config);
- },
- replyToDiscussion(endpoint, data) {
- return axios.post(endpoint, data);
- },
- updateNote(endpoint, data) {
- return axios.put(endpoint, data);
- },
- createNewNote(endpoint, data) {
- return axios.post(endpoint, data);
- },
- toggleResolveNote(endpoint, isResolved) {
- const { RESOLVE_NOTE_METHOD_NAME, UNRESOLVE_NOTE_METHOD_NAME } = constants;
- const method = isResolved ? UNRESOLVE_NOTE_METHOD_NAME : RESOLVE_NOTE_METHOD_NAME;
-
- return axios[method](endpoint);
- },
- poll(data = {}) {
- const endpoint = data.notesData.notesPath;
- const { lastFetchedAt } = data;
- const options = {
- headers: {
- 'X-Last-Fetched-At': lastFetchedAt ? `${lastFetchedAt}` : undefined,
- },
- };
-
- return axios.get(endpoint, options);
- },
- toggleIssueState(endpoint, data) {
- return axios.put(endpoint, data);
- },
-};
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 594e3a14d56..a4b9c64645c 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -8,7 +8,6 @@ import Poll from '../../lib/utils/poll';
import * as types from './mutation_types';
import * as utils from './utils';
import * as constants from '../constants';
-import service from '../services/notes_service';
import loadAwardsHandler from '../../awards_handler';
import sidebarTimeTrackingEventHub from '../../sidebar/event_hub';
import { isInViewport, scrollToElement, isInMRPage } from '../../lib/utils/common_utils';
@@ -47,11 +46,17 @@ export const setNotesFetchedState = ({ commit }, state) =>
export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data);
-export const fetchDiscussions = ({ commit, dispatch }, { path, filter, persistFilter }) =>
- service.fetchDiscussions(path, filter, persistFilter).then(({ data }) => {
+export const fetchDiscussions = ({ commit, dispatch }, { path, filter, persistFilter }) => {
+ const config =
+ filter !== undefined
+ ? { params: { notes_filter: filter, persist_filter: persistFilter } }
+ : null;
+
+ return axios.get(path, config).then(({ data }) => {
commit(types.SET_INITIAL_DISCUSSIONS, data);
dispatch('updateResolvableDiscussionsCounts');
});
+};
export const updateDiscussion = ({ commit, state }, discussion) => {
commit(types.UPDATE_DISCUSSION, discussion);
@@ -78,7 +83,7 @@ export const deleteNote = ({ dispatch }, note) =>
});
export const updateNote = ({ commit, dispatch }, { endpoint, note }) =>
- service.updateNote(endpoint, note).then(({ data }) => {
+ axios.put(endpoint, note).then(({ data }) => {
commit(types.UPDATE_NOTE, data);
dispatch('startTaskList');
});
@@ -109,7 +114,7 @@ export const replyToDiscussion = (
{ commit, state, getters, dispatch },
{ endpoint, data: reply },
) =>
- service.replyToDiscussion(endpoint, reply).then(({ data }) => {
+ axios.post(endpoint, reply).then(({ data }) => {
if (data.discussion) {
commit(types.UPDATE_DISCUSSION, data.discussion);
@@ -126,7 +131,7 @@ export const replyToDiscussion = (
});
export const createNewNote = ({ commit, dispatch }, { endpoint, data: reply }) =>
- service.createNewNote(endpoint, reply).then(({ data }) => {
+ axios.post(endpoint, reply).then(({ data }) => {
if (!data.errors) {
commit(types.ADD_NEW_NOTE, data);
@@ -156,20 +161,24 @@ export const resolveDiscussion = ({ state, dispatch, getters }, { discussionId }
});
};
-export const toggleResolveNote = ({ commit, dispatch }, { endpoint, isResolved, discussion }) =>
- service.toggleResolveNote(endpoint, isResolved).then(({ data }) => {
- const mutationType = discussion ? types.UPDATE_DISCUSSION : types.UPDATE_NOTE;
+export const toggleResolveNote = ({ commit, dispatch }, { endpoint, isResolved, discussion }) => {
+ const method = isResolved
+ ? constants.UNRESOLVE_NOTE_METHOD_NAME
+ : constants.RESOLVE_NOTE_METHOD_NAME;
+ const mutationType = discussion ? types.UPDATE_DISCUSSION : types.UPDATE_NOTE;
+ return axios[method](endpoint).then(({ data }) => {
commit(mutationType, data);
dispatch('updateResolvableDiscussionsCounts');
dispatch('updateMergeRequestWidget');
});
+};
export const closeIssue = ({ commit, dispatch, state }) => {
dispatch('toggleStateButtonLoading', true);
- return service.toggleIssueState(state.notesData.closePath).then(({ data }) => {
+ return axios.put(state.notesData.closePath).then(({ data }) => {
commit(types.CLOSE_ISSUE);
dispatch('emitStateChangedEvent', data);
dispatch('toggleStateButtonLoading', false);
@@ -178,7 +187,7 @@ export const closeIssue = ({ commit, dispatch, state }) => {
export const reopenIssue = ({ commit, dispatch, state }) => {
dispatch('toggleStateButtonLoading', true);
- return service.toggleIssueState(state.notesData.reopenPath).then(({ data }) => {
+ return axios.put(state.notesData.reopenPath).then(({ data }) => {
commit(types.REOPEN_ISSUE);
dispatch('emitStateChangedEvent', data);
dispatch('toggleStateButtonLoading', false);
@@ -355,11 +364,35 @@ const pollSuccessCallBack = (resp, commit, state, getters, dispatch) => {
return resp;
};
+const getFetchDataParams = state => {
+ const endpoint = state.notesData.notesPath;
+ const options = {
+ headers: {
+ 'X-Last-Fetched-At': state.lastFetchedAt ? `${state.lastFetchedAt}` : undefined,
+ },
+ };
+
+ return { endpoint, options };
+};
+
+export const fetchData = ({ commit, state, getters }) => {
+ const { endpoint, options } = getFetchDataParams(state);
+
+ axios
+ .get(endpoint, options)
+ .then(({ data }) => pollSuccessCallBack(data, commit, state, getters))
+ .catch(() => Flash(__('Something went wrong while fetching latest comments.')));
+};
+
export const poll = ({ commit, state, getters, dispatch }) => {
eTagPoll = new Poll({
- resource: service,
+ resource: {
+ poll: () => {
+ const { endpoint, options } = getFetchDataParams(state);
+ return axios.get(endpoint, options);
+ },
+ },
method: 'poll',
- data: state,
successCallback: ({ data }) => pollSuccessCallBack(data, commit, state, getters, dispatch),
errorCallback: () => Flash(__('Something went wrong while fetching latest comments.')),
});
@@ -367,7 +400,7 @@ export const poll = ({ commit, state, getters, dispatch }) => {
if (!Visibility.hidden()) {
eTagPoll.makeRequest();
} else {
- service.poll(state);
+ fetchData({ commit, state, getters });
}
Visibility.change(() => {
@@ -387,18 +420,6 @@ export const restartPolling = () => {
if (eTagPoll) eTagPoll.restart();
};
-export const fetchData = ({ commit, state, getters }) => {
- const requestData = {
- endpoint: state.notesData.notesPath,
- lastFetchedAt: state.lastFetchedAt,
- };
-
- service
- .poll(requestData)
- .then(({ data }) => pollSuccessCallBack(data, commit, state, getters))
- .catch(() => Flash(__('Something went wrong while fetching latest comments.')));
-};
-
export const toggleAward = ({ commit, getters }, { awardName, noteId }) => {
commit(types.TOGGLE_AWARD, { awardName, note: getters.notesById[noteId] });
};
diff --git a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
index 8380cfb6c59..8d779e04673 100644
--- a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
+++ b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import _ from 'underscore';
+import { escape } from 'lodash';
import { s__, n__, sprintf } from '~/locale';
import axios from '../lib/utils/axios_utils';
import PANEL_STATE from './constants';
@@ -69,13 +69,13 @@ export default class PrometheusMetrics {
if (metric.active_metrics > 0) {
totalExporters += 1;
this.$monitoredMetricsList.append(
- `<li>${_.escape(metric.group)}<span class="badge">${_.escape(
+ `<li>${escape(metric.group)}<span class="badge">${escape(
metric.active_metrics,
)}</span></li>`,
);
totalMonitoredMetrics += metric.active_metrics;
if (metric.metrics_missing_requirements > 0) {
- this.$missingEnvVarMetricsList.append(`<li>${_.escape(metric.group)}</li>`);
+ this.$missingEnvVarMetricsList.append(`<li>${escape(metric.group)}</li>`);
totalMissingEnvVarMetrics += 1;
}
}
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index a1bfa03a5ac..0de5aae4b0e 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -59,7 +59,7 @@
max-width: 100%;
}
- &:not(.md-file) img:not(.emoji) {
+ &:not(.md) img:not(.emoji) {
border: 1px solid $white-normal;
padding: 5px;
margin: 5px 0;
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 507e227c952..4d49c96d268 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -312,6 +312,7 @@ class ProjectPolicy < BasePolicy
enable :destroy_artifacts
enable :daily_statistics
enable :admin_operations
+ enable :read_deploy_token
end
rule { (mirror_available & can?(:admin_project)) | admin }.enable :admin_remote_mirror
diff --git a/app/views/projects/_wiki.html.haml b/app/views/projects/_wiki.html.haml
index 6103d86bf5a..57a5d3e2e83 100644
--- a/app/views/projects/_wiki.html.haml
+++ b/app/views/projects/_wiki.html.haml
@@ -1,6 +1,6 @@
- if @wiki_home.present?
%div{ class: container_class }
- .md.md-file.prepend-top-default.append-bottom-default
+ .md.prepend-top-default.append-bottom-default
= render_wiki_content(@wiki_home)
- else
- can_create_wiki = can?(current_user, :create_wiki, @project)
diff --git a/app/views/projects/blob/preview.html.haml b/app/views/projects/blob/preview.html.haml
index 46e76e4d175..41a0045be89 100644
--- a/app/views/projects/blob/preview.html.haml
+++ b/app/views/projects/blob/preview.html.haml
@@ -1,5 +1,5 @@
- if markup?(@blob.name)
- .file-content.md.md-file
+ .file-content.md
= markup(@blob.name, @content)
- else
.diff-file
diff --git a/app/views/projects/blob/viewers/_markup.html.haml b/app/views/projects/blob/viewers/_markup.html.haml
index c71df29354b..8134adcbc32 100644
--- a/app/views/projects/blob/viewers/_markup.html.haml
+++ b/app/views/projects/blob/viewers/_markup.html.haml
@@ -1,4 +1,4 @@
- blob = viewer.blob
- context = blob.respond_to?(:rendered_markup) ? { rendered: blob.rendered_markup } : {}
-.file-content.md.md-file
+.file-content.md
= markup(blob.name, blob.data, context)
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index ebd99cf8605..74798311c2e 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -26,7 +26,7 @@
= (s_("WikiHistoricalPage|You can view the %{most_recent_link} or browse the %{history_link}.") % { most_recent_link: most_recent_link, history_link: history_link }).html_safe
.prepend-top-default.append-bottom-default
- .md.md-file{ data: { qa_selector: 'wiki_page_content' } }
+ .md{ data: { qa_selector: 'wiki_page_content' } }
= render_wiki_content(@page)
= render 'sidebar'
diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml
index 5126351b0bb..fa77566dddb 100644
--- a/app/views/search/results/_snippet_blob.html.haml
+++ b/app/views/search/results/_snippet_blob.html.haml
@@ -23,7 +23,7 @@
%i.fa.fa-file
%strong= snippet.file_name
- if markup?(snippet.file_name)
- .file-content.md.md-file
+ .file-content.md
- snippet_chunks.each do |chunk|
- unless chunk[:data].empty?
= markup(snippet.file_name, chunk[:data])
diff --git a/changelogs/unreleased/197960-package-detail-activity.yml b/changelogs/unreleased/197960-package-detail-activity.yml
new file mode 100644
index 00000000000..3e7d9328d0a
--- /dev/null
+++ b/changelogs/unreleased/197960-package-detail-activity.yml
@@ -0,0 +1,5 @@
+---
+title: Adds new activity panel to package details page
+merge_request: 25534
+author:
+type: added
diff --git a/changelogs/unreleased/205596-empty-state-for-code-review-analytics.yml b/changelogs/unreleased/205596-empty-state-for-code-review-analytics.yml
new file mode 100644
index 00000000000..ef81a166231
--- /dev/null
+++ b/changelogs/unreleased/205596-empty-state-for-code-review-analytics.yml
@@ -0,0 +1,5 @@
+---
+title: Empty state for Code Review Analytics
+merge_request: 25793
+author:
+type: added
diff --git a/changelogs/unreleased/21811-project-list-deploy-tokens.yml b/changelogs/unreleased/21811-project-list-deploy-tokens.yml
new file mode 100644
index 00000000000..ead28fe7595
--- /dev/null
+++ b/changelogs/unreleased/21811-project-list-deploy-tokens.yml
@@ -0,0 +1,5 @@
+---
+title: Add endpoint for listing all deploy tokens for a project
+merge_request: 25186
+author:
+type: added
diff --git a/changelogs/unreleased/update-dast-ado-image-to-0-10-0.yml b/changelogs/unreleased/update-dast-ado-image-to-0-10-0.yml
new file mode 100644
index 00000000000..335d553239f
--- /dev/null
+++ b/changelogs/unreleased/update-dast-ado-image-to-0-10-0.yml
@@ -0,0 +1,5 @@
+---
+title: Update DAST auto-deploy-image to v0.10.0
+merge_request: 25922
+author:
+type: other
diff --git a/doc/api/deploy_tokens.md b/doc/api/deploy_tokens.md
index 501e5c4be36..ed05f46dae0 100644
--- a/doc/api/deploy_tokens.md
+++ b/doc/api/deploy_tokens.md
@@ -2,15 +2,14 @@
## List all deploy tokens
-Get a list of all deploy tokens across all projects of the GitLab instance.
+Get a list of all deploy tokens across the GitLab instance. This endpoint requires admin access.
->**Note:**
-> This endpoint requires admin access.
-
-```
+```plaintext
GET /deploy_tokens
```
+Example request:
+
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/deploy_tokens"
```
@@ -24,7 +23,47 @@ Example response:
"name": "MyToken",
"username": "gitlab+deploy-token-1",
"expires_at": "2020-02-14T00:00:00.000Z",
- "token": "jMRvtPNxrn3crTAGukpZ",
+ "scopes": [
+ "read_repository",
+ "read_registry"
+ ]
+ }
+]
+```
+
+## Project deploy tokens
+
+Project deploy token API endpoints require project maintainer access or higher.
+
+### List project deploy tokens
+
+Get a list of a project's deploy tokens.
+
+```plaintext
+GET /projects/:id/deploy_tokens
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+|:---------------|:---------------|:---------|:-----------------------------------------------------------------------------|
+| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
+
+Example request:
+
+```shell
+curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/deploy_tokens"
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 1,
+ "name": "MyToken",
+ "username": "gitlab+deploy-token-1",
+ "expires_at": "2020-02-14T00:00:00.000Z",
"scopes": [
"read_repository",
"read_registry"
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index 6b62b11750c..8d6fc859ca2 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -2609,12 +2609,12 @@ input EpicSetSubscriptionInput {
clientMutationId: String
"""
- The group the epic to (un)subscribe is in
+ The group the epic to mutate is in
"""
groupPath: ID!
"""
- The iid of the epic to (un)subscribe
+ The iid of the epic to mutate
"""
iid: ID!
@@ -7820,7 +7820,7 @@ input UpdateEpicInput {
"""
The iid of the epic to mutate
"""
- iid: String!
+ iid: ID!
"""
The IDs of labels to be removed from the epic.
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index ba716f82630..4f2cbe81781 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -24410,6 +24410,20 @@
"fields": null,
"inputFields": [
{
+ "name": "iid",
+ "description": "The iid of the epic to mutate",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
"name": "groupPath",
"description": "The group the epic to mutate is in",
"type": {
@@ -24520,20 +24534,6 @@
"defaultValue": null
},
{
- "name": "iid",
- "description": "The iid of the epic to mutate",
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "SCALAR",
- "name": "String",
- "ofType": null
- }
- },
- "defaultValue": null
- },
- {
"name": "stateEvent",
"description": "State event for the epic",
"type": {
@@ -24863,8 +24863,8 @@
"fields": null,
"inputFields": [
{
- "name": "groupPath",
- "description": "The group the epic to (un)subscribe is in",
+ "name": "iid",
+ "description": "The iid of the epic to mutate",
"type": {
"kind": "NON_NULL",
"name": null,
@@ -24877,8 +24877,8 @@
"defaultValue": null
},
{
- "name": "iid",
- "description": "The iid of the epic to (un)subscribe",
+ "name": "groupPath",
+ "description": "The group the epic to mutate is in",
"type": {
"kind": "NON_NULL",
"name": null,
diff --git a/doc/user/project/repository/forking_workflow.md b/doc/user/project/repository/forking_workflow.md
index 45a32655f96..927c1db804a 100644
--- a/doc/user/project/repository/forking_workflow.md
+++ b/doc/user/project/repository/forking_workflow.md
@@ -4,11 +4,14 @@ disqus_identifier: 'https://docs.gitlab.com/ee/workflow/forking_workflow.html'
# Project forking workflow
-Forking a project to your own namespace is useful if you have no write
-access to the project you want to contribute to. Even if you do have write
-access or can request it, we recommend working together in the same
-repository since it is simpler. See our [GitLab Flow](../../../topics/gitlab_flow.md)
-document more information about using branches to work together.
+Whenever possible, it's recommended to work in a common Git repository and use
+[branching strategies](../../../topics/gitlab_flow.md) to manage your work. However,
+if you do not have write access for the repository you want to contribute to, you
+can create a fork.
+
+A fork is a personal copy of the repository and all its branches, which you create
+in a namespace of your choice. This way you can make changes in your own fork and
+submit them through a merge request to the repo you don't have access to.
## Creating a fork
@@ -27,7 +30,7 @@ Forking a project is, in most cases, a two-step process.
The fork is created. The permissions you have in the namespace are the permissions you will have in the fork.
-CAUTION: **CAUTION:**
+CAUTION: **Caution:**
In GitLab 12.6 and later, when project owners [reduce a project's visibility](../../../public_access/public_access.md#reducing-visibility),
it **removes the relationship** between a project and all its forks.
@@ -37,10 +40,11 @@ You can use [repository mirroring](repository_mirroring.md) to keep your fork sy
The main difference is that with repository mirroring your remote fork will be automatically kept up-to-date.
-Without mirroring, to work locally you'll have to user `git pull` to update your local repo with the fork on GitLab. You'll have to fetch locally and push it back to the remote repo to update it.
+Without mirroring, to work locally you'll have to use `git pull` to update your local repo
+with the upstream project, then push the changes back to your fork to update it.
CAUTION: **Caution:**
-With mirroring, before approving a merge request you'll likely to be asked to sync, hence automating it is recommend.
+With mirroring, before approving a merge request, you'll likely be asked to sync; hence automating it is recommend.
Read more about [How to keep your fork up to date with its origin](https://about.gitlab.com/blog/2016/12/01/how-to-keep-your-fork-up-to-date-with-its-origin/).
diff --git a/lib/api/deploy_tokens.rb b/lib/api/deploy_tokens.rb
index bf82c63260d..3224157ca05 100644
--- a/lib/api/deploy_tokens.rb
+++ b/lib/api/deploy_tokens.rb
@@ -4,8 +4,6 @@ module API
class DeployTokens < Grape::API
include PaginationParams
- before { authenticated_as_admin! }
-
desc 'Return all deploy tokens' do
detail 'This feature was introduced in GitLab 12.9.'
success Entities::DeployToken
@@ -14,7 +12,27 @@ module API
use :pagination
end
get 'deploy_tokens' do
+ authenticated_as_admin!
+
present paginate(DeployToken.all), with: Entities::DeployToken
end
+
+ params do
+ requires :id, type: Integer, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ params do
+ use :pagination
+ end
+ desc 'List deploy tokens for a project' do
+ detail 'This feature was introduced in GitLab 12.9'
+ success Entities::DeployToken
+ end
+ get ':id/deploy_tokens' do
+ authorize!(:read_deploy_token, user_project)
+
+ present paginate(user_project.deploy_tokens), with: Entities::DeployToken
+ end
+ end
end
end
diff --git a/lib/api/entities/deploy_token.rb b/lib/api/entities/deploy_token.rb
index cac6846a845..9c5bf54e299 100644
--- a/lib/api/entities/deploy_token.rb
+++ b/lib/api/entities/deploy_token.rb
@@ -3,7 +3,8 @@
module API
module Entities
class DeployToken < Grape::Entity
- expose :id, :name, :username, :expires_at, :token, :scopes
+ # exposing :token is a security risk and should be avoided
+ expose :id, :name, :username, :expires_at, :scopes
end
end
end
diff --git a/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
index 78ee9b28605..3cf4910fe86 100644
--- a/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
@@ -1,5 +1,5 @@
.dast-auto-deploy:
- image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.9.1"
+ image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.10.0"
dast_environment_deploy:
extends: .dast-auto-deploy
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 7028e44eac2..32e1b704a15 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -4820,6 +4820,9 @@ msgstr ""
msgid "Code Review"
msgstr ""
+msgid "Code Review Analytics displays a table of open merge requests considered to be in code review. There are currently no merge requests in review for this project and/or filters."
+msgstr ""
+
msgid "Code owner approval is required"
msgstr ""
@@ -13602,22 +13605,22 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
-msgid "PackageRegistry|Registry Setup"
+msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
-msgid "PackageRegistry|Remove package"
+msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
-msgid "PackageRegistry|There are no packages yet"
+msgid "PackageRegistry|Registry Setup"
msgstr ""
-msgid "PackageRegistry|There was a problem fetching the details for this package."
+msgid "PackageRegistry|Remove package"
msgstr ""
-msgid "PackageRegistry|There was an error fetching the pipeline information."
+msgid "PackageRegistry|There are no packages yet"
msgstr ""
-msgid "PackageRegistry|Unable to fetch pipeline information"
+msgid "PackageRegistry|There was a problem fetching the details for this package."
msgstr ""
msgid "PackageRegistry|Unable to load package"
@@ -22468,6 +22471,9 @@ msgstr ""
msgid "You don't have any deployments right now."
msgstr ""
+msgid "You don't have any open merge requests"
+msgstr ""
+
msgid "You don't have any projects available."
msgstr ""
diff --git a/spec/fixtures/api/schemas/public_api/v4/deploy_token.json b/spec/fixtures/api/schemas/public_api/v4/deploy_token.json
new file mode 100644
index 00000000000..c8a8b8d1e7d
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/deploy_token.json
@@ -0,0 +1,31 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "name",
+ "username",
+ "expires_at",
+ "scopes"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "username": {
+ "type": "string"
+ },
+ "expires_at": {
+ "type": "date"
+ },
+ "scopes": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "additionalProperties": false
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/public_api/v4/deploy_tokens.json b/spec/fixtures/api/schemas/public_api/v4/deploy_tokens.json
new file mode 100644
index 00000000000..71c30cb2a73
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/deploy_tokens.json
@@ -0,0 +1,6 @@
+{
+ "type": "array",
+ "items": {
+ "$ref": "deploy_token.json"
+ }
+} \ No newline at end of file
diff --git a/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js b/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
index 925699f5623..ab5784b8f7a 100644
--- a/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
+++ b/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
@@ -1,9 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import { trimText } from 'helpers/text_helper';
import frequentItemsListItemComponent from '~/frequent_items/components/frequent_items_list_item.vue';
-import mockData from '../mock_data'; // can also use 'mockGroup', but not useful to test here
-
-const mockProject = mockData();
+import { mockProject } from '../mock_data'; // can also use 'mockGroup', but not useful to test here
describe('FrequentItemsListItemComponent', () => {
let wrapper;
diff --git a/spec/frontend/frequent_items/components/frequent_items_list_spec.js b/spec/frontend/frequent_items/components/frequent_items_list_spec.js
new file mode 100644
index 00000000000..238fd508053
--- /dev/null
+++ b/spec/frontend/frequent_items/components/frequent_items_list_spec.js
@@ -0,0 +1,101 @@
+import { mount } from '@vue/test-utils';
+import frequentItemsListComponent from '~/frequent_items/components/frequent_items_list.vue';
+import frequentItemsListItemComponent from '~/frequent_items/components/frequent_items_list_item.vue';
+import { mockFrequentProjects } from '../mock_data';
+
+describe('FrequentItemsListComponent', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = mount(frequentItemsListComponent, {
+ propsData: {
+ namespace: 'projects',
+ items: mockFrequentProjects,
+ isFetchFailed: false,
+ hasSearchQuery: false,
+ matcher: 'lab',
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('computed', () => {
+ describe('isListEmpty', () => {
+ it('should return `true` or `false` representing whether if `items` is empty or not with projects', () => {
+ createComponent({
+ items: [],
+ });
+
+ expect(wrapper.vm.isListEmpty).toBe(true);
+
+ wrapper.setProps({
+ items: mockFrequentProjects,
+ });
+
+ expect(wrapper.vm.isListEmpty).toBe(false);
+ });
+ });
+
+ describe('fetched item messages', () => {
+ it('should return appropriate empty list message based on value of `localStorageFailed` prop with projects', () => {
+ createComponent({
+ isFetchFailed: true,
+ });
+
+ expect(wrapper.vm.listEmptyMessage).toBe(
+ 'This feature requires browser localStorage support',
+ );
+
+ wrapper.setProps({
+ isFetchFailed: false,
+ });
+
+ expect(wrapper.vm.listEmptyMessage).toBe('Projects you visit often will appear here');
+ });
+ });
+
+ describe('searched item messages', () => {
+ it('should return appropriate empty list message based on value of `searchFailed` prop with projects', () => {
+ createComponent({
+ hasSearchQuery: true,
+ isFetchFailed: true,
+ });
+
+ expect(wrapper.vm.listEmptyMessage).toBe('Something went wrong on our end.');
+
+ wrapper.setProps({
+ isFetchFailed: false,
+ });
+
+ expect(wrapper.vm.listEmptyMessage).toBe('Sorry, no projects matched your search');
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('should render component element with list of projects', () => {
+ createComponent();
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.classes('frequent-items-list-container')).toBe(true);
+ expect(wrapper.findAll({ ref: 'frequentItemsList' })).toHaveLength(1);
+ expect(wrapper.findAll(frequentItemsListItemComponent)).toHaveLength(5);
+ });
+ });
+
+ it('should render component element with empty message', () => {
+ createComponent({
+ items: [],
+ });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.vm.$el.querySelectorAll('li.section-empty')).toHaveLength(1);
+ expect(wrapper.findAll(frequentItemsListItemComponent)).toHaveLength(0);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/frequent_items/mock_data.js b/spec/frontend/frequent_items/mock_data.js
index 81f34053543..5cd4cddd877 100644
--- a/spec/frontend/frequent_items/mock_data.js
+++ b/spec/frontend/frequent_items/mock_data.js
@@ -1,9 +1,57 @@
import { TEST_HOST } from 'helpers/test_constants';
-export default () => ({
+export const mockFrequentProjects = [
+ {
+ id: 1,
+ name: 'GitLab Community Edition',
+ namespace: 'gitlab-org / gitlab-ce',
+ webUrl: `${TEST_HOST}/gitlab-org/gitlab-foss`,
+ avatarUrl: null,
+ frequency: 1,
+ lastAccessedOn: Date.now(),
+ },
+ {
+ id: 2,
+ name: 'GitLab CI',
+ namespace: 'gitlab-org / gitlab-ci',
+ webUrl: `${TEST_HOST}/gitlab-org/gitlab-ci`,
+ avatarUrl: null,
+ frequency: 9,
+ lastAccessedOn: Date.now(),
+ },
+ {
+ id: 3,
+ name: 'Typeahead.Js',
+ namespace: 'twitter / typeahead-js',
+ webUrl: `${TEST_HOST}/twitter/typeahead-js`,
+ avatarUrl: '/uploads/-/system/project/avatar/7/TWBS.png',
+ frequency: 2,
+ lastAccessedOn: Date.now(),
+ },
+ {
+ id: 4,
+ name: 'Intel',
+ namespace: 'platform / hardware / bsp / intel',
+ webUrl: `${TEST_HOST}/platform/hardware/bsp/intel`,
+ avatarUrl: null,
+ frequency: 3,
+ lastAccessedOn: Date.now(),
+ },
+ {
+ id: 5,
+ name: 'v4.4',
+ namespace: 'platform / hardware / bsp / kernel / common / v4.4',
+ webUrl: `${TEST_HOST}/platform/hardware/bsp/kernel/common/v4.4`,
+ avatarUrl: null,
+ frequency: 8,
+ lastAccessedOn: Date.now(),
+ },
+];
+
+export const mockProject = {
id: 1,
name: 'GitLab Community Edition',
namespace: 'gitlab-org / gitlab-ce',
webUrl: `${TEST_HOST}/gitlab-org/gitlab-foss`,
avatarUrl: null,
-});
+};
diff --git a/spec/frontend/lib/utils/unit_format/formatter_factory_spec.js b/spec/frontend/lib/utils/unit_format/formatter_factory_spec.js
index 071ecde6a6d..26b942c3567 100644
--- a/spec/frontend/lib/utils/unit_format/formatter_factory_spec.js
+++ b/spec/frontend/lib/utils/unit_format/formatter_factory_spec.js
@@ -2,6 +2,7 @@ import {
numberFormatter,
suffixFormatter,
scaledSIFormatter,
+ scaledBinaryFormatter,
} from '~/lib/utils/unit_format/formatter_factory';
describe('unit_format/formatter_factory', () => {
@@ -12,28 +13,28 @@ describe('unit_format/formatter_factory', () => {
});
it('formats a integer', () => {
- expect(formatNumber(1)).toEqual('1');
- expect(formatNumber(100)).toEqual('100');
- expect(formatNumber(1000)).toEqual('1,000');
- expect(formatNumber(10000)).toEqual('10,000');
- expect(formatNumber(1000000)).toEqual('1,000,000');
+ expect(formatNumber(1)).toBe('1');
+ expect(formatNumber(100)).toBe('100');
+ expect(formatNumber(1000)).toBe('1,000');
+ expect(formatNumber(10000)).toBe('10,000');
+ expect(formatNumber(1000000)).toBe('1,000,000');
});
it('formats a floating point number', () => {
- expect(formatNumber(0.1)).toEqual('0.1');
- expect(formatNumber(0.1, 0)).toEqual('0');
- expect(formatNumber(0.1, 2)).toEqual('0.10');
- expect(formatNumber(0.1, 3)).toEqual('0.100');
+ expect(formatNumber(0.1)).toBe('0.1');
+ expect(formatNumber(0.1, 0)).toBe('0');
+ expect(formatNumber(0.1, 2)).toBe('0.10');
+ expect(formatNumber(0.1, 3)).toBe('0.100');
- expect(formatNumber(12.345)).toEqual('12.345');
- expect(formatNumber(12.345, 2)).toEqual('12.35');
- expect(formatNumber(12.345, 4)).toEqual('12.3450');
+ expect(formatNumber(12.345)).toBe('12.345');
+ expect(formatNumber(12.345, 2)).toBe('12.35');
+ expect(formatNumber(12.345, 4)).toBe('12.3450');
});
it('formats a large integer with a length limit', () => {
- expect(formatNumber(10 ** 7, undefined)).toEqual('10,000,000');
- expect(formatNumber(10 ** 7, undefined, 9)).toEqual('1.00e+7');
- expect(formatNumber(10 ** 7, undefined, 10)).toEqual('10,000,000');
+ expect(formatNumber(10 ** 7, undefined)).toBe('10,000,000');
+ expect(formatNumber(10 ** 7, undefined, 9)).toBe('1.00e+7');
+ expect(formatNumber(10 ** 7, undefined, 10)).toBe('10,000,000');
});
});
@@ -44,112 +45,112 @@ describe('unit_format/formatter_factory', () => {
});
it('formats a integer', () => {
- expect(formatSuffix(1)).toEqual('1pop.');
- expect(formatSuffix(100)).toEqual('100pop.');
- expect(formatSuffix(1000)).toEqual('1,000pop.');
- expect(formatSuffix(10000)).toEqual('10,000pop.');
- expect(formatSuffix(1000000)).toEqual('1,000,000pop.');
+ expect(formatSuffix(1)).toBe('1pop.');
+ expect(formatSuffix(100)).toBe('100pop.');
+ expect(formatSuffix(1000)).toBe('1,000pop.');
+ expect(formatSuffix(10000)).toBe('10,000pop.');
+ expect(formatSuffix(1000000)).toBe('1,000,000pop.');
});
it('formats a floating point number', () => {
- expect(formatSuffix(0.1)).toEqual('0.1pop.');
- expect(formatSuffix(0.1, 0)).toEqual('0pop.');
- expect(formatSuffix(0.1, 2)).toEqual('0.10pop.');
- expect(formatSuffix(0.1, 3)).toEqual('0.100pop.');
+ expect(formatSuffix(0.1)).toBe('0.1pop.');
+ expect(formatSuffix(0.1, 0)).toBe('0pop.');
+ expect(formatSuffix(0.1, 2)).toBe('0.10pop.');
+ expect(formatSuffix(0.1, 3)).toBe('0.100pop.');
- expect(formatSuffix(12.345)).toEqual('12.345pop.');
- expect(formatSuffix(12.345, 2)).toEqual('12.35pop.');
- expect(formatSuffix(12.345, 4)).toEqual('12.3450pop.');
+ expect(formatSuffix(12.345)).toBe('12.345pop.');
+ expect(formatSuffix(12.345, 2)).toBe('12.35pop.');
+ expect(formatSuffix(12.345, 4)).toBe('12.3450pop.');
});
it('formats a negative integer', () => {
- expect(formatSuffix(-1)).toEqual('-1pop.');
- expect(formatSuffix(-100)).toEqual('-100pop.');
- expect(formatSuffix(-1000)).toEqual('-1,000pop.');
- expect(formatSuffix(-10000)).toEqual('-10,000pop.');
- expect(formatSuffix(-1000000)).toEqual('-1,000,000pop.');
+ expect(formatSuffix(-1)).toBe('-1pop.');
+ expect(formatSuffix(-100)).toBe('-100pop.');
+ expect(formatSuffix(-1000)).toBe('-1,000pop.');
+ expect(formatSuffix(-10000)).toBe('-10,000pop.');
+ expect(formatSuffix(-1000000)).toBe('-1,000,000pop.');
});
it('formats a floating point nugative number', () => {
- expect(formatSuffix(-0.1)).toEqual('-0.1pop.');
- expect(formatSuffix(-0.1, 0)).toEqual('-0pop.');
- expect(formatSuffix(-0.1, 2)).toEqual('-0.10pop.');
- expect(formatSuffix(-0.1, 3)).toEqual('-0.100pop.');
+ expect(formatSuffix(-0.1)).toBe('-0.1pop.');
+ expect(formatSuffix(-0.1, 0)).toBe('-0pop.');
+ expect(formatSuffix(-0.1, 2)).toBe('-0.10pop.');
+ expect(formatSuffix(-0.1, 3)).toBe('-0.100pop.');
- expect(formatSuffix(-12.345)).toEqual('-12.345pop.');
- expect(formatSuffix(-12.345, 2)).toEqual('-12.35pop.');
- expect(formatSuffix(-12.345, 4)).toEqual('-12.3450pop.');
+ expect(formatSuffix(-12.345)).toBe('-12.345pop.');
+ expect(formatSuffix(-12.345, 2)).toBe('-12.35pop.');
+ expect(formatSuffix(-12.345, 4)).toBe('-12.3450pop.');
});
it('formats a large integer', () => {
- expect(formatSuffix(10 ** 7)).toEqual('10,000,000pop.');
- expect(formatSuffix(10 ** 10)).toEqual('10,000,000,000pop.');
+ expect(formatSuffix(10 ** 7)).toBe('10,000,000pop.');
+ expect(formatSuffix(10 ** 10)).toBe('10,000,000,000pop.');
});
it('formats a large integer with a length limit', () => {
- expect(formatSuffix(10 ** 7, undefined, 10)).toEqual('1.00e+7pop.');
- expect(formatSuffix(10 ** 10, undefined, 10)).toEqual('1.00e+10pop.');
+ expect(formatSuffix(10 ** 7, undefined, 10)).toBe('1.00e+7pop.');
+ expect(formatSuffix(10 ** 10, undefined, 10)).toBe('1.00e+10pop.');
});
});
describe('scaledSIFormatter', () => {
describe('scaled format', () => {
- let formatScaled;
+ let formatGibibytes;
beforeEach(() => {
- formatScaled = scaledSIFormatter('B');
+ formatGibibytes = scaledSIFormatter('B');
});
it('formats bytes', () => {
- expect(formatScaled(12.345)).toEqual('12.345B');
- expect(formatScaled(12.345, 0)).toEqual('12B');
- expect(formatScaled(12.345, 1)).toEqual('12.3B');
- expect(formatScaled(12.345, 2)).toEqual('12.35B');
+ expect(formatGibibytes(12.345)).toBe('12.345B');
+ expect(formatGibibytes(12.345, 0)).toBe('12B');
+ expect(formatGibibytes(12.345, 1)).toBe('12.3B');
+ expect(formatGibibytes(12.345, 2)).toBe('12.35B');
});
- it('formats bytes in a scale', () => {
- expect(formatScaled(1)).toEqual('1B');
- expect(formatScaled(10)).toEqual('10B');
- expect(formatScaled(10 ** 2)).toEqual('100B');
- expect(formatScaled(10 ** 3)).toEqual('1kB');
- expect(formatScaled(10 ** 4)).toEqual('10kB');
- expect(formatScaled(10 ** 5)).toEqual('100kB');
- expect(formatScaled(10 ** 6)).toEqual('1MB');
- expect(formatScaled(10 ** 7)).toEqual('10MB');
- expect(formatScaled(10 ** 8)).toEqual('100MB');
- expect(formatScaled(10 ** 9)).toEqual('1GB');
- expect(formatScaled(10 ** 10)).toEqual('10GB');
- expect(formatScaled(10 ** 11)).toEqual('100GB');
+ it('formats bytes in a decimal scale', () => {
+ expect(formatGibibytes(1)).toBe('1B');
+ expect(formatGibibytes(10)).toBe('10B');
+ expect(formatGibibytes(10 ** 2)).toBe('100B');
+ expect(formatGibibytes(10 ** 3)).toBe('1kB');
+ expect(formatGibibytes(10 ** 4)).toBe('10kB');
+ expect(formatGibibytes(10 ** 5)).toBe('100kB');
+ expect(formatGibibytes(10 ** 6)).toBe('1MB');
+ expect(formatGibibytes(10 ** 7)).toBe('10MB');
+ expect(formatGibibytes(10 ** 8)).toBe('100MB');
+ expect(formatGibibytes(10 ** 9)).toBe('1GB');
+ expect(formatGibibytes(10 ** 10)).toBe('10GB');
+ expect(formatGibibytes(10 ** 11)).toBe('100GB');
});
});
describe('scaled format with offset', () => {
- let formatScaled;
+ let formatGigaBytes;
beforeEach(() => {
// formats gigabytes
- formatScaled = scaledSIFormatter('B', 3);
+ formatGigaBytes = scaledSIFormatter('B', 3);
});
it('formats floating point numbers', () => {
- expect(formatScaled(12.345)).toEqual('12.345GB');
- expect(formatScaled(12.345, 0)).toEqual('12GB');
- expect(formatScaled(12.345, 1)).toEqual('12.3GB');
- expect(formatScaled(12.345, 2)).toEqual('12.35GB');
+ expect(formatGigaBytes(12.345)).toBe('12.345GB');
+ expect(formatGigaBytes(12.345, 0)).toBe('12GB');
+ expect(formatGigaBytes(12.345, 1)).toBe('12.3GB');
+ expect(formatGigaBytes(12.345, 2)).toBe('12.35GB');
});
it('formats large numbers scaled', () => {
- expect(formatScaled(1)).toEqual('1GB');
- expect(formatScaled(1, 1)).toEqual('1.0GB');
- expect(formatScaled(10)).toEqual('10GB');
- expect(formatScaled(10 ** 2)).toEqual('100GB');
- expect(formatScaled(10 ** 3)).toEqual('1TB');
- expect(formatScaled(10 ** 4)).toEqual('10TB');
- expect(formatScaled(10 ** 5)).toEqual('100TB');
- expect(formatScaled(10 ** 6)).toEqual('1PB');
- expect(formatScaled(10 ** 7)).toEqual('10PB');
- expect(formatScaled(10 ** 8)).toEqual('100PB');
- expect(formatScaled(10 ** 9)).toEqual('1EB');
+ expect(formatGigaBytes(1)).toBe('1GB');
+ expect(formatGigaBytes(1, 1)).toBe('1.0GB');
+ expect(formatGigaBytes(10)).toBe('10GB');
+ expect(formatGigaBytes(10 ** 2)).toBe('100GB');
+ expect(formatGigaBytes(10 ** 3)).toBe('1TB');
+ expect(formatGigaBytes(10 ** 4)).toBe('10TB');
+ expect(formatGigaBytes(10 ** 5)).toBe('100TB');
+ expect(formatGigaBytes(10 ** 6)).toBe('1PB');
+ expect(formatGigaBytes(10 ** 7)).toBe('10PB');
+ expect(formatGigaBytes(10 ** 8)).toBe('100PB');
+ expect(formatGigaBytes(10 ** 9)).toBe('1EB');
});
it('formatting of too large numbers is not suported', () => {
@@ -159,41 +160,116 @@ describe('unit_format/formatter_factory', () => {
});
describe('scaled format with negative offset', () => {
- let formatScaled;
+ let formatMilligrams;
beforeEach(() => {
- formatScaled = scaledSIFormatter('g', -1);
+ formatMilligrams = scaledSIFormatter('g', -1);
});
it('formats floating point numbers', () => {
- expect(formatScaled(12.345)).toEqual('12.345mg');
- expect(formatScaled(12.345, 0)).toEqual('12mg');
- expect(formatScaled(12.345, 1)).toEqual('12.3mg');
- expect(formatScaled(12.345, 2)).toEqual('12.35mg');
+ expect(formatMilligrams(1.0)).toBe('1mg');
+ expect(formatMilligrams(12.345)).toBe('12.345mg');
+ expect(formatMilligrams(12.345, 0)).toBe('12mg');
+ expect(formatMilligrams(12.345, 1)).toBe('12.3mg');
+ expect(formatMilligrams(12.345, 2)).toBe('12.35mg');
});
it('formats large numbers scaled', () => {
- expect(formatScaled(1)).toEqual('1mg');
- expect(formatScaled(1, 1)).toEqual('1.0mg');
- expect(formatScaled(10)).toEqual('10mg');
- expect(formatScaled(10 ** 2)).toEqual('100mg');
- expect(formatScaled(10 ** 3)).toEqual('1g');
- expect(formatScaled(10 ** 4)).toEqual('10g');
- expect(formatScaled(10 ** 5)).toEqual('100g');
- expect(formatScaled(10 ** 6)).toEqual('1kg');
- expect(formatScaled(10 ** 7)).toEqual('10kg');
- expect(formatScaled(10 ** 8)).toEqual('100kg');
+ expect(formatMilligrams(10)).toBe('10mg');
+ expect(formatMilligrams(10 ** 2)).toBe('100mg');
+ expect(formatMilligrams(10 ** 3)).toBe('1g');
+ expect(formatMilligrams(10 ** 4)).toBe('10g');
+ expect(formatMilligrams(10 ** 5)).toBe('100g');
+ expect(formatMilligrams(10 ** 6)).toBe('1kg');
+ expect(formatMilligrams(10 ** 7)).toBe('10kg');
+ expect(formatMilligrams(10 ** 8)).toBe('100kg');
});
it('formats negative numbers scaled', () => {
- expect(formatScaled(-12.345)).toEqual('-12.345mg');
- expect(formatScaled(-12.345, 0)).toEqual('-12mg');
- expect(formatScaled(-12.345, 1)).toEqual('-12.3mg');
- expect(formatScaled(-12.345, 2)).toEqual('-12.35mg');
-
- expect(formatScaled(-10)).toEqual('-10mg');
- expect(formatScaled(-100)).toEqual('-100mg');
- expect(formatScaled(-(10 ** 4))).toEqual('-10g');
+ expect(formatMilligrams(-12.345)).toBe('-12.345mg');
+ expect(formatMilligrams(-12.345, 0)).toBe('-12mg');
+ expect(formatMilligrams(-12.345, 1)).toBe('-12.3mg');
+ expect(formatMilligrams(-12.345, 2)).toBe('-12.35mg');
+
+ expect(formatMilligrams(-10)).toBe('-10mg');
+ expect(formatMilligrams(-100)).toBe('-100mg');
+ expect(formatMilligrams(-(10 ** 4))).toBe('-10g');
+ });
+ });
+ });
+
+ describe('scaledBinaryFormatter', () => {
+ describe('scaled format', () => {
+ let formatScaledBin;
+
+ beforeEach(() => {
+ formatScaledBin = scaledBinaryFormatter('B');
+ });
+
+ it('formats bytes', () => {
+ expect(formatScaledBin(12.345)).toBe('12.345B');
+ expect(formatScaledBin(12.345, 0)).toBe('12B');
+ expect(formatScaledBin(12.345, 1)).toBe('12.3B');
+ expect(formatScaledBin(12.345, 2)).toBe('12.35B');
+ });
+
+ it('formats bytes in a binary scale', () => {
+ expect(formatScaledBin(1)).toBe('1B');
+ expect(formatScaledBin(10)).toBe('10B');
+ expect(formatScaledBin(100)).toBe('100B');
+ expect(formatScaledBin(1000)).toBe('1,000B');
+ expect(formatScaledBin(10000)).toBe('9.766KiB');
+
+ expect(formatScaledBin(1 * 1024)).toBe('1KiB');
+ expect(formatScaledBin(10 * 1024)).toBe('10KiB');
+ expect(formatScaledBin(100 * 1024)).toBe('100KiB');
+
+ expect(formatScaledBin(1 * 1024 ** 2)).toBe('1MiB');
+ expect(formatScaledBin(10 * 1024 ** 2)).toBe('10MiB');
+ expect(formatScaledBin(100 * 1024 ** 2)).toBe('100MiB');
+
+ expect(formatScaledBin(1 * 1024 ** 3)).toBe('1GiB');
+ expect(formatScaledBin(10 * 1024 ** 3)).toBe('10GiB');
+ expect(formatScaledBin(100 * 1024 ** 3)).toBe('100GiB');
+ });
+ });
+
+ describe('scaled format with offset', () => {
+ let formatGibibytes;
+
+ beforeEach(() => {
+ formatGibibytes = scaledBinaryFormatter('B', 3);
+ });
+
+ it('formats floating point numbers', () => {
+ expect(formatGibibytes(12.888)).toBe('12.888GiB');
+ expect(formatGibibytes(12.888, 0)).toBe('13GiB');
+ expect(formatGibibytes(12.888, 1)).toBe('12.9GiB');
+ expect(formatGibibytes(12.888, 2)).toBe('12.89GiB');
+ });
+
+ it('formats large numbers scaled', () => {
+ expect(formatGibibytes(1)).toBe('1GiB');
+ expect(formatGibibytes(10)).toBe('10GiB');
+ expect(formatGibibytes(100)).toBe('100GiB');
+ expect(formatGibibytes(1000)).toBe('1,000GiB');
+
+ expect(formatGibibytes(1 * 1024)).toBe('1TiB');
+ expect(formatGibibytes(10 * 1024)).toBe('10TiB');
+ expect(formatGibibytes(100 * 1024)).toBe('100TiB');
+
+ expect(formatGibibytes(1 * 1024 ** 2)).toBe('1PiB');
+ expect(formatGibibytes(10 * 1024 ** 2)).toBe('10PiB');
+ expect(formatGibibytes(100 * 1024 ** 2)).toBe('100PiB');
+
+ expect(formatGibibytes(1 * 1024 ** 3)).toBe('1EiB');
+ expect(formatGibibytes(10 * 1024 ** 3)).toBe('10EiB');
+ expect(formatGibibytes(100 * 1024 ** 3)).toBe('100EiB');
+ });
+
+ it('formatting of too large numbers is not suported', () => {
+ // formatting YB is out of range
+ expect(() => scaledBinaryFormatter('B', 9)).toThrow();
});
});
});
diff --git a/spec/frontend/lib/utils/unit_format/index_spec.js b/spec/frontend/lib/utils/unit_format/index_spec.js
index e0991f2909b..5b2fdf1f02b 100644
--- a/spec/frontend/lib/utils/unit_format/index_spec.js
+++ b/spec/frontend/lib/utils/unit_format/index_spec.js
@@ -3,109 +3,149 @@ import { getFormatter, SUPPORTED_FORMATS } from '~/lib/utils/unit_format';
describe('unit_format', () => {
describe('when a supported format is provided, the returned function formats', () => {
it('numbers, by default', () => {
- expect(getFormatter()(1)).toEqual('1');
+ expect(getFormatter()(1)).toBe('1');
});
it('numbers', () => {
const formatNumber = getFormatter(SUPPORTED_FORMATS.number);
- expect(formatNumber(1)).toEqual('1');
- expect(formatNumber(100)).toEqual('100');
- expect(formatNumber(1000)).toEqual('1,000');
- expect(formatNumber(10000)).toEqual('10,000');
- expect(formatNumber(1000000)).toEqual('1,000,000');
+ expect(formatNumber(1)).toBe('1');
+ expect(formatNumber(100)).toBe('100');
+ expect(formatNumber(1000)).toBe('1,000');
+ expect(formatNumber(10000)).toBe('10,000');
+ expect(formatNumber(1000000)).toBe('1,000,000');
});
it('percent', () => {
const formatPercent = getFormatter(SUPPORTED_FORMATS.percent);
- expect(formatPercent(1)).toEqual('100%');
- expect(formatPercent(1, 2)).toEqual('100.00%');
+ expect(formatPercent(1)).toBe('100%');
+ expect(formatPercent(1, 2)).toBe('100.00%');
- expect(formatPercent(0.1)).toEqual('10%');
- expect(formatPercent(0.5)).toEqual('50%');
+ expect(formatPercent(0.1)).toBe('10%');
+ expect(formatPercent(0.5)).toBe('50%');
- expect(formatPercent(0.888888)).toEqual('89%');
- expect(formatPercent(0.888888, 2)).toEqual('88.89%');
- expect(formatPercent(0.888888, 5)).toEqual('88.88880%');
+ expect(formatPercent(0.888888)).toBe('89%');
+ expect(formatPercent(0.888888, 2)).toBe('88.89%');
+ expect(formatPercent(0.888888, 5)).toBe('88.88880%');
- expect(formatPercent(2)).toEqual('200%');
- expect(formatPercent(10)).toEqual('1,000%');
+ expect(formatPercent(2)).toBe('200%');
+ expect(formatPercent(10)).toBe('1,000%');
});
it('percentunit', () => {
const formatPercentHundred = getFormatter(SUPPORTED_FORMATS.percentHundred);
- expect(formatPercentHundred(1)).toEqual('1%');
- expect(formatPercentHundred(1, 2)).toEqual('1.00%');
+ expect(formatPercentHundred(1)).toBe('1%');
+ expect(formatPercentHundred(1, 2)).toBe('1.00%');
- expect(formatPercentHundred(88.8888)).toEqual('89%');
- expect(formatPercentHundred(88.8888, 2)).toEqual('88.89%');
- expect(formatPercentHundred(88.8888, 5)).toEqual('88.88880%');
+ expect(formatPercentHundred(88.8888)).toBe('89%');
+ expect(formatPercentHundred(88.8888, 2)).toBe('88.89%');
+ expect(formatPercentHundred(88.8888, 5)).toBe('88.88880%');
- expect(formatPercentHundred(100)).toEqual('100%');
- expect(formatPercentHundred(100, 2)).toEqual('100.00%');
+ expect(formatPercentHundred(100)).toBe('100%');
+ expect(formatPercentHundred(100, 2)).toBe('100.00%');
- expect(formatPercentHundred(200)).toEqual('200%');
- expect(formatPercentHundred(1000)).toEqual('1,000%');
+ expect(formatPercentHundred(200)).toBe('200%');
+ expect(formatPercentHundred(1000)).toBe('1,000%');
});
it('seconds', () => {
- expect(getFormatter(SUPPORTED_FORMATS.seconds)(1)).toEqual('1s');
+ expect(getFormatter(SUPPORTED_FORMATS.seconds)(1)).toBe('1s');
});
- it('miliseconds', () => {
- const formatMiliseconds = getFormatter(SUPPORTED_FORMATS.miliseconds);
+ it('milliseconds', () => {
+ const formatMilliseconds = getFormatter(SUPPORTED_FORMATS.milliseconds);
- expect(formatMiliseconds(1)).toEqual('1ms');
- expect(formatMiliseconds(100)).toEqual('100ms');
- expect(formatMiliseconds(1000)).toEqual('1,000ms');
- expect(formatMiliseconds(10000)).toEqual('10,000ms');
- expect(formatMiliseconds(1000000)).toEqual('1,000,000ms');
+ expect(formatMilliseconds(1)).toBe('1ms');
+ expect(formatMilliseconds(100)).toBe('100ms');
+ expect(formatMilliseconds(1000)).toBe('1,000ms');
+ expect(formatMilliseconds(10000)).toBe('10,000ms');
+ expect(formatMilliseconds(1000000)).toBe('1,000,000ms');
});
- it('bytes', () => {
- const formatBytes = getFormatter(SUPPORTED_FORMATS.bytes);
-
- expect(formatBytes(1)).toEqual('1B');
- expect(formatBytes(1, 1)).toEqual('1.0B');
-
- expect(formatBytes(10)).toEqual('10B');
- expect(formatBytes(10 ** 2)).toEqual('100B');
- expect(formatBytes(10 ** 3)).toEqual('1kB');
- expect(formatBytes(10 ** 4)).toEqual('10kB');
- expect(formatBytes(10 ** 5)).toEqual('100kB');
- expect(formatBytes(10 ** 6)).toEqual('1MB');
- expect(formatBytes(10 ** 7)).toEqual('10MB');
- expect(formatBytes(10 ** 8)).toEqual('100MB');
- expect(formatBytes(10 ** 9)).toEqual('1GB');
- expect(formatBytes(10 ** 10)).toEqual('10GB');
- expect(formatBytes(10 ** 11)).toEqual('100GB');
+ it('decimalBytes', () => {
+ const formatDecimalBytes = getFormatter(SUPPORTED_FORMATS.decimalBytes);
+
+ expect(formatDecimalBytes(1)).toBe('1B');
+ expect(formatDecimalBytes(1, 1)).toBe('1.0B');
+
+ expect(formatDecimalBytes(10)).toBe('10B');
+ expect(formatDecimalBytes(10 ** 2)).toBe('100B');
+ expect(formatDecimalBytes(10 ** 3)).toBe('1kB');
+ expect(formatDecimalBytes(10 ** 4)).toBe('10kB');
+ expect(formatDecimalBytes(10 ** 5)).toBe('100kB');
+ expect(formatDecimalBytes(10 ** 6)).toBe('1MB');
+ expect(formatDecimalBytes(10 ** 7)).toBe('10MB');
+ expect(formatDecimalBytes(10 ** 8)).toBe('100MB');
+ expect(formatDecimalBytes(10 ** 9)).toBe('1GB');
+ expect(formatDecimalBytes(10 ** 10)).toBe('10GB');
+ expect(formatDecimalBytes(10 ** 11)).toBe('100GB');
});
it('kilobytes', () => {
- expect(getFormatter(SUPPORTED_FORMATS.kilobytes)(1)).toEqual('1kB');
- expect(getFormatter(SUPPORTED_FORMATS.kilobytes)(1, 1)).toEqual('1.0kB');
+ expect(getFormatter(SUPPORTED_FORMATS.kilobytes)(1)).toBe('1kB');
+ expect(getFormatter(SUPPORTED_FORMATS.kilobytes)(1, 1)).toBe('1.0kB');
});
it('megabytes', () => {
- expect(getFormatter(SUPPORTED_FORMATS.megabytes)(1)).toEqual('1MB');
- expect(getFormatter(SUPPORTED_FORMATS.megabytes)(1, 1)).toEqual('1.0MB');
+ expect(getFormatter(SUPPORTED_FORMATS.megabytes)(1)).toBe('1MB');
+ expect(getFormatter(SUPPORTED_FORMATS.megabytes)(1, 1)).toBe('1.0MB');
});
it('gigabytes', () => {
- expect(getFormatter(SUPPORTED_FORMATS.gigabytes)(1)).toEqual('1GB');
- expect(getFormatter(SUPPORTED_FORMATS.gigabytes)(1, 1)).toEqual('1.0GB');
+ expect(getFormatter(SUPPORTED_FORMATS.gigabytes)(1)).toBe('1GB');
+ expect(getFormatter(SUPPORTED_FORMATS.gigabytes)(1, 1)).toBe('1.0GB');
});
it('terabytes', () => {
- expect(getFormatter(SUPPORTED_FORMATS.terabytes)(1)).toEqual('1TB');
- expect(getFormatter(SUPPORTED_FORMATS.terabytes)(1, 1)).toEqual('1.0TB');
+ expect(getFormatter(SUPPORTED_FORMATS.terabytes)(1)).toBe('1TB');
+ expect(getFormatter(SUPPORTED_FORMATS.terabytes)(1, 1)).toBe('1.0TB');
});
it('petabytes', () => {
- expect(getFormatter(SUPPORTED_FORMATS.petabytes)(1)).toEqual('1PB');
- expect(getFormatter(SUPPORTED_FORMATS.petabytes)(1, 1)).toEqual('1.0PB');
+ expect(getFormatter(SUPPORTED_FORMATS.petabytes)(1)).toBe('1PB');
+ expect(getFormatter(SUPPORTED_FORMATS.petabytes)(1, 1)).toBe('1.0PB');
+ });
+
+ it('bytes', () => {
+ const formatBytes = getFormatter(SUPPORTED_FORMATS.bytes);
+
+ expect(formatBytes(1)).toBe('1B');
+ expect(formatBytes(1, 1)).toBe('1.0B');
+
+ expect(formatBytes(10)).toBe('10B');
+ expect(formatBytes(100)).toBe('100B');
+ expect(formatBytes(1000)).toBe('1,000B');
+
+ expect(formatBytes(1 * 1024)).toBe('1KiB');
+ expect(formatBytes(1 * 1024 ** 2)).toBe('1MiB');
+ expect(formatBytes(1 * 1024 ** 3)).toBe('1GiB');
+ });
+
+ it('kibibytes', () => {
+ expect(getFormatter(SUPPORTED_FORMATS.kibibytes)(1)).toBe('1KiB');
+ expect(getFormatter(SUPPORTED_FORMATS.kibibytes)(1, 1)).toBe('1.0KiB');
+ });
+
+ it('mebibytes', () => {
+ expect(getFormatter(SUPPORTED_FORMATS.mebibytes)(1)).toBe('1MiB');
+ expect(getFormatter(SUPPORTED_FORMATS.mebibytes)(1, 1)).toBe('1.0MiB');
+ });
+
+ it('gibibytes', () => {
+ expect(getFormatter(SUPPORTED_FORMATS.gibibytes)(1)).toBe('1GiB');
+ expect(getFormatter(SUPPORTED_FORMATS.gibibytes)(1, 1)).toBe('1.0GiB');
+ });
+
+ it('tebibytes', () => {
+ expect(getFormatter(SUPPORTED_FORMATS.tebibytes)(1)).toBe('1TiB');
+ expect(getFormatter(SUPPORTED_FORMATS.tebibytes)(1, 1)).toBe('1.0TiB');
+ });
+
+ it('pebibytes', () => {
+ expect(getFormatter(SUPPORTED_FORMATS.pebibytes)(1)).toBe('1PiB');
+ expect(getFormatter(SUPPORTED_FORMATS.pebibytes)(1, 1)).toBe('1.0PiB');
});
});
diff --git a/spec/frontend/notes/components/note_app_spec.js b/spec/frontend/notes/components/note_app_spec.js
index a51c7c57f6c..2d0cca18647 100644
--- a/spec/frontend/notes/components/note_app_spec.js
+++ b/spec/frontend/notes/components/note_app_spec.js
@@ -5,7 +5,6 @@ import { mount } from '@vue/test-utils';
import { setTestTimeout } from 'helpers/timeout';
import axios from '~/lib/utils/axios_utils';
import NotesApp from '~/notes/components/notes_app.vue';
-import service from '~/notes/services/notes_service';
import createStore from '~/notes/stores';
import '~/behaviors/markdown/render_gfm';
// TODO: use generated fixture (https://gitlab.com/gitlab-org/gitlab-foss/issues/62491)
@@ -192,7 +191,6 @@ describe('note_app', () => {
describe('individual note', () => {
beforeEach(() => {
axiosMock.onAny().reply(mockData.getIndividualNoteResponse);
- jest.spyOn(service, 'updateNote');
wrapper = mountComponent();
return waitForDiscussionsRequest().then(() => {
wrapper.find('.js-note-edit').trigger('click');
@@ -203,18 +201,18 @@ describe('note_app', () => {
expect(wrapper.find('.js-vue-issue-note-form').exists()).toBe(true);
});
- it('calls the service to update the note', () => {
+ it('calls the store action to update the note', () => {
+ jest.spyOn(axios, 'put').mockImplementation(() => Promise.resolve({ data: {} }));
wrapper.find('.js-vue-issue-note-form').value = 'this is a note';
wrapper.find('.js-vue-issue-save').trigger('click');
- expect(service.updateNote).toHaveBeenCalled();
+ expect(axios.put).toHaveBeenCalled();
});
});
describe('discussion note', () => {
beforeEach(() => {
axiosMock.onAny().reply(mockData.getDiscussionNoteResponse);
- jest.spyOn(service, 'updateNote');
wrapper = mountComponent();
return waitForDiscussionsRequest().then(() => {
wrapper.find('.js-note-edit').trigger('click');
@@ -226,10 +224,11 @@ describe('note_app', () => {
});
it('updates the note and resets the edit form', () => {
+ jest.spyOn(axios, 'put').mockImplementation(() => Promise.resolve({ data: {} }));
wrapper.find('.js-vue-issue-note-form').value = 'this is a note';
wrapper.find('.js-vue-issue-save').trigger('click');
- expect(service.updateNote).toHaveBeenCalled();
+ expect(axios.put).toHaveBeenCalled();
});
});
});
diff --git a/spec/javascripts/frequent_items/components/frequent_items_list_spec.js b/spec/javascripts/frequent_items/components/frequent_items_list_spec.js
deleted file mode 100644
index 3fcd79480cc..00000000000
--- a/spec/javascripts/frequent_items/components/frequent_items_list_spec.js
+++ /dev/null
@@ -1,90 +0,0 @@
-import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import frequentItemsListComponent from '~/frequent_items/components/frequent_items_list.vue';
-import { mockFrequentProjects } from '../mock_data';
-
-const createComponent = (namespace = 'projects') => {
- const Component = Vue.extend(frequentItemsListComponent);
-
- return mountComponent(Component, {
- namespace,
- items: mockFrequentProjects,
- isFetchFailed: false,
- hasSearchQuery: false,
- matcher: 'lab',
- });
-};
-
-describe('FrequentItemsListComponent', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('computed', () => {
- describe('isListEmpty', () => {
- it('should return `true` or `false` representing whether if `items` is empty or not with projects', () => {
- vm.items = [];
-
- expect(vm.isListEmpty).toBe(true);
-
- vm.items = mockFrequentProjects;
-
- expect(vm.isListEmpty).toBe(false);
- });
- });
-
- describe('fetched item messages', () => {
- it('should return appropriate empty list message based on value of `localStorageFailed` prop with projects', () => {
- vm.isFetchFailed = true;
-
- expect(vm.listEmptyMessage).toBe('This feature requires browser localStorage support');
-
- vm.isFetchFailed = false;
-
- expect(vm.listEmptyMessage).toBe('Projects you visit often will appear here');
- });
- });
-
- describe('searched item messages', () => {
- it('should return appropriate empty list message based on value of `searchFailed` prop with projects', () => {
- vm.hasSearchQuery = true;
- vm.isFetchFailed = true;
-
- expect(vm.listEmptyMessage).toBe('Something went wrong on our end.');
-
- vm.isFetchFailed = false;
-
- expect(vm.listEmptyMessage).toBe('Sorry, no projects matched your search');
- });
- });
- });
-
- describe('template', () => {
- it('should render component element with list of projects', done => {
- vm.items = mockFrequentProjects;
-
- Vue.nextTick(() => {
- expect(vm.$el.classList.contains('frequent-items-list-container')).toBe(true);
- expect(vm.$el.querySelectorAll('ul.list-unstyled').length).toBe(1);
- expect(vm.$el.querySelectorAll('li.frequent-items-list-item-container').length).toBe(5);
- done();
- });
- });
-
- it('should render component element with empty message', done => {
- vm.items = [];
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelectorAll('li.section-empty').length).toBe(1);
- expect(vm.$el.querySelectorAll('li.frequent-items-list-item-container').length).toBe(0);
- done();
- });
- });
- });
-});
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index c0501fb16c6..dd1588036b6 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -178,67 +178,63 @@ describe Issue do
let(:namespace) { build(:namespace, path: 'sample-namespace') }
let(:project) { build(:project, name: 'sample-project', namespace: namespace) }
let(:issue) { build(:issue, iid: 1, project: project) }
- let(:group) { create(:group, name: 'Group', path: 'sample-group') }
context 'when nil argument' do
it 'returns issue id' do
expect(issue.to_reference).to eq "#1"
end
- end
- context 'when full is true' do
- it 'returns complete path to the issue' do
- expect(issue.to_reference(full: true)).to eq 'sample-namespace/sample-project#1'
- expect(issue.to_reference(project, full: true)).to eq 'sample-namespace/sample-project#1'
- expect(issue.to_reference(group, full: true)).to eq 'sample-namespace/sample-project#1'
- end
- end
-
- context 'when same project argument' do
- it 'returns issue id' do
- expect(issue.to_reference(project)).to eq("#1")
+ it 'returns complete path to the issue with full: true' do
+ expect(issue.to_reference(full: true)).to eq 'sample-namespace/sample-project#1'
end
end
- context 'when cross namespace project argument' do
- let(:another_namespace_project) { create(:project, name: 'another-project') }
+ context 'when argument is a project' do
+ context 'when same project' do
+ it 'returns issue id' do
+ expect(issue.to_reference(project)).to eq("#1")
+ end
- it 'returns complete path to the issue' do
- expect(issue.to_reference(another_namespace_project)).to eq 'sample-namespace/sample-project#1'
+ it 'returns full reference with full: true' do
+ expect(issue.to_reference(project, full: true)).to eq 'sample-namespace/sample-project#1'
+ end
end
- end
- it 'supports a cross-project reference' do
- another_project = build(:project, name: 'another-project', namespace: project.namespace)
- expect(issue.to_reference(another_project)).to eq "sample-project#1"
- end
-
- context 'when same namespace / cross-project argument' do
- let(:another_project) { create(:project, namespace: namespace) }
+ context 'when cross-project in same namespace' do
+ let(:another_project) do
+ build(:project, name: 'another-project', namespace: project.namespace)
+ end
- it 'returns path to the issue with the project name' do
- expect(issue.to_reference(another_project)).to eq 'sample-project#1'
+ it 'returns a cross-project reference' do
+ expect(issue.to_reference(another_project)).to eq "sample-project#1"
+ end
end
- end
- context 'when different namespace / cross-project argument' do
- let(:another_namespace) { create(:namespace, path: 'another-namespace') }
- let(:another_project) { create(:project, path: 'another-project', namespace: another_namespace) }
+ context 'when cross-project in different namespace' do
+ let(:another_namespace) { build(:namespace, path: 'another-namespace') }
+ let(:another_namespace_project) { build(:project, path: 'another-project', namespace: another_namespace) }
- it 'returns full path to the issue' do
- expect(issue.to_reference(another_project)).to eq 'sample-namespace/sample-project#1'
+ it 'returns complete path to the issue' do
+ expect(issue.to_reference(another_namespace_project)).to eq 'sample-namespace/sample-project#1'
+ end
end
end
context 'when argument is a namespace' do
- context 'with same project path' do
+ context 'when same as issue' do
it 'returns path to the issue with the project name' do
expect(issue.to_reference(namespace)).to eq 'sample-project#1'
end
+
+ it 'returns full reference with full: true' do
+ expect(issue.to_reference(namespace, full: true)).to eq 'sample-namespace/sample-project#1'
+ end
end
- context 'with different project path' do
- it 'returns full path to the issue' do
+ context 'when different to issue namespace' do
+ let(:group) { build(:group, name: 'Group', path: 'sample-group') }
+
+ it 'returns full path to the issue with full: true' do
expect(issue.to_reference(group)).to eq 'sample-namespace/sample-project#1'
end
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index e1466ad2b73..635349955b1 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -52,7 +52,7 @@ describe ProjectPolicy do
admin_snippet admin_project_member admin_note admin_wiki admin_project
admin_commit_status admin_build admin_container_image
admin_pipeline admin_environment admin_deployment destroy_release add_cluster
- daily_statistics
+ daily_statistics read_deploy_token
]
end
diff --git a/spec/requests/api/deploy_tokens_spec.rb b/spec/requests/api/deploy_tokens_spec.rb
index 0e3256edcd8..9aa181db7fc 100644
--- a/spec/requests/api/deploy_tokens_spec.rb
+++ b/spec/requests/api/deploy_tokens_spec.rb
@@ -3,43 +3,84 @@
require 'spec_helper'
describe API::DeployTokens do
+ let(:user) { create(:user) }
let(:creator) { create(:user) }
let(:project) { create(:project, creator_id: creator.id) }
let!(:deploy_token) { create(:deploy_token, projects: [project]) }
describe 'GET /deploy_tokens' do
- subject { get api('/deploy_tokens', user) }
+ subject do
+ get api('/deploy_tokens', user)
+ response
+ end
context 'when unauthenticated' do
let(:user) { nil }
- it 'rejects the response as unauthorized' do
- subject
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
+ it { is_expected.to have_gitlab_http_status(:unauthorized) }
end
context 'when authenticated as non-admin user' do
let(:user) { creator }
- it 'rejects the response as forbidden' do
- subject
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
+ it { is_expected.to have_gitlab_http_status(:forbidden) }
end
context 'when authenticated as admin' do
let(:user) { create(:admin) }
+ it { is_expected.to have_gitlab_http_status(:ok) }
+
it 'returns all deploy tokens' do
subject
- expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.first['id']).to eq(deploy_token.id)
+ expect(response).to match_response_schema('public_api/v4/deploy_tokens')
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/deploy_tokens' do
+ subject do
+ get api("/projects/#{project.id}/deploy_tokens", user)
+ response
+ end
+
+ context 'when unauthenticated' do
+ let(:user) { nil }
+
+ it { is_expected.to have_gitlab_http_status(:not_found) }
+ end
+
+ context 'when authenticated as non-admin user' do
+ before do
+ project.add_developer(user)
+ end
+
+ it { is_expected.to have_gitlab_http_status(:forbidden) }
+ end
+
+ context 'when authenticated as maintainer' do
+ let!(:other_deploy_token) { create(:deploy_token) }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ it { is_expected.to have_gitlab_http_status(:ok) }
+
+ it 'returns all deploy tokens for the project' do
+ subject
+
+ expect(response).to include_pagination_headers
+ expect(response).to match_response_schema('public_api/v4/deploy_tokens')
+ end
+
+ it 'does not return deploy tokens for other projects' do
+ subject
+
+ token_ids = json_response.map { |token| token['id'] }
+ expect(token_ids).not_to include(other_deploy_token.id)
end
end
end