diff options
352 files changed, 15318 insertions, 9812 deletions
diff --git a/.scss-lint.yml b/.scss-lint.yml index dcd4cac780a..180d377d6f8 100644 --- a/.scss-lint.yml +++ b/.scss-lint.yml @@ -59,6 +59,8 @@ linters: # Reports when you define the same property twice in a single rule set. DuplicateProperty: enabled: true + ignore_consecutive: + - cursor # Separate rule, function, and mixin declarations with empty lines. EmptyLineBetweenBlocks: diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 9188543ea64..c5c735103b2 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.93.0 +0.94.0 diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 21c8c7b46b8..a8a18875682 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -7.1.1 +7.1.2 @@ -384,6 +384,7 @@ group :test do gem 'email_spec', '~> 1.6.0' gem 'json-schema', '~> 2.8.0' gem 'webmock', '~> 2.3.2' + gem 'rails-controller-testing' if rails5? # Rails5 only gem. gem 'test_after_commit', '~> 1.1' unless rails5? # Remove this gem when migrated to rails 5.0. It's been integrated to rails 5.0. gem 'sham_rack', '~> 1.3.6' gem 'concurrent-ruby', '~> 1.0.5' @@ -421,7 +422,7 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly-proto', '~> 0.91.0', require: 'gitaly' +gem 'gitaly-proto', '~> 0.94.0', require: 'gitaly' gem 'grpc', '~> 1.10.0' # Locked until https://github.com/google/protobuf/issues/4210 is closed diff --git a/Gemfile.lock b/Gemfile.lock index 55e7bd9492a..a1150dfccdd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -290,7 +290,7 @@ GEM po_to_json (>= 1.0.0) rails (>= 3.2.0) gherkin-ruby (0.3.2) - gitaly-proto (0.91.0) + gitaly-proto (0.94.0) google-protobuf (~> 3.1) grpc (~> 1.0) github-linguist (5.3.3) @@ -587,7 +587,7 @@ GEM orm_adapter (0.5.0) os (0.9.6) parallel (1.12.1) - parser (2.5.0.3) + parser (2.5.0.5) ast (~> 2.4.0) parslet (1.5.0) blankslate (~> 2.0) @@ -1061,7 +1061,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.3) - gitaly-proto (~> 0.91.0) + gitaly-proto (~> 0.94.0) github-linguist (~> 5.3.3) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.6.2) diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock index 08ae3fb514c..03fe5f2ed26 100644 --- a/Gemfile.rails5.lock +++ b/Gemfile.rails5.lock @@ -291,7 +291,7 @@ GEM po_to_json (>= 1.0.0) rails (>= 3.2.0) gherkin-ruby (0.3.2) - gitaly-proto (0.91.0) + gitaly-proto (0.94.0) google-protobuf (~> 3.1) grpc (~> 1.0) github-linguist (5.3.3) @@ -587,7 +587,7 @@ GEM orm_adapter (0.5.0) os (0.9.6) parallel (1.12.1) - parser (2.5.0.4) + parser (2.5.0.5) ast (~> 2.4.0) parslet (1.5.0) blankslate (~> 2.0) @@ -678,6 +678,10 @@ GEM bundler (>= 1.3.0) railties (= 5.0.6) sprockets-rails (>= 2.0.0) + rails-controller-testing (1.0.2) + actionpack (~> 5.x, >= 5.0.1) + actionview (~> 5.x, >= 5.0.1) + activesupport (~> 5.x) rails-deprecated_sanitizer (1.0.3) activesupport (>= 4.2.0.alpha) rails-dom-testing (2.0.3) @@ -1062,7 +1066,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.3) - gitaly-proto (~> 0.91.0) + gitaly-proto (~> 0.94.0) github-linguist (~> 5.3.3) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.6.2) @@ -1145,6 +1149,7 @@ DEPENDENCIES rack-oauth2 (~> 1.2.1) rack-proxy (~> 0.6.0) rails (= 5.0.6) + rails-controller-testing rails-deprecated_sanitizer (~> 1.0.3) rails-i18n (~> 5.1) rainbow (~> 2.2) diff --git a/app/assets/javascripts/badges/components/badge.vue b/app/assets/javascripts/badges/components/badge.vue new file mode 100644 index 00000000000..6e6cb31e3ac --- /dev/null +++ b/app/assets/javascripts/badges/components/badge.vue @@ -0,0 +1,121 @@ +<script> +import Icon from '~/vue_shared/components/icon.vue'; +import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; +import Tooltip from '~/vue_shared/directives/tooltip'; + +export default { + name: 'Badge', + components: { + Icon, + LoadingIcon, + Tooltip, + }, + directives: { + Tooltip, + }, + props: { + imageUrl: { + type: String, + required: true, + }, + linkUrl: { + type: String, + required: true, + }, + }, + data() { + return { + hasError: false, + isLoading: true, + numRetries: 0, + }; + }, + computed: { + imageUrlWithRetries() { + if (this.numRetries === 0) { + return this.imageUrl; + } + + return `${this.imageUrl}#retries=${this.numRetries}`; + }, + }, + watch: { + imageUrl() { + this.hasError = false; + this.isLoading = true; + this.numRetries = 0; + }, + }, + methods: { + onError() { + this.isLoading = false; + this.hasError = true; + }, + onLoad() { + this.isLoading = false; + }, + reloadImage() { + this.hasError = false; + this.isLoading = true; + this.numRetries += 1; + }, + }, +}; +</script> + +<template> + <div> + <a + v-show="!isLoading && !hasError" + :href="linkUrl" + target="_blank" + rel="noopener noreferrer" + > + <img + class="project-badge" + :src="imageUrlWithRetries" + @load="onLoad" + @error="onError" + aria-hidden="true" + /> + </a> + + <loading-icon + v-show="isLoading" + :inline="true" + /> + + <div + v-show="hasError" + class="btn-group" + > + <div class="btn btn-default btn-xs disabled"> + <icon + class="prepend-left-8 append-right-8" + name="doc_image" + :size="16" + aria-hidden="true" + /> + </div> + <div + class="btn btn-default btn-xs disabled" + > + <span class="prepend-left-8 append-right-8">{{ s__('Badges|No badge image') }}</span> + </div> + </div> + + <button + v-show="hasError" + class="btn btn-transparent btn-xs text-primary" + type="button" + v-tooltip + :title="s__('Badges|Reload badge image')" + @click="reloadImage" + > + <icon + name="retry" + :size="16" + /> + </button> + </div> +</template> diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue new file mode 100644 index 00000000000..ae942b2c1a7 --- /dev/null +++ b/app/assets/javascripts/badges/components/badge_form.vue @@ -0,0 +1,219 @@ +<script> +import _ from 'underscore'; +import { mapActions, mapState } from 'vuex'; +import createFlash from '~/flash'; +import { s__, sprintf } from '~/locale'; +import LoadingButton from '~/vue_shared/components/loading_button.vue'; +import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; +import createEmptyBadge from '../empty_badge'; +import Badge from './badge.vue'; + +const badgePreviewDelayInMilliseconds = 1500; + +export default { + name: 'BadgeForm', + components: { + Badge, + LoadingButton, + LoadingIcon, + }, + props: { + isEditing: { + type: Boolean, + required: true, + }, + }, + computed: { + ...mapState([ + 'badgeInAddForm', + 'badgeInEditForm', + 'docsUrl', + 'isRendering', + 'isSaving', + 'renderedBadge', + ]), + badge() { + if (this.isEditing) { + return this.badgeInEditForm; + } + + return this.badgeInAddForm; + }, + canSubmit() { + return ( + this.badge !== null && + this.badge.imageUrl && + this.badge.imageUrl.trim() !== '' && + this.badge.linkUrl && + this.badge.linkUrl.trim() !== '' && + !this.isSaving + ); + }, + helpText() { + const placeholders = ['project_path', 'project_id', 'default_branch', 'commit_sha'] + .map(placeholder => `<code>%{${placeholder}}</code>`) + .join(', '); + return sprintf( + s__('Badges|The %{docsLinkStart}variables%{docsLinkEnd} GitLab supports: %{placeholders}'), + { + docsLinkEnd: '</a>', + docsLinkStart: `<a href="${_.escape(this.docsUrl)}">`, + placeholders, + }, + false, + ); + }, + renderedImageUrl() { + return this.renderedBadge ? this.renderedBadge.renderedImageUrl : ''; + }, + renderedLinkUrl() { + return this.renderedBadge ? this.renderedBadge.renderedLinkUrl : ''; + }, + imageUrl: { + get() { + return this.badge ? this.badge.imageUrl : ''; + }, + set(imageUrl) { + const badge = this.badge || createEmptyBadge(); + this.updateBadgeInForm({ + ...badge, + imageUrl, + }); + }, + }, + linkUrl: { + get() { + return this.badge ? this.badge.linkUrl : ''; + }, + set(linkUrl) { + const badge = this.badge || createEmptyBadge(); + this.updateBadgeInForm({ + ...badge, + linkUrl, + }); + }, + }, + submitButtonLabel() { + if (this.isEditing) { + return s__('Badges|Save changes'); + } + return s__('Badges|Add badge'); + }, + }, + methods: { + ...mapActions(['addBadge', 'renderBadge', 'saveBadge', 'stopEditing', 'updateBadgeInForm']), + debouncedPreview: _.debounce(function preview() { + this.renderBadge(); + }, badgePreviewDelayInMilliseconds), + onCancel() { + this.stopEditing(); + }, + onSubmit() { + if (!this.canSubmit) { + return Promise.resolve(); + } + + if (this.isEditing) { + return this.saveBadge() + .then(() => { + createFlash(s__('Badges|The badge was saved.'), 'notice'); + }) + .catch(error => { + createFlash( + s__('Badges|Saving the badge failed, please check the entered URLs and try again.'), + ); + throw error; + }); + } + + return this.addBadge() + .then(() => { + createFlash(s__('Badges|A new badge was added.'), 'notice'); + }) + .catch(error => { + createFlash( + s__('Badges|Adding the badge failed, please check the entered URLs and try again.'), + ); + throw error; + }); + }, + }, + badgeImageUrlPlaceholder: + 'https://example.gitlab.com/%{project_path}/badges/%{default_branch}/<badge>.svg', + badgeLinkUrlPlaceholder: 'https://example.gitlab.com/%{project_path}', +}; +</script> + +<template> + <form + class="prepend-top-default append-bottom-default" + @submit.prevent.stop="onSubmit" + > + <div class="form-group"> + <label for="badge-link-url">{{ s__('Badges|Link') }}</label> + <input + id="badge-link-url" + type="text" + class="form-control" + v-model="linkUrl" + :placeholder="$options.badgeLinkUrlPlaceholder" + @input="debouncedPreview" + /> + <span + class="help-block" + v-html="helpText" + ></span> + </div> + + <div class="form-group"> + <label for="badge-image-url">{{ s__('Badges|Badge image URL') }}</label> + <input + id="badge-image-url" + type="text" + class="form-control" + v-model="imageUrl" + :placeholder="$options.badgeImageUrlPlaceholder" + @input="debouncedPreview" + /> + <span + class="help-block" + v-html="helpText" + ></span> + </div> + + <div class="form-group"> + <label for="badge-preview">{{ s__('Badges|Badge image preview') }}</label> + <badge + id="badge-preview" + v-show="renderedBadge && !isRendering" + :image-url="renderedImageUrl" + :link-url="renderedLinkUrl" + /> + <p v-show="isRendering"> + <loading-icon + :inline="true" + /> + </p> + <p + v-show="!renderedBadge && !isRendering" + class="disabled-content" + >{{ s__('Badges|No image to preview') }}</p> + </div> + + <div class="row-content-block"> + <loading-button + type="submit" + container-class="btn btn-success" + :disabled="!canSubmit" + :loading="isSaving" + :label="submitButtonLabel" + /> + <button + class="btn btn-cancel" + type="button" + v-if="isEditing" + @click="onCancel" + >{{ __('Cancel') }}</button> + </div> + </form> +</template> diff --git a/app/assets/javascripts/badges/components/badge_list.vue b/app/assets/javascripts/badges/components/badge_list.vue new file mode 100644 index 00000000000..ca7197e1e0f --- /dev/null +++ b/app/assets/javascripts/badges/components/badge_list.vue @@ -0,0 +1,57 @@ +<script> +import { mapState } from 'vuex'; +import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; +import BadgeListRow from './badge_list_row.vue'; +import { GROUP_BADGE } from '../constants'; + +export default { + name: 'BadgeList', + components: { + BadgeListRow, + LoadingIcon, + }, + computed: { + ...mapState(['badges', 'isLoading', 'kind']), + hasNoBadges() { + return !this.isLoading && (!this.badges || !this.badges.length); + }, + isGroupBadge() { + return this.kind === GROUP_BADGE; + }, + }, +}; +</script> + +<template> + <div class="panel panel-default"> + <div class="panel-heading"> + {{ s__('Badges|Your badges') }} + <span + v-show="!isLoading" + class="badge" + >{{ badges.length }}</span> + </div> + <loading-icon + v-show="isLoading" + class="panel-body" + size="2" + /> + <div + v-if="hasNoBadges" + class="panel-body" + > + <span v-if="isGroupBadge">{{ s__('Badges|This group has no badges') }}</span> + <span v-else>{{ s__('Badges|This project has no badges') }}</span> + </div> + <div + v-else + class="panel-body" + > + <badge-list-row + v-for="badge in badges" + :key="badge.id" + :badge="badge" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/badges/components/badge_list_row.vue b/app/assets/javascripts/badges/components/badge_list_row.vue new file mode 100644 index 00000000000..af062bdf8c6 --- /dev/null +++ b/app/assets/javascripts/badges/components/badge_list_row.vue @@ -0,0 +1,89 @@ +<script> +import { mapActions, mapState } from 'vuex'; +import { s__ } from '~/locale'; +import Icon from '~/vue_shared/components/icon.vue'; +import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; +import { PROJECT_BADGE } from '../constants'; +import Badge from './badge.vue'; + +export default { + name: 'BadgeListRow', + components: { + Badge, + Icon, + LoadingIcon, + }, + props: { + badge: { + type: Object, + required: true, + }, + }, + computed: { + ...mapState(['kind']), + badgeKindText() { + if (this.badge.kind === PROJECT_BADGE) { + return s__('Badges|Project Badge'); + } + + return s__('Badges|Group Badge'); + }, + canEditBadge() { + return this.badge.kind === this.kind; + }, + }, + methods: { + ...mapActions(['editBadge', 'updateBadgeInModal']), + }, +}; +</script> + +<template> + <div class="gl-responsive-table-row-layout gl-responsive-table-row"> + <badge + class="table-section section-30" + :image-url="badge.renderedImageUrl" + :link-url="badge.renderedLinkUrl" + /> + <span class="table-section section-50 str-truncated">{{ badge.linkUrl }}</span> + <div class="table-section section-10"> + <span class="badge">{{ badgeKindText }}</span> + </div> + <div class="table-section section-10 table-button-footer"> + <div + v-if="canEditBadge" + class="table-action-buttons"> + <button + class="btn btn-default append-right-8" + type="button" + :disabled="badge.isDeleting" + @click="editBadge(badge)" + > + <icon + name="pencil" + :size="16" + :aria-label="__('Edit')" + /> + </button> + <button + class="btn btn-danger" + type="button" + data-toggle="modal" + data-target="#delete-badge-modal" + :disabled="badge.isDeleting" + @click="updateBadgeInModal(badge)" + > + <icon + name="remove" + :size="16" + :aria-label="__('Delete')" + /> + </button> + <loading-icon + v-show="badge.isDeleting" + :inline="true" + /> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/badges/components/badge_settings.vue b/app/assets/javascripts/badges/components/badge_settings.vue new file mode 100644 index 00000000000..83f78394238 --- /dev/null +++ b/app/assets/javascripts/badges/components/badge_settings.vue @@ -0,0 +1,70 @@ +<script> +import { mapState, mapActions } from 'vuex'; +import createFlash from '~/flash'; +import { s__ } from '~/locale'; +import GlModal from '~/vue_shared/components/gl_modal.vue'; +import Badge from './badge.vue'; +import BadgeForm from './badge_form.vue'; +import BadgeList from './badge_list.vue'; + +export default { + name: 'BadgeSettings', + components: { + Badge, + BadgeForm, + BadgeList, + GlModal, + }, + computed: { + ...mapState(['badgeInModal', 'isEditing']), + deleteModalText() { + return s__( + 'Badges|You are going to delete this badge. Deleted badges <strong>cannot</strong> be restored.', + ); + }, + }, + methods: { + ...mapActions(['deleteBadge']), + onSubmitModal() { + this.deleteBadge(this.badgeInModal) + .then(() => { + createFlash(s__('Badges|The badge was deleted.'), 'notice'); + }) + .catch(error => { + createFlash(s__('Badges|Deleting the badge failed, please try again.')); + throw error; + }); + }, + }, +}; +</script> + +<template> + <div class="badge-settings"> + <gl-modal + id="delete-badge-modal" + :header-title-text="s__('Badges|Delete badge?')" + footer-primary-button-variant="danger" + :footer-primary-button-text="s__('Badges|Delete badge')" + @submit="onSubmitModal"> + <div class="well"> + <badge + :image-url="badgeInModal ? badgeInModal.renderedImageUrl : ''" + :link-url="badgeInModal ? badgeInModal.renderedLinkUrl : ''" + /> + </div> + <p v-html="deleteModalText"></p> + </gl-modal> + + <badge-form + v-show="isEditing" + :is-editing="true" + /> + + <badge-form + v-show="!isEditing" + :is-editing="false" + /> + <badge-list v-show="!isEditing" /> + </div> +</template> diff --git a/app/assets/javascripts/badges/constants.js b/app/assets/javascripts/badges/constants.js new file mode 100644 index 00000000000..8fbe3db5ef1 --- /dev/null +++ b/app/assets/javascripts/badges/constants.js @@ -0,0 +1,2 @@ +export const GROUP_BADGE = 'group'; +export const PROJECT_BADGE = 'project'; diff --git a/app/assets/javascripts/badges/empty_badge.js b/app/assets/javascripts/badges/empty_badge.js new file mode 100644 index 00000000000..49a9b5e1be8 --- /dev/null +++ b/app/assets/javascripts/badges/empty_badge.js @@ -0,0 +1,7 @@ +export default () => ({ + imageUrl: '', + isDeleting: false, + linkUrl: '', + renderedImageUrl: '', + renderedLinkUrl: '', +}); diff --git a/app/assets/javascripts/badges/store/actions.js b/app/assets/javascripts/badges/store/actions.js new file mode 100644 index 00000000000..5542278b3e0 --- /dev/null +++ b/app/assets/javascripts/badges/store/actions.js @@ -0,0 +1,167 @@ +import axios from '~/lib/utils/axios_utils'; +import types from './mutation_types'; + +export const transformBackendBadge = badge => ({ + id: badge.id, + imageUrl: badge.image_url, + kind: badge.kind, + linkUrl: badge.link_url, + renderedImageUrl: badge.rendered_image_url, + renderedLinkUrl: badge.rendered_link_url, + isDeleting: false, +}); + +export default { + requestNewBadge({ commit }) { + commit(types.REQUEST_NEW_BADGE); + }, + receiveNewBadge({ commit }, newBadge) { + commit(types.RECEIVE_NEW_BADGE, newBadge); + }, + receiveNewBadgeError({ commit }) { + commit(types.RECEIVE_NEW_BADGE_ERROR); + }, + addBadge({ dispatch, state }) { + const newBadge = state.badgeInAddForm; + const endpoint = state.apiEndpointUrl; + dispatch('requestNewBadge'); + return axios + .post(endpoint, { + image_url: newBadge.imageUrl, + link_url: newBadge.linkUrl, + }) + .catch(error => { + dispatch('receiveNewBadgeError'); + throw error; + }) + .then(res => { + dispatch('receiveNewBadge', transformBackendBadge(res.data)); + }); + }, + requestDeleteBadge({ commit }, badgeId) { + commit(types.REQUEST_DELETE_BADGE, badgeId); + }, + receiveDeleteBadge({ commit }, badgeId) { + commit(types.RECEIVE_DELETE_BADGE, badgeId); + }, + receiveDeleteBadgeError({ commit }, badgeId) { + commit(types.RECEIVE_DELETE_BADGE_ERROR, badgeId); + }, + deleteBadge({ dispatch, state }, badge) { + const badgeId = badge.id; + dispatch('requestDeleteBadge', badgeId); + const endpoint = `${state.apiEndpointUrl}/${badgeId}`; + return axios + .delete(endpoint) + .catch(error => { + dispatch('receiveDeleteBadgeError', badgeId); + throw error; + }) + .then(() => { + dispatch('receiveDeleteBadge', badgeId); + }); + }, + + editBadge({ commit }, badge) { + commit(types.START_EDITING, badge); + }, + + requestLoadBadges({ commit }, data) { + commit(types.REQUEST_LOAD_BADGES, data); + }, + receiveLoadBadges({ commit }, badges) { + commit(types.RECEIVE_LOAD_BADGES, badges); + }, + receiveLoadBadgesError({ commit }) { + commit(types.RECEIVE_LOAD_BADGES_ERROR); + }, + + loadBadges({ dispatch, state }, data) { + dispatch('requestLoadBadges', data); + const endpoint = state.apiEndpointUrl; + return axios + .get(endpoint) + .catch(error => { + dispatch('receiveLoadBadgesError'); + throw error; + }) + .then(res => { + dispatch('receiveLoadBadges', res.data.map(transformBackendBadge)); + }); + }, + + requestRenderedBadge({ commit }) { + commit(types.REQUEST_RENDERED_BADGE); + }, + receiveRenderedBadge({ commit }, renderedBadge) { + commit(types.RECEIVE_RENDERED_BADGE, renderedBadge); + }, + receiveRenderedBadgeError({ commit }) { + commit(types.RECEIVE_RENDERED_BADGE_ERROR); + }, + + renderBadge({ dispatch, state }) { + const badge = state.isEditing ? state.badgeInEditForm : state.badgeInAddForm; + const { linkUrl, imageUrl } = badge; + if (!linkUrl || linkUrl.trim() === '' || !imageUrl || imageUrl.trim() === '') { + return Promise.resolve(badge); + } + + dispatch('requestRenderedBadge'); + + const parameters = [ + `link_url=${encodeURIComponent(linkUrl)}`, + `image_url=${encodeURIComponent(imageUrl)}`, + ].join('&'); + const renderEndpoint = `${state.apiEndpointUrl}/render?${parameters}`; + return axios + .get(renderEndpoint) + .catch(error => { + dispatch('receiveRenderedBadgeError'); + throw error; + }) + .then(res => { + dispatch('receiveRenderedBadge', transformBackendBadge(res.data)); + }); + }, + + requestUpdatedBadge({ commit }) { + commit(types.REQUEST_UPDATED_BADGE); + }, + receiveUpdatedBadge({ commit }, updatedBadge) { + commit(types.RECEIVE_UPDATED_BADGE, updatedBadge); + }, + receiveUpdatedBadgeError({ commit }) { + commit(types.RECEIVE_UPDATED_BADGE_ERROR); + }, + + saveBadge({ dispatch, state }) { + const badge = state.badgeInEditForm; + const endpoint = `${state.apiEndpointUrl}/${badge.id}`; + dispatch('requestUpdatedBadge'); + return axios + .put(endpoint, { + image_url: badge.imageUrl, + link_url: badge.linkUrl, + }) + .catch(error => { + dispatch('receiveUpdatedBadgeError'); + throw error; + }) + .then(res => { + dispatch('receiveUpdatedBadge', transformBackendBadge(res.data)); + }); + }, + + stopEditing({ commit }) { + commit(types.STOP_EDITING); + }, + + updateBadgeInForm({ commit }, badge) { + commit(types.UPDATE_BADGE_IN_FORM, badge); + }, + + updateBadgeInModal({ commit }, badge) { + commit(types.UPDATE_BADGE_IN_MODAL, badge); + }, +}; diff --git a/app/assets/javascripts/badges/store/index.js b/app/assets/javascripts/badges/store/index.js new file mode 100644 index 00000000000..7a5df403a0e --- /dev/null +++ b/app/assets/javascripts/badges/store/index.js @@ -0,0 +1,13 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import createState from './state'; +import actions from './actions'; +import mutations from './mutations'; + +Vue.use(Vuex); + +export default new Vuex.Store({ + state: createState(), + actions, + mutations, +}); diff --git a/app/assets/javascripts/badges/store/mutation_types.js b/app/assets/javascripts/badges/store/mutation_types.js new file mode 100644 index 00000000000..d73f91b6005 --- /dev/null +++ b/app/assets/javascripts/badges/store/mutation_types.js @@ -0,0 +1,21 @@ +export default { + RECEIVE_DELETE_BADGE: 'RECEIVE_DELETE_BADGE', + RECEIVE_DELETE_BADGE_ERROR: 'RECEIVE_DELETE_BADGE_ERROR', + RECEIVE_LOAD_BADGES: 'RECEIVE_LOAD_BADGES', + RECEIVE_LOAD_BADGES_ERROR: 'RECEIVE_LOAD_BADGES_ERROR', + RECEIVE_NEW_BADGE: 'RECEIVE_NEW_BADGE', + RECEIVE_NEW_BADGE_ERROR: 'RECEIVE_NEW_BADGE_ERROR', + RECEIVE_RENDERED_BADGE: 'RECEIVE_RENDERED_BADGE', + RECEIVE_RENDERED_BADGE_ERROR: 'RECEIVE_RENDERED_BADGE_ERROR', + RECEIVE_UPDATED_BADGE: 'RECEIVE_UPDATED_BADGE', + RECEIVE_UPDATED_BADGE_ERROR: 'RECEIVE_UPDATED_BADGE_ERROR', + REQUEST_DELETE_BADGE: 'REQUEST_DELETE_BADGE', + REQUEST_LOAD_BADGES: 'REQUEST_LOAD_BADGES', + REQUEST_NEW_BADGE: 'REQUEST_NEW_BADGE', + REQUEST_RENDERED_BADGE: 'REQUEST_RENDERED_BADGE', + REQUEST_UPDATED_BADGE: 'REQUEST_UPDATED_BADGE', + START_EDITING: 'START_EDITING', + STOP_EDITING: 'STOP_EDITING', + UPDATE_BADGE_IN_FORM: 'UPDATE_BADGE_IN_FORM', + UPDATE_BADGE_IN_MODAL: 'UPDATE_BADGE_IN_MODAL', +}; diff --git a/app/assets/javascripts/badges/store/mutations.js b/app/assets/javascripts/badges/store/mutations.js new file mode 100644 index 00000000000..bd84e68c00f --- /dev/null +++ b/app/assets/javascripts/badges/store/mutations.js @@ -0,0 +1,158 @@ +import types from './mutation_types'; +import { PROJECT_BADGE } from '../constants'; + +const reorderBadges = badges => + badges.sort((a, b) => { + if (a.kind !== b.kind) { + return a.kind === PROJECT_BADGE ? 1 : -1; + } + + return a.id - b.id; + }); + +export default { + [types.RECEIVE_NEW_BADGE](state, newBadge) { + Object.assign(state, { + badgeInAddForm: null, + badges: reorderBadges(state.badges.concat(newBadge)), + isSaving: false, + renderedBadge: null, + }); + }, + [types.RECEIVE_NEW_BADGE_ERROR](state) { + Object.assign(state, { + isSaving: false, + }); + }, + [types.REQUEST_NEW_BADGE](state) { + Object.assign(state, { + isSaving: true, + }); + }, + + [types.RECEIVE_UPDATED_BADGE](state, updatedBadge) { + const badges = state.badges.map(badge => { + if (badge.id === updatedBadge.id) { + return updatedBadge; + } + return badge; + }); + Object.assign(state, { + badgeInEditForm: null, + badges, + isEditing: false, + isSaving: false, + renderedBadge: null, + }); + }, + [types.RECEIVE_UPDATED_BADGE_ERROR](state) { + Object.assign(state, { + isSaving: false, + }); + }, + [types.REQUEST_UPDATED_BADGE](state) { + Object.assign(state, { + isSaving: true, + }); + }, + + [types.RECEIVE_LOAD_BADGES](state, badges) { + Object.assign(state, { + badges: reorderBadges(badges), + isLoading: false, + }); + }, + [types.RECEIVE_LOAD_BADGES_ERROR](state) { + Object.assign(state, { + isLoading: false, + }); + }, + [types.REQUEST_LOAD_BADGES](state, data) { + Object.assign(state, { + kind: data.kind, // project or group + apiEndpointUrl: data.apiEndpointUrl, + docsUrl: data.docsUrl, + isLoading: true, + }); + }, + + [types.RECEIVE_DELETE_BADGE](state, badgeId) { + const badges = state.badges.filter(badge => badge.id !== badgeId); + Object.assign(state, { + badges, + }); + }, + [types.RECEIVE_DELETE_BADGE_ERROR](state, badgeId) { + const badges = state.badges.map(badge => { + if (badge.id === badgeId) { + return { + ...badge, + isDeleting: false, + }; + } + + return badge; + }); + Object.assign(state, { + badges, + }); + }, + [types.REQUEST_DELETE_BADGE](state, badgeId) { + const badges = state.badges.map(badge => { + if (badge.id === badgeId) { + return { + ...badge, + isDeleting: true, + }; + } + + return badge; + }); + Object.assign(state, { + badges, + }); + }, + + [types.RECEIVE_RENDERED_BADGE](state, renderedBadge) { + Object.assign(state, { isRendering: false, renderedBadge }); + }, + [types.RECEIVE_RENDERED_BADGE_ERROR](state) { + Object.assign(state, { isRendering: false }); + }, + [types.REQUEST_RENDERED_BADGE](state) { + Object.assign(state, { isRendering: true }); + }, + + [types.START_EDITING](state, badge) { + Object.assign(state, { + badgeInEditForm: { ...badge }, + isEditing: true, + renderedBadge: { ...badge }, + }); + }, + [types.STOP_EDITING](state) { + Object.assign(state, { + badgeInEditForm: null, + isEditing: false, + renderedBadge: null, + }); + }, + + [types.UPDATE_BADGE_IN_FORM](state, badge) { + if (state.isEditing) { + Object.assign(state, { + badgeInEditForm: badge, + }); + } else { + Object.assign(state, { + badgeInAddForm: badge, + }); + } + }, + + [types.UPDATE_BADGE_IN_MODAL](state, badge) { + Object.assign(state, { + badgeInModal: badge, + }); + }, +}; diff --git a/app/assets/javascripts/badges/store/state.js b/app/assets/javascripts/badges/store/state.js new file mode 100644 index 00000000000..43413aeb5bb --- /dev/null +++ b/app/assets/javascripts/badges/store/state.js @@ -0,0 +1,13 @@ +export default () => ({ + apiEndpointUrl: null, + badgeInAddForm: null, + badgeInEditForm: null, + badgeInModal: null, + badges: [], + docsUrl: null, + renderedBadge: null, + isEditing: false, + isLoading: false, + isRendering: false, + isSaving: false, +}); diff --git a/app/assets/javascripts/blob/file_template_mediator.js b/app/assets/javascripts/blob/file_template_mediator.js index 030ca1907e5..ff1cbcad145 100644 --- a/app/assets/javascripts/blob/file_template_mediator.js +++ b/app/assets/javascripts/blob/file_template_mediator.js @@ -94,7 +94,7 @@ export default class FileTemplateMediator { const hash = urlPieces[1]; if (hash === 'preview') { this.hideTemplateSelectorMenu(); - } else if (hash === 'editor') { + } else if (hash === 'editor' && !this.typeSelector.isHidden()) { this.showTemplateSelectorMenu(); } }); diff --git a/app/assets/javascripts/blob/file_template_selector.js b/app/assets/javascripts/blob/file_template_selector.js index e52cf249f3a..02228434a29 100644 --- a/app/assets/javascripts/blob/file_template_selector.js +++ b/app/assets/javascripts/blob/file_template_selector.js @@ -32,6 +32,10 @@ export default class FileTemplateSelector { } } + isHidden() { + return this.$wrapper.hasClass('hidden'); + } + getToggleText() { return this.$dropdownToggleText.text(); } diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js index 3cffd91716a..bea818010a4 100644 --- a/app/assets/javascripts/boards/components/board.js +++ b/app/assets/javascripts/boards/components/board.js @@ -5,7 +5,7 @@ import Sortable from 'vendor/Sortable'; import Vue from 'vue'; import AccessorUtilities from '../../lib/utils/accessor'; import boardList from './board_list.vue'; -import boardBlankState from './board_blank_state'; +import BoardBlankState from './board_blank_state.vue'; import './board_delete'; const Store = gl.issueBoards.BoardsStore; @@ -18,7 +18,7 @@ gl.issueBoards.Board = Vue.extend({ components: { boardList, 'board-delete': gl.issueBoards.BoardDelete, - boardBlankState, + BoardBlankState, }, props: { list: Object, diff --git a/app/assets/javascripts/boards/components/board_blank_state.js b/app/assets/javascripts/boards/components/board_blank_state.vue index 72db626d3c7..2049eeb9c30 100644 --- a/app/assets/javascripts/boards/components/board_blank_state.js +++ b/app/assets/javascripts/boards/components/board_blank_state.vue @@ -1,42 +1,11 @@ +<script> /* global ListLabel */ - import _ from 'underscore'; import Cookies from 'js-cookie'; const Store = gl.issueBoards.BoardsStore; export default { - template: ` - <div class="board-blank-state"> - <p> - Add the following default lists to your Issue Board with one click: - </p> - <ul class="board-blank-state-list"> - <li v-for="label in predefinedLabels"> - <span - class="label-color" - :style="{ backgroundColor: label.color }"> - </span> - {{ label.title }} - </li> - </ul> - <p> - Starting out with the default set of lists will get you right on the way to making the most of your board. - </p> - <button - class="btn btn-create btn-inverted btn-block" - type="button" - @click.stop="addDefaultLists"> - Add default lists - </button> - <button - class="btn btn-default btn-block" - type="button" - @click.stop="clearBlankState"> - Nevermind, I'll use my own - </button> - </div> - `, data() { return { predefinedLabels: [ @@ -89,3 +58,41 @@ export default { clearBlankState: Store.removeBlankState.bind(Store), }, }; + +</script> + +<template> + <div class="board-blank-state"> + <p> + Add the following default lists to your Issue Board with one click: + </p> + <ul class="board-blank-state-list"> + <li + v-for="(label, index) in predefinedLabels" + :key="index" + > + <span + class="label-color" + :style="{ backgroundColor: label.color }"> + </span> + {{ label.title }} + </li> + </ul> + <p> + Starting out with the default set of lists will get you + right on the way to making the most of your board. + </p> + <button + class="btn btn-create btn-inverted btn-block" + type="button" + @click.stop="addDefaultLists"> + Add default lists + </button> + <button + class="btn btn-default btn-block" + type="button" + @click.stop="clearBlankState"> + Nevermind, I'll use my own + </button> + </div> +</template> diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js b/app/assets/javascripts/boards/components/modal/empty_state.js index e571b11a83d..9e37f95cdd6 100644 --- a/app/assets/javascripts/boards/components/modal/empty_state.js +++ b/app/assets/javascripts/boards/components/modal/empty_state.js @@ -1,9 +1,9 @@ import Vue from 'vue'; - -const ModalStore = gl.issueBoards.ModalStore; +import ModalStore from '../../stores/modal_store'; +import modalMixin from '../../mixins/modal_mixins'; gl.issueBoards.ModalEmptyState = Vue.extend({ - mixins: [gl.issueBoards.ModalMixins], + mixins: [modalMixin], data() { return ModalStore.store; }, diff --git a/app/assets/javascripts/boards/components/modal/footer.js b/app/assets/javascripts/boards/components/modal/footer.js index 03cd7ef65cb..9735e0ddacc 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js +++ b/app/assets/javascripts/boards/components/modal/footer.js @@ -3,11 +3,11 @@ import Flash from '../../../flash'; import { __ } from '../../../locale'; import './lists_dropdown'; import { pluralize } from '../../../lib/utils/text_utility'; - -const ModalStore = gl.issueBoards.ModalStore; +import ModalStore from '../../stores/modal_store'; +import modalMixin from '../../mixins/modal_mixins'; gl.issueBoards.ModalFooter = Vue.extend({ - mixins: [gl.issueBoards.ModalMixins], + mixins: [modalMixin], data() { return { modal: ModalStore.store, diff --git a/app/assets/javascripts/boards/components/modal/header.js b/app/assets/javascripts/boards/components/modal/header.js index 31f59d295bf..67c29ebca72 100644 --- a/app/assets/javascripts/boards/components/modal/header.js +++ b/app/assets/javascripts/boards/components/modal/header.js @@ -1,11 +1,11 @@ import Vue from 'vue'; import modalFilters from './filters'; import './tabs'; - -const ModalStore = gl.issueBoards.ModalStore; +import ModalStore from '../../stores/modal_store'; +import modalMixin from '../../mixins/modal_mixins'; gl.issueBoards.ModalHeader = Vue.extend({ - mixins: [gl.issueBoards.ModalMixins], + mixins: [modalMixin], props: { projectId: { type: Number, diff --git a/app/assets/javascripts/boards/components/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js index d825ff38587..3083b3e4405 100644 --- a/app/assets/javascripts/boards/components/modal/index.js +++ b/app/assets/javascripts/boards/components/modal/index.js @@ -7,8 +7,7 @@ import './header'; import './list'; import './footer'; import './empty_state'; - -const ModalStore = gl.issueBoards.ModalStore; +import ModalStore from '../../stores/modal_store'; gl.issueBoards.IssuesModal = Vue.extend({ props: { diff --git a/app/assets/javascripts/boards/components/modal/list.js b/app/assets/javascripts/boards/components/modal/list.js index 7c62134b3a3..6b04a6c7a6c 100644 --- a/app/assets/javascripts/boards/components/modal/list.js +++ b/app/assets/javascripts/boards/components/modal/list.js @@ -2,8 +2,7 @@ import Vue from 'vue'; import bp from '../../../breakpoints'; - -const ModalStore = gl.issueBoards.ModalStore; +import ModalStore from '../../stores/modal_store'; gl.issueBoards.ModalList = Vue.extend({ props: { diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.js b/app/assets/javascripts/boards/components/modal/lists_dropdown.js index 4684ea76647..e644de2d4fc 100644 --- a/app/assets/javascripts/boards/components/modal/lists_dropdown.js +++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.js @@ -1,6 +1,5 @@ import Vue from 'vue'; - -const ModalStore = gl.issueBoards.ModalStore; +import ModalStore from '../../stores/modal_store'; gl.issueBoards.ModalFooterListsDropdown = Vue.extend({ data() { diff --git a/app/assets/javascripts/boards/components/modal/tabs.js b/app/assets/javascripts/boards/components/modal/tabs.js index 3e5d08e3d75..b6465a88e5e 100644 --- a/app/assets/javascripts/boards/components/modal/tabs.js +++ b/app/assets/javascripts/boards/components/modal/tabs.js @@ -1,9 +1,9 @@ import Vue from 'vue'; - -const ModalStore = gl.issueBoards.ModalStore; +import ModalStore from '../../stores/modal_store'; +import modalMixin from '../../mixins/modal_mixins'; gl.issueBoards.ModalTabs = Vue.extend({ - mixins: [gl.issueBoards.ModalMixins], + mixins: [modalMixin], data() { return ModalStore.store; }, diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index 8b1c14c04ff..a6f8681cfac 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -17,9 +17,9 @@ import './models/milestone'; import './models/project'; import './models/assignee'; import './stores/boards_store'; -import './stores/modal_store'; +import ModalStore from './stores/modal_store'; import BoardService from './services/board_service'; -import './mixins/modal_mixins'; +import modalMixin from './mixins/modal_mixins'; import './mixins/sortable_default_options'; import './filters/due_date_filters'; import './components/board'; @@ -31,7 +31,6 @@ import '~/vue_shared/vue_resource_interceptor'; // eslint-disable-line import/fi export default () => { const $boardApp = document.getElementById('board-app'); const Store = gl.issueBoards.BoardsStore; - const ModalStore = gl.issueBoards.ModalStore; window.gl = window.gl || {}; @@ -176,7 +175,7 @@ export default () => { gl.IssueBoardsModalAddBtn = new Vue({ el: document.getElementById('js-add-issues-btn'), - mixins: [gl.issueBoards.ModalMixins], + mixins: [modalMixin], data() { return { modal: ModalStore.store, diff --git a/app/assets/javascripts/boards/mixins/modal_mixins.js b/app/assets/javascripts/boards/mixins/modal_mixins.js index 2b0a1aaa89f..6c97e1629bf 100644 --- a/app/assets/javascripts/boards/mixins/modal_mixins.js +++ b/app/assets/javascripts/boards/mixins/modal_mixins.js @@ -1,6 +1,6 @@ -const ModalStore = gl.issueBoards.ModalStore; +import ModalStore from '../stores/modal_store'; -gl.issueBoards.ModalMixins = { +export default { methods: { toggleModal(toggle) { ModalStore.store.showAddIssuesModal = toggle; diff --git a/app/assets/javascripts/boards/stores/modal_store.js b/app/assets/javascripts/boards/stores/modal_store.js index 4fdc925c825..a4220cd840d 100644 --- a/app/assets/javascripts/boards/stores/modal_store.js +++ b/app/assets/javascripts/boards/stores/modal_store.js @@ -1,6 +1,3 @@ -window.gl = window.gl || {}; -window.gl.issueBoards = window.gl.issueBoards || {}; - class ModalStore { constructor() { this.store = { @@ -95,4 +92,4 @@ class ModalStore { } } -gl.issueBoards.ModalStore = new ModalStore(); +export default new ModalStore(); diff --git a/app/assets/javascripts/ide/components/ide_file_buttons.vue b/app/assets/javascripts/ide/components/ide_file_buttons.vue index 6d07329df71..a6c6f46a144 100644 --- a/app/assets/javascripts/ide/components/ide_file_buttons.vue +++ b/app/assets/javascripts/ide/components/ide_file_buttons.vue @@ -36,6 +36,7 @@ export default { > <a v-tooltip + v-if="!file.binary" :href="file.blamePath" :title="__('Blame')" class="btn btn-xs btn-transparent blame" diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue index 9c386896448..152a5f632ad 100644 --- a/app/assets/javascripts/ide/components/ide_status_bar.vue +++ b/app/assets/javascripts/ide/components/ide_status_bar.vue @@ -1,25 +1,23 @@ <script> - import icon from '~/vue_shared/components/icon.vue'; - import tooltip from '~/vue_shared/directives/tooltip'; - import timeAgoMixin from '~/vue_shared/mixins/timeago'; +import icon from '~/vue_shared/components/icon.vue'; +import tooltip from '~/vue_shared/directives/tooltip'; +import timeAgoMixin from '~/vue_shared/mixins/timeago'; - export default { - components: { - icon, +export default { + components: { + icon, + }, + directives: { + tooltip, + }, + mixins: [timeAgoMixin], + props: { + file: { + type: Object, + required: true, }, - directives: { - tooltip, - }, - mixins: [ - timeAgoMixin, - ], - props: { - file: { - type: Object, - required: true, - }, - }, - }; + }, +}; </script> <template> @@ -50,7 +48,9 @@ <div class="text-right"> {{ file.eol }} </div> - <div class="text-right"> + <div + class="text-right" + v-if="!file.binary"> {{ file.editorRow }}:{{ file.editorColumn }} </div> <div class="text-right"> diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index 8a709b31ea0..6aa44ca2c11 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -171,10 +171,10 @@ export default { id="ide" class="blob-viewer-container blob-editor-container" > - <div - class="ide-mode-tabs clearfix" - v-if="!shouldHideEditor"> - <ul class="nav-links pull-left"> + <div class="ide-mode-tabs clearfix"> + <ul + class="nav-links pull-left" + v-if="!shouldHideEditor"> <li :class="editTabCSS"> <a href="javascript:void(0);" @@ -210,9 +210,10 @@ export default { > </div> <content-viewer - v-if="!shouldHideEditor && file.viewMode === 'preview'" + v-if="shouldHideEditor || file.viewMode === 'preview'" :content="file.content || file.raw" - :path="file.path" + :path="file.rawPath" + :file-size="file.size" :project-path="file.projectId"/> </div> </template> diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js index 6a143e518f9..eeb14b5490c 100644 --- a/app/assets/javascripts/ide/stores/mutations/file.js +++ b/app/assets/javascripts/ide/stores/mutations/file.js @@ -43,6 +43,7 @@ export default { raw: null, baseRaw: null, html: data.html, + size: data.size, }); }, [types.SET_FILE_RAW_DATA](state, { file, raw }) { diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js index 4befcc501ef..05a019de54f 100644 --- a/app/assets/javascripts/ide/stores/utils.js +++ b/app/assets/javascripts/ide/stores/utils.js @@ -40,6 +40,7 @@ export const dataStructure = () => ({ eol: '', viewMode: 'edit', previewMode: null, + size: 0, }); export const decorateData = entity => { diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 824d3f7ca09..d0050abb8e9 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -10,6 +10,7 @@ import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; import DropdownUtils from './filtered_search/dropdown_utils'; import CreateLabelDropdown from './create_label'; import flash from './flash'; +import ModalStore from './boards/stores/modal_store'; export default class LabelsSelect { constructor(els, options = {}) { @@ -350,7 +351,7 @@ export default class LabelsSelect { } if ($dropdown.closest('.add-issues-modal').length) { - boardsModel = gl.issueBoards.ModalStore.store.filter; + boardsModel = ModalStore.store.filter; } if (boardsModel) { diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index 94d03621bff..b54ecd2d543 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -7,7 +7,8 @@ * @param {String} text * @returns {String} */ -export const addDelimiter = text => (text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') : text); +export const addDelimiter = text => + (text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') : text); /** * Returns '99+' for numbers bigger than 99. @@ -22,7 +23,8 @@ export const highCountTrim = count => (count > 99 ? '99+' : count); * @param {String} string * @requires {String} */ -export const humanize = string => string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1); +export const humanize = string => + string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1); /** * Adds an 's' to the end of the string when count is bigger than 0 @@ -53,7 +55,7 @@ export const slugify = str => str.trim().toLowerCase(); * @param {Number} maxLength * @returns {String} */ -export const truncate = (string, maxLength) => `${string.substr(0, (maxLength - 3))}...`; +export const truncate = (string, maxLength) => `${string.substr(0, maxLength - 3)}...`; /** * Capitalizes first character @@ -80,3 +82,15 @@ export const stripHtml = (string, replace = '') => string.replace(/<[^>]*>/g, re * @param {*} string */ export const convertToCamelCase = string => string.replace(/(_\w)/g, s => s[1].toUpperCase()); + +/** + * Converts a sentence to lower case from the second word onwards + * e.g. Hello World => Hello world + * + * @param {*} string + */ +export const convertToSentenceCase = string => { + const splitWord = string.split(' ').map((word, index) => (index > 0 ? word.toLowerCase() : word)); + + return splitWord.join(' '); +}; diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index c749042a14a..d0a2b27b0e6 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -6,6 +6,7 @@ import $ from 'jquery'; import _ from 'underscore'; import axios from './lib/utils/axios_utils'; import { timeFor } from './lib/utils/datetime_utility'; +import ModalStore from './boards/stores/modal_store'; export default class MilestoneSelect { constructor(currentProject, els, options = {}) { @@ -164,7 +165,7 @@ export default class MilestoneSelect { } if ($dropdown.closest('.add-issues-modal').length) { - boardsStore = gl.issueBoards.ModalStore.store.filter; + boardsStore = ModalStore.store.filter; } if (boardsStore) { diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue index 04d546fafa0..f93b1da4f58 100644 --- a/app/assets/javascripts/monitoring/components/graph.vue +++ b/app/assets/javascripts/monitoring/components/graph.vue @@ -1,8 +1,10 @@ <script> import { scaleLinear, scaleTime } from 'd3-scale'; import { axisLeft, axisBottom } from 'd3-axis'; +import _ from 'underscore'; import { max, extent } from 'd3-array'; import { select } from 'd3-selection'; +import GraphAxis from './graph/axis.vue'; import GraphLegend from './graph/legend.vue'; import GraphFlag from './graph/flag.vue'; import GraphDeployment from './graph/deployment.vue'; @@ -18,10 +20,11 @@ const d3 = { scaleLinear, scaleTime, axisLeft, axisBottom, max, extent, select } export default { components: { - GraphLegend, + GraphAxis, GraphFlag, GraphDeployment, GraphPath, + GraphLegend, }, mixins: [MonitoringMixin], props: { @@ -138,7 +141,7 @@ export default { this.legendTitle = query.label || 'Average'; this.graphWidth = this.$refs.baseSvg.clientWidth - this.margin.left - this.margin.right; this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom; - this.baseGraphHeight = this.graphHeight; + this.baseGraphHeight = this.graphHeight - 50; this.baseGraphWidth = this.graphWidth; // pixel offsets inside the svg and outside are not 1:1 @@ -177,10 +180,8 @@ export default { this.graphHeightOffset, ); - if (!this.showLegend) { - this.baseGraphHeight -= 50; - } else if (this.timeSeries.length > 3) { - this.baseGraphHeight = this.baseGraphHeight += (this.timeSeries.length - 3) * 20; + if (_.findWhere(this.timeSeries, { renderCanary: true })) { + this.timeSeries = this.timeSeries.map(series => ({ ...series, renderCanary: true })); } const axisXScale = d3.scaleTime().range([0, this.graphWidth - 70]); @@ -251,17 +252,13 @@ export default { class="y-axis" transform="translate(70, 20)" /> - <graph-legend + <graph-axis :graph-width="graphWidth" :graph-height="graphHeight" :margin="margin" :measurements="measurements" - :legend-title="legendTitle" :y-axis-label="yAxisLabel" - :time-series="timeSeries" :unit-of-display="unitOfDisplay" - :current-data-index="currentDataIndex" - :show-legend-group="showLegend" /> <svg class="graph-data" @@ -306,5 +303,10 @@ export default { :deployment-flag-data="deploymentFlagData" /> </div> + <graph-legend + v-if="showLegend" + :legend-title="legendTitle" + :time-series="timeSeries" + /> </div> </template> diff --git a/app/assets/javascripts/monitoring/components/graph/axis.vue b/app/assets/javascripts/monitoring/components/graph/axis.vue new file mode 100644 index 00000000000..fc4b3689dfd --- /dev/null +++ b/app/assets/javascripts/monitoring/components/graph/axis.vue @@ -0,0 +1,142 @@ +<script> +import { convertToSentenceCase } from '~/lib/utils/text_utility'; +import { s__ } from '~/locale'; + +export default { + props: { + graphWidth: { + type: Number, + required: true, + }, + graphHeight: { + type: Number, + required: true, + }, + margin: { + type: Object, + required: true, + }, + measurements: { + type: Object, + required: true, + }, + yAxisLabel: { + type: String, + required: true, + }, + unitOfDisplay: { + type: String, + required: true, + }, + }, + data() { + return { + yLabelWidth: 0, + yLabelHeight: 0, + }; + }, + computed: { + textTransform() { + const yCoordinate = + (this.graphHeight - + this.margin.top + + this.measurements.axisLabelLineOffset) / + 2 || 0; + + return `translate(15, ${yCoordinate}) rotate(-90)`; + }, + + rectTransform() { + const yCoordinate = + (this.graphHeight - + this.margin.top + + this.measurements.axisLabelLineOffset) / + 2 + + this.yLabelWidth / 2 || 0; + + return `translate(0, ${yCoordinate}) rotate(-90)`; + }, + + xPosition() { + return ( + (this.graphWidth + this.measurements.axisLabelLineOffset) / 2 - + this.margin.right || 0 + ); + }, + + yPosition() { + return ( + this.graphHeight - + this.margin.top + + this.measurements.axisLabelLineOffset || 0 + ); + }, + + yAxisLabelSentenceCase() { + return `${convertToSentenceCase(this.yAxisLabel)} (${this.unitOfDisplay})`; + }, + + timeString() { + return s__('PrometheusDashboard|Time'); + }, + }, + mounted() { + this.$nextTick(() => { + const bbox = this.$refs.ylabel.getBBox(); + this.yLabelWidth = bbox.width + 10; // Added some padding + this.yLabelHeight = bbox.height + 5; + }); + }, +}; +</script> +<template> + <g class="axis-label-container"> + <line + class="label-x-axis-line" + stroke="#000000" + stroke-width="1" + x1="10" + :y1="yPosition" + :x2="graphWidth + 20" + :y2="yPosition" + /> + <line + class="label-y-axis-line" + stroke="#000000" + stroke-width="1" + x1="10" + y1="0" + :x2="10" + :y2="yPosition" + /> + <rect + class="rect-axis-text" + :transform="rectTransform" + :width="yLabelWidth" + :height="yLabelHeight" + /> + <text + class="label-axis-text y-label-text" + text-anchor="middle" + :transform="textTransform" + ref="ylabel" + > + {{ yAxisLabelSentenceCase }} + </text> + <rect + class="rect-axis-text" + :x="xPosition + 60" + :y="graphHeight - 80" + width="35" + height="50" + /> + <text + class="label-axis-text x-label-text" + :x="xPosition + 60" + :y="yPosition" + dy=".35em" + > + {{ timeString }} + </text> + </g> +</template> diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue index 906c7c51f52..b8202e25685 100644 --- a/app/assets/javascripts/monitoring/components/graph/flag.vue +++ b/app/assets/javascripts/monitoring/components/graph/flag.vue @@ -1,11 +1,13 @@ <script> import { dateFormat, timeFormat } from '../../utils/date_time_formatters'; import { formatRelevantDigits } from '../../../lib/utils/number_utils'; -import icon from '../../../vue_shared/components/icon.vue'; +import Icon from '../../../vue_shared/components/icon.vue'; +import TrackLine from './track_line.vue'; export default { components: { - icon, + Icon, + TrackLine, }, props: { currentXCoordinate: { @@ -107,11 +109,6 @@ export default { } return `series ${index + 1}`; }, - strokeDashArray(type) { - if (type === 'dashed') return '6, 3'; - if (type === 'dotted') return '3, 3'; - return null; - }, }, }; </script> @@ -160,28 +157,13 @@ export default { </div> </div> <div class="popover-content"> - <table> + <table class="prometheus-table"> <tr v-for="(series, index) in timeSeries" :key="index" > - <td> - <svg - width="15" - height="6" - > - <line - :stroke="series.lineColor" - :stroke-dasharray="strokeDashArray(series.lineStyle)" - stroke-width="4" - x1="0" - x2="15" - y1="2" - y2="2" - /> - </svg> - </td> - <td>{{ seriesMetricLabel(index, series) }}</td> + <track-line :track="series"/> + <td>{{ series.track }} {{ seriesMetricLabel(index, series) }}</td> <td> <strong>{{ seriesMetricValue(series) }}</strong> </td> diff --git a/app/assets/javascripts/monitoring/components/graph/legend.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue index a7a058a9203..da9280cf1f1 100644 --- a/app/assets/javascripts/monitoring/components/graph/legend.vue +++ b/app/assets/javascripts/monitoring/components/graph/legend.vue @@ -1,204 +1,72 @@ <script> -import { formatRelevantDigits } from '../../../lib/utils/number_utils'; +import TrackLine from './track_line.vue'; +import TrackInfo from './track_info.vue'; export default { + components: { + TrackLine, + TrackInfo, + }, props: { - graphWidth: { - type: Number, - required: true, - }, - graphHeight: { - type: Number, - required: true, - }, - margin: { - type: Object, - required: true, - }, - measurements: { - type: Object, - required: true, - }, legendTitle: { type: String, required: true, }, - yAxisLabel: { - type: String, - required: true, - }, timeSeries: { type: Array, required: true, }, - unitOfDisplay: { - type: String, - required: true, - }, - currentDataIndex: { - type: Number, - required: true, - }, - showLegendGroup: { - type: Boolean, - required: false, - default: true, - }, - }, - data() { - return { - yLabelWidth: 0, - yLabelHeight: 0, - seriesXPosition: 0, - metricUsageXPosition: 0, - }; - }, - computed: { - textTransform() { - const yCoordinate = - (this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset) / 2 || 0; - - return `translate(15, ${yCoordinate}) rotate(-90)`; - }, - rectTransform() { - const yCoordinate = - (this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset) / 2 + - this.yLabelWidth / 2 || 0; - - return `translate(0, ${yCoordinate}) rotate(-90)`; - }, - xPosition() { - return (this.graphWidth + this.measurements.axisLabelLineOffset) / 2 - this.margin.right || 0; - }, - yPosition() { - return this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset || 0; - }, - }, - mounted() { - this.$nextTick(() => { - const bbox = this.$refs.ylabel.getBBox(); - this.metricUsageXPosition = 0; - this.seriesXPosition = 0; - if (this.$refs.legendTitleSvg != null) { - this.seriesXPosition = this.$refs.legendTitleSvg[0].getBBox().width; - } - if (this.$refs.seriesTitleSvg != null) { - this.metricUsageXPosition = this.$refs.seriesTitleSvg[0].getBBox().width; - } - this.yLabelWidth = bbox.width + 10; // Added some padding - this.yLabelHeight = bbox.height + 5; - }); }, methods: { - translateLegendGroup(index) { - return `translate(0, ${12 * index})`; - }, - formatMetricUsage(series) { - const value = - series.values[this.currentDataIndex] && series.values[this.currentDataIndex].value; - if (isNaN(value)) { - return '-'; - } - return `${formatRelevantDigits(value)} ${this.unitOfDisplay}`; - }, - createSeriesString(index, series) { - if (series.metricTag) { - return `${series.metricTag} ${this.formatMetricUsage(series)}`; - } - return `${this.legendTitle} series ${index + 1} ${this.formatMetricUsage(series)}`; - }, - strokeDashArray(type) { - if (type === 'dashed') return '6, 3'; - if (type === 'dotted') return '3, 3'; - return null; + isStable(track) { + return { + 'prometheus-table-row-highlight': track.trackName !== 'Canary' && track.renderCanary, + }; }, }, }; </script> <template> - <g class="axis-label-container"> - <line - class="label-x-axis-line" - stroke="#000000" - stroke-width="1" - x1="10" - :y1="yPosition" - :x2="graphWidth + 20" - :y2="yPosition" - /> - <line - class="label-y-axis-line" - stroke="#000000" - stroke-width="1" - x1="10" - y1="0" - :x2="10" - :y2="yPosition" - /> - <rect - class="rect-axis-text" - :transform="rectTransform" - :width="yLabelWidth" - :height="yLabelHeight" - /> - <text - class="label-axis-text y-label-text" - text-anchor="middle" - :transform="textTransform" - ref="ylabel" - > - {{ yAxisLabel }} - </text> - <rect - class="rect-axis-text" - :x="xPosition + 60" - :y="graphHeight - 80" - width="35" - height="50" - /> - <text - class="label-axis-text x-label-text" - :x="xPosition + 60" - :y="yPosition" - dy=".35em" - > - Time - </text> - <template v-if="showLegendGroup"> - <g - class="legend-group" + <div class="prometheus-graph-legends prepend-left-10"> + <table class="prometheus-table"> + <tr v-for="(series, index) in timeSeries" :key="index" - :transform="translateLegendGroup(index)" + v-if="series.shouldRenderLegend" + :class="isStable(series)" > - <line - :stroke="series.lineColor" - :stroke-width="measurements.legends.height" - :stroke-dasharray="strokeDashArray(series.lineStyle)" - :x1="measurements.legends.offsetX" - :x2="measurements.legends.offsetX + measurements.legends.width" - :y1="graphHeight - measurements.legends.offsetY" - :y2="graphHeight - measurements.legends.offsetY" - /> - <text - v-if="timeSeries.length > 1" - class="legend-metric-title" - ref="legendTitleSvg" - x="38" - :y="graphHeight - 30" - > - {{ createSeriesString(index, series) }} - </text> - <text - v-else + <td> + <strong v-if="series.renderCanary">{{ series.trackName }}</strong> + </td> + <track-line :track="series" /> + <td class="legend-metric-title" - ref="legendTitleSvg" - x="38" - :y="graphHeight - 30" - > - {{ legendTitle }} {{ formatMetricUsage(series) }} - </text> - </g> - </template> - </g> + v-if="timeSeries.length > 1"> + <track-info + :track="series" + v-if="series.metricTag" /> + <track-info + v-else + :track="series"> + <strong>{{ legendTitle }}</strong> series {{ index + 1 }} + </track-info> + </td> + <td v-else> + <track-info :track="series"> + <strong>{{ legendTitle }}</strong> + </track-info> + </td> + <template v-for="(track, trackIndex) in series.tracksLegend"> + <track-line + :track="track" + :key="`track-line-${trackIndex}`"/> + <td :key="`track-info-${trackIndex}`"> + <track-info + class="legend-metric-title" + :track="track" /> + </td> + </template> + </tr> + </table> + </div> </template> diff --git a/app/assets/javascripts/monitoring/components/graph/track_info.vue b/app/assets/javascripts/monitoring/components/graph/track_info.vue new file mode 100644 index 00000000000..ec1c2222af9 --- /dev/null +++ b/app/assets/javascripts/monitoring/components/graph/track_info.vue @@ -0,0 +1,29 @@ +<script> +import { formatRelevantDigits } from '~/lib/utils/number_utils'; + +export default { + name: 'TrackInfo', + props: { + track: { + type: Object, + required: true, + }, + }, + computed: { + summaryMetrics() { + return `Avg: ${formatRelevantDigits(this.track.average)} · Max: ${formatRelevantDigits( + this.track.max, + )}`; + }, + }, +}; +</script> +<template> + <span> + <slot> + <strong> {{ track.metricTag }} </strong> + </slot> + {{ summaryMetrics }} + </span> +</template> + diff --git a/app/assets/javascripts/monitoring/components/graph/track_line.vue b/app/assets/javascripts/monitoring/components/graph/track_line.vue new file mode 100644 index 00000000000..79b322e2e42 --- /dev/null +++ b/app/assets/javascripts/monitoring/components/graph/track_line.vue @@ -0,0 +1,36 @@ +<script> +export default { + name: 'TrackLine', + props: { + track: { + type: Object, + required: true, + }, + }, + computed: { + stylizedLine() { + if (this.track.lineStyle === 'dashed') return '6, 3'; + if (this.track.lineStyle === 'dotted') return '3, 3'; + return null; + }, + }, +}; +</script> +<template> + <td> + <svg + width="15" + height="6"> + <line + :stroke-dasharray="stylizedLine" + :stroke="track.lineColor" + stroke-width="4" + :x1="0" + :x2="15" + :y1="2" + :y2="2" + /> + </svg> + </td> +</template> + diff --git a/app/assets/javascripts/monitoring/stores/monitoring_store.js b/app/assets/javascripts/monitoring/stores/monitoring_store.js index 854636e9a89..535c415cd6d 100644 --- a/app/assets/javascripts/monitoring/stores/monitoring_store.js +++ b/app/assets/javascripts/monitoring/stores/monitoring_store.js @@ -1,7 +1,7 @@ import _ from 'underscore'; function sortMetrics(metrics) { - return _.chain(metrics).sortBy('weight').sortBy('title').value(); + return _.chain(metrics).sortBy('title').sortBy('weight').value(); } function normalizeMetrics(metrics) { diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js index b5b8e3c255d..8a93c7e6bae 100644 --- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js +++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js @@ -1,10 +1,21 @@ import _ from 'underscore'; import { scaleLinear, scaleTime } from 'd3-scale'; import { line, area, curveLinear } from 'd3-shape'; -import { extent, max } from 'd3-array'; +import { extent, max, sum } from 'd3-array'; import { timeMinute } from 'd3-time'; - -const d3 = { scaleLinear, scaleTime, line, area, curveLinear, extent, max, timeMinute }; +import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; + +const d3 = { + scaleLinear, + scaleTime, + line, + area, + curveLinear, + extent, + max, + timeMinute, + sum, +}; const defaultColorPalette = { blue: ['#1f78d1', '#8fbce8'], @@ -20,6 +31,8 @@ const defaultStyleOrder = ['solid', 'dashed', 'dotted']; function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom, yDom, lineStyle) { let usedColors = []; + let renderCanary = false; + const timeSeriesParsed = []; function pickColor(name) { let pick; @@ -38,16 +51,23 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom return defaultColorPalette[pick]; } - return query.result.map((timeSeries, timeSeriesNumber) => { + query.result.forEach((timeSeries, timeSeriesNumber) => { let metricTag = ''; let lineColor = ''; let areaColor = ''; + let shouldRenderLegend = true; + const timeSeriesValues = timeSeries.values.map(d => d.value); + const maximumValue = d3.max(timeSeriesValues); + const accum = d3.sum(timeSeriesValues); + const trackName = capitalizeFirstCharacter(query.track ? query.track : 'Stable'); + + if (trackName === 'Canary') { + renderCanary = true; + } - const timeSeriesScaleX = d3.scaleTime() - .range([0, graphWidth - 70]); + const timeSeriesScaleX = d3.scaleTime().range([0, graphWidth - 70]); - const timeSeriesScaleY = d3.scaleLinear() - .range([graphHeight - graphHeightOffset, 0]); + const timeSeriesScaleY = d3.scaleLinear().range([graphHeight - graphHeightOffset, 0]); timeSeriesScaleX.domain(xDom); timeSeriesScaleX.ticks(d3.timeMinute, 60); @@ -55,13 +75,15 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom const defined = d => !isNaN(d.value) && d.value != null; - const lineFunction = d3.line() + const lineFunction = d3 + .line() .defined(defined) .curve(d3.curveLinear) // d3 v4 uses curbe instead of interpolate .x(d => timeSeriesScaleX(d.time)) .y(d => timeSeriesScaleY(d.value)); - const areaFunction = d3.area() + const areaFunction = d3 + .area() .defined(defined) .curve(d3.curveLinear) .x(d => timeSeriesScaleX(d.time)) @@ -69,38 +91,62 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom .y1(d => timeSeriesScaleY(d.value)); const timeSeriesMetricLabel = timeSeries.metric[Object.keys(timeSeries.metric)[0]]; - const seriesCustomizationData = query.series != null && - _.findWhere(query.series[0].when, { value: timeSeriesMetricLabel }); + const seriesCustomizationData = + query.series != null && _.findWhere(query.series[0].when, { value: timeSeriesMetricLabel }); if (seriesCustomizationData) { metricTag = seriesCustomizationData.value || timeSeriesMetricLabel; [lineColor, areaColor] = pickColor(seriesCustomizationData.color); + shouldRenderLegend = false; } else { metricTag = timeSeriesMetricLabel || query.label || `series ${timeSeriesNumber + 1}`; [lineColor, areaColor] = pickColor(); + if (timeSeriesParsed.length > 1) { + shouldRenderLegend = false; + } } - if (query.track) { - metricTag += ` - ${query.track}`; + if (!shouldRenderLegend) { + if (!timeSeriesParsed[0].tracksLegend) { + timeSeriesParsed[0].tracksLegend = []; + } + timeSeriesParsed[0].tracksLegend.push({ + max: maximumValue, + average: accum / timeSeries.values.length, + lineStyle, + lineColor, + metricTag, + }); } - return { + timeSeriesParsed.push({ linePath: lineFunction(timeSeries.values), areaPath: areaFunction(timeSeries.values), timeSeriesScaleX, values: timeSeries.values, + max: maximumValue, + average: accum / timeSeries.values.length, lineStyle, lineColor, areaColor, metricTag, - }; + trackName, + shouldRenderLegend, + renderCanary, + }); }); + + return timeSeriesParsed; } export default function createTimeSeries(queries, graphWidth, graphHeight, graphHeightOffset) { - const allValues = queries.reduce((allQueryResults, query) => allQueryResults.concat( - query.result.reduce((allResults, result) => allResults.concat(result.values), []), - ), []); + const allValues = queries.reduce( + (allQueryResults, query) => + allQueryResults.concat( + query.result.reduce((allResults, result) => allResults.concat(result.values), []), + ), + [], + ); const xDom = d3.extent(allValues, d => d.time); const yDom = [0, d3.max(allValues.map(d => d.value))]; diff --git a/app/assets/javascripts/pages/groups/settings/badges/index.js b/app/assets/javascripts/pages/groups/settings/badges/index.js new file mode 100644 index 00000000000..74e96ee4a8f --- /dev/null +++ b/app/assets/javascripts/pages/groups/settings/badges/index.js @@ -0,0 +1,10 @@ +import Vue from 'vue'; +import Translate from '~/vue_shared/translate'; +import { GROUP_BADGE } from '~/badges/constants'; +import mountBadgeSettings from '~/pages/shared/mount_badge_settings'; + +Vue.use(Translate); + +document.addEventListener('DOMContentLoaded', () => { + mountBadgeSettings(GROUP_BADGE); +}); diff --git a/app/assets/javascripts/pages/projects/settings/badges/index/index.js b/app/assets/javascripts/pages/projects/settings/badges/index/index.js new file mode 100644 index 00000000000..30469550866 --- /dev/null +++ b/app/assets/javascripts/pages/projects/settings/badges/index/index.js @@ -0,0 +1,10 @@ +import Vue from 'vue'; +import Translate from '~/vue_shared/translate'; +import { PROJECT_BADGE } from '~/badges/constants'; +import mountBadgeSettings from '~/pages/shared/mount_badge_settings'; + +Vue.use(Translate); + +document.addEventListener('DOMContentLoaded', () => { + mountBadgeSettings(PROJECT_BADGE); +}); diff --git a/app/assets/javascripts/pages/projects/settings/repository/create_deploy_token/index.js b/app/assets/javascripts/pages/projects/settings/repository/create_deploy_token/index.js new file mode 100644 index 00000000000..ffc84dc106b --- /dev/null +++ b/app/assets/javascripts/pages/projects/settings/repository/create_deploy_token/index.js @@ -0,0 +1,3 @@ +import initForm from '../form'; + +document.addEventListener('DOMContentLoaded', initForm); diff --git a/app/assets/javascripts/pages/projects/settings/repository/form.js b/app/assets/javascripts/pages/projects/settings/repository/form.js new file mode 100644 index 00000000000..a5c17ab322c --- /dev/null +++ b/app/assets/javascripts/pages/projects/settings/repository/form.js @@ -0,0 +1,19 @@ +/* eslint-disable no-new */ + +import ProtectedTagCreate from '~/protected_tags/protected_tag_create'; +import ProtectedTagEditList from '~/protected_tags/protected_tag_edit_list'; +import initSettingsPanels from '~/settings_panels'; +import initDeployKeys from '~/deploy_keys'; +import ProtectedBranchCreate from '~/protected_branches/protected_branch_create'; +import ProtectedBranchEditList from '~/protected_branches/protected_branch_edit_list'; +import DueDateSelectors from '~/due_date_select'; + +export default () => { + new ProtectedTagCreate(); + new ProtectedTagEditList(); + initDeployKeys(); + initSettingsPanels(); + new ProtectedBranchCreate(); // eslint-disable-line no-new + new ProtectedBranchEditList(); // eslint-disable-line no-new + new DueDateSelectors(); +}; diff --git a/app/assets/javascripts/pages/projects/settings/repository/show/index.js b/app/assets/javascripts/pages/projects/settings/repository/show/index.js index 788d86d1192..ffc84dc106b 100644 --- a/app/assets/javascripts/pages/projects/settings/repository/show/index.js +++ b/app/assets/javascripts/pages/projects/settings/repository/show/index.js @@ -1,17 +1,3 @@ -/* eslint-disable no-new */ +import initForm from '../form'; -import ProtectedTagCreate from '~/protected_tags/protected_tag_create'; -import ProtectedTagEditList from '~/protected_tags/protected_tag_edit_list'; -import initSettingsPanels from '~/settings_panels'; -import initDeployKeys from '~/deploy_keys'; -import ProtectedBranchCreate from '~/protected_branches/protected_branch_create'; -import ProtectedBranchEditList from '~/protected_branches/protected_branch_edit_list'; - -document.addEventListener('DOMContentLoaded', () => { - new ProtectedTagCreate(); - new ProtectedTagEditList(); - initDeployKeys(); - initSettingsPanels(); - new ProtectedBranchCreate(); // eslint-disable-line no-new - new ProtectedBranchEditList(); // eslint-disable-line no-new -}); +document.addEventListener('DOMContentLoaded', initForm); diff --git a/app/assets/javascripts/pages/shared/mount_badge_settings.js b/app/assets/javascripts/pages/shared/mount_badge_settings.js new file mode 100644 index 00000000000..1397c0834ff --- /dev/null +++ b/app/assets/javascripts/pages/shared/mount_badge_settings.js @@ -0,0 +1,24 @@ +import Vue from 'vue'; +import BadgeSettings from '~/badges/components/badge_settings.vue'; +import store from '~/badges/store'; + +export default kind => { + const badgeSettingsElement = document.getElementById('badge-settings'); + + store.dispatch('loadBadges', { + kind, + apiEndpointUrl: badgeSettingsElement.dataset.apiEndpointUrl, + docsUrl: badgeSettingsElement.dataset.docsUrl, + }); + + return new Vue({ + el: badgeSettingsElement, + store, + components: { + BadgeSettings, + }, + render(createElement) { + return createElement(BadgeSettings); + }, + }); +}; diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue index d7effb27bff..e99d949801f 100644 --- a/app/assets/javascripts/pipelines/components/graph/action_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue @@ -1,60 +1,72 @@ <script> - import tooltip from '../../../vue_shared/directives/tooltip'; - import icon from '../../../vue_shared/components/icon.vue'; - import { dasherize } from '../../../lib/utils/text_utility'; - /** - * Renders either a cancel, retry or play icon pointing to the given path. - * TODO: Remove UJS from here and use an async request instead. - */ - export default { - components: { - icon, - }, +import $ from 'jquery'; +import tooltip from '../../../vue_shared/directives/tooltip'; +import Icon from '../../../vue_shared/components/icon.vue'; +import { dasherize } from '../../../lib/utils/text_utility'; +import eventHub from '../../event_hub'; +/** + * Renders either a cancel, retry or play icon pointing to the given path. + */ +export default { + components: { + Icon, + }, - directives: { - tooltip, - }, + directives: { + tooltip, + }, - props: { - tooltipText: { - type: String, - required: true, - }, + props: { + tooltipText: { + type: String, + required: true, + }, - link: { - type: String, - required: true, - }, + link: { + type: String, + required: true, + }, - actionMethod: { - type: String, - required: true, - }, + actionIcon: { + type: String, + required: true, + }, - actionIcon: { - type: String, - required: true, - }, + buttonDisabled: { + type: String, + required: false, + default: null, + }, + }, + computed: { + cssClass() { + const actionIconDash = dasherize(this.actionIcon); + return `${actionIconDash} js-icon-${actionIconDash}`; + }, + isDisabled() { + return this.buttonDisabled === this.link; }, + }, - computed: { - cssClass() { - const actionIconDash = dasherize(this.actionIcon); - return `${actionIconDash} js-icon-${actionIconDash}`; - }, + methods: { + onClickAction() { + $(this.$el).tooltip('hide'); + eventHub.$emit('graphAction', this.link); }, - }; + }, +}; </script> <template> - <a + <button + type="button" + @click="onClickAction" v-tooltip - :data-method="actionMethod" :title="tooltipText" - :href="link" - class="ci-action-icon-container ci-action-icon-wrapper" + class="btn btn-blank btn-transparent ci-action-icon-container ci-action-icon-wrapper" :class="cssClass" data-container="body" + :disabled="isDisabled" > <icon :name="actionIcon" /> - </a> + </button> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index ab84711d4a2..ac9ce7e47d6 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -1,54 +1,59 @@ <script> - import loadingIcon from '~/vue_shared/components/loading_icon.vue'; - import stageColumnComponent from './stage_column_component.vue'; +import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; +import StageColumnComponent from './stage_column_component.vue'; - export default { - components: { - stageColumnComponent, - loadingIcon, - }, +export default { + components: { + StageColumnComponent, + LoadingIcon, + }, - props: { - isLoading: { - type: Boolean, - required: true, - }, - pipeline: { - type: Object, - required: true, - }, + props: { + isLoading: { + type: Boolean, + required: true, + }, + pipeline: { + type: Object, + required: true, + }, + actionDisabled: { + type: String, + required: false, + default: null, }, + }, - computed: { - graph() { - return this.pipeline.details && this.pipeline.details.stages; - }, + computed: { + graph() { + return this.pipeline.details && this.pipeline.details.stages; }, + }, - methods: { - capitalizeStageName(name) { - return name.charAt(0).toUpperCase() + name.slice(1); - }, + methods: { + capitalizeStageName(name) { + return name.charAt(0).toUpperCase() + name.slice(1); + }, - isFirstColumn(index) { - return index === 0; - }, + isFirstColumn(index) { + return index === 0; + }, - stageConnectorClass(index, stage) { - let className; + stageConnectorClass(index, stage) { + let className; - // If it's the first stage column and only has one job - if (index === 0 && stage.groups.length === 1) { - className = 'no-margin'; - } else if (index > 0) { - // If it is not the first column - className = 'left-margin'; - } + // If it's the first stage column and only has one job + if (index === 0 && stage.groups.length === 1) { + className = 'no-margin'; + } else if (index > 0) { + // If it is not the first column + className = 'left-margin'; + } - return className; - }, + return className; }, - }; + }, +}; </script> <template> <div class="build-content middle-block js-pipeline-graph"> @@ -70,6 +75,7 @@ :key="stage.name" :stage-connector-class="stageConnectorClass(index, stage)" :is-first-column="isFirstColumn(index)" + :action-disabled="actionDisabled" /> </ul> </div> diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue index d501c465a96..c6e5ae6df41 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue @@ -1,96 +1,102 @@ <script> - import actionComponent from './action_component.vue'; - import dropdownActionComponent from './dropdown_action_component.vue'; - import jobNameComponent from './job_name_component.vue'; - import tooltip from '../../../vue_shared/directives/tooltip'; - - /** - * Renders the badge for the pipeline graph and the job's dropdown. - * - * The following object should be provided as `job`: - * - * { - * "id": 4256, - * "name": "test", - * "status": { - * "icon": "icon_status_success", - * "text": "passed", - * "label": "passed", - * "group": "success", - * "tooltip": "passed", - * "details_path": "/root/ci-mock/builds/4256", - * "action": { - * "icon": "retry", - * "title": "Retry", - * "path": "/root/ci-mock/builds/4256/retry", - * "method": "post" - * } - * } - * } - */ - - export default { - components: { - actionComponent, - dropdownActionComponent, - jobNameComponent, +import ActionComponent from './action_component.vue'; +import DropdownActionComponent from './dropdown_action_component.vue'; +import JobNameComponent from './job_name_component.vue'; +import tooltip from '../../../vue_shared/directives/tooltip'; + +/** + * Renders the badge for the pipeline graph and the job's dropdown. + * + * The following object should be provided as `job`: + * + * { + * "id": 4256, + * "name": "test", + * "status": { + * "icon": "icon_status_success", + * "text": "passed", + * "label": "passed", + * "group": "success", + * "tooltip": "passed", + * "details_path": "/root/ci-mock/builds/4256", + * "action": { + * "icon": "retry", + * "title": "Retry", + * "path": "/root/ci-mock/builds/4256/retry", + * "method": "post" + * } + * } + * } + */ + +export default { + components: { + ActionComponent, + DropdownActionComponent, + JobNameComponent, + }, + + directives: { + tooltip, + }, + props: { + job: { + type: Object, + required: true, }, - directives: { - tooltip, + cssClassJobName: { + type: String, + required: false, + default: '', }, - props: { - job: { - type: Object, - required: true, - }, - - cssClassJobName: { - type: String, - required: false, - default: '', - }, - - isDropdown: { - type: Boolean, - required: false, - default: false, - }, + + isDropdown: { + type: Boolean, + required: false, + default: false, + }, + + actionDisabled: { + type: String, + required: false, + default: null, + }, + }, + + computed: { + status() { + return this.job && this.job.status ? this.job.status : {}; + }, + + tooltipText() { + const textBuilder = []; + + if (this.job.name) { + textBuilder.push(this.job.name); + } + + if (this.job.name && this.status.tooltip) { + textBuilder.push('-'); + } + + if (this.status.tooltip) { + textBuilder.push(`${this.job.status.tooltip}`); + } + + return textBuilder.join(' '); }, - computed: { - status() { - return this.job && this.job.status ? this.job.status : {}; - }, - - tooltipText() { - const textBuilder = []; - - if (this.job.name) { - textBuilder.push(this.job.name); - } - - if (this.job.name && this.status.tooltip) { - textBuilder.push('-'); - } - - if (this.status.tooltip) { - textBuilder.push(`${this.job.status.tooltip}`); - } - - return textBuilder.join(' '); - }, - - /** - * Verifies if the provided job has an action path - * - * @return {Boolean} - */ - hasAction() { - return this.job.status && this.job.status.action && this.job.status.action.path; - }, + /** + * Verifies if the provided job has an action path + * + * @return {Boolean} + */ + hasAction() { + return this.job.status && this.job.status.action && this.job.status.action.path; }, - }; + }, +}; </script> <template> <div class="ci-job-component"> @@ -132,7 +138,7 @@ :tooltip-text="status.action.title" :link="status.action.path" :action-icon="status.action.icon" - :action-method="status.action.method" + :button-disabled="actionDisabled" /> <dropdown-action-component diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue index 7adcf4017b8..f6e6569e15b 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -1,50 +1,55 @@ <script> - import jobComponent from './job_component.vue'; - import dropdownJobComponent from './dropdown_job_component.vue'; +import JobComponent from './job_component.vue'; +import DropdownJobComponent from './dropdown_job_component.vue'; - export default { - components: { - jobComponent, - dropdownJobComponent, +export default { + components: { + JobComponent, + DropdownJobComponent, + }, + props: { + title: { + type: String, + required: true, }, - props: { - title: { - type: String, - required: true, - }, - jobs: { - type: Array, - required: true, - }, + jobs: { + type: Array, + required: true, + }, - isFirstColumn: { - type: Boolean, - required: false, - default: false, - }, + isFirstColumn: { + type: Boolean, + required: false, + default: false, + }, - stageConnectorClass: { - type: String, - required: false, - default: '', - }, + stageConnectorClass: { + type: String, + required: false, + default: '', + }, + actionDisabled: { + type: String, + required: false, + default: null, }, + }, - methods: { - firstJob(list) { - return list[0]; - }, + methods: { + firstJob(list) { + return list[0]; + }, - jobId(job) { - return `ci-badge-${job.name}`; - }, + jobId(job) { + return `ci-badge-${job.name}`; + }, - buildConnnectorClass(index) { - return index === 0 && !this.isFirstColumn ? 'left-connector' : ''; - }, + buildConnnectorClass(index) { + return index === 0 && !this.isFirstColumn ? 'left-connector' : ''; }, - }; + }, +}; </script> <template> <li @@ -69,6 +74,7 @@ v-if="job.size === 1" :job="job" css-class-job-name="build-content" + :action-disabled="actionDisabled" /> <dropdown-job-component diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index 6b26708148c..900eb7855f4 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -25,13 +25,36 @@ export default () => { data() { return { mediator, + actionDisabled: null, }; }, + created() { + eventHub.$on('graphAction', this.postAction); + }, + beforeDestroy() { + eventHub.$off('graphAction', this.postAction); + }, + methods: { + postAction(action) { + this.actionDisabled = action; + + this.mediator.service.postAction(action) + .then(() => { + this.mediator.refreshPipeline(); + this.actionDisabled = null; + }) + .catch(() => { + this.actionDisabled = null; + Flash(__('An error occurred while making the request.')); + }); + }, + }, render(createElement) { return createElement('pipeline-graph', { props: { isLoading: this.mediator.state.isLoading, pipeline: this.mediator.store.state.pipeline, + actionDisabled: this.actionDisabled, }, }); }, diff --git a/app/assets/javascripts/pipelines/pipeline_details_mediator.js b/app/assets/javascripts/pipelines/pipeline_details_mediator.js index 10f238fe73b..621969cd622 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_mediator.js +++ b/app/assets/javascripts/pipelines/pipeline_details_mediator.js @@ -52,8 +52,11 @@ export default class pipelinesMediator { } refreshPipeline() { - this.service.getPipeline() + this.poll.stop(); + + return this.service.getPipeline() .then(response => this.successCallback(response)) - .catch(() => this.errorCallback()); + .catch(() => this.errorCallback()) + .finally(() => this.poll.restart()); } } diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js index 7dd3e9858c6..2da022fde63 100644 --- a/app/assets/javascripts/search_autocomplete.js +++ b/app/assets/javascripts/search_autocomplete.js @@ -233,21 +233,21 @@ export default class SearchAutocomplete { const issueItems = [ { text: 'Issues assigned to me', - url: `${issuesPath}/?assignee_username=${userName}`, + url: `${issuesPath}/?assignee_id=${userId}`, }, { text: "Issues I've created", - url: `${issuesPath}/?author_username=${userName}`, + url: `${issuesPath}/?author_id=${userId}`, }, ]; const mergeRequestItems = [ { text: 'Merge requests assigned to me', - url: `${mrPath}/?assignee_username=${userName}`, + url: `${mrPath}/?assignee_id=${userId}`, }, { text: "Merge requests I've created", - url: `${mrPath}/?author_username=${userName}`, + url: `${mrPath}/?author_id=${userId}`, }, ]; diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index f3b961eb109..520a0b3f424 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -5,6 +5,7 @@ import $ from 'jquery'; import _ from 'underscore'; import axios from './lib/utils/axios_utils'; +import ModalStore from './boards/stores/modal_store'; // TODO: remove eventHub hack after code splitting refactor window.emitSidebarEvent = window.emitSidebarEvent || $.noop; @@ -441,7 +442,7 @@ function UsersSelect(currentUser, els, options = {}) { return; } if ($el.closest('.add-issues-modal').length) { - gl.issueBoards.ModalStore.store.filter[$dropdown.data('fieldName')] = user.id; + ModalStore.store.filter[$dropdown.data('fieldName')] = user.id; } else if (handleClick) { e.preventDefault(); handleClick(user, isMarking); diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue index fb8ccea91c7..4155e1bab9c 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue @@ -1,17 +1,24 @@ <script> import { viewerInformationForPath } from './lib/viewer_utils'; import MarkdownViewer from './viewers/markdown_viewer.vue'; +import ImageViewer from './viewers/image_viewer.vue'; +import DownloadViewer from './viewers/download_viewer.vue'; export default { props: { content: { type: String, - required: true, + default: '', }, path: { type: String, required: true, }, + fileSize: { + type: Number, + required: false, + default: 0, + }, projectPath: { type: String, required: false, @@ -20,12 +27,18 @@ export default { }, computed: { viewer() { + if (!this.path) return null; + const previewInfo = viewerInformationForPath(this.path); + if (!previewInfo) return DownloadViewer; + switch (previewInfo.id) { case 'markdown': return MarkdownViewer; + case 'image': + return ImageViewer; default: - return null; + return DownloadViewer; } }, }, @@ -36,6 +49,8 @@ export default { <div class="preview-container"> <component :is="viewer" + :path="path" + :file-size="fileSize" :project-path="projectPath" :content="content" /> diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js b/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js index 4f2e1e47dd1..f01a51da0b3 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js +++ b/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js @@ -1,4 +1,7 @@ const viewers = { + image: { + id: 'image', + }, markdown: { id: 'markdown', previewTitle: 'Preview Markdown', @@ -7,6 +10,12 @@ const viewers = { const fileNameViewers = {}; const fileExtensionViewers = { + jpg: 'image', + jpeg: 'image', + gif: 'image', + png: 'image', + bmp: 'image', + ico: 'image', md: 'markdown', markdown: 'markdown', }; diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue new file mode 100644 index 00000000000..395a71acccf --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue @@ -0,0 +1,52 @@ +<script> +import Icon from '../../icon.vue'; +import { numberToHumanSize } from '../../../../lib/utils/number_utils'; + +export default { + components: { + Icon, + }, + props: { + path: { + type: String, + required: true, + }, + fileSize: { + type: Number, + required: false, + default: 0, + }, + }, + computed: { + fileSizeReadable() { + return numberToHumanSize(this.fileSize); + }, + fileName() { + return this.path.split('/').pop(); + }, + }, +}; +</script> + +<template> + <div class="file-container"> + <div class="file-content"> + <p class="prepend-top-10 file-info"> + {{ fileName }} ({{ fileSizeReadable }}) + </p> + <a + :href="path" + class="btn btn-default" + rel="nofollow" + download + target="_blank"> + <icon + name="download" + css-classes="pull-left append-right-8" + :size="16" + /> + {{ __('Download') }} + </a> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue new file mode 100644 index 00000000000..a5999f909ca --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue @@ -0,0 +1,68 @@ +<script> +import { numberToHumanSize } from '../../../../lib/utils/number_utils'; + +export default { + props: { + path: { + type: String, + required: true, + }, + fileSize: { + type: Number, + required: false, + default: 0, + }, + }, + data() { + return { + width: 0, + height: 0, + isZoomable: false, + isZoomed: false, + }; + }, + computed: { + fileSizeReadable() { + return numberToHumanSize(this.fileSize); + }, + }, + methods: { + onImgLoad() { + const contentImg = this.$refs.contentImg; + this.isZoomable = + contentImg.naturalWidth > contentImg.width || contentImg.naturalHeight > contentImg.height; + + this.width = contentImg.naturalWidth; + this.height = contentImg.naturalHeight; + }, + onImgClick() { + if (this.isZoomable) this.isZoomed = !this.isZoomed; + }, + }, +}; +</script> + +<template> + <div class="file-container"> + <div class="file-content image_file"> + <img + ref="contentImg" + :class="{ 'isZoomable': isZoomable, 'isZoomed': isZoomed }" + :src="path" + :alt="path" + @load="onImgLoad" + @click="onImgClick"/> + <p class="file-info prepend-top-10"> + <template v-if="fileSize>0"> + {{ fileSizeReadable }} + </template> + <template v-if="fileSize>0 && width && height"> + - + </template> + <template v-if="width && height"> + {{ width }} x {{ height }} + </template> + </p> + </div> + </div> +</template> diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss index df1cafc9f8e..62a0fba3da3 100644 --- a/app/assets/stylesheets/framework/images.scss +++ b/app/assets/stylesheets/framework/images.scss @@ -20,7 +20,7 @@ width: 100%; } - $image-widths: 80 250 306 394 430; + $image-widths: 80 130 250 306 394 430; @each $width in $image-widths { &.svg-#{$width} { img, diff --git a/app/assets/stylesheets/framework/responsive_tables.scss b/app/assets/stylesheets/framework/responsive_tables.scss index 7829d722560..34fccf6f0a4 100644 --- a/app/assets/stylesheets/framework/responsive_tables.scss +++ b/app/assets/stylesheets/framework/responsive_tables.scss @@ -39,7 +39,7 @@ .table-section { white-space: nowrap; - $section-widths: 10 15 20 25 30 40 100; + $section-widths: 10 15 20 25 30 40 50 100; @each $width in $section-widths { &.section-#{$width} { flex: 0 0 #{$width + '%'}; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index a81904d5338..8ee1bb03d55 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -767,3 +767,8 @@ $border-color-settings: #e1e1e1; Modals */ $modal-body-height: 134px; + +/* +Prometheus +*/ +$prometheus-table-row-highlight-color: $theme-gray-100; diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index b487f6278c2..86cdda0359e 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -107,7 +107,6 @@ } } - .commits-compare-switch { float: left; margin-right: 9px; @@ -179,7 +178,7 @@ .commit-detail { display: flex; justify-content: space-between; - align-items: flex-start; + align-items: center; flex-grow: 1; .merge-request-branches & { @@ -200,37 +199,63 @@ } .ci-status-link { - display: inline-block; - position: relative; - top: 2px; + display: inline-flex; } - .btn-clipboard, - .btn-transparent { - padding-left: 0; - padding-right: 0; + > .ci-status-link, + > .btn, + > .commit-sha-group { + margin-left: $gl-padding-8; } +} +.commit-sha-group { + display: inline-flex; + + .label, .btn { - &:not(:first-child) { - margin-left: $gl-padding; - } + padding: $gl-vert-padding $gl-btn-padding; + border: 1px $border-color solid; + font-size: $gl-font-size; + line-height: $line-height-base; + border-radius: 0; + display: flex; + align-items: center; + } + + .label-monospace { + @extend .monospace; + user-select: text; + color: $gl-text-color; + background-color: $gray-light; } - .commit-sha { - font-size: 14px; - font-weight: $gl-font-weight-bold; + .btn svg { + top: auto; + fill: $gl-text-color-secondary; } - .ci-status-icon { - position: relative; - top: 2px; + .fa-clipboard { + color: $gl-text-color-secondary; + } + + :first-child { + border-bottom-left-radius: $border-radius-default; + border-top-left-radius: $border-radius-default; + } + + :not(:first-child) { + border-left: 0; + } + + :last-child { + border-bottom-right-radius: $border-radius-default; + border-top-right-radius: $border-radius-default; } } .commit, .generic_commit_status { - a, button { color: $gl-text-color; @@ -303,10 +328,8 @@ } } - .gpg-status-box { padding: 2px 10px; - margin-right: $gl-padding; &:empty { display: none; diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 58700661142..3a300086fa3 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -273,21 +273,6 @@ line-height: 1.2; } - table { - border-collapse: collapse; - padding: 0; - margin: 0; - } - - td { - vertical-align: middle; - - + td { - padding-left: 5px; - vertical-align: top; - } - } - .deploy-meta-content { border-bottom: 1px solid $white-dark; @@ -323,6 +308,26 @@ } } +.prometheus-table { + border-collapse: collapse; + padding: 0; + margin: 0; + + td { + vertical-align: middle; + + + td { + padding-left: 5px; + vertical-align: top; + } + } + + .legend-metric-title { + font-size: 12px; + vertical-align: middle; + } +} + .prometheus-svg-container { position: relative; height: 0; @@ -330,8 +335,7 @@ padding: 0; padding-bottom: 100%; - .text-metric-usage, - .legend-metric-title { + .text-metric-usage { fill: $black; font-weight: $gl-font-weight-normal; font-size: 12px; @@ -374,10 +378,6 @@ } } - .text-metric-title { - font-size: 12px; - } - .y-label-text, .x-label-text { fill: $gray-darkest; @@ -414,3 +414,7 @@ } } } + +.prometheus-table-row-highlight { + background-color: $prometheus-table-row-highlight-color; +} diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index ce2f1482456..8d5eb2e8c5a 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -495,17 +495,17 @@ svg { fill: $gl-text-color-secondary; position: relative; - left: 5px; - top: 2px; - width: 18px; - height: 18px; + left: 1px; + top: -1px; + width: 16px; + height: 16px; } &.play { svg { - width: #{$ci-action-icon-size - 8}; - height: #{$ci-action-icon-size - 8}; - left: 8px; + width: 16px; + height: 16px; + left: 3px; } } } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 9a770d77685..790e91e4431 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -1143,3 +1143,11 @@ pre.light-well { white-space: pre-wrap; } } + +.project-badge { + opacity: 0.9; + + &:hover { + opacity: 1; + } +} diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index 8cc5c8fc877..a414deb8921 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -312,6 +312,45 @@ height: 100%; overflow: auto; + .file-container { + background-color: $gray-darker; + display: flex; + height: 100%; + align-items: center; + justify-content: center; + + text-align: center; + + .file-content { + padding: $gl-padding; + max-width: 100%; + max-height: 100%; + + img { + max-width: 90%; + max-height: 90%; + } + + .isZoomable { + cursor: pointer; + cursor: zoom-in; + + &.isZoomed { + cursor: pointer; + cursor: zoom-out; + max-width: none; + max-height: none; + margin-right: $gl-padding; + } + } + } + + .file-info { + font-size: $label-font-size; + color: $diff-image-info-color; + } + } + .md-previewer { padding: $gl-padding; } diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index a6ca8ed5016..c410049bc0b 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -284,3 +284,23 @@ .deprecated-service { cursor: default; } + +.personal-access-tokens-never-expires-label { + color: $note-disabled-comment-color; +} + +.created-deploy-token-container { + .deploy-token-field { + width: 90%; + display: inline; + } + + .btn-clipboard { + margin-left: 5px; + } + + .deploy-token-help-block { + display: block; + margin-bottom: 0; + } +} diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb index 2753f83c3cf..2fdf346ef44 100644 --- a/app/controllers/concerns/authenticates_with_two_factor.rb +++ b/app/controllers/concerns/authenticates_with_two_factor.rb @@ -10,7 +10,7 @@ module AuthenticatesWithTwoFactor # This action comes from DeviseController, but because we call `sign_in` # manually, not skipping this action would cause a "You are already signed # in." error message to be shown upon successful login. - skip_before_action :require_no_authentication, only: [:create] + skip_before_action :require_no_authentication, only: [:create], raise: false end # Store the user's ID in the session for later retrieval and render the diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 280ed93faf8..68d328fa797 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -2,9 +2,17 @@ class DashboardController < Dashboard::ApplicationController include IssuesAction include MergeRequestsAction + FILTER_PARAMS = [ + :author_id, + :assignee_id, + :milestone_title, + :label_name + ].freeze + before_action :event_filter, only: :activity before_action :projects, only: [:issues, :merge_requests] before_action :set_show_full_reference, only: [:issues, :merge_requests] + before_action :check_filters_presence!, only: [:issues, :merge_requests] respond_to :html @@ -39,4 +47,15 @@ class DashboardController < Dashboard::ApplicationController def set_show_full_reference @show_full_reference = true end + + def check_filters_presence! + @no_filters_set = FILTER_PARAMS.none? { |k| params.key?(k) } + + return unless @no_filters_set + + respond_to do |format| + format.html + format.atom { head :bad_request } + end + end end diff --git a/app/controllers/groups/settings/badges_controller.rb b/app/controllers/groups/settings/badges_controller.rb new file mode 100644 index 00000000000..edb334a3d88 --- /dev/null +++ b/app/controllers/groups/settings/badges_controller.rb @@ -0,0 +1,13 @@ +module Groups + module Settings + class BadgesController < Groups::ApplicationController + include GrapeRouteHelpers::NamedRouteMatcher + + before_action :authorize_admin_group! + + def index + @badge_api_endpoint = api_v4_groups_badges_path(id: @group.id) + end + end + end +end diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 7d6fe6a0232..67057b5b126 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -25,8 +25,7 @@ class JwtController < ApplicationController authenticate_with_http_basic do |login, password| @authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip) - if @authentication_result.failed? || - (@authentication_result.actor.present? && !@authentication_result.actor.is_a?(User)) + if @authentication_result.failed? render_unauthorized end end diff --git a/app/controllers/projects/deploy_tokens_controller.rb b/app/controllers/projects/deploy_tokens_controller.rb new file mode 100644 index 00000000000..2f91b8f36de --- /dev/null +++ b/app/controllers/projects/deploy_tokens_controller.rb @@ -0,0 +1,10 @@ +class Projects::DeployTokensController < Projects::ApplicationController + before_action :authorize_admin_project! + + def revoke + @token = @project.deploy_tokens.find(params[:id]) + @token.revoke! + + redirect_to project_settings_repository_path(project) + end +end diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb index d5af0341d18..937b0e39cbd 100644 --- a/app/controllers/projects/repositories_controller.rb +++ b/app/controllers/projects/repositories_controller.rb @@ -1,6 +1,9 @@ class Projects::RepositoriesController < Projects::ApplicationController + include ExtractsPath + # Authorize before_action :require_non_empty_project, except: :create + before_action :assign_archive_vars, only: :archive before_action :authorize_download_code! before_action :authorize_admin_project!, only: :create @@ -11,9 +14,26 @@ class Projects::RepositoriesController < Projects::ApplicationController end def archive - send_git_archive @repository, ref: params[:ref], format: params[:format] + append_sha = params[:append_sha] + + if @ref + shortname = "#{@project.path}-#{@ref.tr('/', '-')}" + append_sha = false if @filename == shortname + end + + send_git_archive @repository, ref: @ref, format: params[:format], append_sha: append_sha rescue => ex logger.error("#{self.class.name}: #{ex}") return git_not_found! end + + def assign_archive_vars + @id = params[:id] + + return unless @id + + @ref, @filename = extract_ref(@id) + rescue InvalidPathError + render_404 + end end diff --git a/app/controllers/projects/settings/badges_controller.rb b/app/controllers/projects/settings/badges_controller.rb new file mode 100644 index 00000000000..f7b70dd4b7b --- /dev/null +++ b/app/controllers/projects/settings/badges_controller.rb @@ -0,0 +1,13 @@ +module Projects + module Settings + class BadgesController < Projects::ApplicationController + include GrapeRouteHelpers::NamedRouteMatcher + + before_action :authorize_admin_project! + + def index + @badge_api_endpoint = api_v4_projects_badges_path(id: @project.id) + end + end + end +end diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb index dd9e4a2af3e..f17056f13e0 100644 --- a/app/controllers/projects/settings/repository_controller.rb +++ b/app/controllers/projects/settings/repository_controller.rb @@ -4,13 +4,31 @@ module Projects before_action :authorize_admin_project! def show - @deploy_keys = DeployKeysPresenter.new(@project, current_user: current_user) + render_show + end - define_protected_refs + def create_deploy_token + @new_deploy_token = DeployTokens::CreateService.new(@project, current_user, deploy_token_params).execute + + if @new_deploy_token.persisted? + flash.now[:notice] = s_('DeployTokens|Your new project deploy token has been created.') + end + + render_show end private + def render_show + @deploy_keys = DeployKeysPresenter.new(@project, current_user: current_user) + @deploy_tokens = @project.deploy_tokens.active + + define_deploy_token + define_protected_refs + + render 'show' + end + def define_protected_refs @protected_branches = @project.protected_branches.order(:name).page(params[:page]) @protected_tags = @project.protected_tags.order(:name).page(params[:page]) @@ -51,6 +69,14 @@ module Projects gon.push(protectable_branches_for_dropdown) gon.push(access_levels_options) end + + def define_deploy_token + @new_deploy_token ||= DeployToken.new + end + + def deploy_token_params + params.require(:deploy_token).permit(:name, :expires_at, :read_repository, :read_registry) + end end end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 86ec500ceb3..228c8d2e8f9 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -228,9 +228,7 @@ module ApplicationHelper scope: params[:scope], milestone_title: params[:milestone_title], assignee_id: params[:assignee_id], - assignee_username: params[:assignee_username], author_id: params[:author_id], - author_username: params[:author_username], search: params[:search], label_name: params[:label_name] } diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 0333c29e2fd..7cc56de24e4 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -93,25 +93,18 @@ module CommitsHelper return unless current_controller?(:commits) if @path.blank? - return link_to( - _("Browse Files"), - project_tree_path(project, commit), - class: "btn btn-default" - ) + url = project_tree_path(project, commit) + tooltip = _("Browse Files") elsif @repo.blob_at(commit.id, @path) - return link_to( - _("Browse File"), - project_blob_path(project, - tree_join(commit.id, @path)), - class: "btn btn-default" - ) + url = project_blob_path(project, tree_join(commit.id, @path)) + tooltip = _("Browse File") elsif @path.present? - return link_to( - _("Browse Directory"), - project_tree_path(project, - tree_join(commit.id, @path)), - class: "btn btn-default" - ) + url = project_tree_path(project, tree_join(commit.id, @path)) + tooltip = _("Browse Directory") + end + + link_to url, class: "btn btn-default has-tooltip", title: tooltip, data: { container: "body" } do + sprite_icon('folder-open') end end diff --git a/app/helpers/deploy_tokens_helper.rb b/app/helpers/deploy_tokens_helper.rb new file mode 100644 index 00000000000..bd921322476 --- /dev/null +++ b/app/helpers/deploy_tokens_helper.rb @@ -0,0 +1,12 @@ +module DeployTokensHelper + def expand_deploy_tokens_section?(deploy_token) + deploy_token.persisted? || + deploy_token.errors.present? || + Rails.env.test? + end + + def container_registry_enabled?(project) + Gitlab.config.registry.enabled && + can?(current_user, :read_container_image, project) + end +end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 16eceb3f48f..95fea2f18d1 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -1,6 +1,6 @@ module GroupsHelper def group_nav_link_paths - %w[groups#projects groups#edit ci_cd#show ldap_group_links#index hooks#index audit_events#index pipeline_quota#index] + %w[groups#projects groups#edit badges#index ci_cd#show ldap_group_links#index hooks#index audit_events#index pipeline_quota#index] end def group_sidebar_links diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 6d6b840f485..06c3e569c84 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -159,16 +159,18 @@ module IssuablesHelper label_names.join(', ') end - def issuables_state_counter_text(issuable_type, state) + def issuables_state_counter_text(issuable_type, state, display_count) titles = { opened: "Open" } state_title = titles[state] || state.to_s.humanize - count = issuables_count_for_state(issuable_type, state) - html = content_tag(:span, state_title) - html << " " << content_tag(:span, number_with_delimiter(count), class: 'badge') + + if display_count + count = issuables_count_for_state(issuable_type, state) + html << " " << content_tag(:span, number_with_delimiter(count), class: 'badge') + end html.html_safe end @@ -191,24 +193,10 @@ module IssuablesHelper end end - def issuable_filter_params - [ - :search, - :author_id, - :assignee_id, - :milestone_title, - :label_name - ] - end - def issuable_reference(issuable) @show_full_reference ? issuable.to_reference(full: true) : issuable.to_reference(@group || @project) end - def issuable_filter_present? - issuable_filter_params.any? { |k| params.key?(k) } - end - def issuable_initial_data(issuable) data = { endpoint: issuable_path(issuable), diff --git a/app/helpers/workhorse_helper.rb b/app/helpers/workhorse_helper.rb index 88f374be1e5..9f78b80c71d 100644 --- a/app/helpers/workhorse_helper.rb +++ b/app/helpers/workhorse_helper.rb @@ -24,8 +24,8 @@ module WorkhorseHelper end # Archive a Git repository and send it through Workhorse - def send_git_archive(repository, ref:, format:) - headers.store(*Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format)) + def send_git_archive(repository, **kwargs) + headers.store(*Gitlab::Workhorse.send_git_archive(repository, **kwargs)) head :ok end diff --git a/app/models/commit.rb b/app/models/commit.rb index 3f7f36e83c0..de860df4b9c 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -30,6 +30,8 @@ class Commit MIN_SHA_LENGTH = Gitlab::Git::Commit::MIN_SHA_LENGTH COMMIT_SHA_PATTERN = /\h{#{MIN_SHA_LENGTH},40}/.freeze + # Used by GFM to match and present link extensions on node texts and hrefs. + LINK_EXTENSION_PATTERN = /(patch)/.freeze def banzai_render_context(field) pipeline = field == :description ? :commit_description : :single_line @@ -143,7 +145,8 @@ class Commit end def self.link_reference_pattern - @link_reference_pattern ||= super("commit", /(?<commit>#{COMMIT_SHA_PATTERN})/) + @link_reference_pattern ||= + super("commit", /(?<commit>#{COMMIT_SHA_PATTERN})?(\.(?<extension>#{LINK_EXTENSION_PATTERN}))?/) end def to_reference(from = nil, full: false) diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb new file mode 100644 index 00000000000..fe726b156d4 --- /dev/null +++ b/app/models/deploy_token.rb @@ -0,0 +1,61 @@ +class DeployToken < ActiveRecord::Base + include Expirable + include TokenAuthenticatable + add_authentication_token_field :token + + AVAILABLE_SCOPES = %i(read_repository read_registry).freeze + + default_value_for(:expires_at) { Forever.date } + + has_many :project_deploy_tokens, inverse_of: :deploy_token + has_many :projects, through: :project_deploy_tokens + + validate :ensure_at_least_one_scope + before_save :ensure_token + + accepts_nested_attributes_for :project_deploy_tokens + + scope :active, -> { where("revoked = false AND expires_at >= NOW()") } + + def revoke! + update!(revoked: true) + end + + def active? + !revoked + end + + def scopes + AVAILABLE_SCOPES.select { |token_scope| read_attribute(token_scope) } + end + + def username + "gitlab+deploy-token-#{id}" + end + + def has_access_to?(requested_project) + project == requested_project + end + + # This is temporal. Currently we limit DeployToken + # to a single project, later we're going to extend + # that to be for multiple projects and namespaces. + def project + projects.first + end + + def expires_at + expires_at = read_attribute(:expires_at) + expires_at != Forever.date ? expires_at : nil + end + + def expires_at=(value) + write_attribute(:expires_at, value.presence || Forever.date) + end + + private + + def ensure_at_least_one_scope + errors.add(:base, "Scopes can't be blank") unless read_repository || read_registry + end +end diff --git a/app/models/environment.rb b/app/models/environment.rb index 9517723d9d9..fddb269af4b 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -224,7 +224,7 @@ class Environment < ActiveRecord::Base end def deployment_platform - project.deployment_platform(environment: self) + project.deployment_platform(environment: self.name) end private diff --git a/app/models/group.rb b/app/models/group.rb index 3cfe21ac93b..8ff781059cc 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -286,6 +286,10 @@ class Group < Namespace false end + def refresh_project_authorizations + refresh_members_authorized_projects(blocking: false) + end + private def update_two_factor_requirement diff --git a/app/models/namespace.rb b/app/models/namespace.rb index e350b675639..2b63aa33222 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -252,6 +252,10 @@ class Namespace < ActiveRecord::Base [] end + def refresh_project_authorizations + owner.refresh_authorized_projects + end + private def path_or_parent_changed? diff --git a/app/models/project.rb b/app/models/project.rb index 1b29cbf28d2..3f805dd1fc9 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -222,6 +222,8 @@ class Project < ActiveRecord::Base has_many :environments has_many :deployments has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule' + has_many :project_deploy_tokens + has_many :deploy_tokens, through: :project_deploy_tokens has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner' @@ -1472,7 +1474,9 @@ class Project < ActiveRecord::Base end def rename_repo_notify! - send_move_instructions(full_path_was) + # When we import a project overwriting the original project, there + # is a move operation. In that case we don't want to send the instructions. + send_move_instructions(full_path_was) unless started? expires_full_path_cache self.old_path_with_namespace = full_path_was diff --git a/app/models/project_deploy_token.rb b/app/models/project_deploy_token.rb new file mode 100644 index 00000000000..ab4482f0c0b --- /dev/null +++ b/app/models/project_deploy_token.rb @@ -0,0 +1,8 @@ +class ProjectDeployToken < ActiveRecord::Base + belongs_to :project + belongs_to :deploy_token, inverse_of: :project_deploy_tokens + + validates :deploy_token, presence: true + validates :project, presence: true + validates :deploy_token_id, uniqueness: { scope: [:project_id] } +end diff --git a/app/models/service.rb b/app/models/service.rb index e9b6f005aec..f7e3f7590ad 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -206,7 +206,11 @@ class Service < ActiveRecord::Base args.each do |arg| class_eval %{ def #{arg}? - ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(#{arg}) + if Gitlab.rails5? + !ActiveModel::Type::Boolean::FALSE_VALUES.include?(#{arg}) + else + ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(#{arg}) + end end } end diff --git a/app/models/user.rb b/app/models/user.rb index ce56b39b1c8..42bb27d4753 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -700,10 +700,6 @@ class User < ActiveRecord::Base projects_limit - personal_projects_count end - def personal_projects_count - @personal_projects_count ||= personal_projects.count - end - def recent_push(project = nil) service = Users::LastPushEventService.new(self) @@ -997,7 +993,7 @@ class User < ActiveRecord::Base def ci_authorized_runners @ci_authorized_runners ||= begin runner_ids = Ci::RunnerProject - .where("ci_runner_projects.project_id IN (#{ci_projects_union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection + .where(project: authorized_projects(Gitlab::Access::MASTER)) .select(:runner_id) Ci::Runner.specific.where(id: runner_ids) end @@ -1046,6 +1042,12 @@ class User < ActiveRecord::Base end end + def personal_projects_count(force: false) + Rails.cache.fetch(['users', id, 'personal_projects_count'], force: force, expires_in: 24.hours, raw: true) do + personal_projects.count + end.to_i + end + def update_todos_count_cache todos_done_count(force: true) todos_pending_count(force: true) @@ -1056,6 +1058,7 @@ class User < ActiveRecord::Base invalidate_merge_request_cache_counts invalidate_todos_done_count invalidate_todos_pending_count + invalidate_personal_projects_count end def invalidate_issue_cache_counts @@ -1074,6 +1077,10 @@ class User < ActiveRecord::Base Rails.cache.delete(['users', id, 'todos_pending_count']) end + def invalidate_personal_projects_count + Rails.cache.delete(['users', id, 'personal_projects_count']) + end + # This is copied from Devise::Models::Lockable#valid_for_authentication?, as our auth # flow means we don't call that automatically (and can't conveniently do so). # @@ -1197,15 +1204,6 @@ class User < ActiveRecord::Base ], remove_duplicates: false) end - def ci_projects_union - scope = { access_level: [Gitlab::Access::MASTER, Gitlab::Access::OWNER] } - groups = groups_projects.where(members: scope) - other = projects.where(members: scope) - - Gitlab::SQL::Union.new([personal_projects.select(:id), groups.select(:id), - other.select(:id)]) - end - # Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration def send_devise_notification(notification, *args) return true unless can?(:receive_notifications) diff --git a/app/policies/deploy_token_policy.rb b/app/policies/deploy_token_policy.rb new file mode 100644 index 00000000000..7aa9106e8b1 --- /dev/null +++ b/app/policies/deploy_token_policy.rb @@ -0,0 +1,11 @@ +class DeployTokenPolicy < BasePolicy + with_options scope: :subject, score: 0 + condition(:master) { @subject.project.team.master?(@user) } + + rule { anonymous }.prevent_all + + rule { master }.policy do + enable :create_deploy_token + enable :update_deploy_token + end +end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index b1ed034cd00..21bb0934dee 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -143,7 +143,7 @@ class ProjectPolicy < BasePolicy end # These abilities are not allowed to admins that are not members of the project, - # that's why they are defined separatly. + # that's why they are defined separately. rule { guest & can?(:download_code) }.enable :build_download_code rule { guest & can?(:read_container_image) }.enable :build_read_container_image diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index 2b77f6be72a..8f050072f74 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -109,7 +109,7 @@ module Auth case requested_action when 'pull' - build_can_pull?(requested_project) || user_can_pull?(requested_project) + build_can_pull?(requested_project) || user_can_pull?(requested_project) || deploy_token_can_pull?(requested_project) when 'push' build_can_push?(requested_project) || user_can_push?(requested_project) when '*' @@ -123,22 +123,33 @@ module Auth Gitlab.config.registry end + def can_user?(ability, project) + user = current_user.is_a?(User) ? current_user : nil + can?(user, ability, project) + end + def build_can_pull?(requested_project) # Build can: # 1. pull from its own project (for ex. a build) # 2. read images from dependent projects if creator of build is a team member has_authentication_ability?(:build_read_container_image) && - (requested_project == project || can?(current_user, :build_read_container_image, requested_project)) + (requested_project == project || can_user?(:build_read_container_image, requested_project)) end def user_can_admin?(requested_project) has_authentication_ability?(:admin_container_image) && - can?(current_user, :admin_container_image, requested_project) + can_user?(:admin_container_image, requested_project) end def user_can_pull?(requested_project) has_authentication_ability?(:read_container_image) && - can?(current_user, :read_container_image, requested_project) + can_user?(:read_container_image, requested_project) + end + + def deploy_token_can_pull?(requested_project) + has_authentication_ability?(:read_container_image) && + current_user.is_a?(DeployToken) && + current_user.has_access_to?(requested_project) end ## @@ -154,7 +165,7 @@ module Auth def user_can_push?(requested_project) has_authentication_ability?(:create_container_image) && - can?(current_user, :create_container_image, requested_project) + can_user?(:create_container_image, requested_project) end def error(code, status:, message: '') diff --git a/app/services/deploy_tokens/create_service.rb b/app/services/deploy_tokens/create_service.rb new file mode 100644 index 00000000000..52f545947af --- /dev/null +++ b/app/services/deploy_tokens/create_service.rb @@ -0,0 +1,7 @@ +module DeployTokens + class CreateService < BaseService + def execute + @project.deploy_tokens.create(params) + end + end +end diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index e4be953e810..b82d9c64296 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -54,8 +54,7 @@ module NotificationRecipientService users = users.includes(:notification_settings) end - users = Array(users) - users.compact! + users = Array(users).compact recipients.concat(users.map { |u| make_recipient(u, type, reason) }) end diff --git a/app/services/projects/base_move_relations_service.rb b/app/services/projects/base_move_relations_service.rb new file mode 100644 index 00000000000..e8fd3ef57e5 --- /dev/null +++ b/app/services/projects/base_move_relations_service.rb @@ -0,0 +1,22 @@ +module Projects + class BaseMoveRelationsService < BaseService + attr_reader :source_project + def execute(source_project, remove_remaining_elements: true) + return if source_project.blank? + + @source_project = source_project + + true + end + + private + + def prepare_relation(relation, id_param = :id) + if Gitlab::Database.postgresql? + relation + else + relation.model.where("#{id_param}": relation.pluck(id_param)) + end + end + end +end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 633e2c8236c..d361d070993 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -96,6 +96,8 @@ module Projects system_hook_service.execute_hooks_for(@project, :create) setup_authorizations + + current_user.invalidate_personal_projects_count end # Refresh the current user's authorizations inline (so they can access the diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 4b8f955ae69..aa14206db3b 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -34,6 +34,8 @@ module Projects system_hook_service.execute_hooks_for(project, :destroy) log_info("Project \"#{project.full_path}\" was removed") + current_user.invalidate_personal_projects_count + true rescue => error attempt_rollback(project, error.message) @@ -44,6 +46,20 @@ module Projects raise end + def attempt_repositories_rollback + return unless @project + + flush_caches(@project) + + unless mv_repository(removal_path(repo_path), repo_path) + raise_error('Failed to restore project repository. Please contact the administrator.') + end + + unless mv_repository(removal_path(wiki_path), wiki_path) + raise_error('Failed to restore wiki repository. Please contact the administrator.') + end + end + private def repo_path @@ -68,12 +84,9 @@ module Projects # Skip repository removal. We use this flag when remove user or group return true if params[:skip_repo] == true - # There is a possibility project does not have repository or wiki - return true unless gitlab_shell.exists?(project.repository_storage_path, path + '.git') - new_path = removal_path(path) - if gitlab_shell.mv_repository(project.repository_storage_path, path, new_path) + if mv_repository(path, new_path) log_info("Repository \"#{path}\" moved to \"#{new_path}\"") project.run_after_commit do @@ -85,6 +98,13 @@ module Projects end end + def mv_repository(from_path, to_path) + # There is a possibility project does not have repository or wiki + return true unless gitlab_shell.exists?(project.repository_storage_path, from_path + '.git') + + gitlab_shell.mv_repository(project.repository_storage_path, from_path, to_path) + end + def attempt_rollback(project, message) return unless project diff --git a/app/services/projects/gitlab_projects_import_service.rb b/app/services/projects/gitlab_projects_import_service.rb index fb4afb85588..a16268f4fd2 100644 --- a/app/services/projects/gitlab_projects_import_service.rb +++ b/app/services/projects/gitlab_projects_import_service.rb @@ -15,9 +15,18 @@ module Projects file = params.delete(:file) FileUtils.copy_entry(file.path, import_upload_path) + @overwrite = params.delete(:overwrite) + data = {} + data[:override_params] = @override_params if @override_params + + if overwrite_project? + data[:original_path] = params[:path] + params[:path] += "-#{tmp_filename}" + end + params[:import_type] = 'gitlab_project' params[:import_source] = import_upload_path - params[:import_data] = { data: { override_params: @override_params } } if @override_params + params[:import_data] = { data: data } if data.present? ::Projects::CreateService.new(current_user, params).execute end @@ -31,5 +40,17 @@ module Projects def tmp_filename SecureRandom.hex end + + def overwrite_project? + @overwrite && project_with_same_full_path? + end + + def project_with_same_full_path? + Project.find_by_full_path("#{current_namespace.full_path}/#{params[:path]}").present? + end + + def current_namespace + @current_namespace ||= Namespace.find_by(id: params[:namespace_id]) + end end end diff --git a/app/services/projects/move_access_service.rb b/app/services/projects/move_access_service.rb new file mode 100644 index 00000000000..3af3a22d486 --- /dev/null +++ b/app/services/projects/move_access_service.rb @@ -0,0 +1,25 @@ +module Projects + class MoveAccessService < BaseMoveRelationsService + def execute(source_project, remove_remaining_elements: true) + return unless super + + @project.with_transaction_returning_status do + if @project.namespace != source_project.namespace + @project.run_after_commit do + source_project.namespace.refresh_project_authorizations + self.namespace.refresh_project_authorizations + end + end + + ::Projects::MoveProjectMembersService.new(@project, @current_user) + .execute(source_project, remove_remaining_elements: remove_remaining_elements) + ::Projects::MoveProjectGroupLinksService.new(@project, @current_user) + .execute(source_project, remove_remaining_elements: remove_remaining_elements) + ::Projects::MoveProjectAuthorizationsService.new(@project, @current_user) + .execute(source_project, remove_remaining_elements: remove_remaining_elements) + + success + end + end + end +end diff --git a/app/services/projects/move_deploy_keys_projects_service.rb b/app/services/projects/move_deploy_keys_projects_service.rb new file mode 100644 index 00000000000..dde420655b0 --- /dev/null +++ b/app/services/projects/move_deploy_keys_projects_service.rb @@ -0,0 +1,31 @@ +module Projects + class MoveDeployKeysProjectsService < BaseMoveRelationsService + def execute(source_project, remove_remaining_elements: true) + return unless super + + Project.transaction(requires_new: true) do + move_deploy_keys_projects + remove_remaining_deploy_keys_projects if remove_remaining_elements + + success + end + end + + private + + def move_deploy_keys_projects + prepare_relation(non_existent_deploy_keys_projects) + .update_all(project_id: @project.id) + end + + def non_existent_deploy_keys_projects + source_project.deploy_keys_projects + .joins(:deploy_key) + .where.not(keys: { fingerprint: @project.deploy_keys.select(:fingerprint) }) + end + + def remove_remaining_deploy_keys_projects + source_project.deploy_keys_projects.destroy_all + end + end +end diff --git a/app/services/projects/move_forks_service.rb b/app/services/projects/move_forks_service.rb new file mode 100644 index 00000000000..d2901ea1457 --- /dev/null +++ b/app/services/projects/move_forks_service.rb @@ -0,0 +1,42 @@ +module Projects + class MoveForksService < BaseMoveRelationsService + def execute(source_project, remove_remaining_elements: true) + return unless super && source_project.fork_network + + Project.transaction(requires_new: true) do + move_forked_project_links + move_fork_network_members + update_root_project + refresh_forks_count + + success + end + end + + private + + def move_forked_project_links + # Update ancestor + ForkedProjectLink.where(forked_to_project: source_project) + .update_all(forked_to_project_id: @project.id) + + # Update the descendants + ForkedProjectLink.where(forked_from_project: source_project) + .update_all(forked_from_project_id: @project.id) + end + + def move_fork_network_members + ForkNetworkMember.where(project: source_project).update_all(project_id: @project.id) + ForkNetworkMember.where(forked_from_project: source_project).update_all(forked_from_project_id: @project.id) + end + + def update_root_project + # Update root network project + ForkNetwork.where(root_project: source_project).update_all(root_project_id: @project.id) + end + + def refresh_forks_count + Projects::ForksCountService.new(@project).refresh_cache + end + end +end diff --git a/app/services/projects/move_lfs_objects_projects_service.rb b/app/services/projects/move_lfs_objects_projects_service.rb new file mode 100644 index 00000000000..298da5f1a82 --- /dev/null +++ b/app/services/projects/move_lfs_objects_projects_service.rb @@ -0,0 +1,29 @@ +module Projects + class MoveLfsObjectsProjectsService < BaseMoveRelationsService + def execute(source_project, remove_remaining_elements: true) + return unless super + + Project.transaction(requires_new: true) do + move_lfs_objects_projects + remove_remaining_lfs_objects_project if remove_remaining_elements + + success + end + end + + private + + def move_lfs_objects_projects + prepare_relation(non_existent_lfs_objects_projects) + .update_all(project_id: @project.lfs_storage_project.id) + end + + def remove_remaining_lfs_objects_project + source_project.lfs_objects_projects.destroy_all + end + + def non_existent_lfs_objects_projects + source_project.lfs_objects_projects.where.not(lfs_object: @project.lfs_objects) + end + end +end diff --git a/app/services/projects/move_notification_settings_service.rb b/app/services/projects/move_notification_settings_service.rb new file mode 100644 index 00000000000..f7be461a5da --- /dev/null +++ b/app/services/projects/move_notification_settings_service.rb @@ -0,0 +1,38 @@ +module Projects + class MoveNotificationSettingsService < BaseMoveRelationsService + def execute(source_project, remove_remaining_elements: true) + return unless super + + Project.transaction(requires_new: true) do + move_notification_settings + remove_remaining_notification_settings if remove_remaining_elements + + success + end + end + + private + + def move_notification_settings + prepare_relation(non_existent_notifications) + .update_all(source_id: @project.id) + end + + # Remove remaining notification settings from source_project + def remove_remaining_notification_settings + source_project.notification_settings.destroy_all + end + + # Get users of current notification_settings + def users_in_target_project + @project.notification_settings.select(:user_id) + end + + # Look for notification_settings in source_project that are not in the target project + def non_existent_notifications + source_project.notification_settings + .select(:id) + .where.not(user_id: users_in_target_project) + end + end +end diff --git a/app/services/projects/move_project_authorizations_service.rb b/app/services/projects/move_project_authorizations_service.rb new file mode 100644 index 00000000000..5ef12fc49e5 --- /dev/null +++ b/app/services/projects/move_project_authorizations_service.rb @@ -0,0 +1,40 @@ +# NOTE: This service cannot be used directly because it is part of a +# a bigger process. Instead, use the service MoveAccessService which moves +# project memberships, project group links, authorizations and refreshes +# the authorizations if neccessary +module Projects + class MoveProjectAuthorizationsService < BaseMoveRelationsService + def execute(source_project, remove_remaining_elements: true) + return unless super + + Project.transaction(requires_new: true) do + move_project_authorizations + + remove_remaining_authorizations if remove_remaining_elements + + success + end + end + + private + + def move_project_authorizations + prepare_relation(non_existent_authorization, :user_id) + .update_all(project_id: @project.id) + end + + def remove_remaining_authorizations + # I think because the Project Authorization table does not have a primary key + # it brings a lot a problems/bugs. First, Rails raises PG::SyntaxException if we use + # destroy_all instead of delete_all. + source_project.project_authorizations.delete_all(:delete_all) + end + + # Look for authorizations in source_project that are not in the target project + def non_existent_authorization + source_project.project_authorizations + .select(:user_id) + .where.not(user: @project.authorized_users) + end + end +end diff --git a/app/services/projects/move_project_group_links_service.rb b/app/services/projects/move_project_group_links_service.rb new file mode 100644 index 00000000000..dbeffd7dae9 --- /dev/null +++ b/app/services/projects/move_project_group_links_service.rb @@ -0,0 +1,40 @@ +# NOTE: This service cannot be used directly because it is part of a +# a bigger process. Instead, use the service MoveAccessService which moves +# project memberships, project group links, authorizations and refreshes +# the authorizations if neccessary +module Projects + class MoveProjectGroupLinksService < BaseMoveRelationsService + def execute(source_project, remove_remaining_elements: true) + return unless super + + Project.transaction(requires_new: true) do + move_group_links + remove_remaining_project_group_links if remove_remaining_elements + + success + end + end + + private + + def move_group_links + prepare_relation(non_existent_group_links) + .update_all(project_id: @project.id) + end + + # Remove remaining project group links from source_project + def remove_remaining_project_group_links + source_project.reload.project_group_links.destroy_all + end + + def group_links_in_target_project + @project.project_group_links.select(:group_id) + end + + # Look for groups in source_project that are not in the target project + def non_existent_group_links + source_project.project_group_links + .where.not(group_id: group_links_in_target_project) + end + end +end diff --git a/app/services/projects/move_project_members_service.rb b/app/services/projects/move_project_members_service.rb new file mode 100644 index 00000000000..22a5f0a3fe6 --- /dev/null +++ b/app/services/projects/move_project_members_service.rb @@ -0,0 +1,40 @@ +# NOTE: This service cannot be used directly because it is part of a +# a bigger process. Instead, use the service MoveAccessService which moves +# project memberships, project group links, authorizations and refreshes +# the authorizations if neccessary +module Projects + class MoveProjectMembersService < BaseMoveRelationsService + def execute(source_project, remove_remaining_elements: true) + return unless super + + Project.transaction(requires_new: true) do + move_project_members + remove_remaining_members if remove_remaining_elements + + success + end + end + + private + + def move_project_members + prepare_relation(non_existent_members).update_all(source_id: @project.id) + end + + def remove_remaining_members + # Remove remaining members and authorizations from source_project + source_project.project_members.destroy_all + end + + def project_members_in_target_project + @project.project_members.select(:user_id) + end + + # Look for members in source_project that are not in the target project + def non_existent_members + source_project.members + .select(:id) + .where.not(user_id: @project.project_members.select(:user_id)) + end + end +end diff --git a/app/services/projects/move_users_star_projects_service.rb b/app/services/projects/move_users_star_projects_service.rb new file mode 100644 index 00000000000..079fd5b9685 --- /dev/null +++ b/app/services/projects/move_users_star_projects_service.rb @@ -0,0 +1,20 @@ +module Projects + class MoveUsersStarProjectsService < BaseMoveRelationsService + def execute(source_project, remove_remaining_elements: true) + return unless super + + user_stars = source_project.users_star_projects + + return unless user_stars.any? + + Project.transaction(requires_new: true) do + user_stars.update_all(project_id: @project.id) + + Project.reset_counters @project.id, :users_star_projects + Project.reset_counters source_project.id, :users_star_projects + + success + end + end + end +end diff --git a/app/services/projects/overwrite_project_service.rb b/app/services/projects/overwrite_project_service.rb new file mode 100644 index 00000000000..ce94f147aa9 --- /dev/null +++ b/app/services/projects/overwrite_project_service.rb @@ -0,0 +1,69 @@ +module Projects + class OverwriteProjectService < BaseService + def execute(source_project) + return unless source_project && source_project.namespace == @project.namespace + + Project.transaction do + move_before_destroy_relationships(source_project) + destroy_old_project(source_project) + rename_project(source_project.name, source_project.path) + + @project + end + # Projects::DestroyService can raise Exceptions, but we don't want + # to pass that kind of exception to the caller. Instead, we change it + # for a StandardError exception + rescue Exception => e # rubocop:disable Lint/RescueException + attempt_restore_repositories(source_project) + + if e.class == Exception + raise StandardError, e.message + else + raise + end + end + + private + + def move_before_destroy_relationships(source_project) + options = { remove_remaining_elements: false } + + ::Projects::MoveUsersStarProjectsService.new(@project, @current_user).execute(source_project, options) + ::Projects::MoveAccessService.new(@project, @current_user).execute(source_project, options) + ::Projects::MoveDeployKeysProjectsService.new(@project, @current_user).execute(source_project, options) + ::Projects::MoveNotificationSettingsService.new(@project, @current_user).execute(source_project, options) + ::Projects::MoveForksService.new(@project, @current_user).execute(source_project, options) + ::Projects::MoveLfsObjectsProjectsService.new(@project, @current_user).execute(source_project, options) + add_source_project_to_fork_network(source_project) + end + + def destroy_old_project(source_project) + # Delete previous project (synchronously) and unlink relations + ::Projects::DestroyService.new(source_project, @current_user).execute + end + + def rename_project(name, path) + # Update de project's name and path to the original name/path + ::Projects::UpdateService.new(@project, + @current_user, + { name: name, path: path }) + .execute + end + + def attempt_restore_repositories(project) + ::Projects::DestroyService.new(project, @current_user).attempt_repositories_rollback + end + + def add_source_project_to_fork_network(source_project) + return unless @project.fork_network + + # Because he have moved all references in the fork network from the source_project + # we won't be able to query the database (only through its cached data), + # for its former relationships. That's why we're adding it to the network + # as a fork of the target project + ForkNetworkMember.create!(fork_network: @project.fork_network, + project: source_project, + forked_from_project: @project) + end + end +end diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 26765e5c3f3..5a23f0f0a62 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -24,6 +24,8 @@ module Projects transfer(project) + current_user.invalidate_personal_projects_count + true rescue Projects::TransferService::TransferError => ex project.reload diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb index 7e228d1833d..de77f6bf585 100644 --- a/app/services/projects/update_pages_service.rb +++ b/app/services/projects/update_pages_service.rb @@ -74,25 +74,13 @@ module Projects end def extract_archive!(temp_path) - if artifacts.ends_with?('.tar.gz') || artifacts.ends_with?('.tgz') - extract_tar_archive!(temp_path) - elsif artifacts.ends_with?('.zip') + if artifacts.ends_with?('.zip') extract_zip_archive!(temp_path) else raise InvaildStateError, 'unsupported artifacts format' end end - def extract_tar_archive!(temp_path) - build.artifacts_file.use_file do |artifacts_path| - results = Open3.pipeline(%W(gunzip -c #{artifacts_path}), - %W(dd bs=#{BLOCK_SIZE} count=#{blocks}), - %W(tar -x -C #{temp_path} #{SITE_PATH}), - err: '/dev/null') - raise FailedToExtractError, 'pages failed to extract' unless results.compact.all?(&:success?) - end - end - def extract_zip_archive!(temp_path) raise InvaildStateError, 'missing artifacts metadata' unless build.artifacts_metadata? diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml index 3e85535dae0..bb472b4c900 100644 --- a/app/views/dashboard/issues.html.haml +++ b/app/views/dashboard/issues.html.haml @@ -1,15 +1,19 @@ - @hide_top_links = true -- page_title "Issues" -- header_title "Issues", issues_dashboard_path(assignee_id: current_user.id) +- page_title _("Issues") +- @breadcrumb_link = issues_dashboard_path(assignee_id: current_user.id) = content_for :meta_tags do = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{current_user.name} issues") .top-area - = render 'shared/issuable/nav', type: :issues + = render 'shared/issuable/nav', type: :issues, display_count: !@no_filters_set .nav-controls = link_to params.merge(rss_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: 'Subscribe' do = icon('rss') = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", with_feature_enabled: 'issues', type: :issues = render 'shared/issuable/filter', type: :issues -= render 'shared/issues' + +- if current_user && @no_filters_set + = render 'shared/dashboard/no_filter_selected' +- else + = render 'shared/issues' diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml index 53cd1130299..61aae31be60 100644 --- a/app/views/dashboard/merge_requests.html.haml +++ b/app/views/dashboard/merge_requests.html.haml @@ -1,11 +1,15 @@ - @hide_top_links = true -- page_title "Merge Requests" -- header_title "Merge Requests", merge_requests_dashboard_path(assignee_id: current_user.id) +- page_title _("Merge Requests") +- @breadcrumb_link = merge_requests_dashboard_path(assignee_id: current_user.id) .top-area - = render 'shared/issuable/nav', type: :merge_requests + = render 'shared/issuable/nav', type: :merge_requests, display_count: !@no_filters_set .nav-controls = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", with_feature_enabled: 'merge_requests', type: :merge_requests = render 'shared/issuable/filter', type: :merge_requests -= render 'shared/merge_requests' + +- if current_user && @no_filters_set + = render 'shared/dashboard/no_filter_selected' +- else + = render 'shared/merge_requests' diff --git a/app/views/groups/settings/badges/index.html.haml b/app/views/groups/settings/badges/index.html.haml new file mode 100644 index 00000000000..c7afb25d0f8 --- /dev/null +++ b/app/views/groups/settings/badges/index.html.haml @@ -0,0 +1,4 @@ +- breadcrumb_title _('Project Badges') +- page_title _('Project Badges') + += render 'shared/badges/badge_settings' diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml index 5ea19c9882d..517d9aa3d99 100644 --- a/app/views/layouts/nav/sidebar/_group.html.haml +++ b/app/views/layouts/nav/sidebar/_group.html.haml @@ -112,7 +112,7 @@ %span.nav-item-name Settings %ul.sidebar-sub-level-items - = nav_link(path: %w[groups#projects groups#edit ci_cd#show], html_options: { class: "fly-out-top-item" } ) do + = nav_link(path: %w[groups#projects groups#edit badges#index ci_cd#show], html_options: { class: "fly-out-top-item" } ) do = link_to edit_group_path(@group) do %strong.fly-out-top-item-name #{ _('Settings') } @@ -122,6 +122,12 @@ %span General + = nav_link(controller: :badges) do + = link_to group_settings_badges_path(@group), title: _('Project Badges') do + %span + = _('Project Badges') + + = nav_link(path: 'groups#projects') do = link_to projects_group_path(@group), title: 'Projects' do %span diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index 5c90d13420f..93f674b9d3c 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -258,7 +258,7 @@ #{ _('Snippets') } - if project_nav_tab? :settings - = nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show pages#show]) do + = nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show badges#index pages#show]) do = link_to edit_project_path(@project), class: 'shortcuts-tree' do .nav-icon-container = sprite_icon('settings') @@ -268,7 +268,7 @@ %ul.sidebar-sub-level-items - can_edit = can?(current_user, :admin_project, @project) - if can_edit - = nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show pages#show], html_options: { class: "fly-out-top-item" } ) do + = nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show badges#index pages#show], html_options: { class: "fly-out-top-item" } ) do = link_to edit_project_path(@project) do %strong.fly-out-top-item-name #{ _('Settings') } @@ -282,6 +282,11 @@ %span Members - if can_edit + = nav_link(controller: :badges) do + = link_to project_settings_badges_path(@project), title: _('Badges') do + %span + = _('Badges') + - if can_edit = nav_link(controller: [:integrations, :services, :hooks, :hook_logs]) do = link_to project_settings_integrations_path(@project), title: 'Integrations' do %span diff --git a/app/views/notify/push_to_merge_request_email.html.haml b/app/views/notify/push_to_merge_request_email.html.haml index 4c507c08ed7..67744ec1cee 100644 --- a/app/views/notify/push_to_merge_request_email.html.haml +++ b/app/views/notify/push_to_merge_request_email.html.haml @@ -7,7 +7,7 @@ - count = @existing_commits.size %ul %li - - if count.one? + - if count == 1 - commit_id = @existing_commits.first[:short_id] = link_to(commit_id, project_commit_url(@merge_request.target_project, commit_id)) - else diff --git a/app/views/notify/push_to_merge_request_email.text.haml b/app/views/notify/push_to_merge_request_email.text.haml index 553f771f1a6..95759d127e2 100644 --- a/app/views/notify/push_to_merge_request_email.text.haml +++ b/app/views/notify/push_to_merge_request_email.text.haml @@ -4,7 +4,7 @@ \ - if @existing_commits.any? - count = @existing_commits.size - - commits_id = count.one? ? @existing_commits.first[:short_id] : "#{@existing_commits.first[:short_id]}...#{@existing_commits.last[:short_id]}" + - commits_id = count == 1 ? @existing_commits.first[:short_id] : "#{@existing_commits.first[:short_id]}...#{@existing_commits.last[:short_id]}" - commits_text = "#{count} commit".pluralize(count) * #{commits_id} - #{commits_text} from branch `#{@merge_request.target_branch}` diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml index b96251cd982..9b87a7aaca8 100644 --- a/app/views/profiles/personal_access_tokens/index.html.haml +++ b/app/views/profiles/personal_access_tokens/index.html.haml @@ -2,7 +2,6 @@ - page_title "Personal Access Tokens" - @content_class = "limit-container-width" unless fluid_layout - .row.prepend-top-default .col-lg-4.profile-settings-sidebar %h4.prepend-top-0 diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index a2ecfddb163..043057e79ee 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -23,11 +23,14 @@ - deleted_message = s_('ForkedFromProjectPath|Forked from %{project_name} (deleted)') = deleted_message % { project_name: fork_source_name(@project) } - .project-badges + .project-badges.prepend-top-default.append-bottom-default - @project.badges.each do |badge| - - badge_link_url = badge.rendered_link_url(@project) - %a{ href: badge_link_url, target: '_blank', rel: 'noopener noreferrer' } - %img{ src: badge.rendered_image_url(@project), alt: badge_link_url } + %a.append-right-8{ href: badge.rendered_link_url(@project), + target: '_blank', + rel: 'noopener noreferrer' }> + %img.project-badge{ src: badge.rendered_image_url(@project), + 'aria-hidden': true, + alt: '' }> .project-repo-buttons .count-buttons diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index fa9a9bfc8f7..f49f6e630d2 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -1,6 +1,7 @@ - pipeline = local_assigns.fetch(:pipeline) { project.latest_successful_pipeline_for(ref) } - if !project.empty_repo? && can?(current_user, :download_code, project) + - archive_prefix = "#{project.path}-#{ref.tr('/', '-')}" .project-action-button.dropdown.inline> %button.btn.has-tooltip{ title: s_('DownloadSource|Download'), 'data-toggle' => 'dropdown', 'aria-label' => s_('DownloadSource|Download') } = sprite_icon('download') @@ -10,16 +11,16 @@ %li.dropdown-header #{ _('Source code') } %li - = link_to archive_project_repository_path(project, ref: ref, format: 'zip'), rel: 'nofollow', download: '' do + = link_to project_archive_path(project, id: tree_join(ref, archive_prefix), format: 'zip'), rel: 'nofollow', download: '' do %span= _('Download zip') %li - = link_to archive_project_repository_path(project, ref: ref, format: 'tar.gz'), rel: 'nofollow', download: '' do + = link_to project_archive_path(project, id: tree_join(ref, archive_prefix), format: 'tar.gz'), rel: 'nofollow', download: '' do %span= _('Download tar.gz') %li - = link_to archive_project_repository_path(project, ref: ref, format: 'tar.bz2'), rel: 'nofollow', download: '' do + = link_to project_archive_path(project, id: tree_join(ref, archive_prefix), format: 'tar.bz2'), rel: 'nofollow', download: '' do %span= _('Download tar.bz2') %li - = link_to archive_project_repository_path(project, ref: ref, format: 'tar'), rel: 'nofollow', download: '' do + = link_to project_archive_path(project, id: tree_join(ref, archive_prefix), format: 'tar'), rel: 'nofollow', download: '' do %span= _('Download tar') - if pipeline && pipeline.latest_builds_with_artifacts.any? diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 078bd0eee63..163432c9263 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -22,7 +22,10 @@ .commit-detail.flex-list .commit-content - = link_to_markdown_field(commit, :title, link, class: "commit-row-message item-title") + - if view_details && merge_request + = link_to commit.title, project_commit_path(project, commit.id, merge_request_iid: merge_request.iid), class: "commit-row-message item-title" + - else + = link_to_markdown_field(commit, :title, link, class: "commit-row-message item-title") %span.commit-row-message.visible-xs-inline · = commit.short_id @@ -52,9 +55,9 @@ = render_commit_status(commit, ref: ref) .js-commit-pipeline-status{ data: { endpoint: pipelines_project_commit_path(project, commit.id) } } - = link_to commit.short_id, link, class: "commit-sha btn btn-transparent btn-link" - = clipboard_button(text: commit.id, title: _("Copy commit SHA to clipboard")) - = link_to_browse_code(project, commit) - - if view_details && merge_request - = link_to "View details", project_commit_path(project, commit.id, merge_request_iid: merge_request.iid), class: "btn btn-default" + .commit-sha-group + .label.label-monospace + = commit.short_id + = clipboard_button(text: commit.id, title: _("Copy commit SHA to clipboard"), class: "btn btn-default", container: "body") + = link_to_browse_code(project, commit) diff --git a/app/views/projects/deploy_tokens/_form.html.haml b/app/views/projects/deploy_tokens/_form.html.haml new file mode 100644 index 00000000000..f8db30df7b4 --- /dev/null +++ b/app/views/projects/deploy_tokens/_form.html.haml @@ -0,0 +1,29 @@ +%p.profile-settings-content + = s_("DeployTokens|Pick a name for the application, and we'll give you a unique deploy token.") + += form_for token, url: create_deploy_token_namespace_project_settings_repository_path(project.namespace, project), method: :post do |f| + = form_errors(token) + + .form-group + = f.label :name, class: 'label-light' + = f.text_field :name, class: 'form-control', required: true + + .form-group + = f.label :expires_at, class: 'label-light' + = f.text_field :expires_at, class: 'datepicker form-control', value: f.object.expires_at + + .form-group + = f.label :scopes, class: 'label-light' + %fieldset + = f.check_box :read_repository + = label_tag ("deploy_token_read_repository"), 'read_repository' + %span= s_('DeployTokens|Allows read-only access to the repository') + + - if container_registry_enabled?(project) + %fieldset + = f.check_box :read_registry + = label_tag ("deploy_token_read_registry"), 'read_registry' + %span= s_('DeployTokens|Allows read-only access to the registry images') + + .prepend-top-default + = f.submit s_('DeployTokens|Create deploy token'), class: 'btn btn-success' diff --git a/app/views/projects/deploy_tokens/_index.html.haml b/app/views/projects/deploy_tokens/_index.html.haml new file mode 100644 index 00000000000..50e5950ced4 --- /dev/null +++ b/app/views/projects/deploy_tokens/_index.html.haml @@ -0,0 +1,18 @@ +- expanded = expand_deploy_tokens_section?(@new_deploy_token) + +%section.settings.no-animate{ class: ('expanded' if expanded) } + .settings-header + %h4= s_('DeployTokens|Deploy Tokens') + %button.btn.js-settings-toggle.qa-expand-deploy-keys{ type: 'button' } + = expanded ? 'Collapse' : 'Expand' + %p + = s_('DeployTokens|Deploy tokens allow read-only access to your repository and registry images.') + .settings-content + - if @new_deploy_token.persisted? + = render 'projects/deploy_tokens/new_deploy_token', deploy_token: @new_deploy_token + - else + %h5.prepend-top-0 + = s_('DeployTokens|Add a deploy token') + = render 'projects/deploy_tokens/form', project: @project, token: @new_deploy_token, presenter: @deploy_tokens + %hr + = render 'projects/deploy_tokens/table', project: @project, active_tokens: @deploy_tokens diff --git a/app/views/projects/deploy_tokens/_new_deploy_token.html.haml b/app/views/projects/deploy_tokens/_new_deploy_token.html.haml new file mode 100644 index 00000000000..1e715681e59 --- /dev/null +++ b/app/views/projects/deploy_tokens/_new_deploy_token.html.haml @@ -0,0 +1,14 @@ +.created-deploy-token-container + %h5.prepend-top-0 + = s_('DeployTokens|Your New Deploy Token') + + .form-group + = text_field_tag 'deploy-token-user', deploy_token.username, readonly: true, class: 'deploy-token-field form-control js-select-on-focus' + = clipboard_button(text: deploy_token.username, title: s_('DeployTokens|Copy username to clipboard'), placement: 'left') + %span.deploy-token-help-block.prepend-top-5.text-success= s_("DeployTokens|Use this username as a login.") + + .form-group + = text_field_tag 'deploy-token', deploy_token.token, readonly: true, class: 'deploy-token-field form-control js-select-on-focus' + = clipboard_button(text: deploy_token.token, title: s_('DeployTokens|Copy deploy token to clipboard'), placement: 'left') + %span.deploy-token-help-block.prepend-top-5.text-danger= s_("DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again.") +%hr diff --git a/app/views/projects/deploy_tokens/_revoke_modal.html.haml b/app/views/projects/deploy_tokens/_revoke_modal.html.haml new file mode 100644 index 00000000000..085964fe22e --- /dev/null +++ b/app/views/projects/deploy_tokens/_revoke_modal.html.haml @@ -0,0 +1,17 @@ +.modal{ id: "revoke-modal-#{token.id}" } + .modal-dialog + .modal-content + .modal-header + %h4.modal-title.pull-left + = s_('DeployTokens|Revoke') + %b #{token.name}? + %button.close{ 'aria-label' => _('Close'), 'data-dismiss' => 'modal', type: 'button' } + %span{ 'aria-hidden' => 'true' } × + .modal-body + %p + = s_('DeployTokens|You are about to revoke') + %b #{token.name}. + = s_('DeployTokens|This action cannot be undone.') + .modal-footer + %a{ href: '#', data: { dismiss: 'modal' }, class: 'btn btn-default' }= _('Cancel') + = link_to s_('DeployTokens|Revoke %{name}') % { name: token.name }, revoke_project_deploy_token_path(project, token), method: :put, class: 'btn btn-danger' diff --git a/app/views/projects/deploy_tokens/_table.html.haml b/app/views/projects/deploy_tokens/_table.html.haml new file mode 100644 index 00000000000..5013a9b250d --- /dev/null +++ b/app/views/projects/deploy_tokens/_table.html.haml @@ -0,0 +1,31 @@ +%h5= s_("DeployTokens|Active Deploy Tokens (%{active_tokens})") % { active_tokens: active_tokens.length } + +- if active_tokens.present? + .table-responsive.deploy-tokens + %table.table + %thead + %tr + %th= s_('DeployTokens|Name') + %th= s_('DeployTokens|Username') + %th= s_('DeployTokens|Created') + %th= s_('DeployTokens|Expires') + %th= s_('DeployTokens|Scopes') + %th + %tbody + - active_tokens.each do |token| + %tr + %td= token.name + %td= token.username + %td= token.created_at.to_date.to_s(:medium) + %td + - if token.expires? + %span{ class: ('text-warning' if token.expires_soon?) } + In #{distance_of_time_in_words_to_now(token.expires_at)} + - else + %span.token-never-expires-label Never + %td= token.scopes.present? ? token.scopes.join(", ") : "<no scopes selected>" + %td= link_to s_('DeployTokens|Revoke'), "#", class: "btn btn-danger pull-right", data: { toggle: "modal", target: "#revoke-modal-#{token.id}"} + = render 'projects/deploy_tokens/revoke_modal', token: token, project: project +- else + .settings-message.text-center + = s_('DeployTokens|This project has no active Deploy Tokens.') diff --git a/app/views/projects/jobs/_empty_state.html.haml b/app/views/projects/jobs/_empty_state.html.haml index c66313bdbf3..311934d9c33 100644 --- a/app/views/projects/jobs/_empty_state.html.haml +++ b/app/views/projects/jobs/_empty_state.html.haml @@ -1,7 +1,7 @@ - illustration = local_assigns.fetch(:illustration) - illustration_size = local_assigns.fetch(:illustration_size) - title = local_assigns.fetch(:title) -- content = local_assigns.fetch(:content) +- content = local_assigns.fetch(:content, nil) - action = local_assigns.fetch(:action, nil) .row.empty-state @@ -11,7 +11,8 @@ .col-xs-12 .text-content %h4.text-center= title - %p= content + - if content + %p= content - if action .text-center = action diff --git a/app/views/projects/jobs/_empty_states.html.haml b/app/views/projects/jobs/_empty_states.html.haml new file mode 100644 index 00000000000..e5198d047df --- /dev/null +++ b/app/views/projects/jobs/_empty_states.html.haml @@ -0,0 +1,9 @@ +- detailed_status = @build.detailed_status(current_user) +- illustration = detailed_status.illustration + += render 'empty_state', + illustration: illustration[:image], + illustration_size: illustration[:size], + title: illustration[:title], + content: illustration[:content], + action: detailed_status.has_action? ? link_to(detailed_status.action_button_title, detailed_status.action_path, method: detailed_status.action_method, class: 'btn btn-primary', title: detailed_status.action_button_title) : nil diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml index dece4dfe167..8beb4ffef45 100644 --- a/app/views/projects/jobs/show.html.haml +++ b/app/views/projects/jobs/show.html.haml @@ -54,7 +54,8 @@ Job has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)} - else Job has been erased #{time_ago_with_tooltip(@build.erased_at)} - - if @build.started? + + - if @build.has_trace? .build-trace-container.prepend-top-default .top-bar.js-top-bar .js-truncated-info.truncated-info.hidden-xs.pull-left.hidden< @@ -88,25 +89,9 @@ %pre.build-trace#build-trace %code.bash.js-build-output .build-loader-animation.js-build-refresh - - elsif @build.playable? - = render 'empty_state', - illustration: 'illustrations/manual_action.svg', - illustration_size: 'svg-394', - title: _('This job requires a manual action'), - content: _('This job depends on a user to trigger its process. Often they are used to deploy code to production environments'), - action: ( link_to _('Trigger this manual action'), play_project_job_path(@project, @build), method: :post, class: 'btn btn-primary', title: _('Trigger this manual action') ) - - elsif @build.created? - = render 'empty_state', - illustration: 'illustrations/job_not_triggered.svg', - illustration_size: 'svg-306', - title: _('This job has not been triggered yet'), - content: _('This job depends on upstream jobs that need to succeed in order for this job to be triggered') - else - = render 'empty_state', - illustration: 'illustrations/pending_job_empty.svg', - illustration_size: 'svg-430', - title: _('This job has not started yet'), - content: _('This job is in pending state and is waiting to be picked by a runner') + = render "empty_states" + = render "sidebar", builds: @builds .js-build-options{ data: javascript_build_options } diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml index 12d56e244ce..2c80f7c3fa3 100644 --- a/app/views/projects/registry/repositories/index.html.haml +++ b/app/views/projects/registry/repositories/index.html.haml @@ -29,6 +29,10 @@ docker login #{Gitlab.config.registry.host_port} %br %p + - deploy_token = link_to(_('deploy token'), help_page_path('user/projects/deploy_tokens/index', anchor: 'read-container-registry-images'), target: '_blank') + = s_('ContainerRegistry|You can also %{deploy_token} for read-only access to the registry images.').html_safe % { deploy_token: deploy_token } + %br + %p = s_('ContainerRegistry|Once you log in, you’re free to create and upload a container image using the common %{build} and %{push} commands').html_safe % { build: "<code>build</code>".html_safe, push: "<code>push</code>".html_safe } %pre :plain diff --git a/app/views/projects/settings/badges/index.html.haml b/app/views/projects/settings/badges/index.html.haml new file mode 100644 index 00000000000..b68ed70de89 --- /dev/null +++ b/app/views/projects/settings/badges/index.html.haml @@ -0,0 +1,4 @@ +- breadcrumb_title _('Badges') +- page_title _('Badges') + += render 'shared/badges/badge_settings' diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml index 6bef4d19434..f57590a908f 100644 --- a/app/views/projects/settings/repository/show.html.haml +++ b/app/views/projects/settings/repository/show.html.haml @@ -9,3 +9,4 @@ = render "projects/protected_branches/index" = render "projects/protected_tags/index" = render @deploy_keys += render "projects/deploy_tokens/index" diff --git a/app/views/shared/_recaptcha_form.html.haml b/app/views/shared/_recaptcha_form.html.haml index 93a4301f366..a0ba1afc284 100644 --- a/app/views/shared/_recaptcha_form.html.haml +++ b/app/views/shared/_recaptcha_form.html.haml @@ -10,7 +10,7 @@ = hidden_field(resource_name, field, value: value) = hidden_field_tag(:spam_log_id, spammable.spam_log.id) = hidden_field_tag(:recaptcha_verification, true) - = recaptcha_tags script: script, callback: 'recaptchaDialogCallback' + = recaptcha_tags script: script, callback: 'recaptchaDialogCallback' unless Rails.env.test? -# Yields a block with given extra params. = yield diff --git a/app/views/shared/badges/_badge_settings.html.haml b/app/views/shared/badges/_badge_settings.html.haml new file mode 100644 index 00000000000..b7c250d3b1c --- /dev/null +++ b/app/views/shared/badges/_badge_settings.html.haml @@ -0,0 +1,4 @@ +#badge-settings{ data: { api_endpoint_url: @badge_api_endpoint, + docs_url: help_page_path('user/project/badges')} } + .text-center.prepend-top-default + = icon('spinner spin 2x') diff --git a/app/views/shared/dashboard/_no_filter_selected.html.haml b/app/views/shared/dashboard/_no_filter_selected.html.haml new file mode 100644 index 00000000000..b2e6967f6aa --- /dev/null +++ b/app/views/shared/dashboard/_no_filter_selected.html.haml @@ -0,0 +1,8 @@ +.row.empty-state.text-center + .col-xs-12 + .svg-130.prepend-top-default + = image_tag 'illustrations/issue-dashboard_results-without-filter.svg' + .col-xs-12 + .text-content + %h4 + = _("Please select at least one filter to see results") diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 7704c88905b..1bd5b4164b1 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -24,12 +24,9 @@ .filter-item.inline.labels-filter = render "shared/issuable/label_dropdown", selected: selected_labels, use_id: false, selected_toggle: params[:label_name], data_options: { field_name: "label_name[]" } - - if issuable_filter_present? - .filter-item.inline.reset-filters - %a{ href: page_filter_path(without: issuable_filter_params) } Reset filters - - .pull-right - = render 'shared/sort_dropdown' + - unless @no_filters_set + .pull-right + = render 'shared/sort_dropdown' - has_labels = @labels && @labels.any? .row-content-block.second-block.filtered-labels{ class: ("hidden" unless has_labels) } diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml index 4d8109eb90c..a5f40ea934b 100644 --- a/app/views/shared/issuable/_nav.html.haml +++ b/app/views/shared/issuable/_nav.html.haml @@ -1,22 +1,23 @@ - type = local_assigns.fetch(:type, :issues) - page_context_word = type.to_s.humanize(capitalize: false) +- display_count = local_assigns.fetch(:display_count, :true) %ul.nav-links.issues-state-filters.mobile-separator %li{ class: active_when(params[:state] == 'opened') }> = link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened.", data: { state: 'opened' } do - #{issuables_state_counter_text(type, :opened)} + #{issuables_state_counter_text(type, :opened, display_count)} - if type == :merge_requests %li{ class: active_when(params[:state] == 'merged') }> = link_to page_filter_path(state: 'merged', label: true), id: 'state-merged', title: 'Filter by merge requests that are currently merged.', data: { state: 'merged' } do - #{issuables_state_counter_text(type, :merged)} + #{issuables_state_counter_text(type, :merged, display_count)} %li{ class: active_when(params[:state] == 'closed') }> = link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by merge requests that are currently closed and unmerged.', data: { state: 'closed' } do - #{issuables_state_counter_text(type, :closed)} + #{issuables_state_counter_text(type, :closed, display_count)} - else %li{ class: active_when(params[:state] == 'closed') }> = link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by issues that are currently closed.', data: { state: 'closed' } do - #{issuables_state_counter_text(type, :closed)} + #{issuables_state_counter_text(type, :closed, display_count)} - = render 'shared/issuable/nav_links/all', page_context_word: page_context_word, counter: issuables_state_counter_text(type, :all) + = render 'shared/issuable/nav_links/all', page_context_word: page_context_word, counter: issuables_state_counter_text(type, :all, display_count) diff --git a/changelogs/unreleased/17939-osw-patch-support-gfm.yml b/changelogs/unreleased/17939-osw-patch-support-gfm.yml new file mode 100644 index 00000000000..576581e25d6 --- /dev/null +++ b/changelogs/unreleased/17939-osw-patch-support-gfm.yml @@ -0,0 +1,5 @@ +--- +title: Add support for patch link extension for commit links on GitLab Flavored Markdown +merge_request: +author: +type: added diff --git a/changelogs/unreleased/31591-project-deploy-tokens-to-allow-permanent-access.yml b/changelogs/unreleased/31591-project-deploy-tokens-to-allow-permanent-access.yml new file mode 100644 index 00000000000..5546d26d0fb --- /dev/null +++ b/changelogs/unreleased/31591-project-deploy-tokens-to-allow-permanent-access.yml @@ -0,0 +1,5 @@ +--- +title: Create Deploy Tokens to allow permanent access to repository and registry +merge_request: 17894 +author: +type: added diff --git a/changelogs/unreleased/32617-fix-template-selector-menu-visibility.yml b/changelogs/unreleased/32617-fix-template-selector-menu-visibility.yml new file mode 100644 index 00000000000..c73be5a901e --- /dev/null +++ b/changelogs/unreleased/32617-fix-template-selector-menu-visibility.yml @@ -0,0 +1,6 @@ +--- +title: Fix template selector menu visibility when toggling preview mode in file edit + view +merge_request: 18118 +author: Fabian Schneider +type: fixed diff --git a/changelogs/unreleased/41981-allow-group-owner-to-enable-runners-from-subgroups.yml b/changelogs/unreleased/41981-allow-group-owner-to-enable-runners-from-subgroups.yml new file mode 100644 index 00000000000..30481e7af84 --- /dev/null +++ b/changelogs/unreleased/41981-allow-group-owner-to-enable-runners-from-subgroups.yml @@ -0,0 +1,5 @@ +--- +title: 'Allow group owner to enable runners from subgroups (#41981)' +merge_request: 18009 +author: +type: fixed diff --git a/changelogs/unreleased/42448-change-commit-row-actions-and-sha-design-for-project-commit-list.yml b/changelogs/unreleased/42448-change-commit-row-actions-and-sha-design-for-project-commit-list.yml new file mode 100644 index 00000000000..77d1ebf69df --- /dev/null +++ b/changelogs/unreleased/42448-change-commit-row-actions-and-sha-design-for-project-commit-list.yml @@ -0,0 +1,6 @@ +--- +title: Improved visual styles and consistency for commit hash and possible actions + across commit lists +merge_request: 17406 +author: +type: changed diff --git a/changelogs/unreleased/42568-pipeline-empty-state.yml b/changelogs/unreleased/42568-pipeline-empty-state.yml new file mode 100644 index 00000000000..d36edcf1b37 --- /dev/null +++ b/changelogs/unreleased/42568-pipeline-empty-state.yml @@ -0,0 +1,5 @@ +--- +title: Improve empty state for canceled job +merge_request: 17646 +author: +type: fixed diff --git a/changelogs/unreleased/43246-checkfilter.yml b/changelogs/unreleased/43246-checkfilter.yml new file mode 100644 index 00000000000..e6c0e716213 --- /dev/null +++ b/changelogs/unreleased/43246-checkfilter.yml @@ -0,0 +1,6 @@ +--- +title: Require at least one filter when listing issues or merge requests on dashboard + page +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/44224-remove-gl.yml b/changelogs/unreleased/44224-remove-gl.yml new file mode 100644 index 00000000000..1c792883f09 --- /dev/null +++ b/changelogs/unreleased/44224-remove-gl.yml @@ -0,0 +1,5 @@ +--- +title: Removes modal boards store and mixins from global scope +merge_request: +author: +type: other diff --git a/changelogs/unreleased/ab-37462-cache-personal-projects-count.yml b/changelogs/unreleased/ab-37462-cache-personal-projects-count.yml new file mode 100644 index 00000000000..55069b1f2d2 --- /dev/null +++ b/changelogs/unreleased/ab-37462-cache-personal-projects-count.yml @@ -0,0 +1,5 @@ +--- +title: Cache personal projects count. +merge_request: 18197 +author: +type: performance diff --git a/changelogs/unreleased/add-cpu-mem-totals.yml b/changelogs/unreleased/add-cpu-mem-totals.yml new file mode 100644 index 00000000000..bc8babab731 --- /dev/null +++ b/changelogs/unreleased/add-cpu-mem-totals.yml @@ -0,0 +1,5 @@ +--- +title: Add Total CPU/Memory consumption metrics for Kubernetes +merge_request: 17731 +author: +type: added diff --git a/changelogs/unreleased/da-gitaly-calculate-repository-checksum.yml b/changelogs/unreleased/da-gitaly-calculate-repository-checksum.yml new file mode 100644 index 00000000000..de09f87a7c9 --- /dev/null +++ b/changelogs/unreleased/da-gitaly-calculate-repository-checksum.yml @@ -0,0 +1,5 @@ +--- +title: Repository checksum calculation is handled by Gitaly when feature is enabled +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/fix-500-error-when-mr-ref-is-not-yet-fetched.yml b/changelogs/unreleased/fix-500-error-when-mr-ref-is-not-yet-fetched.yml new file mode 100644 index 00000000000..e21554f091a --- /dev/null +++ b/changelogs/unreleased/fix-500-error-when-mr-ref-is-not-yet-fetched.yml @@ -0,0 +1,6 @@ +--- +title: Fix 500 error when a merge request from a fork has conflicts and has not yet + been updated +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/fix-dashboard-sorting.yml b/changelogs/unreleased/fix-dashboard-sorting.yml new file mode 100644 index 00000000000..2ba13a93fa9 --- /dev/null +++ b/changelogs/unreleased/fix-dashboard-sorting.yml @@ -0,0 +1,5 @@ +--- +title: Prioritize weight over title when sorting charts +merge_request: 18233 +author: +type: fixed diff --git a/changelogs/unreleased/fj-41900-import-endpoint-with-overwrite-support.yml b/changelogs/unreleased/fj-41900-import-endpoint-with-overwrite-support.yml new file mode 100644 index 00000000000..0553cc684ce --- /dev/null +++ b/changelogs/unreleased/fj-41900-import-endpoint-with-overwrite-support.yml @@ -0,0 +1,5 @@ +--- +title: Extend API for importing a project export with overwrite support +merge_request: 17883 +author: +type: added diff --git a/changelogs/unreleased/fl-fix-annoying-actions.yml b/changelogs/unreleased/fl-fix-annoying-actions.yml new file mode 100644 index 00000000000..fe17f9a8978 --- /dev/null +++ b/changelogs/unreleased/fl-fix-annoying-actions.yml @@ -0,0 +1,5 @@ +--- +title: Stop redirecting the page in pipeline main actions +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/jivl-summary-statistics-prometheus-dashboard.yml b/changelogs/unreleased/jivl-summary-statistics-prometheus-dashboard.yml new file mode 100644 index 00000000000..c5cdbcf7b40 --- /dev/null +++ b/changelogs/unreleased/jivl-summary-statistics-prometheus-dashboard.yml @@ -0,0 +1,5 @@ +--- +title: Add average and maximum summary statistics to the prometheus dashboard +merge_request: 17921 +author: +type: changed diff --git a/changelogs/unreleased/jramsay-38830-tarball.yml b/changelogs/unreleased/jramsay-38830-tarball.yml new file mode 100644 index 00000000000..6d40c305614 --- /dev/null +++ b/changelogs/unreleased/jramsay-38830-tarball.yml @@ -0,0 +1,5 @@ +--- +title: Add alternate archive route for simplified packaging +merge_request: 17225 +author: +type: added diff --git a/changelogs/unreleased/move-board-blank-state-vue-component.yml b/changelogs/unreleased/move-board-blank-state-vue-component.yml new file mode 100644 index 00000000000..0a278a8c009 --- /dev/null +++ b/changelogs/unreleased/move-board-blank-state-vue-component.yml @@ -0,0 +1,5 @@ +--- +title: Move BoardBlankState vue component +merge_request: 17666 +author: George Tsiolis +type: performance diff --git a/changelogs/unreleased/remove-pages-tar-support.yml b/changelogs/unreleased/remove-pages-tar-support.yml new file mode 100644 index 00000000000..73448687912 --- /dev/null +++ b/changelogs/unreleased/remove-pages-tar-support.yml @@ -0,0 +1,5 @@ +--- +title: Remove support for legacy tar.gz pages artifacts +merge_request: 18090 +author: +type: deprecated diff --git a/changelogs/unreleased/sh-add-cleanup-rpc-gitaly.yml b/changelogs/unreleased/sh-add-cleanup-rpc-gitaly.yml new file mode 100644 index 00000000000..81b48fc255b --- /dev/null +++ b/changelogs/unreleased/sh-add-cleanup-rpc-gitaly.yml @@ -0,0 +1,5 @@ +--- +title: Automatically cleanup stale worktrees and lock files upon a push +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/winh-41174-projects-groups-badges-ui.yml b/changelogs/unreleased/winh-41174-projects-groups-badges-ui.yml new file mode 100644 index 00000000000..14114eca2b2 --- /dev/null +++ b/changelogs/unreleased/winh-41174-projects-groups-badges-ui.yml @@ -0,0 +1,5 @@ +--- +title: Projects and groups badges settings UI +merge_request: 17114 +author: +type: added diff --git a/changelogs/unreleased/zj-find-license-opt-out.yml b/changelogs/unreleased/zj-find-license-opt-out.yml new file mode 100644 index 00000000000..be2656601a9 --- /dev/null +++ b/changelogs/unreleased/zj-find-license-opt-out.yml @@ -0,0 +1,5 @@ +--- +title: Detect repository license on Gitaly by default +merge_request: +author: +type: performance diff --git a/config/karma.config.js b/config/karma.config.js index 7ede745b591..c378e621953 100644 --- a/config/karma.config.js +++ b/config/karma.config.js @@ -39,7 +39,7 @@ module.exports = function(config) { frameworks: ['jasmine'], files: [ { pattern: 'spec/javascripts/test_bundle.js', watched: false }, - { pattern: 'spec/javascripts/fixtures/**/*@(.json|.html|.html.raw)', included: false }, + { pattern: 'spec/javascripts/fixtures/**/*@(.json|.html|.html.raw|.png)', included: false }, ], preprocessors: { 'spec/javascripts/**/*.js': ['webpack', 'sourcemap'], diff --git a/config/prometheus/additional_metrics.yml b/config/prometheus/additional_metrics.yml index c4f60eb2687..10ca612b246 100644 --- a/config/prometheus/additional_metrics.yml +++ b/config/prometheus/additional_metrics.yml @@ -26,7 +26,7 @@ weight: 1 queries: - query_range: 'avg(nginx_upstream_response_msecs_avg{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"})' - label: Average + label: Pod average unit: ms - title: "HTTP Error Rate" y_label: "HTTP 500 Errors / Sec" @@ -139,21 +139,39 @@ - group: System metrics (Kubernetes) priority: 5 metrics: - - title: "Memory Usage" + - title: "Memory Usage (Total)" + y_label: "Total Memory Used" + required_metrics: + - container_memory_usage_bytes + weight: 4 + queries: + - 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' + label: Total + unit: GB + - title: "Core Usage (Total)" + y_label: "Total Cores" + required_metrics: + - container_cpu_usage_seconds_total + weight: 3 + queries: + - query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job)' + label: Total + unit: "cores" + - title: "Memory Usage (Pod average)" y_label: "Memory Used per Pod" required_metrics: - container_memory_usage_bytes - weight: 1 + weight: 2 queries: - - query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024' - label: Average + - query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024' + label: Pod average unit: MB - - title: "CPU Usage" + - title: "Core Usage (Pod average)" y_label: "Cores per Pod" required_metrics: - container_cpu_usage_seconds_total weight: 1 queries: - - query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name))' - label: Average + - query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name))' + label: Pod average unit: "cores"
\ No newline at end of file diff --git a/config/routes/group.rb b/config/routes/group.rb index d89a714c7d6..170508e893d 100644 --- a/config/routes/group.rb +++ b/config/routes/group.rb @@ -24,6 +24,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do constraints: { group_id: Gitlab::PathRegex.full_namespace_route_regex }) do namespace :settings do resource :ci_cd, only: [:show], controller: 'ci_cd' + resources :badges, only: [:index] end resource :variables, only: [:show, :update] diff --git a/config/routes/project.rb b/config/routes/project.rb index 0f2ea1c01d1..2a1bcb8cde2 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -88,6 +88,12 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do end end + resources :deploy_tokens, constraints: { id: /\d+/ }, only: [] do + member do + put :revoke + end + end + resources :forks, only: [:index, :new, :create] resource :import, only: [:new, :create, :show] @@ -249,6 +255,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do end scope '-' do + get 'archive/*id', constraints: { format: Gitlab::PathRegex.archive_formats_regex, id: /.+?/ }, to: 'repositories#archive', as: 'archive' + resources :jobs, only: [:index, :show], constraints: { id: /\d+/ } do collection do post :cancel_all @@ -424,7 +432,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do post :reset_cache end resource :integrations, only: [:show] - resource :repository, only: [:show], controller: :repository + resource :repository, only: [:show], controller: :repository do + post :create_deploy_token, path: 'deploy_token/create' + end + resources :badges, only: [:index] end # Since both wiki and repository routing contains wildcard characters diff --git a/config/routes/repository.rb b/config/routes/repository.rb index eace3a615b4..9e506a1a43a 100644 --- a/config/routes/repository.rb +++ b/config/routes/repository.rb @@ -2,10 +2,11 @@ resource :repository, only: [:create] do member do - get ':ref/archive', constraints: { format: Gitlab::PathRegex.archive_formats_regex, ref: /.+/ }, action: 'archive', as: 'archive' - # deprecated since GitLab 9.5 - get 'archive', constraints: { format: Gitlab::PathRegex.archive_formats_regex }, as: 'archive_alternative' + get 'archive', constraints: { format: Gitlab::PathRegex.archive_formats_regex }, as: 'archive_alternative', defaults: { append_sha: true } + + # deprecated since GitLab 10.7 + get ':id/archive', constraints: { format: Gitlab::PathRegex.archive_formats_regex, id: /.+/ }, action: 'archive', as: 'archive_deprecated', defaults: { append_sha: true } end end diff --git a/db/migrate/20180319190020_create_deploy_tokens.rb b/db/migrate/20180319190020_create_deploy_tokens.rb new file mode 100644 index 00000000000..d129459ea0a --- /dev/null +++ b/db/migrate/20180319190020_create_deploy_tokens.rb @@ -0,0 +1,19 @@ +class CreateDeployTokens < ActiveRecord::Migration + DOWNTIME = false + + def change + create_table :deploy_tokens do |t| + t.boolean :revoked, default: false + t.boolean :read_repository, null: false, default: false + t.boolean :read_registry, null: false, default: false + + t.datetime_with_timezone :expires_at, null: false + t.datetime_with_timezone :created_at, null: false + + t.string :name, null: false + t.string :token, index: { unique: true }, null: false + + t.index [:token, :expires_at, :id], where: "(revoked IS FALSE)" + end + end +end diff --git a/db/migrate/20180405142733_create_project_deploy_tokens.rb b/db/migrate/20180405142733_create_project_deploy_tokens.rb new file mode 100644 index 00000000000..9d8f89243a8 --- /dev/null +++ b/db/migrate/20180405142733_create_project_deploy_tokens.rb @@ -0,0 +1,16 @@ +class CreateProjectDeployTokens < ActiveRecord::Migration + DOWNTIME = false + + def change + create_table :project_deploy_tokens do |t| + t.integer :project_id, null: false + t.integer :deploy_token_id, null: false + t.datetime_with_timezone :created_at, null: false + + t.foreign_key :deploy_tokens, column: :deploy_token_id, on_delete: :cascade + t.foreign_key :projects, column: :project_id, on_delete: :cascade + + t.index [:project_id, :deploy_token_id], unique: true + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 2cd51b200b3..fd75b176318 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20180405101928) do +ActiveRecord::Schema.define(version: 20180405142733) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -683,6 +683,19 @@ ActiveRecord::Schema.define(version: 20180405101928) do add_index "deploy_keys_projects", ["project_id"], name: "index_deploy_keys_projects_on_project_id", using: :btree + create_table "deploy_tokens", force: :cascade do |t| + t.boolean "revoked", default: false + t.boolean "read_repository", default: false, null: false + t.boolean "read_registry", default: false, null: false + t.datetime_with_timezone "expires_at", null: false + t.datetime_with_timezone "created_at", null: false + t.string "name", null: false + t.string "token", null: false + end + + add_index "deploy_tokens", ["token", "expires_at", "id"], name: "index_deploy_tokens_on_token_and_expires_at_and_id", where: "(revoked IS FALSE)", using: :btree + add_index "deploy_tokens", ["token"], name: "index_deploy_tokens_on_token", unique: true, using: :btree + create_table "deployments", force: :cascade do |t| t.integer "iid", null: false t.integer "project_id", null: false @@ -1430,6 +1443,14 @@ ActiveRecord::Schema.define(version: 20180405101928) do add_index "project_custom_attributes", ["key", "value"], name: "index_project_custom_attributes_on_key_and_value", using: :btree add_index "project_custom_attributes", ["project_id", "key"], name: "index_project_custom_attributes_on_project_id_and_key", unique: true, using: :btree + create_table "project_deploy_tokens", force: :cascade do |t| + t.integer "project_id", null: false + t.integer "deploy_token_id", null: false + t.datetime_with_timezone "created_at", null: false + end + + add_index "project_deploy_tokens", ["project_id", "deploy_token_id"], name: "index_project_deploy_tokens_on_project_id_and_deploy_token_id", unique: true, using: :btree + create_table "project_features", force: :cascade do |t| t.integer "project_id" t.integer "merge_requests_access_level" @@ -2137,6 +2158,8 @@ ActiveRecord::Schema.define(version: 20180405101928) do add_foreign_key "project_authorizations", "users", on_delete: :cascade add_foreign_key "project_auto_devops", "projects", on_delete: :cascade add_foreign_key "project_custom_attributes", "projects", on_delete: :cascade + add_foreign_key "project_deploy_tokens", "deploy_tokens", on_delete: :cascade + add_foreign_key "project_deploy_tokens", "projects", on_delete: :cascade add_foreign_key "project_features", "projects", name: "fk_18513d9b92", on_delete: :cascade add_foreign_key "project_group_links", "projects", name: "fk_daa8cee94c", on_delete: :cascade add_foreign_key "project_import_data", "projects", name: "fk_ffb9ee3a10", on_delete: :cascade diff --git a/doc/administration/index.md b/doc/administration/index.md index c8f27719ce9..0906821d6a3 100644 --- a/doc/administration/index.md +++ b/doc/administration/index.md @@ -39,6 +39,7 @@ Learn how to install, configure, update, and maintain your GitLab instance. - [GitLab Pages configuration for GitLab source installations](pages/source.md): Enable and configure GitLab Pages on [source installations](../install/installation.md#installation-from-source). - [Environment variables](environment_variables.md): Supported environment variables that can be used to override their defaults values in order to configure GitLab. +- [Plugins](plugins.md): With custom plugins, GitLab administrators can introduce custom integrations without modifying GitLab's source code. #### Customizing GitLab's appearance diff --git a/doc/administration/plugins.md b/doc/administration/plugins.md index c91ac3012b9..3ae41638ac3 100644 --- a/doc/administration/plugins.md +++ b/doc/administration/plugins.md @@ -1,66 +1,80 @@ -# Plugins +# GitLab Plugin system -**Note:** Plugins must be configured on the filesystem of the GitLab -server. Only GitLab server administrators will be able to complete these tasks. -Please explore [system hooks] or [webhooks] as an option if you do not -have filesystem access. +> Introduced in GitLab 10.6. -Introduced in GitLab 10.6. +With custom plugins, GitLab administrators can introduce custom integrations +without modifying GitLab's source code. -A plugin will run on each event so it's up to you to filter events or projects within a plugin code. You can have as many plugins as you want. Each plugin will be triggered by GitLab asynchronously in case of an event. For a list of events please see [system hooks] documentation. +NOTE: **Note:** +Instead of writing and supporting your own plugin you can make changes +directly to the GitLab source code and contribute back upstream. This way we can +ensure functionality is preserved across versions and covered by tests. + +NOTE: **Note:** +Plugins must be configured on the filesystem of the GitLab server. Only GitLab +server administrators will be able to complete these tasks. Explore +[system hooks] or [webhooks] as an option if you do not have filesystem access. + +A plugin will run on each event so it's up to you to filter events or projects +within a plugin code. You can have as many plugins as you want. Each plugin will +be triggered by GitLab asynchronously in case of an event. For a list of events +see the [system hooks] documentation. ## Setup -Plugins must be placed directly into `plugins` directory, subdirectories will be ignored. -There is an `example` directory inside `plugins` where you can find some basic examples. +The plugins must be placed directly into the `plugins` directory, subdirectories +will be ignored. There is an +[`example` directory inside `plugins`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/plugins/examples) +where you can find some basic examples. Follow the steps below to set up a custom hook: -1. On the GitLab server, navigate to the project's plugin directory. +1. On the GitLab server, navigate to the plugin directory. For an installation from source the path is usually `/home/git/gitlab/plugins/`. For Omnibus installs the path is usually `/opt/gitlab/embedded/service/gitlab-rails/plugins`. -1. Inside the `plugins` directory, create a file with a name of your choice, but without spaces or special characters. +1. Inside the `plugins` directory, create a file with a name of your choice, + without spaces or special characters. 1. Make the hook file executable and make sure it's owned by the git user. -1. Write the code to make the plugin function as expected. Plugin can be - in any language. Ensure the 'shebang' at the top properly reflects the language - type. For example, if the script is in Ruby the shebang will probably be - `#!/usr/bin/env ruby`. -1. The data to the plugin will be provided as JSON on STDIN. It will be exactly same as one for [system hooks] +1. Write the code to make the plugin function as expected. That can be + in any language, and ensure the 'shebang' at the top properly reflects the + language type. For example, if the script is in Ruby the shebang will + probably be `#!/usr/bin/env ruby`. +1. The data to the plugin will be provided as JSON on STDIN. It will be exactly + same as for [system hooks] -That's it! Assuming the plugin code is properly implemented the hook will fire -as appropriate. Plugins file list is updated for each event. There is no need to restart GitLab to apply a new plugin. +That's it! Assuming the plugin code is properly implemented, the hook will fire +as appropriate. The plugins file list is updated for each event, there is no +need to restart GitLab to apply a new plugin. If a plugin executes with non-zero exit code or GitLab fails to execute it, a message will be logged to `plugin.log`. ## Validation -Writing own plugin can be tricky and its easier if you can check it without altering the system. -We provided a rake task you can use with staging environment to test your plugin before using it in production. -The rake task will use a sample data and execute each of plugins. By output you should be able to determine if -system sees your plugin and if it was executed without errors. +Writing your own plugin can be tricky and it's easier if you can check it +without altering the system. A rake task is provided so that you can use it +in a staging environment to test your plugin before using it in production. +The rake task will use a sample data and execute each of plugin. The output +should be enough to determine if the system sees your plugin and if it was +executed without errors. ```bash # Omnibus installations sudo gitlab-rake plugins:validate # Installations from source +cd /home/git/gitlab bundle exec rake plugins:validate RAILS_ENV=production ``` -Example of output can be next: +Example of output: ``` --> bundle exec rake plugins:validate RAILS_ENV=production Validating plugins from /plugins directory * /home/git/gitlab/plugins/save_to_file.clj succeed (zero exit code) * /home/git/gitlab/plugins/save_to_file.rb failure (non-zero exit code) ``` -[hooks]: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks#Server-Side-Hooks [system hooks]: ../system_hooks/system_hooks.md [webhooks]: ../user/project/integrations/webhooks.md -[5073]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5073 -[93]: https://gitlab.com/gitlab-org/gitlab-shell/merge_requests/93 - diff --git a/doc/api/group_badges.md b/doc/api/group_badges.md index 3e0683f378d..0d7d0fd9c42 100644 --- a/doc/api/group_badges.md +++ b/doc/api/group_badges.md @@ -1,5 +1,8 @@ # Group badges API +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17082) +in GitLab 10.6. + ## Placeholder tokens Badges support placeholders that will be replaced in real time in both the link and image URL. The allowed placeholders are: @@ -182,7 +185,7 @@ curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/a Example response: ```json -{ +{ "link_url": "http://example.com/ci_status.svg?project=%{project_path}&ref=%{default_branch}", "image_url": "https://shields.io/my/badge", "rendered_link_url": "http://example.com/ci_status.svg?project=example-org/example-project&ref=master", diff --git a/doc/api/project_badges.md b/doc/api/project_badges.md index 3f6e348b5b4..94389273e9c 100644 --- a/doc/api/project_badges.md +++ b/doc/api/project_badges.md @@ -1,5 +1,8 @@ # Project badges API +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17082) +in GitLab 10.6. + ## Placeholder tokens Badges support placeholders that will be replaced in real time in both the link and image URL. The allowed placeholders are: @@ -179,7 +182,7 @@ curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/a Example response: ```json -{ +{ "link_url": "http://example.com/ci_status.svg?project=%{project_path}&ref=%{default_branch}", "image_url": "https://shields.io/my/badge", "rendered_link_url": "http://example.com/ci_status.svg?project=example-org/example-project&ref=master", diff --git a/doc/api/project_import_export.md b/doc/api/project_import_export.md index 995f10571d0..d8f61852b11 100644 --- a/doc/api/project_import_export.md +++ b/doc/api/project_import_export.md @@ -111,6 +111,7 @@ POST /projects/import | `namespace` | integer/string | no | The ID or path of the namespace that the project will be imported to. Defaults to the current user's namespace | | `file` | string | yes | The file to be uploaded | | `path` | string | yes | Name and path for new project | +| `overwrite` | boolean | no | If there is a project with the same path the import will overwrite it. Default to false | | `override_params` | Hash | no | Supports all fields defined in the [Project API](projects.md)] | The override params passed will take precendence over all values defined inside the export file. diff --git a/doc/ci/environments.md b/doc/ci/environments.md index 58c4a71cef9..b3d9f0bc96c 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -247,10 +247,19 @@ declaring their names dynamically in `.gitlab-ci.yml`. Dynamic environments is the basis of [Review apps](review_apps/index.md). >**Note:** -The `name` and `url` parameters can use any of the defined CI variables, +The `name` and `url` parameters can use most of the defined CI variables, including predefined, secure variables and `.gitlab-ci.yml` -[`variables`](yaml/README.md#variables). -You however cannot use variables defined under `script` or on the Runner's side. +[`variables`](yaml/README.md#variables). You however cannot use variables +defined under `script` or on the Runner's side. There are other variables that +are unsupported in environment name context: +- `CI_JOB_ID` +- `CI_JOB_TOKEN` +- `CI_BUILD_ID` +- `CI_BUILD_TOKEN` +- `CI_REGISTRY_USER` +- `CI_REGISTRY_PASSWORD` +- `CI_REPOSITORY_URL` +- `CI_ENVIRONMENT_URL` GitLab Runner exposes various [environment variables][variables] when a job runs, and as such, you can use them as environment names. Let's add another job in diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 9f268f47e6f..4a504a98902 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -454,8 +454,8 @@ export CI_REGISTRY_PASSWORD="longalfanumstring" > Variables expressions were added in GitLab 10.7. It is possible to use variables expressions with only / except policies in -`.gitlab-ci.yml`. By using this approach you can limit what builds are going to -be created within a pipeline after pushing code to GitLab. +`.gitlab-ci.yml`. By using this approach you can limit what jobs are going to +be created within a pipeline after pushing a code to GitLab. This is particularly useful in combination with secret variables and triggered pipeline variables. @@ -470,22 +470,21 @@ deploy: - $STAGING ``` -Each provided variables expression is going to be evaluated before creating -a pipeline. +Each expression provided is going to be evaluated before creating a pipeline. If any of the conditions in `variables` evaluates to truth when using `only`, a new job is going to be created. If any of the expressions evaluates to truth when `except` is being used, a job is not going to be created. -This follows usual rules for `only` / `except` policies. +This follows usual rules for [`only` / `except` policies][builds-policies]. ### Supported syntax -Below you can find currently supported syntax reference: +Below you can find supported syntax reference: 1. Equality matching using a string - Example: `$VARIABLE == "some value"` + > Example: `$VARIABLE == "some value"` You can use equality operator `==` to compare a variable content to a string. We support both, double quotes and single quotes to define a string @@ -494,26 +493,62 @@ Below you can find currently supported syntax reference: 1. Checking for an undefined value - It sometimes happens that you want to check whether variable is defined or - not. To do that, you can compare variable to `null` value, like + > Example: `$VARIABLE == null` + + It sometimes happens that you want to check whether a variable is defined + or not. To do that, you can compare a variable to `null` keyword, like `$VARIABLE == null`. This expression is going to evaluate to truth if - variable is not set. + variable is not defined. 1. Checking for an empty variable + > Example: `$VARIABLE == ""` + If you want to check whether a variable is defined, but is empty, you can simply compare it against an empty string, like `$VAR == ''`. 1. Comparing two variables - It is possible to compare two variables. `$VARIABLE_1 == $VARIABLE_2`. + > Example: `$VARIABLE_1 == $VARIABLE_2` + + It is possible to compare two variables. This is going to compare values + of these variables. 1. Variable presence check + > Example: `$STAGING` + If you only want to create a job when there is some variable present, which means that it is defined and non-empty, you can simply use variable name as an expression, like `$STAGING`. If `$STAGING` variable is defined, and is non empty, expression will evaluate to truth. + `$STAGING` value needs to a string, with length higher than zero. + Variable that contains only whitespace characters is not an empty variable. + +### Unsupported predefined variables + +Because GitLab evaluates variables before creating jobs, we do not support a +few variables that depend on persistence layer, like `$CI_JOB_ID`. + +Environments (like `production` or `staging`) are also being created based on +what jobs pipeline consists of, thus some environment-specific variables are +not supported as well. + +We do not support variables containing tokens because of security reasons. + +You can find a full list of unsupported variables below: + +- `CI_JOB_ID` +- `CI_JOB_TOKEN` +- `CI_BUILD_ID` +- `CI_BUILD_TOKEN` +- `CI_REGISTRY_USER` +- `CI_REGISTRY_PASSWORD` +- `CI_REPOSITORY_URL` +- `CI_ENVIRONMENT_URL` + +These variables are also not supported in a contex of a +[dynamic environment name][dynamic-environments]. [ce-13784]: https://gitlab.com/gitlab-org/gitlab-ce/issues/13784 "Simple protection of CI secret variables" [eep]: https://about.gitlab.com/products/ "Available only in GitLab Premium" @@ -525,3 +560,5 @@ Below you can find currently supported syntax reference: [triggered]: ../triggers/README.md [triggers]: ../triggers/README.md#pass-job-variables-to-a-trigger [subgroups]: ../../user/group/subgroups/index.md +[builds-policies]: ../yaml/README.md#only-and-except-complex +[dynamic-environments]: ../environments.md#dynamic-environments diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index be114e7008e..68aa64b3834 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -354,7 +354,7 @@ deploy: - $STAGING ``` -Learn more about variables expressions on a separate page. +Learn more about variables expressions on [a separate page][variables-expressions]. ## `tags` @@ -1574,3 +1574,4 @@ CI with various languages. [ce-7447]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7447 [ce-12909]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12909 [schedules]: ../../user/project/pipelines/schedules.md +[variables-expressions]: ../variables/README.md#variables-expressions diff --git a/doc/development/changelog.md b/doc/development/changelog.md index 1962392a9eb..d5a4acff481 100644 --- a/doc/development/changelog.md +++ b/doc/development/changelog.md @@ -44,6 +44,7 @@ the `author` field. GitLab team members **should not**. - _Any_ contribution from a community member, no matter how small, **may** have a changelog entry regardless of these guidelines if the contributor wants one. Example: "Fixed a typo on the search results page. (Jane Smith)" +- Performance improvements **should** have a changelog entry. ## Writing good changelog entries diff --git a/doc/development/emails.md b/doc/development/emails.md index 4dbf064fd75..73cac82caf0 100644 --- a/doc/development/emails.md +++ b/doc/development/emails.md @@ -74,6 +74,24 @@ See the [Rails guides] for more info. 1. Reply by email should now be working. +## Email namespace + +If you need to implement a new feature which requires a new email handler, follow these rules: + + - You must choose a namespace. The namespace cannot contain `/` or `+`, and cannot match `\h{16}`. + - If your feature is related to a project, you will append the namespace **after** the project path, separated by a `+` + - If you have different actions in the namespace, you add the actions **after** the namespace separated by a `+`. The action name cannot contain `/` or `+`, , and cannot match `\h{16}`. + - You will register your handlers in `lib/gitlab/email/handler.rb` + +Therefore, these are the only valid formats for an email handler: + + - `path/to/project+namespace` + - `path/to/project+namespace+action` + - `namespace` + - `namespace+action` + +Please note that `path/to/project` is used in GitLab Premium as handler for the Service Desk feature. + --- [Return to Development documentation](README.md) diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md index d5f77191938..7baccb796c6 100644 --- a/doc/user/gitlab_com/index.md +++ b/doc/user/gitlab_com/index.md @@ -72,15 +72,23 @@ The maximum size your Git repository is allowed to be including LFS. ## Shared Runners Shared Runners on GitLab.com run in [autoscale mode] and powered by -DigitalOcean. Autoscaling means reduced waiting times to spin up builds, -and isolated VMs for each project, thus maximizing security. +Google Cloud Platform and DigitalOcean. Autoscaling means reduced +waiting times to spin up CI/CD jobs, and isolated VMs for each project, +thus maximizing security. They're free to use for public open source projects and limited to 2000 CI minutes per month per group for private projects. Read about all [GitLab.com plans](https://about.gitlab.com/pricing/). -All your builds run on 2GB (RAM) ephemeral instances, with CoreOS and the latest -Docker Engine installed. The default region of the VMs is NYC. +In case of DigitalOcean based Runners, all your CI/CD jobs run on ephemeral +instances with 2GB of RAM, CoreOS and the latest Docker Engine installed. +Instances provide 2 vCPUs and 60GB of SSD disk space. The default region of the +VMs is NYC1. + +In case of Google Cloud Platform based Runners, all your CI/CD jobs run on +ephemeral instances with 3.75GB of RAM, CoreOS and the latest Docker Engine +installed. Instances provide 1 vCPU and 25GB of HDD disk space. The default +region of the VMs is US East1. Below are the shared Runners settings. @@ -88,52 +96,116 @@ Below are the shared Runners settings. | ----------- | ----------------- | ---------- | | [GitLab Runner] | [Runner versions dashboard][ci_version_dashboard] | - | | Executor | `docker+machine` | - | -| Default Docker image | `ruby:2.1` | - | +| Default Docker image | `ruby:2.5` | - | | `privileged` (run [Docker in Docker]) | `true` | `false` | -[ci_version_dashboard]: https://monitor.gitlab.net/dashboard/db/ci?refresh=5m&orgId=1&panelId=12&fullscreen&from=now-1h&to=now&var-runner_type=All&var-cache_server=All&var-gl_monitor_fqdn=postgres-01.db.prd.gitlab.com&var-has_minutes=yes&var-hanging_droplets_cleaner=All&var-droplet_zero_machines_cleaner=All&var-runner_job_failure_reason=All&theme=light +[ci_version_dashboard]: https://monitor.gitlab.net/dashboard/db/ci?from=now-1h&to=now&refresh=5m&orgId=1&panelId=12&fullscreen&theme=light ### `config.toml` The full contents of our `config.toml` are: +**DigitalOcean** + ```toml +concurrent = X +check_interval = 1 +metrics_server = "X" +sentry_dsn = "X" + [[runners]] name = "docker-auto-scale" - limit = X request_concurrency = X - url = "https://gitlab.com/ci" + url = "https://gitlab.com/" token = "SHARED_RUNNER_TOKEN" executor = "docker+machine" environment = [ "DOCKER_DRIVER=overlay2" ] + limit = X [runners.docker] - image = "ruby:2.1" + image = "ruby:2.5" privileged = true [runners.machine] - IdleCount = 40 + IdleCount = 20 IdleTime = 1800 + OffPeakPeriods = ["* * * * * sat,sun *"] + OffPeakTimezone = "UTC" + OffPeakIdleCount = 5 + OffPeakIdleTime = 1800 MaxBuilds = 1 + MachineName = "srm-%s" MachineDriver = "digitalocean" - MachineName = "machine-%s-digital-ocean-2gb" MachineOptions = [ - "digitalocean-image=coreos-stable", + "digitalocean-image=X", "digitalocean-ssh-user=core", - "digitalocean-access-token=DIGITAL_OCEAN_ACCESS_TOKEN", "digitalocean-region=nyc1", - "digitalocean-size=2gb", + "digitalocean-size=s-2vcpu-2gb", "digitalocean-private-networking", - "digitalocean-userdata=/etc/gitlab-runner/cloudinit.sh", - "engine-registry-mirror=http://IP_TO_OUR_REGISTRY_MIRROR" + "digitalocean-tags=shared_runners,gitlab_com", + "engine-registry-mirror=http://INTERNAL_IP_OF_OUR_REGISTRY_MIRROR", + "digitalocean-access-token=DIGITAL_OCEAN_ACCESS_TOKEN", ] [runners.cache] Type = "s3" - ServerAddress = "IP_TO_OUR_CACHE_SERVER" + BucketName = "runner" + Insecure = true + Shared = true + ServerAddress = "INTERNAL_IP_OF_OUR_CACHE_SERVER" AccessKey = "ACCESS_KEY" SecretKey = "ACCESS_SECRET_KEY" +``` + +**Google Cloud Platform** + +```toml +concurrent = X +check_interval = 1 +metrics_server = "X" +sentry_dsn = "X" + +[[runners]] + name = "docker-auto-scale" + request_concurrency = X + url = "https://gitlab.com/" + token = "SHARED_RUNNER_TOKEN" + executor = "docker+machine" + environment = [ + "DOCKER_DRIVER=overlay2" + ] + limit = X + [runners.docker] + image = "ruby:2.5" + privileged = true + [runners.machine] + IdleCount = 20 + IdleTime = 1800 + OffPeakPeriods = ["* * * * * sat,sun *"] + OffPeakTimezone = "UTC" + OffPeakIdleCount = 5 + OffPeakIdleTime = 1800 + MaxBuilds = 1 + MachineName = "srm-%s" + MachineDriver = "google" + MachineOptions = [ + "google-project=PROJECT", + "google-disk-size=25", + "google-machine-type=n1-standard-1", + "google-username=core", + "google-tags=gitlab-com,srm", + "google-use-internal-ip", + "google-zone=us-east1-d", + "google-machine-image=PROJECT/global/images/IMAGE", + "engine-registry-mirror=http://INTERNAL_IP_OF_OUR_REGISTRY_MIRROR" + ] + [runners.cache] + Type = "s3" BucketName = "runner" + Insecure = true Shared = true + ServerAddress = "INTERNAL_IP_OF_OUR_CACHE_SERVER" + AccessKey = "ACCESS_KEY" + SecretKey = "ACCESS_SECRET_KEY" ``` ## Sidekiq diff --git a/doc/user/project/badges.md b/doc/user/project/badges.md new file mode 100644 index 00000000000..c4e59444ef7 --- /dev/null +++ b/doc/user/project/badges.md @@ -0,0 +1,73 @@ +# Badges + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/41174) +in GitLab 10.7. + +Badges are a unified way to present condensed pieces of information about your +projects. They consist of a small image and additionally a URL that the image +points to. Examples for badges can be the [pipeline status], [test coverage], +or ways to contact the project maintainers. + +![Badges on Project overview page](img/project_overview_badges.png) + +## Project badges + +Badges can be added to a project and will then be visible on the project's overview page. +If you find that you have to add the same badges to several projects, you may want to add them at the [group level](#group-badges). + +To add a new badge to a project: + +1. Navigate to your project's **Settings > Badges**. +1. Under "Link", enter the URL that the badges should point to and under + "Badge image URL" the URL of the image that should be displayed. +1. Submit the badge by clicking the **Add badge** button. + +After adding a badge to a project, you can see it in the list below the form. +You can edit it by clicking on the pen icon next to it or to delete it by +clicking on the trash icon. + +Badges associated with a group can only be edited or deleted on the +[group level](#group-badges). + +## Group badges + +Badges can be added to a group and will then be visible on every project's +overview page that's under that group. In this case, they cannot be edited or +deleted on the project level. If you need to have individual badges for each +project, consider adding them on the [project level](#project-badges) or use +[placeholders](#placeholders). + +To add a new badge to a group: + +1. Navigate to your group's **Settings > Project Badges**. +1. Under "Link", enter the URL that the badges should point to and under + "Badge image URL" the URL of the image that should be displayed. +1. Submit the badge by clicking the **Add badge** button. + +After adding a badge to a group, you can see it in the list below the form. +You can edit the badge by clicking on the pen icon next to it or to delete it +by clicking on the trash icon. + +Badges directly associated with a project can be configured on the +[project level](#project-badges). + +## Placeholders + +The URL a badge points to, as well as the image URL, can contain placeholders +which will be evaluated when displaying the badge. The following placeholders +are available: + +- `%{project_path}`: Path of a project including the parent groups +- `%{project_id}`: Database ID associated with a project +- `%{default_branch}`: Default branch name configured for a project's repository +- `%{commit_sha}`: ID of the most recent commit to the default branch of a + project's repository + +## API + +You can also configure badges via the GitLab API. As in the settings, there is +a distinction between endpoints for badges on the +[project level](../../api/project_badges.md) and [group level](../../api/group_badges.md). + +[pipeline status]: pipelines/settings.md#pipeline-status-badge +[test coverage]: pipelines/settings.md#test-coverage-report-badge diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md index 394aa9209e4..9c5e3509046 100644 --- a/doc/user/project/container_registry.md +++ b/doc/user/project/container_registry.md @@ -115,15 +115,16 @@ and [Using the GitLab Container Registry documentation](../../ci/docker/using_do ## Using with private projects -> [Introduced][ce-11845] in GitLab 9.3. +> Personal Access tokens were [introduced][ce-11845] in GitLab 9.3. +> Project Deploy Tokens were [introduced][ce-17894] in GitLab 10.7 If a project is private, credentials will need to be provided for authorization. -The preferred way to do this, is by using [personal access tokens][pat]. -The minimal scope needed is `read_registry`. +The preferred way to do this, is either by using a [personal access tokens][pat] or a [project deploy token][pdt]. +The minimal scope needed for both of them is `read_registry`. Example of using a personal access token: ``` -docker login registry.example.com -u <your_username> -p <your_personal_access_token> +docker login registry.example.com -u <your_username> -p <your_access_token> ``` ## Troubleshooting the GitLab Container Registry @@ -270,5 +271,7 @@ Once the right permissions were set, the error will go away. [ce-4040]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4040 [ce-11845]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11845 +[ce-17894]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17894 [docker-docs]: https://docs.docker.com/engine/userguide/intro/ [pat]: ../profile/personal_access_tokens.md +[pdt]: ../project/deploy_tokens/index.md diff --git a/doc/user/project/deploy_tokens/img/deploy_tokens.png b/doc/user/project/deploy_tokens/img/deploy_tokens.png Binary files differnew file mode 100644 index 00000000000..7e2d67a3120 --- /dev/null +++ b/doc/user/project/deploy_tokens/img/deploy_tokens.png diff --git a/doc/user/project/deploy_tokens/index.md b/doc/user/project/deploy_tokens/index.md new file mode 100644 index 00000000000..86fc58020e8 --- /dev/null +++ b/doc/user/project/deploy_tokens/index.md @@ -0,0 +1,76 @@ +# Deploy Tokens + +> [Introduced][ce-17894] in GitLab 10.7. + +Deploy tokens allow to download (through `git clone`), or read the container registry images of a project without the need of having a user and a password. + +Please note, that the expiration of deploy tokens happens on the date you define, +at midnight UTC and that they can be only managed by [masters](https://docs.gitlab.com/ee/user/permissions.html). + +## Creating a Deploy Token + +You can create as many deploy tokens as you like from the settings of your project: + +1. Log in to your GitLab account. +1. Go to the project you want to create Deploy Tokens for. +1. Go to **Settings** > **Repository** +1. Click on "Expand" on **Deploy Tokens** section +1. Choose a name and optionally an expiry date for the token. +1. Choose the [desired scopes](#limiting-scopes-of-a-deploy-token). +1. Click on **Create deploy token**. +1. Save the deploy token somewhere safe. Once you leave or refresh + the page, **you won't be able to access it again**. + +![Personal access tokens page](img/deploy_tokens.png) + +## Revoking a personal access token + +At any time, you can revoke any deploy token by just clicking the +respective **Revoke** button under the 'Active deploy tokens' area. + +## Limiting scopes of a deploy token + +Deploy tokens can be created with two different scopes that allow various +actions that a given token can perform. The available scopes are depicted in +the following table. + +| Scope | Description | +| ----- | ----------- | +| `read_repository` | Allows read-access to the repository through `git clone` | +| `read_registry` | Allows read-access to [container registry] images if a project is private and authorization is required. | + +## Usage + +### Git clone a repository + +To download a repository using a Deploy Token, you just need to: + +1. Create a Deploy Token with `read_repository` as a scope. +2. Take note of your `username` and `token` +3. `git clone` the project using the Deploy Token: + + +```bash +git clone http://<username>:<deploy_token>@gitlab.example.com/tanuki/awesome_project.git +``` + +Just replace `<username>` and `<deploy_token>` with the proper values + +### Read container registry images + +To read the container registry images, you'll need to: + +1. Create a Deploy Token with `read_registry` as a scope. +2. Take note of your `username` and `token` +3. Log in to GitLab’s Container Registry using the deploy token: + +``` +docker login registry.example.com -u <username> -p <deploy_token> +``` + +Just replace `<username>` and `<deploy_token>` with the proper values. Then you can simply +pull images from your Container Registry. + +[ce-17894]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17894 +[ce-11845]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11845 +[container registry]: ../container_registry.md diff --git a/doc/user/project/img/project_overview_badges.png b/doc/user/project/img/project_overview_badges.png Binary files differnew file mode 100644 index 00000000000..3067a7dfa13 --- /dev/null +++ b/doc/user/project/img/project_overview_badges.png diff --git a/doc/user/project/index.md b/doc/user/project/index.md index f94e93dd7d8..5ce4ebfa811 100644 --- a/doc/user/project/index.md +++ b/doc/user/project/index.md @@ -27,6 +27,7 @@ integrated platform - [Protected tags](protected_tags.md): Control over who has permission to create tags, and prevent accidental update or deletion - [Signing commits](gpg_signed_commits/index.md): use GPG to sign your commits + - [Deploy tokens](deploy_tokens/index.md): Manage project-based deploy tokens that allow permanent access to the repository and Container Registry. - [Merge Requests](merge_requests/index.md): Apply your branching strategy and get reviewed by your team - [Merge Request Approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) (**Starter/Premium**): Ask for approval before @@ -73,6 +74,7 @@ website with GitLab Pages - [Cycle Analytics](cycle_analytics.md): Review your development lifecycle - [Syntax highlighting](highlighting.md): An alternative to customize your code blocks, overriding GitLab's default choice of language +- [Badges](badges.md): Badges for the project overview ### Project's integrations diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md index 6cead7b9961..14f2e522f01 100644 --- a/doc/user/project/pipelines/settings.md +++ b/doc/user/project/pipelines/settings.md @@ -106,7 +106,7 @@ If you want to auto-cancel all pending non-HEAD pipelines on branch, when new pipeline will be created (after your git push or manually from UI), check **Auto-cancel pending pipelines** checkbox and save the changes. -## Badges +## Pipeline Badges In the pipelines settings page you can find pipeline status and test coverage badges for your project. The latest successful pipeline will be used to read diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb index a3e4459f169..f5950145348 100644 --- a/features/steps/shared/builds.rb +++ b/features/steps/shared/builds.rb @@ -11,7 +11,7 @@ module SharedBuilds step 'project has a recent build' do @pipeline = create(:ci_empty_pipeline, project: @project, sha: @project.commit.sha, ref: 'master') - @build = create(:ci_build, :running, :coverage, pipeline: @pipeline) + @build = create(:ci_build, :running, :coverage, :trace_artifact, pipeline: @pipeline) end step 'recent build is successful' do diff --git a/lib/api/badges.rb b/lib/api/badges.rb index 334948b2995..8ceffe9c5ef 100644 --- a/lib/api/badges.rb +++ b/lib/api/badges.rb @@ -127,6 +127,7 @@ module API end destroy_conditionally!(badge) + body false end end end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index a582aa0ec2c..61dab1dd5cb 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -468,8 +468,8 @@ module API header(*Gitlab::Workhorse.send_git_blob(repository, blob)) end - def send_git_archive(repository, ref:, format:) - header(*Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format)) + def send_git_archive(repository, **kwargs) + header(*Gitlab::Workhorse.send_git_archive(repository, **kwargs)) end def send_artifacts_entry(build, entry) diff --git a/lib/api/project_import.rb b/lib/api/project_import.rb index 303b58a5942..bc5152e539f 100644 --- a/lib/api/project_import.rb +++ b/lib/api/project_import.rb @@ -26,6 +26,7 @@ module API requires :path, type: String, desc: 'The new project path and name' requires :file, type: File, desc: 'The project export file to be imported' optional :namespace, type: String, desc: "The ID or name of the namespace that the project will be imported into. Defaults to the current user's namespace." + optional :overwrite, type: Boolean, default: false, desc: 'If there is a project in the same namespace and with the same name overwrite it' optional :override_params, type: Hash, desc: 'New project params to override values in the export' do @@ -50,7 +51,8 @@ module API project_params = { path: import_params[:path], namespace_id: namespace.id, - file: import_params[:file]['tempfile'] + file: import_params[:file]['tempfile'], + overwrite: import_params[:overwrite] } override_params = import_params.delete(:override_params) diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index 9638c53a1df..2396dc73f0e 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -88,7 +88,7 @@ module API end get ':id/repository/archive', requirements: { format: Gitlab::PathRegex.archive_formats_regex } do begin - send_git_archive user_project.repository, ref: params[:sha], format: params[:format] + send_git_archive user_project.repository, ref: params[:sha], format: params[:format], append_sha: true rescue not_found!('File') end diff --git a/lib/api/v3/repositories.rb b/lib/api/v3/repositories.rb index 5b54734bb45..f701d64e886 100644 --- a/lib/api/v3/repositories.rb +++ b/lib/api/v3/repositories.rb @@ -75,7 +75,7 @@ module API end get ':id/repository/archive', requirements: { format: Gitlab::PathRegex.archive_formats_regex } do begin - send_git_archive user_project.repository, ref: params[:sha], format: params[:format] + send_git_archive user_project.repository, ref: params[:sha], format: params[:format], append_sha: true rescue not_found!('File') end diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index 6efaed7e624..a848154b2d4 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -215,6 +215,10 @@ module Banzai extras << "comment #{$1}" end + extension = matches[:extension] if matches.names.include?("extension") + + extras << extension if extension + extras end diff --git a/lib/forever.rb b/lib/forever.rb new file mode 100644 index 00000000000..7df17912544 --- /dev/null +++ b/lib/forever.rb @@ -0,0 +1,13 @@ +class Forever + POSTGRESQL_DATE = DateTime.new(3000, 1, 1) + MYSQL_DATE = DateTime.new(2038, 01, 19) + + # MySQL timestamp has a range of '1970-01-01 00:00:01' UTC to '2038-01-19 03:14:07' UTC + def self.date + if Gitlab::Database.postgresql? + POSTGRESQL_DATE + else + MYSQL_DATE + end + end +end diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 6af763faf10..2a44e11efb6 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -5,7 +5,7 @@ module Gitlab REGISTRY_SCOPES = [:read_registry].freeze # Scopes used for GitLab API access - API_SCOPES = [:api, :read_user, :sudo].freeze + API_SCOPES = [:api, :read_user, :sudo, :read_repository].freeze # Scopes used for OpenID Connect OPENID_SCOPES = [:openid].freeze @@ -26,6 +26,7 @@ module Gitlab lfs_token_check(login, password, project) || oauth_access_token_check(login, password) || personal_access_token_check(password) || + deploy_token_check(login, password) || user_with_password_for_git(login, password) || Gitlab::Auth::Result.new @@ -163,7 +164,8 @@ module Gitlab def abilities_for_scopes(scopes) abilities_by_scope = { api: full_authentication_abilities, - read_registry: [:read_container_image] + read_registry: [:read_container_image], + read_repository: [:download_code] } scopes.flat_map do |scope| @@ -171,6 +173,22 @@ module Gitlab end.uniq end + def deploy_token_check(login, password) + return unless password.present? + + token = + DeployToken.active.find_by(token: password) + + return unless token && login + return if login != token.username + + scopes = abilities_for_scopes(token.scopes) + + if valid_scoped_token?(token, available_scopes) + Gitlab::Auth::Result.new(token, token.project, :deploy_token, scopes) + end + end + def lfs_token_check(login, password, project) deploy_key_matches = login.match(/\Alfs\+deploy-key-(\d+)\z/) diff --git a/lib/gitlab/ci/status/build/cancelable.rb b/lib/gitlab/ci/status/build/cancelable.rb index 2d9166d6bdd..024047d4983 100644 --- a/lib/gitlab/ci/status/build/cancelable.rb +++ b/lib/gitlab/ci/status/build/cancelable.rb @@ -23,6 +23,10 @@ module Gitlab 'Cancel' end + def action_button_title + _('Cancel this job') + end + def self.matches?(build, user) build.cancelable? end diff --git a/lib/gitlab/ci/status/build/canceled.rb b/lib/gitlab/ci/status/build/canceled.rb new file mode 100644 index 00000000000..c83e2734a73 --- /dev/null +++ b/lib/gitlab/ci/status/build/canceled.rb @@ -0,0 +1,21 @@ +module Gitlab + module Ci + module Status + module Build + class Canceled < Status::Extended + def illustration + { + image: 'illustrations/canceled-job_empty.svg', + size: 'svg-430', + title: _('This job has been canceled') + } + end + + def self.matches?(build, user) + build.canceled? + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/build/created.rb b/lib/gitlab/ci/status/build/created.rb new file mode 100644 index 00000000000..5be8e9de425 --- /dev/null +++ b/lib/gitlab/ci/status/build/created.rb @@ -0,0 +1,22 @@ +module Gitlab + module Ci + module Status + module Build + class Created < Status::Extended + def illustration + { + image: 'illustrations/job_not_triggered.svg', + size: 'svg-306', + title: _('This job has not been triggered yet'), + content: _('This job depends on upstream jobs that need to succeed in order for this job to be triggered') + } + end + + def self.matches?(build, user) + build.created? + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/build/erased.rb b/lib/gitlab/ci/status/build/erased.rb new file mode 100644 index 00000000000..3a5113b16b6 --- /dev/null +++ b/lib/gitlab/ci/status/build/erased.rb @@ -0,0 +1,21 @@ +module Gitlab + module Ci + module Status + module Build + class Erased < Status::Extended + def illustration + { + image: 'illustrations/skipped-job_empty.svg', + size: 'svg-430', + title: _('Job has been erased') + } + end + + def self.matches?(build, user) + build.erased? + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/build/factory.rb b/lib/gitlab/ci/status/build/factory.rb index 20a319caf86..2b26ebb45a1 100644 --- a/lib/gitlab/ci/status/build/factory.rb +++ b/lib/gitlab/ci/status/build/factory.rb @@ -4,7 +4,13 @@ module Gitlab module Build class Factory < Status::Factory def self.extended_statuses - [[Status::Build::Cancelable, + [[Status::Build::Erased, + Status::Build::Manual, + Status::Build::Canceled, + Status::Build::Created, + Status::Build::Pending, + Status::Build::Skipped], + [Status::Build::Cancelable, Status::Build::Retryable], [Status::Build::Failed], [Status::Build::FailedAllowed, diff --git a/lib/gitlab/ci/status/build/manual.rb b/lib/gitlab/ci/status/build/manual.rb new file mode 100644 index 00000000000..042da6392d3 --- /dev/null +++ b/lib/gitlab/ci/status/build/manual.rb @@ -0,0 +1,22 @@ +module Gitlab + module Ci + module Status + module Build + class Manual < Status::Extended + def illustration + { + image: 'illustrations/manual_action.svg', + size: 'svg-394', + title: _('This job requires a manual action'), + content: _('This job depends on a user to trigger its process. Often they are used to deploy code to production environments') + } + end + + def self.matches?(build, user) + build.playable? + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/build/pending.rb b/lib/gitlab/ci/status/build/pending.rb new file mode 100644 index 00000000000..9dd9a27ad57 --- /dev/null +++ b/lib/gitlab/ci/status/build/pending.rb @@ -0,0 +1,22 @@ +module Gitlab + module Ci + module Status + module Build + class Pending < Status::Extended + def illustration + { + image: 'illustrations/pending_job_empty.svg', + size: 'svg-430', + title: _('This job has not started yet'), + content: _('This job is in pending state and is waiting to be picked by a runner') + } + end + + def self.matches?(build, user) + build.pending? + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb index b7b45466d3b..a8b9ebf0803 100644 --- a/lib/gitlab/ci/status/build/play.rb +++ b/lib/gitlab/ci/status/build/play.rb @@ -19,6 +19,10 @@ module Gitlab 'Play' end + def action_button_title + _('Trigger this manual action') + end + def action_path play_project_job_path(subject.project, subject) end diff --git a/lib/gitlab/ci/status/build/retryable.rb b/lib/gitlab/ci/status/build/retryable.rb index 44ffe783e50..5aeb8e51480 100644 --- a/lib/gitlab/ci/status/build/retryable.rb +++ b/lib/gitlab/ci/status/build/retryable.rb @@ -15,6 +15,10 @@ module Gitlab 'Retry' end + def action_button_title + _('Retry this job') + end + def action_path retry_project_job_path(subject.project, subject) end diff --git a/lib/gitlab/ci/status/build/skipped.rb b/lib/gitlab/ci/status/build/skipped.rb new file mode 100644 index 00000000000..3e678d0baee --- /dev/null +++ b/lib/gitlab/ci/status/build/skipped.rb @@ -0,0 +1,21 @@ +module Gitlab + module Ci + module Status + module Build + class Skipped < Status::Extended + def illustration + { + image: 'illustrations/skipped-job_empty.svg', + size: 'svg-430', + title: _('This job has been skipped') + } + end + + def self.matches?(build, user) + build.skipped? + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb index 46e730797e4..dea838bfa39 100644 --- a/lib/gitlab/ci/status/build/stop.rb +++ b/lib/gitlab/ci/status/build/stop.rb @@ -19,6 +19,10 @@ module Gitlab 'Stop' end + def action_button_title + _('Stop this environment') + end + def action_path play_project_job_path(subject.project, subject) end diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb index daab6bb2de5..9d6a2f51c11 100644 --- a/lib/gitlab/ci/status/core.rb +++ b/lib/gitlab/ci/status/core.rb @@ -22,6 +22,10 @@ module Gitlab raise NotImplementedError end + def illustration + raise NotImplementedError + end + def label raise NotImplementedError end @@ -58,6 +62,10 @@ module Gitlab raise NotImplementedError end + def action_button_title + raise NotImplementedError + end + # Hint that appears on all the pipeline graph tooltips and builds on the right sidebar in Job detail view def status_tooltip label diff --git a/lib/gitlab/email/handler/create_issue_handler.rb b/lib/gitlab/email/handler/create_issue_handler.rb index a616a80e8f5..05a60deb7d3 100644 --- a/lib/gitlab/email/handler/create_issue_handler.rb +++ b/lib/gitlab/email/handler/create_issue_handler.rb @@ -14,7 +14,7 @@ module Gitlab end def can_handle? - !incoming_email_token.nil? + !incoming_email_token.nil? && !incoming_email_token.include?("+") && !mail_key.include?(Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX) end def execute diff --git a/lib/gitlab/git/checksum.rb b/lib/gitlab/git/checksum.rb deleted file mode 100644 index 3ef0f0a8854..00000000000 --- a/lib/gitlab/git/checksum.rb +++ /dev/null @@ -1,82 +0,0 @@ -module Gitlab - module Git - class Checksum - include Gitlab::Git::Popen - - EMPTY_REPOSITORY_CHECKSUM = '0000000000000000000000000000000000000000'.freeze - - Failure = Class.new(StandardError) - - attr_reader :path, :relative_path, :storage, :storage_path - - def initialize(storage, relative_path) - @storage = storage - @storage_path = Gitlab.config.repositories.storages[storage].legacy_disk_path - @relative_path = "#{relative_path}.git" - @path = File.join(storage_path, @relative_path) - end - - def calculate - unless repository_exists? - failure!(Gitlab::Git::Repository::NoRepository, 'No repository for such path') - end - - calculate_checksum_by_shelling_out - end - - private - - def repository_exists? - raw_repository.exists? - end - - def calculate_checksum_by_shelling_out - args = %W(--git-dir=#{path} show-ref --heads --tags) - output, status = run_git(args) - - if status&.zero? - refs = output.split("\n") - - result = refs.inject(nil) do |checksum, ref| - value = Digest::SHA1.hexdigest(ref).hex - - if checksum.nil? - value - else - checksum ^ value - end - end - - result.to_s(16) - else - # Empty repositories return with a non-zero status and an empty output. - if output&.empty? - EMPTY_REPOSITORY_CHECKSUM - else - failure!(Gitlab::Git::Checksum::Failure, output) - end - end - end - - def failure!(klass, message) - Gitlab::GitLogger.error("'git show-ref --heads --tags' in #{path}: #{message}") - - raise klass.new("Could not calculate the checksum for #{path}: #{message}") - end - - def circuit_breaker - @circuit_breaker ||= Gitlab::Git::Storage::CircuitBreaker.for_storage(storage) - end - - def raw_repository - Gitlab::Git::Repository.new(storage, relative_path, nil) - end - - def run_git(args) - circuit_breaker.perform do - popen([Gitlab.config.git.bin_path, *args], path) - end - end - end - end -end diff --git a/lib/gitlab/git/conflict/resolver.rb b/lib/gitlab/git/conflict/resolver.rb index 07b7e811a34..c3cb0264112 100644 --- a/lib/gitlab/git/conflict/resolver.rb +++ b/lib/gitlab/git/conflict/resolver.rb @@ -23,7 +23,7 @@ module Gitlab end rescue GRPC::FailedPrecondition => e raise Gitlab::Git::Conflict::Resolver::ConflictSideMissing.new(e.message) - rescue Rugged::OdbError, GRPC::BadStatus => e + rescue Rugged::ReferenceError, Rugged::OdbError, GRPC::BadStatus => e raise Gitlab::Git::CommandError.new(e) end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 8d97bfb0e6a..f1b575bd872 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -23,6 +23,7 @@ module Gitlab SQUASH_WORKTREE_PREFIX = 'squash'.freeze GITALY_INTERNAL_URL = 'ssh://gitaly/internal.git'.freeze GITLAB_PROJECTS_TIMEOUT = Gitlab.config.gitlab_shell.git_timeout + EMPTY_REPOSITORY_CHECKSUM = '0000000000000000000000000000000000000000'.freeze NoRepository = Class.new(StandardError) InvalidBlobName = Class.new(StandardError) @@ -31,6 +32,7 @@ module Gitlab DeleteBranchError = Class.new(StandardError) CreateTreeError = Class.new(StandardError) TagExistsError = Class.new(StandardError) + ChecksumError = Class.new(StandardError) class << self # Unlike `new`, `create` takes the repository path @@ -394,17 +396,24 @@ module Gitlab nil end - def archive_prefix(ref, sha) + def archive_prefix(ref, sha, append_sha:) + append_sha = (ref != sha) if append_sha.nil? + project_name = self.name.chomp('.git') - "#{project_name}-#{ref.tr('/', '-')}-#{sha}" + formatted_ref = ref.tr('/', '-') + + prefix_segments = [project_name, formatted_ref] + prefix_segments << sha if append_sha + + prefix_segments.join('-') end - def archive_metadata(ref, storage_path, format = "tar.gz") + def archive_metadata(ref, storage_path, format = "tar.gz", append_sha:) ref ||= root_ref commit = Gitlab::Git::Commit.find(self, ref) return {} if commit.nil? - prefix = archive_prefix(ref, commit.id) + prefix = archive_prefix(ref, commit.id, append_sha: append_sha) { 'RepoPath' => path, @@ -1036,7 +1045,8 @@ module Gitlab end def license_short_name - gitaly_migrate(:license_short_name) do |is_enabled| + gitaly_migrate(:license_short_name, + status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| if is_enabled gitaly_repository_client.license_short_name else @@ -1362,6 +1372,18 @@ module Gitlab raise CommandError.new(e) end + def clean_stale_repository_files + gitaly_migrate(:repository_cleanup, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| + gitaly_repository_client.cleanup if is_enabled && exists? + end + rescue Gitlab::Git::CommandError => e # Don't fail if we can't cleanup + Rails.logger.error("Unable to clean repository on storage #{storage} with path #{path}: #{e.message}") + Gitlab::Metrics.counter( + :failed_repository_cleanup_total, + 'Number of failed repository cleanup events' + ).increment + end + def branch_names_contains_sha(sha) gitaly_migrate(:branch_names_contains_sha) do |is_enabled| if is_enabled @@ -1456,6 +1478,43 @@ module Gitlab run_git!(['rev-list', '--max-count=1', oldrev, "^#{newrev}"]) end + def with_worktree(worktree_path, branch, sparse_checkout_files: nil, env:) + base_args = %w(worktree add --detach) + + # Note that we _don't_ want to test for `.present?` here: If the caller + # passes an non nil empty value it means it still wants sparse checkout + # but just isn't interested in any file, perhaps because it wants to + # checkout files in by a changeset but that changeset only adds files. + if sparse_checkout_files + # Create worktree without checking out + run_git!(base_args + ['--no-checkout', worktree_path], env: env) + worktree_git_path = run_git!(%w(rev-parse --git-dir), chdir: worktree_path).chomp + + configure_sparse_checkout(worktree_git_path, sparse_checkout_files) + + # After sparse checkout configuration, checkout `branch` in worktree + run_git!(%W(checkout --detach #{branch}), chdir: worktree_path, env: env) + else + # Create worktree and checkout `branch` in it + run_git!(base_args + [worktree_path, branch], env: env) + end + + yield + ensure + FileUtils.rm_rf(worktree_path) if File.exist?(worktree_path) + FileUtils.rm_rf(worktree_git_path) if worktree_git_path && File.exist?(worktree_git_path) + end + + def checksum + gitaly_migrate(:calculate_checksum) do |is_enabled| + if is_enabled + gitaly_repository_client.calculate_checksum + else + calculate_checksum_by_shelling_out + end + end + end + private def local_write_ref(ref_path, ref, old_ref: nil, shell: true) @@ -1542,33 +1601,6 @@ module Gitlab File.exist?(path) && !clean_stuck_worktree(path) end - def with_worktree(worktree_path, branch, sparse_checkout_files: nil, env:) - base_args = %w(worktree add --detach) - - # Note that we _don't_ want to test for `.present?` here: If the caller - # passes an non nil empty value it means it still wants sparse checkout - # but just isn't interested in any file, perhaps because it wants to - # checkout files in by a changeset but that changeset only adds files. - if sparse_checkout_files - # Create worktree without checking out - run_git!(base_args + ['--no-checkout', worktree_path], env: env) - worktree_git_path = run_git!(%w(rev-parse --git-dir), chdir: worktree_path).chomp - - configure_sparse_checkout(worktree_git_path, sparse_checkout_files) - - # After sparse checkout configuration, checkout `branch` in worktree - run_git!(%W(checkout --detach #{branch}), chdir: worktree_path, env: env) - else - # Create worktree and checkout `branch` in it - run_git!(base_args + [worktree_path, branch], env: env) - end - - yield - ensure - FileUtils.rm_rf(worktree_path) if File.exist?(worktree_path) - FileUtils.rm_rf(worktree_git_path) if worktree_git_path && File.exist?(worktree_git_path) - end - def clean_stuck_worktree(path) return false unless File.mtime(path) < 15.minutes.ago @@ -2401,6 +2433,34 @@ module Gitlab def sha_from_ref(ref) rev_parse_target(ref).oid end + + def calculate_checksum_by_shelling_out + raise NoRepository unless exists? + + args = %W(--git-dir=#{path} show-ref --heads --tags) + output, status = run_git(args) + + if status.nil? || !status.zero? + # Empty repositories return with a non-zero status and an empty output. + return EMPTY_REPOSITORY_CHECKSUM if output&.empty? + + raise ChecksumError, output + end + + refs = output.split("\n") + + result = refs.inject(nil) do |checksum, ref| + value = Digest::SHA1.hexdigest(ref).hex + + if checksum.nil? + value + else + checksum ^ value + end + end + + result.to_s(16) + end end end end diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index 6a01957184d..0d1ee73ca1a 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -208,6 +208,7 @@ module Gitlab def check_download_access! passed = deploy_key? || + deploy_token? || user_can_download_code? || build_can_download_code? || guest_can_download_code? @@ -238,6 +239,11 @@ module Gitlab end def check_change_access!(changes) + # If there are worktrees with a HEAD pointing to a non-existent object, + # calls to `git rev-list --all` will fail in git 2.15+. This should also + # clear stale lock files. + project.repository.clean_stale_repository_files + changes_list = Gitlab::ChangesList.new(changes) # Iterate over all changes to find if user allowed all of them to be applied @@ -269,6 +275,14 @@ module Gitlab actor.is_a?(DeployKey) end + def deploy_token + actor if deploy_token? + end + + def deploy_token? + actor.is_a?(DeployToken) + end + def ci? actor == :ci end @@ -276,6 +290,8 @@ module Gitlab def can_read_project? if deploy_key? deploy_key.has_access_to?(project) + elsif deploy_token? + deploy_token.has_access_to?(project) elsif user user.can?(:read_project, project) elsif ci? diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb index e1bc2f9ab61..6441065f5fe 100644 --- a/lib/gitlab/gitaly_client/repository_service.rb +++ b/lib/gitlab/gitaly_client/repository_service.rb @@ -19,6 +19,11 @@ module Gitlab response.exists end + def cleanup + request = Gitaly::CleanupRequest.new(repository: @gitaly_repo) + GitalyClient.call(@storage, :repository_service, :cleanup, request) + end + def garbage_collect(create_bitmap) request = Gitaly::GarbageCollectRequest.new(repository: @gitaly_repo, create_bitmap: create_bitmap) GitalyClient.call(@storage, :repository_service, :garbage_collect, request) @@ -257,6 +262,12 @@ module Gitlab response.license_short_name.presence end + + def calculate_checksum + request = Gitaly::CalculateChecksumRequest.new(repository: @gitaly_repo) + response = GitalyClient.call(@storage, :repository_service, :calculate_checksum, request) + response.checksum.presence + end end end end diff --git a/lib/gitlab/import_export/importer.rb b/lib/gitlab/import_export/importer.rb index c490bf059d2..63cab07324a 100644 --- a/lib/gitlab/import_export/importer.rb +++ b/lib/gitlab/import_export/importer.rb @@ -1,6 +1,9 @@ module Gitlab module ImportExport class Importer + include Gitlab::Allowable + include Gitlab::Utils::StrongMemoize + def self.imports_repository? true end @@ -13,12 +16,14 @@ module Gitlab end def execute - if import_file && check_version! && restorers.all?(&:restore) + if import_file && check_version! && restorers.all?(&:restore) && overwrite_project project_tree.restored_project else raise Projects::ImportService::Error.new(@shared.errors.join(', ')) end - + rescue => e + raise Projects::ImportService::Error.new(e.message) + ensure remove_import_file end @@ -26,7 +31,7 @@ module Gitlab def restorers [repo_restorer, wiki_restorer, project_tree, avatar_restorer, - uploads_restorer, lfs_restorer] + uploads_restorer, lfs_restorer, statistics_restorer] end def import_file @@ -69,6 +74,10 @@ module Gitlab Gitlab::ImportExport::LfsRestorer.new(project: project_tree.restored_project, shared: @shared) end + def statistics_restorer + Gitlab::ImportExport::StatisticsRestorer.new(project: project_tree.restored_project, shared: @shared) + end + def path_with_namespace File.join(@project.namespace.full_path, @project.path) end @@ -84,6 +93,33 @@ module Gitlab def remove_import_file FileUtils.rm_rf(@archive_file) end + + def overwrite_project + project = project_tree.restored_project + + return unless can?(@current_user, :admin_namespace, project.namespace) + + if overwrite_project? + ::Projects::OverwriteProjectService.new(project, @current_user) + .execute(project_to_overwrite) + end + + true + end + + def original_path + @project.import_data&.data&.fetch('original_path', nil) + end + + def overwrite_project? + original_path.present? && project_to_overwrite.present? + end + + def project_to_overwrite + strong_memoize(:project_to_overwrite) do + Project.find_by_full_path("#{@project.namespace.full_path}/#{original_path}") + end + end end end end diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index 2c315207298..d5590dde40f 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -92,7 +92,7 @@ module Gitlab end def override_params - return {} unless params = @project.import_data&.data&.fetch('override_params') + return {} unless params = @project.import_data&.data&.fetch('override_params', nil) @override_params ||= params.select do |key, _value| Project.column_names.include?(key.to_s) && diff --git a/lib/gitlab/import_export/statistics_restorer.rb b/lib/gitlab/import_export/statistics_restorer.rb new file mode 100644 index 00000000000..bcdd9c12c85 --- /dev/null +++ b/lib/gitlab/import_export/statistics_restorer.rb @@ -0,0 +1,17 @@ +module Gitlab + module ImportExport + class StatisticsRestorer + def initialize(project:, shared:) + @project = project + @shared = shared + end + + def restore + @project.statistics.refresh! + rescue => e + @shared.error(e) + false + end + end + end +end diff --git a/lib/gitlab/prometheus/queries/query_additional_metrics.rb b/lib/gitlab/prometheus/queries/query_additional_metrics.rb index aad76e335af..f5879de1e94 100644 --- a/lib/gitlab/prometheus/queries/query_additional_metrics.rb +++ b/lib/gitlab/prometheus/queries/query_additional_metrics.rb @@ -79,7 +79,7 @@ module Gitlab def common_query_context(environment, timeframe_start:, timeframe_end:) base_query_context(timeframe_start, timeframe_end).merge({ ci_environment_slug: environment.slug, - kube_namespace: environment.project.deployment_platform&.actual_namespace || '', + kube_namespace: environment.deployment_platform&.actual_namespace || '', environment_filter: %{container_name!="POD",environment="#{environment.slug}"} }) end diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index 2faeaf16d55..153cb2a8bb1 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -59,10 +59,10 @@ module Gitlab ] end - def send_git_archive(repository, ref:, format:) + def send_git_archive(repository, ref:, format:, append_sha:) format ||= 'tar.gz' format.downcase! - params = repository.archive_metadata(ref, Gitlab.config.gitlab.repository_downloads_path, format) + params = repository.archive_metadata(ref, Gitlab.config.gitlab.repository_downloads_path, format, append_sha: append_sha) raise "Repository or ref not found" if params.empty? if Gitlab::GitalyClient.feature_enabled?(:workhorse_archive, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) diff --git a/package.json b/package.json index 56fd2575e91..27f612aca38 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js" }, "dependencies": { - "@gitlab-org/gitlab-svgs": "^1.16.0", + "@gitlab-org/gitlab-svgs": "^1.17.0", "autosize": "^4.0.0", "axios": "^0.17.1", "babel-core": "^6.26.0", diff --git a/rubocop/cop/rspec/factories_in_migration_specs.rb b/rubocop/cop/rspec/factories_in_migration_specs.rb new file mode 100644 index 00000000000..0c5aa838a2c --- /dev/null +++ b/rubocop/cop/rspec/factories_in_migration_specs.rb @@ -0,0 +1,40 @@ +require_relative '../../spec_helpers' + +module RuboCop + module Cop + module RSpec + # This cop checks for the usage of factories in migration specs + # + # @example + # + # # bad + # let(:user) { create(:user) } + # + # # good + # let(:users) { table(:users) } + # let(:user) { users.create!(name: 'User 1', username: 'user1') } + class FactoriesInMigrationSpecs < RuboCop::Cop::Cop + include SpecHelpers + + MESSAGE = "Don't use FactoryBot.%s in migration specs, use `table` instead.".freeze + FORBIDDEN_METHODS = %i[build build_list create create_list].freeze + + def_node_search :forbidden_factory_usage?, <<~PATTERN + (send {(const nil? :FactoryBot) nil?} {#{FORBIDDEN_METHODS.map(&:inspect).join(' ')}} ...) + PATTERN + + # Following is what node.children looks like on a match: + # - Without FactoryBot namespace: [nil, :build, s(:sym, :user)] + # - With FactoryBot namespace: [s(:const, nil, :FactoryBot), :build, s(:sym, :user)] + def on_send(node) + return unless in_migration_spec?(node) + return unless forbidden_factory_usage?(node) + + method = node.children[1] + + add_offense(node, location: :expression, message: MESSAGE % method) + end + end + end + end +end diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb index 0b4c7d31442..406ec95ffc9 100644 --- a/rubocop/rubocop.rb +++ b/rubocop/rubocop.rb @@ -21,4 +21,5 @@ require_relative 'cop/migration/update_column_in_batches' require_relative 'cop/migration/update_large_table' require_relative 'cop/project_path_helper' require_relative 'cop/rspec/env_assignment' +require_relative 'cop/rspec/factories_in_migration_specs' require_relative 'cop/sidekiq_options_queue' diff --git a/rubocop/spec_helpers.rb b/rubocop/spec_helpers.rb index a702a083958..6c0f0193b1a 100644 --- a/rubocop/spec_helpers.rb +++ b/rubocop/spec_helpers.rb @@ -6,7 +6,18 @@ module RuboCop def in_spec?(node) path = node.location.expression.source_buffer.name - !SPEC_HELPERS.include?(File.basename(path)) && path.start_with?(File.join(Dir.pwd, 'spec')) + !SPEC_HELPERS.include?(File.basename(path)) && + path.start_with?(File.join(Dir.pwd, 'spec'), File.join(Dir.pwd, 'ee', 'spec')) + end + + # Returns true if the given node originated from a migration spec. + def in_migration_spec?(node) + path = node.location.expression.source_buffer.name + + in_spec?(node) && + path.start_with?( + File.join(Dir.pwd, 'spec', 'migrations'), + File.join(Dir.pwd, 'ee', 'spec', 'migrations')) end end end diff --git a/spec/controllers/dashboard_controller_spec.rb b/spec/controllers/dashboard_controller_spec.rb index 97c2c3fb940..3458d679107 100644 --- a/spec/controllers/dashboard_controller_spec.rb +++ b/spec/controllers/dashboard_controller_spec.rb @@ -11,9 +11,11 @@ describe DashboardController do describe 'GET issues' do it_behaves_like 'issuables list meta-data', :issue, :issues + it_behaves_like 'issuables requiring filter', :issues end describe 'GET merge requests' do it_behaves_like 'issuables list meta-data', :merge_request, :merge_requests + it_behaves_like 'issuables requiring filter', :merge_requests end end diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb index 04d16e98913..c3b71458e38 100644 --- a/spec/controllers/projects/repositories_controller_spec.rb +++ b/spec/controllers/projects/repositories_controller_spec.rb @@ -6,7 +6,7 @@ describe Projects::RepositoriesController do describe "GET archive" do context 'as a guest' do it 'responds with redirect in correct format' do - get :archive, namespace_id: project.namespace, project_id: project, format: "zip", ref: 'master' + get :archive, namespace_id: project.namespace, project_id: project, id: "master", format: "zip" expect(response.header["Content-Type"]).to start_with('text/html') expect(response).to be_redirect @@ -22,7 +22,20 @@ describe Projects::RepositoriesController do end it "uses Gitlab::Workhorse" do - get :archive, namespace_id: project.namespace, project_id: project, ref: "master", format: "zip" + get :archive, namespace_id: project.namespace, project_id: project, id: "master", format: "zip" + + expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:") + end + + it 'responds with redirect to the short name archive if fully qualified' do + get :archive, namespace_id: project.namespace, project_id: project, id: "master/#{project.path}-master", format: "zip" + + expect(assigns(:ref)).to eq("master") + expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:") + end + + it 'handles legacy queries with no ref' do + get :archive, namespace_id: project.namespace, project_id: project, format: "zip" expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:") end @@ -33,7 +46,7 @@ describe Projects::RepositoriesController do end it "renders Not Found" do - get :archive, namespace_id: project.namespace, project_id: project, ref: "master", format: "zip" + get :archive, namespace_id: project.namespace, project_id: project, id: "master", format: "zip" expect(response).to have_gitlab_http_status(404) end diff --git a/spec/factories/deploy_tokens.rb b/spec/factories/deploy_tokens.rb new file mode 100644 index 00000000000..5fea4a9d5a6 --- /dev/null +++ b/spec/factories/deploy_tokens.rb @@ -0,0 +1,14 @@ +FactoryBot.define do + factory :deploy_token do + token { SecureRandom.hex(50) } + sequence(:name) { |n| "PDT #{n}" } + read_repository true + read_registry true + revoked false + expires_at { 5.days.from_now } + + trait :revoked do + revoked true + end + end +end diff --git a/spec/factories/project_deploy_tokens.rb b/spec/factories/project_deploy_tokens.rb new file mode 100644 index 00000000000..4866cb58d88 --- /dev/null +++ b/spec/factories/project_deploy_tokens.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :project_deploy_token do + project + deploy_token + end +end diff --git a/spec/factories/users_star_projects.rb b/spec/factories/users_star_projects.rb new file mode 100644 index 00000000000..6afd08a2084 --- /dev/null +++ b/spec/factories/users_star_projects.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :users_star_project do + project + user + end +end diff --git a/spec/features/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb index d673bac4995..fb6c71ce997 100644 --- a/spec/features/atom/dashboard_issues_spec.rb +++ b/spec/features/atom/dashboard_issues_spec.rb @@ -13,17 +13,26 @@ describe "Dashboard Issues Feed" do end describe "atom feed" do - it "renders atom feed via personal access token" do + it "returns 400 if no filter is used" do personal_access_token = create(:personal_access_token, user: user) visit issues_dashboard_path(:atom, private_token: personal_access_token.token) expect(response_headers['Content-Type']).to have_content('application/atom+xml') + expect(page.status_code).to eq(400) + end + + it "renders atom feed via personal access token" do + personal_access_token = create(:personal_access_token, user: user) + + visit issues_dashboard_path(:atom, private_token: personal_access_token.token, assignee_id: user.id) + + expect(response_headers['Content-Type']).to have_content('application/atom+xml') expect(body).to have_selector('title', text: "#{user.name} issues") end it "renders atom feed via RSS token" do - visit issues_dashboard_path(:atom, rss_token: user.rss_token) + visit issues_dashboard_path(:atom, rss_token: user.rss_token, assignee_id: user.id) expect(response_headers['Content-Type']).to have_content('application/atom+xml') expect(body).to have_selector('title', text: "#{user.name} issues") @@ -44,7 +53,7 @@ describe "Dashboard Issues Feed" do let!(:issue2) { create(:issue, author: user, assignees: [assignee], project: project2, description: 'test desc') } it "renders issue fields" do - visit issues_dashboard_path(:atom, rss_token: user.rss_token) + visit issues_dashboard_path(:atom, rss_token: user.rss_token, assignee_id: assignee.id) entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue2.title}')]") @@ -67,7 +76,7 @@ describe "Dashboard Issues Feed" do end it "renders issue label and milestone info" do - visit issues_dashboard_path(:atom, rss_token: user.rss_token) + visit issues_dashboard_path(:atom, rss_token: user.rss_token, assignee_id: assignee.id) entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue1.title}')]") diff --git a/spec/features/dashboard/issues_filter_spec.rb b/spec/features/dashboard/issues_filter_spec.rb index 029fc45c791..bab34ac9346 100644 --- a/spec/features/dashboard/issues_filter_spec.rb +++ b/spec/features/dashboard/issues_filter_spec.rb @@ -17,6 +17,12 @@ feature 'Dashboard Issues filtering', :js do visit_issues end + context 'without any filter' do + it 'shows error message' do + expect(page).to have_content 'Please select at least one filter to see results' + end + end + context 'filtering by milestone' do it 'shows all issues with no milestone' do show_milestone_dropdown @@ -27,15 +33,6 @@ feature 'Dashboard Issues filtering', :js do expect(page).to have_selector('.issue', count: 1) end - it 'shows all issues with any milestone' do - show_milestone_dropdown - - click_link 'Any Milestone' - - expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) - expect(page).to have_selector('.issue', count: 2) - end - it 'shows all issues with the selected milestone' do show_milestone_dropdown @@ -68,13 +65,6 @@ feature 'Dashboard Issues filtering', :js do let(:label) { create(:label, project: project) } let!(:label_link) { create(:label_link, label: label, target: issue) } - it 'shows all issues without filter' do - page.within 'ul.content-list' do - expect(page).to have_content issue.title - expect(page).to have_content issue2.title - end - end - it 'shows all issues with the selected label' do page.within '.labels-filter' do find('.dropdown').click @@ -89,9 +79,13 @@ feature 'Dashboard Issues filtering', :js do end context 'sorting' do - it 'shows sorted issues' do + before do + visit_issues(assignee_id: user.id) + end + + it 'remembers last sorting value' do sort_by('Created date') - visit_issues + visit_issues(assignee_id: user.id) expect(find('.issues-filters')).to have_content('Created date') end diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb index 8d1d5a51750..e41a2e4ce09 100644 --- a/spec/features/dashboard/issues_spec.rb +++ b/spec/features/dashboard/issues_spec.rb @@ -51,15 +51,6 @@ RSpec.describe 'Dashboard Issues' do expect(page).not_to have_content(other_issue.title) end - it 'shows all issues' do - click_link('Reset filters') - - expect(page).to have_content(authored_issue.title) - expect(page).to have_content(authored_issue_on_public_project.title) - expect(page).to have_content(assigned_issue.title) - expect(page).to have_content(other_issue.title) - end - it 'state filter tabs work' do find('#state-closed').click expect(page).to have_current_path(issues_dashboard_url(assignee_id: current_user.id, state: 'closed'), url: true) diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb index 4a9344115d2..0965b745c03 100644 --- a/spec/features/dashboard/merge_requests_spec.rb +++ b/spec/features/dashboard/merge_requests_spec.rb @@ -103,15 +103,11 @@ feature 'Dashboard Merge Requests' do expect(page).not_to have_content(other_merge_request.title) end - it 'shows all merge requests', :js do + it 'shows error message without filter', :js do filter_item_select('Any Assignee', '.js-assignee-search') filter_item_select('Any Author', '.js-author-search') - expect(page).to have_content(authored_merge_request.title) - expect(page).to have_content(authored_merge_request_from_fork.title) - expect(page).to have_content(assigned_merge_request.title) - expect(page).to have_content(assigned_merge_request_from_fork.title) - expect(page).to have_content(other_merge_request.title) + expect(page).to have_content('Please select at least one filter to see results') end it 'shows sorted merge requests' do diff --git a/spec/features/groups/settings/group_badges_spec.rb b/spec/features/groups/settings/group_badges_spec.rb new file mode 100644 index 00000000000..92217294446 --- /dev/null +++ b/spec/features/groups/settings/group_badges_spec.rb @@ -0,0 +1,124 @@ +require 'spec_helper' + +feature 'Group Badges' do + include WaitForRequests + + let(:user) { create(:user) } + let(:group) { create(:group) } + let(:badge_link_url) { 'https://gitlab.com/gitlab-org/gitlab-ee/commits/master'} + let(:badge_image_url) { 'https://gitlab.com/gitlab-org/gitlab-ee/badges/master/build.svg'} + let!(:badge_1) { create(:group_badge, group: group) } + let!(:badge_2) { create(:group_badge, group: group) } + + before do + group.add_owner(user) + sign_in(user) + + visit(group_settings_badges_path(group)) + end + + it 'shows a list of badges', :js do + page.within '.badge-settings' do + wait_for_requests + + rows = all('.panel-body > div') + expect(rows.length).to eq 2 + expect(rows[0]).to have_content badge_1.link_url + expect(rows[1]).to have_content badge_2.link_url + end + end + + context 'adding a badge', :js do + it 'user can preview a badge' do + page.within '.badge-settings form' do + fill_in 'badge-link-url', with: badge_link_url + fill_in 'badge-image-url', with: badge_image_url + within '#badge-preview' do + expect(find('a')[:href]).to eq badge_link_url + expect(find('a img')[:src]).to eq badge_image_url + end + end + end + + it do + page.within '.badge-settings' do + fill_in 'badge-link-url', with: badge_link_url + fill_in 'badge-image-url', with: badge_image_url + + click_button 'Add badge' + wait_for_requests + + within '.panel-body' do + expect(find('a')[:href]).to eq badge_link_url + expect(find('a img')[:src]).to eq badge_image_url + end + end + end + end + + context 'editing a badge', :js do + it 'form is shown when clicking edit button in list' do + page.within '.badge-settings' do + wait_for_requests + rows = all('.panel-body > div') + expect(rows.length).to eq 2 + rows[1].find('[aria-label="Edit"]').click + + within 'form' do + expect(find('#badge-link-url').value).to eq badge_2.link_url + expect(find('#badge-image-url').value).to eq badge_2.image_url + end + end + end + + it 'updates a badge when submitting the edit form' do + page.within '.badge-settings' do + wait_for_requests + rows = all('.panel-body > div') + expect(rows.length).to eq 2 + rows[1].find('[aria-label="Edit"]').click + within 'form' do + fill_in 'badge-link-url', with: badge_link_url + fill_in 'badge-image-url', with: badge_image_url + + click_button 'Save changes' + wait_for_requests + end + + rows = all('.panel-body > div') + expect(rows.length).to eq 2 + expect(rows[1]).to have_content badge_link_url + end + end + end + + context 'deleting a badge', :js do + def click_delete_button(badge_row) + badge_row.find('[aria-label="Delete"]').click + end + + it 'shows a modal when deleting a badge' do + wait_for_requests + rows = all('.panel-body > div') + expect(rows.length).to eq 2 + + click_delete_button(rows[1]) + + expect(find('.modal .modal-title')).to have_content 'Delete badge?' + end + + it 'deletes a badge when confirming the modal' do + wait_for_requests + rows = all('.panel-body > div') + expect(rows.length).to eq 2 + click_delete_button(rows[1]) + + find('.modal .btn-danger').click + wait_for_requests + + rows = all('.panel-body > div') + expect(rows.length).to eq 1 + expect(rows[0]).to have_content badge_1.link_url + end + end +end diff --git a/spec/features/issues/spam_issues_spec.rb b/spec/features/issues/spam_issues_spec.rb index a75ca1d42b3..73022afbda2 100644 --- a/spec/features/issues/spam_issues_spec.rb +++ b/spec/features/issues/spam_issues_spec.rb @@ -34,9 +34,6 @@ describe 'New issue', :js do click_button 'Submit issue' - # reCAPTCHA alerts when it can't contact the server, so just accept it and move on - page.driver.browser.switch_to.alert.accept - # it is impossible to test recaptcha automatically and there is no possibility to fill in recaptcha # recaptcha verification is skipped in test environment and it always returns true expect(page).not_to have_content('issue title') diff --git a/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb b/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb index a43ba05c64c..fd1629746ef 100644 --- a/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb +++ b/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb @@ -9,6 +9,7 @@ describe 'Merge request < User sees mini pipeline graph', :js do before do build.run + build.trace.set('hello') sign_in(user) visit_merge_request end @@ -26,15 +27,15 @@ describe 'Merge request < User sees mini pipeline graph', :js do let(:artifacts_file2) { fixture_file_upload(Rails.root.join('spec/fixtures/dk.png'), 'image/png') } before do - create(:ci_build, pipeline: pipeline, legacy_artifacts_file: artifacts_file1) - create(:ci_build, pipeline: pipeline, when: 'manual') + create(:ci_build, :success, :trace_artifact, pipeline: pipeline, legacy_artifacts_file: artifacts_file1) + create(:ci_build, :manual, pipeline: pipeline, when: 'manual') end it 'avoids repeated database queries' do before = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') } - create(:ci_build, pipeline: pipeline, legacy_artifacts_file: artifacts_file2) - create(:ci_build, pipeline: pipeline, when: 'manual') + create(:ci_build, :success, :trace_artifact, pipeline: pipeline, legacy_artifacts_file: artifacts_file2) + create(:ci_build, :manual, pipeline: pipeline, when: 'manual') after = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') } diff --git a/spec/features/projects/files/template_selector_menu_spec.rb b/spec/features/projects/files/template_selector_menu_spec.rb new file mode 100644 index 00000000000..b549a69ddf3 --- /dev/null +++ b/spec/features/projects/files/template_selector_menu_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +feature 'Template selector menu', :js do + let(:project) { create(:project, :repository) } + let(:user) { create(:user) } + + before do + project.add_master(user) + sign_in user + end + + context 'editing a non-matching file' do + before do + create_and_edit_file('README.md') + end + + scenario 'is not displayed' do + check_template_selector_menu_display(false) + end + + context 'user toggles preview' do + before do + click_link 'Preview' + end + + scenario 'template selector menu is not displayed' do + check_template_selector_menu_display(false) + click_link 'Write' + check_template_selector_menu_display(false) + end + end + end + + context 'editing a matching file' do + before do + visit project_edit_blob_path(project, File.join(project.default_branch, 'LICENSE')) + end + + scenario 'is displayed' do + check_template_selector_menu_display(true) + end + + context 'user toggles preview' do + before do + click_link 'Preview' + end + + scenario 'template selector menu is hidden and shown correctly' do + check_template_selector_menu_display(false) + click_link 'Write' + check_template_selector_menu_display(true) + end + end + end +end + +def check_template_selector_menu_display(is_visible) + count = is_visible ? 1 : 0 + expect(page).to have_css('.template-selectors-menu', count: count) +end + +def create_and_edit_file(file_name) + visit project_new_blob_path(project, 'master', file_name: file_name) + click_button "Commit changes" + visit project_edit_blob_path(project, File.join(project.default_branch, file_name)) +end diff --git a/spec/features/projects/jobs/user_browses_job_spec.rb b/spec/features/projects/jobs/user_browses_job_spec.rb index b7eee39052a..bff5bbe99af 100644 --- a/spec/features/projects/jobs/user_browses_job_spec.rb +++ b/spec/features/projects/jobs/user_browses_job_spec.rb @@ -1,16 +1,15 @@ require 'spec_helper' describe 'User browses a job', :js do - let!(:build) { create(:ci_build, :running, :coverage, pipeline: pipeline) } - let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.sha, ref: 'master') } - let(:project) { create(:project, :repository, namespace: user.namespace) } let(:user) { create(:user) } + let(:user_access_level) { :developer } + let(:project) { create(:project, :repository, namespace: user.namespace) } + let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.sha, ref: 'master') } + let!(:build) { create(:ci_build, :success, :trace_artifact, :coverage, pipeline: pipeline) } before do project.add_master(user) project.enable_ci - build.success - build.trace.set('job trace') sign_in(user) @@ -21,7 +20,9 @@ describe 'User browses a job', :js do expect(page).to have_content("Job ##{build.id}") expect(page).to have_css('#build-trace') - accept_confirm { click_link('Erase') } + # scroll to the top of the page first + execute_script "window.scrollTo(0,0)" + accept_confirm { find('.js-erase-link').click } expect(page).to have_no_css('.artifacts') expect(build).not_to have_trace @@ -36,7 +37,7 @@ describe 'User browses a job', :js do end context 'with a failed job' do - let!(:build) { create(:ci_build, :failed, pipeline: pipeline) } + let!(:build) { create(:ci_build, :failed, :trace_artifact, pipeline: pipeline) } it 'displays the failure reason' do within('.builds-container') do @@ -47,7 +48,7 @@ describe 'User browses a job', :js do end context 'when a failed job has been retried' do - let!(:build) { create(:ci_build, :failed, :retried, pipeline: pipeline) } + let!(:build) { create(:ci_build, :failed, :retried, :trace_artifact, pipeline: pipeline) } it 'displays the failure reason and retried label' do within('.builds-container') do diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index 5d311f2dde3..749a1b81872 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -113,7 +113,7 @@ feature 'Jobs' do describe "GET /:project/jobs/:id" do context "Job from project" do - let(:job) { create(:ci_build, :success, pipeline: pipeline) } + let(:job) { create(:ci_build, :success, :trace_live, pipeline: pipeline) } before do visit project_job_path(project, job) @@ -136,7 +136,7 @@ feature 'Jobs' do end context 'when job is not running', :js do - let(:job) { create(:ci_build, :success, pipeline: pipeline) } + let(:job) { create(:ci_build, :success, :trace_artifact, pipeline: pipeline) } before do visit project_job_path(project, job) @@ -153,7 +153,7 @@ feature 'Jobs' do end context 'if job failed' do - let(:job) { create(:ci_build, :failed, pipeline: pipeline) } + let(:job) { create(:ci_build, :failed, :trace_artifact, pipeline: pipeline) } before do visit project_job_path(project, job) @@ -339,7 +339,7 @@ feature 'Jobs' do context 'job is successfull and has deployment' do let(:deployment) { create(:deployment) } - let(:job) { create(:ci_build, :success, environment: environment.name, deployments: [deployment], pipeline: pipeline) } + let(:job) { create(:ci_build, :success, :trace_artifact, environment: environment.name, deployments: [deployment], pipeline: pipeline) } it 'shows a link for the job' do visit project_job_path(project, job) @@ -349,7 +349,7 @@ feature 'Jobs' do end context 'job is complete and not successful' do - let(:job) { create(:ci_build, :failed, environment: environment.name, pipeline: pipeline) } + let(:job) { create(:ci_build, :failed, :trace_artifact, environment: environment.name, pipeline: pipeline) } it 'shows a link for the job' do visit project_job_path(project, job) @@ -360,7 +360,7 @@ feature 'Jobs' do context 'job creates a new deployment' do let!(:deployment) { create(:deployment, environment: environment, sha: project.commit.id) } - let(:job) { create(:ci_build, :success, environment: environment.name, pipeline: pipeline) } + let(:job) { create(:ci_build, :success, :trace_artifact, environment: environment.name, pipeline: pipeline) } it 'shows a link to latest deployment' do visit project_job_path(project, job) @@ -379,6 +379,7 @@ feature 'Jobs' do end it 'shows manual action empty state' do + expect(page).to have_content(job.detailed_status(user).illustration[:title]) expect(page).to have_content('This job requires a manual action') expect(page).to have_content('This job depends on a user to trigger its process. Often they are used to deploy code to production environments') expect(page).to have_link('Trigger this manual action') @@ -402,6 +403,7 @@ feature 'Jobs' do end it 'shows empty state' do + expect(page).to have_content(job.detailed_status(user).illustration[:title]) expect(page).to have_content('This job has not been triggered yet') expect(page).to have_content('This job depends on upstream jobs that need to succeed in order for this job to be triggered') end @@ -415,10 +417,53 @@ feature 'Jobs' do end it 'shows pending empty state' do + expect(page).to have_content(job.detailed_status(user).illustration[:title]) expect(page).to have_content('This job has not started yet') expect(page).to have_content('This job is in pending state and is waiting to be picked by a runner') end end + + context 'Canceled job' do + context 'with log' do + let(:job) { create(:ci_build, :canceled, :trace_artifact, pipeline: pipeline) } + + before do + visit project_job_path(project, job) + end + + it 'renders job log' do + expect(page).to have_selector('.js-build-output') + end + end + + context 'without log' do + let(:job) { create(:ci_build, :canceled, pipeline: pipeline) } + + before do + visit project_job_path(project, job) + end + + it 'renders empty state' do + expect(page).to have_content(job.detailed_status(user).illustration[:title]) + expect(page).not_to have_selector('.js-build-output') + expect(page).to have_content('This job has been canceled') + end + end + end + + context 'Skipped job' do + let(:job) { create(:ci_build, :skipped, pipeline: pipeline) } + + before do + visit project_job_path(project, job) + end + + it 'renders empty state' do + expect(page).to have_content(job.detailed_status(user).illustration[:title]) + expect(page).not_to have_selector('.js-build-output') + expect(page).to have_content('This job has been skipped') + end + end end describe "POST /:project/jobs/:id/cancel", :js do diff --git a/spec/features/projects/settings/project_badges_spec.rb b/spec/features/projects/settings/project_badges_spec.rb new file mode 100644 index 00000000000..cc3551a4c21 --- /dev/null +++ b/spec/features/projects/settings/project_badges_spec.rb @@ -0,0 +1,125 @@ +require 'spec_helper' + +feature 'Project Badges' do + include WaitForRequests + + let(:user) { create(:user) } + let(:group) { create(:group) } + let(:project) { create(:project, namespace: group) } + let(:badge_link_url) { 'https://gitlab.com/gitlab-org/gitlab-ee/commits/master'} + let(:badge_image_url) { 'https://gitlab.com/gitlab-org/gitlab-ee/badges/master/build.svg'} + let!(:project_badge) { create(:project_badge, project: project) } + let!(:group_badge) { create(:group_badge, group: group) } + + before do + group.add_master(user) + sign_in(user) + + visit(project_settings_badges_path(project)) + end + + it 'shows a list of badges', :js do + page.within '.badge-settings' do + wait_for_requests + + rows = all('.panel-body > div') + expect(rows.length).to eq 2 + expect(rows[0]).to have_content group_badge.link_url + expect(rows[1]).to have_content project_badge.link_url + end + end + + context 'adding a badge', :js do + it 'user can preview a badge' do + page.within '.badge-settings form' do + fill_in 'badge-link-url', with: badge_link_url + fill_in 'badge-image-url', with: badge_image_url + within '#badge-preview' do + expect(find('a')[:href]).to eq badge_link_url + expect(find('a img')[:src]).to eq badge_image_url + end + end + end + + it do + page.within '.badge-settings' do + fill_in 'badge-link-url', with: badge_link_url + fill_in 'badge-image-url', with: badge_image_url + + click_button 'Add badge' + wait_for_requests + + within '.panel-body' do + expect(find('a')[:href]).to eq badge_link_url + expect(find('a img')[:src]).to eq badge_image_url + end + end + end + end + + context 'editing a badge', :js do + it 'form is shown when clicking edit button in list' do + page.within '.badge-settings' do + wait_for_requests + rows = all('.panel-body > div') + expect(rows.length).to eq 2 + rows[1].find('[aria-label="Edit"]').click + + within 'form' do + expect(find('#badge-link-url').value).to eq project_badge.link_url + expect(find('#badge-image-url').value).to eq project_badge.image_url + end + end + end + + it 'updates a badge when submitting the edit form' do + page.within '.badge-settings' do + wait_for_requests + rows = all('.panel-body > div') + expect(rows.length).to eq 2 + rows[1].find('[aria-label="Edit"]').click + within 'form' do + fill_in 'badge-link-url', with: badge_link_url + fill_in 'badge-image-url', with: badge_image_url + + click_button 'Save changes' + wait_for_requests + end + + rows = all('.panel-body > div') + expect(rows.length).to eq 2 + expect(rows[1]).to have_content badge_link_url + end + end + end + + context 'deleting a badge', :js do + def click_delete_button(badge_row) + badge_row.find('[aria-label="Delete"]').click + end + + it 'shows a modal when deleting a badge' do + wait_for_requests + rows = all('.panel-body > div') + expect(rows.length).to eq 2 + + click_delete_button(rows[1]) + + expect(find('.modal .modal-title')).to have_content 'Delete badge?' + end + + it 'deletes a badge when confirming the modal' do + wait_for_requests + rows = all('.panel-body > div') + expect(rows.length).to eq 2 + click_delete_button(rows[1]) + + find('.modal .btn-danger').click + wait_for_requests + + rows = all('.panel-body > div') + expect(rows.length).to eq 1 + expect(rows[0]).to have_content group_badge.link_url + end + end +end diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb index 14670e91006..f2c371b7df5 100644 --- a/spec/features/projects/settings/repository_settings_spec.rb +++ b/spec/features/projects/settings/repository_settings_spec.rb @@ -88,5 +88,32 @@ feature 'Repository settings' do expect(page).not_to have_content(private_deploy_key.title) end end + + context 'Deploy tokens' do + let!(:deploy_token) { create(:deploy_token, projects: [project]) } + + before do + stub_container_registry_config(enabled: true) + visit project_settings_repository_path(project) + end + + scenario 'view deploy tokens' do + within('.deploy-tokens') do + expect(page).to have_content(deploy_token.name) + expect(page).to have_content('read_repository') + expect(page).to have_content('read_registry') + end + end + + scenario 'add a new deploy token' do + fill_in 'deploy_token_name', with: 'new_deploy_key' + fill_in 'deploy_token_expires_at', with: (Date.today + 1.month).to_s + check 'deploy_token_read_repository' + check 'deploy_token_read_registry' + click_button 'Create deploy token' + + expect(page).to have_content('Your new project deploy token has been created') + end + end end end diff --git a/spec/features/search/user_uses_header_search_field_spec.rb b/spec/features/search/user_uses_header_search_field_spec.rb index 5ddea36add5..a9128104b87 100644 --- a/spec/features/search/user_uses_header_search_field_spec.rb +++ b/spec/features/search/user_uses_header_search_field_spec.rb @@ -9,49 +9,25 @@ describe 'User uses header search field' do before do project.add_reporter(user) sign_in(user) - - visit(project_path(project)) - end - - it 'starts searching by pressing the enter key', :js do - fill_in('search', with: 'gitlab') - find('#search').native.send_keys(:enter) - - page.within('.breadcrumbs-sub-title') do - expect(page).to have_content('Search') - end end - it 'contains location badge' do - expect(page).to have_selector('.has-location-badge') - end - - context 'when clicking the search field', :js do + context 'when user is in a global scope', :js do before do + visit(root_path) page.find('#search').click end - it 'shows category search dropdown' do - expect(page).to have_selector('.dropdown-header', text: /#{project.name}/i) - end - context 'when clicking issues' do - let!(:issue) { create(:issue, project: project, author: user, assignees: [user]) } - it 'shows assigned issues' do - find('.dropdown-menu').click_link('Issues assigned to me') + find('.search-input-container .dropdown-menu').click_link('Issues assigned to me') - expect(page).to have_selector('.filtered-search') - expect_tokens([assignee_token(user.name)]) - expect_filtered_search_input_empty + expect(find('.js-assignee-search')).to have_content(user.name) end it 'shows created issues' do - find('.dropdown-menu').click_link("Issues I've created") + find('.search-input-container .dropdown-menu').click_link("Issues I've created") - expect(page).to have_selector('.filtered-search') - expect_tokens([author_token(user.name)]) - expect_filtered_search_input_empty + expect(find('.js-author-search')).to have_content(user.name) end end @@ -59,32 +35,97 @@ describe 'User uses header search field' do let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignee: user) } it 'shows assigned merge requests' do - find('.dropdown-menu').click_link('Merge requests assigned to me') + find('.search-input-container .dropdown-menu').click_link('Merge requests assigned to me') - expect(page).to have_selector('.merge-requests-holder') - expect_tokens([assignee_token(user.name)]) - expect_filtered_search_input_empty + expect(find('.js-assignee-search')).to have_content(user.name) end it 'shows created merge requests' do - find('.dropdown-menu').click_link("Merge requests I've created") + find('.search-input-container .dropdown-menu').click_link("Merge requests I've created") - expect(page).to have_selector('.merge-requests-holder') - expect_tokens([author_token(user.name)]) - expect_filtered_search_input_empty + expect(find('.js-author-search')).to have_content(user.name) end end end - context 'when entering text into the search field', :js do + context 'when user is in a project scope' do before do - page.within('.search-input-wrap') do - fill_in('search', with: project.name[0..3]) + visit(project_path(project)) + end + + it 'starts searching by pressing the enter key', :js do + fill_in('search', with: 'gitlab') + find('#search').native.send_keys(:enter) + + page.within('.breadcrumbs-sub-title') do + expect(page).to have_content('Search') end end - it 'does not display the category search dropdown' do - expect(page).not_to have_selector('.dropdown-header', text: /#{project.name}/i) + it 'contains location badge' do + expect(page).to have_selector('.has-location-badge') + end + + context 'when clicking the search field', :js do + before do + page.find('#search').click + end + + it 'shows category search dropdown' do + expect(page).to have_selector('.dropdown-header', text: /#{project.name}/i) + end + + context 'when clicking issues' do + let!(:issue) { create(:issue, project: project, author: user, assignees: [user]) } + + it 'shows assigned issues' do + find('.dropdown-menu').click_link('Issues assigned to me') + + expect(page).to have_selector('.filtered-search') + expect_tokens([assignee_token(user.name)]) + expect_filtered_search_input_empty + end + + it 'shows created issues' do + find('.dropdown-menu').click_link("Issues I've created") + + expect(page).to have_selector('.filtered-search') + expect_tokens([author_token(user.name)]) + expect_filtered_search_input_empty + end + end + + context 'when clicking merge requests' do + let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignee: user) } + + it 'shows assigned merge requests' do + find('.dropdown-menu').click_link('Merge requests assigned to me') + + expect(page).to have_selector('.merge-requests-holder') + expect_tokens([assignee_token(user.name)]) + expect_filtered_search_input_empty + end + + it 'shows created merge requests' do + find('.dropdown-menu').click_link("Merge requests I've created") + + expect(page).to have_selector('.merge-requests-holder') + expect_tokens([author_token(user.name)]) + expect_filtered_search_input_empty + end + end + end + + context 'when entering text into the search field', :js do + before do + page.within('.search-input-wrap') do + fill_in('search', with: project.name[0..3]) + end + end + + it 'does not display the category search dropdown' do + expect(page).not_to have_selector('.dropdown-header', text: /#{project.name}/i) + end end end end diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb index 2fecd1a3d27..4224cea4652 100644 --- a/spec/helpers/issuables_helper_spec.rb +++ b/spec/helpers/issuables_helper_spec.rb @@ -40,22 +40,22 @@ describe IssuablesHelper do end it 'returns "Open" when state is :opened' do - expect(helper.issuables_state_counter_text(:issues, :opened)) + expect(helper.issuables_state_counter_text(:issues, :opened, true)) .to eq('<span>Open</span> <span class="badge">42</span>') end it 'returns "Closed" when state is :closed' do - expect(helper.issuables_state_counter_text(:issues, :closed)) + expect(helper.issuables_state_counter_text(:issues, :closed, true)) .to eq('<span>Closed</span> <span class="badge">42</span>') end it 'returns "Merged" when state is :merged' do - expect(helper.issuables_state_counter_text(:merge_requests, :merged)) + expect(helper.issuables_state_counter_text(:merge_requests, :merged, true)) .to eq('<span>Merged</span> <span class="badge">42</span>') end it 'returns "All" when state is :all' do - expect(helper.issuables_state_counter_text(:merge_requests, :all)) + expect(helper.issuables_state_counter_text(:merge_requests, :all, true)) .to eq('<span>All</span> <span class="badge">42</span>') end end @@ -101,27 +101,6 @@ describe IssuablesHelper do end end - describe '#issuable_filter_present?' do - it 'returns true when any key is present' do - allow(helper).to receive(:params).and_return( - ActionController::Parameters.new(milestone_title: 'Velit consectetur asperiores natus delectus.', - project_id: 'gitlabhq', - scope: 'all') - ) - - expect(helper.issuable_filter_present?).to be_truthy - end - - it 'returns false when no key is present' do - allow(helper).to receive(:params).and_return( - ActionController::Parameters.new(project_id: 'gitlabhq', - scope: 'all') - ) - - expect(helper.issuable_filter_present?).to be_falsey - end - end - describe '#updated_at_by' do let(:user) { create(:user) } let(:unedited_issuable) { create(:issue) } diff --git a/spec/javascripts/badges/components/badge_form_spec.js b/spec/javascripts/badges/components/badge_form_spec.js new file mode 100644 index 00000000000..dd21ec279cb --- /dev/null +++ b/spec/javascripts/badges/components/badge_form_spec.js @@ -0,0 +1,171 @@ +import Vue from 'vue'; +import store from '~/badges/store'; +import BadgeForm from '~/badges/components/badge_form.vue'; +import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { createDummyBadge } from '../dummy_badge'; + +describe('BadgeForm component', () => { + const Component = Vue.extend(BadgeForm); + let vm; + + beforeEach(() => { + setFixtures(` + <div id="dummy-element"></div> + `); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('methods', () => { + beforeEach(() => { + vm = mountComponentWithStore(Component, { + el: '#dummy-element', + store, + props: { + isEditing: false, + }, + }); + }); + + describe('onCancel', () => { + it('calls stopEditing', () => { + spyOn(vm, 'stopEditing'); + + vm.onCancel(); + + expect(vm.stopEditing).toHaveBeenCalled(); + }); + }); + + describe('onSubmit', () => { + describe('if isEditing is true', () => { + beforeEach(() => { + spyOn(vm, 'saveBadge').and.returnValue(Promise.resolve()); + store.replaceState({ + ...store.state, + isSaving: false, + badgeInEditForm: createDummyBadge(), + }); + vm.isEditing = true; + }); + + it('returns immediately if imageUrl is empty', () => { + store.state.badgeInEditForm.imageUrl = ''; + + vm.onSubmit(); + + expect(vm.saveBadge).not.toHaveBeenCalled(); + }); + + it('returns immediately if linkUrl is empty', () => { + store.state.badgeInEditForm.linkUrl = ''; + + vm.onSubmit(); + + expect(vm.saveBadge).not.toHaveBeenCalled(); + }); + + it('returns immediately if isSaving is true', () => { + store.state.isSaving = true; + + vm.onSubmit(); + + expect(vm.saveBadge).not.toHaveBeenCalled(); + }); + + it('calls saveBadge', () => { + vm.onSubmit(); + + expect(vm.saveBadge).toHaveBeenCalled(); + }); + }); + + describe('if isEditing is false', () => { + beforeEach(() => { + spyOn(vm, 'addBadge').and.returnValue(Promise.resolve()); + store.replaceState({ + ...store.state, + isSaving: false, + badgeInAddForm: createDummyBadge(), + }); + vm.isEditing = false; + }); + + it('returns immediately if imageUrl is empty', () => { + store.state.badgeInAddForm.imageUrl = ''; + + vm.onSubmit(); + + expect(vm.addBadge).not.toHaveBeenCalled(); + }); + + it('returns immediately if linkUrl is empty', () => { + store.state.badgeInAddForm.linkUrl = ''; + + vm.onSubmit(); + + expect(vm.addBadge).not.toHaveBeenCalled(); + }); + + it('returns immediately if isSaving is true', () => { + store.state.isSaving = true; + + vm.onSubmit(); + + expect(vm.addBadge).not.toHaveBeenCalled(); + }); + + it('calls addBadge', () => { + vm.onSubmit(); + + expect(vm.addBadge).toHaveBeenCalled(); + }); + }); + }); + }); + + describe('if isEditing is false', () => { + beforeEach(() => { + vm = mountComponentWithStore(Component, { + el: '#dummy-element', + store, + props: { + isEditing: false, + }, + }); + }); + + it('renders one button', () => { + const buttons = vm.$el.querySelectorAll('.row-content-block button'); + expect(buttons.length).toBe(1); + const buttonAddElement = buttons[0]; + expect(buttonAddElement).toBeVisible(); + expect(buttonAddElement).toHaveText('Add badge'); + }); + }); + + describe('if isEditing is true', () => { + beforeEach(() => { + vm = mountComponentWithStore(Component, { + el: '#dummy-element', + store, + props: { + isEditing: true, + }, + }); + }); + + it('renders two buttons', () => { + const buttons = vm.$el.querySelectorAll('.row-content-block button'); + expect(buttons.length).toBe(2); + const buttonSaveElement = buttons[0]; + expect(buttonSaveElement).toBeVisible(); + expect(buttonSaveElement).toHaveText('Save changes'); + const buttonCancelElement = buttons[1]; + expect(buttonCancelElement).toBeVisible(); + expect(buttonCancelElement).toHaveText('Cancel'); + }); + }); +}); diff --git a/spec/javascripts/badges/components/badge_list_row_spec.js b/spec/javascripts/badges/components/badge_list_row_spec.js new file mode 100644 index 00000000000..21bd00d82f0 --- /dev/null +++ b/spec/javascripts/badges/components/badge_list_row_spec.js @@ -0,0 +1,97 @@ +import $ from 'jquery'; +import Vue from 'vue'; +import { GROUP_BADGE, PROJECT_BADGE } from '~/badges/constants'; +import store from '~/badges/store'; +import BadgeListRow from '~/badges/components/badge_list_row.vue'; +import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { createDummyBadge } from '../dummy_badge'; + +describe('BadgeListRow component', () => { + const Component = Vue.extend(BadgeListRow); + let badge; + let vm; + + beforeEach(() => { + setFixtures(` + <div id="delete-badge-modal" class="modal"></div> + <div id="dummy-element"></div> + `); + store.replaceState({ + ...store.state, + kind: PROJECT_BADGE, + }); + badge = createDummyBadge(); + vm = mountComponentWithStore(Component, { + el: '#dummy-element', + store, + props: { badge }, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders the badge', () => { + const badgeElement = vm.$el.querySelector('.project-badge'); + expect(badgeElement).not.toBeNull(); + expect(badgeElement.getAttribute('src')).toBe(badge.renderedImageUrl); + }); + + it('renders the badge link', () => { + expect(vm.$el).toContainText(badge.linkUrl); + }); + + it('renders the badge kind', () => { + expect(vm.$el).toContainText('Project Badge'); + }); + + it('shows edit and delete buttons', () => { + const buttons = vm.$el.querySelectorAll('.table-button-footer button'); + expect(buttons).toHaveLength(2); + const buttonEditElement = buttons[0]; + expect(buttonEditElement).toBeVisible(); + expect(buttonEditElement).toHaveSpriteIcon('pencil'); + const buttonDeleteElement = buttons[1]; + expect(buttonDeleteElement).toBeVisible(); + expect(buttonDeleteElement).toHaveSpriteIcon('remove'); + }); + + it('calls editBadge when clicking then edit button', () => { + spyOn(vm, 'editBadge'); + + const editButton = vm.$el.querySelector('.table-button-footer button:first-of-type'); + editButton.click(); + + expect(vm.editBadge).toHaveBeenCalled(); + }); + + it('calls updateBadgeInModal and shows modal when clicking then delete button', done => { + spyOn(vm, 'updateBadgeInModal'); + $('#delete-badge-modal').on('shown.bs.modal', () => done()); + + const deleteButton = vm.$el.querySelector('.table-button-footer button:last-of-type'); + deleteButton.click(); + + expect(vm.updateBadgeInModal).toHaveBeenCalled(); + }); + + describe('for a group badge', () => { + beforeEach(done => { + badge.kind = GROUP_BADGE; + + Vue.nextTick() + .then(done) + .catch(done.fail); + }); + + it('renders the badge kind', () => { + expect(vm.$el).toContainText('Group Badge'); + }); + + it('hides edit and delete buttons', () => { + const buttons = vm.$el.querySelectorAll('.table-button-footer button'); + expect(buttons).toHaveLength(0); + }); + }); +}); diff --git a/spec/javascripts/badges/components/badge_list_spec.js b/spec/javascripts/badges/components/badge_list_spec.js new file mode 100644 index 00000000000..9439c578973 --- /dev/null +++ b/spec/javascripts/badges/components/badge_list_spec.js @@ -0,0 +1,88 @@ +import Vue from 'vue'; +import { GROUP_BADGE, PROJECT_BADGE } from '~/badges/constants'; +import store from '~/badges/store'; +import BadgeList from '~/badges/components/badge_list.vue'; +import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { createDummyBadge } from '../dummy_badge'; + +describe('BadgeList component', () => { + const Component = Vue.extend(BadgeList); + const numberOfDummyBadges = 3; + let vm; + + beforeEach(() => { + setFixtures('<div id="dummy-element"></div>'); + const badges = []; + for (let id = 0; id < numberOfDummyBadges; id += 1) { + badges.push({ id, ...createDummyBadge() }); + } + store.replaceState({ + ...store.state, + badges, + kind: PROJECT_BADGE, + isLoading: false, + }); + vm = mountComponentWithStore(Component, { + el: '#dummy-element', + store, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders a header with the badge count', () => { + const header = vm.$el.querySelector('.panel-heading'); + expect(header).toHaveText(new RegExp(`Your badges\\s+${numberOfDummyBadges}`)); + }); + + it('renders a row for each badge', () => { + const rows = vm.$el.querySelectorAll('.gl-responsive-table-row'); + expect(rows).toHaveLength(numberOfDummyBadges); + }); + + it('renders a message if no badges exist', done => { + store.state.badges = []; + + Vue.nextTick() + .then(() => { + expect(vm.$el).toContainText('This project has no badges'); + }) + .then(done) + .catch(done.fail); + }); + + it('shows a loading icon when loading', done => { + store.state.isLoading = true; + + Vue.nextTick() + .then(() => { + const loadingIcon = vm.$el.querySelector('.fa-spinner'); + expect(loadingIcon).toBeVisible(); + }) + .then(done) + .catch(done.fail); + }); + + describe('for group badges', () => { + beforeEach(done => { + store.state.kind = GROUP_BADGE; + + Vue.nextTick() + .then(done) + .catch(done.fail); + }); + + it('renders a message if no badges exist', done => { + store.state.badges = []; + + Vue.nextTick() + .then(() => { + expect(vm.$el).toContainText('This group has no badges'); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/badges/components/badge_settings_spec.js b/spec/javascripts/badges/components/badge_settings_spec.js new file mode 100644 index 00000000000..3db02982ad4 --- /dev/null +++ b/spec/javascripts/badges/components/badge_settings_spec.js @@ -0,0 +1,109 @@ +import $ from 'jquery'; +import Vue from 'vue'; +import store from '~/badges/store'; +import BadgeSettings from '~/badges/components/badge_settings.vue'; +import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { createDummyBadge } from '../dummy_badge'; + +describe('BadgeSettings component', () => { + const Component = Vue.extend(BadgeSettings); + let vm; + + beforeEach(() => { + setFixtures(` + <div id="dummy-element"></div> + <button + id="dummy-modal-button" + type="button" + data-toggle="modal" + data-target="#delete-badge-modal" + >Show modal</button> + `); + vm = mountComponentWithStore(Component, { + el: '#dummy-element', + store, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('displays modal if button is clicked', done => { + const badge = createDummyBadge(); + store.state.badgeInModal = badge; + const modal = vm.$el.querySelector('#delete-badge-modal'); + const button = document.getElementById('dummy-modal-button'); + + $(modal).on('shown.bs.modal', () => { + expect(modal).toContainText('Delete badge?'); + const badgeElement = modal.querySelector('img.project-badge'); + expect(badgeElement).not.toBe(null); + expect(badgeElement.getAttribute('src')).toBe(badge.renderedImageUrl); + + done(); + }); + + Vue.nextTick() + .then(() => { + button.click(); + }) + .catch(done.fail); + }); + + it('displays a form to add a badge', () => { + const form = vm.$el.querySelector('form:nth-of-type(2)'); + expect(form).not.toBe(null); + const button = form.querySelector('.btn-success'); + expect(button).not.toBe(null); + expect(button).toHaveText(/Add badge/); + }); + + it('displays badge list', () => { + const badgeListElement = vm.$el.querySelector('.panel'); + expect(badgeListElement).not.toBe(null); + expect(badgeListElement).toBeVisible(); + expect(badgeListElement).toContainText('Your badges'); + }); + + describe('when editing', () => { + beforeEach(done => { + store.state.isEditing = true; + + Vue.nextTick() + .then(done) + .catch(done.fail); + }); + + it('displays a form to edit a badge', () => { + const form = vm.$el.querySelector('form:nth-of-type(1)'); + expect(form).not.toBe(null); + const submitButton = form.querySelector('.btn-success'); + expect(submitButton).not.toBe(null); + expect(submitButton).toHaveText(/Save changes/); + const cancelButton = form.querySelector('.btn-cancel'); + expect(cancelButton).not.toBe(null); + expect(cancelButton).toHaveText(/Cancel/); + }); + + it('displays no badge list', () => { + const badgeListElement = vm.$el.querySelector('.panel'); + expect(badgeListElement).toBeHidden(); + }); + }); + + describe('methods', () => { + describe('onSubmitModal', () => { + it('triggers ', () => { + spyOn(vm, 'deleteBadge').and.callFake(() => Promise.resolve()); + const modal = vm.$el.querySelector('#delete-badge-modal'); + const deleteButton = modal.querySelector('.btn-danger'); + + deleteButton.click(); + + const badge = store.state.badgeInModal; + expect(vm.deleteBadge).toHaveBeenCalledWith(badge); + }); + }); + }); +}); diff --git a/spec/javascripts/badges/components/badge_spec.js b/spec/javascripts/badges/components/badge_spec.js new file mode 100644 index 00000000000..fd1ecc9cdd8 --- /dev/null +++ b/spec/javascripts/badges/components/badge_spec.js @@ -0,0 +1,147 @@ +import Vue from 'vue'; +import Badge from '~/badges/components/badge.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import { DUMMY_IMAGE_URL, TEST_HOST } from 'spec/test_constants'; + +describe('Badge component', () => { + const Component = Vue.extend(Badge); + const dummyProps = { + imageUrl: DUMMY_IMAGE_URL, + linkUrl: `${TEST_HOST}/badge/link/url`, + }; + let vm; + + const findElements = () => { + const buttons = vm.$el.querySelectorAll('button'); + return { + badgeImage: vm.$el.querySelector('img.project-badge'), + loadingIcon: vm.$el.querySelector('.fa-spinner'), + reloadButton: buttons[buttons.length - 1], + }; + }; + + const createComponent = (props, el = null) => { + vm = mountComponent(Component, props, el); + const { badgeImage } = findElements(); + return new Promise(resolve => badgeImage.addEventListener('load', resolve)).then(() => + Vue.nextTick(), + ); + }; + + afterEach(() => { + vm.$destroy(); + }); + + describe('watchers', () => { + describe('imageUrl', () => { + it('sets isLoading and resets numRetries and hasError', done => { + const props = { ...dummyProps }; + createComponent(props) + .then(() => { + expect(vm.isLoading).toBe(false); + vm.hasError = true; + vm.numRetries = 42; + + vm.imageUrl = `${props.imageUrl}#something/else`; + + return Vue.nextTick(); + }) + .then(() => { + expect(vm.isLoading).toBe(true); + expect(vm.numRetries).toBe(0); + expect(vm.hasError).toBe(false); + }) + .then(done) + .catch(done.fail); + }); + }); + }); + + describe('methods', () => { + beforeEach(done => { + createComponent({ ...dummyProps }) + .then(done) + .catch(done.fail); + }); + + it('onError resets isLoading and sets hasError', () => { + vm.hasError = false; + vm.isLoading = true; + + vm.onError(); + + expect(vm.hasError).toBe(true); + expect(vm.isLoading).toBe(false); + }); + + it('onLoad sets isLoading', () => { + vm.isLoading = true; + + vm.onLoad(); + + expect(vm.isLoading).toBe(false); + }); + + it('reloadImage resets isLoading and hasError and increases numRetries', () => { + vm.hasError = true; + vm.isLoading = false; + vm.numRetries = 0; + + vm.reloadImage(); + + expect(vm.hasError).toBe(false); + expect(vm.isLoading).toBe(true); + expect(vm.numRetries).toBe(1); + }); + }); + + describe('behavior', () => { + beforeEach(done => { + setFixtures('<div id="dummy-element"></div>'); + createComponent({ ...dummyProps }, '#dummy-element') + .then(done) + .catch(done.fail); + }); + + it('shows a badge image after loading', () => { + expect(vm.isLoading).toBe(false); + expect(vm.hasError).toBe(false); + const { badgeImage, loadingIcon, reloadButton } = findElements(); + expect(badgeImage).toBeVisible(); + expect(loadingIcon).toBeHidden(); + expect(reloadButton).toBeHidden(); + expect(vm.$el.innerText).toBe(''); + }); + + it('shows a loading icon when loading', done => { + vm.isLoading = true; + + Vue.nextTick() + .then(() => { + const { badgeImage, loadingIcon, reloadButton } = findElements(); + expect(badgeImage).toBeHidden(); + expect(loadingIcon).toBeVisible(); + expect(reloadButton).toBeHidden(); + expect(vm.$el.innerText).toBe(''); + }) + .then(done) + .catch(done.fail); + }); + + it('shows an error and reload button if loading failed', done => { + vm.hasError = true; + + Vue.nextTick() + .then(() => { + const { badgeImage, loadingIcon, reloadButton } = findElements(); + expect(badgeImage).toBeHidden(); + expect(loadingIcon).toBeHidden(); + expect(reloadButton).toBeVisible(); + expect(reloadButton).toHaveSpriteIcon('retry'); + expect(vm.$el.innerText.trim()).toBe('No badge image'); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/badges/dummy_badge.js b/spec/javascripts/badges/dummy_badge.js new file mode 100644 index 00000000000..6aaff21c503 --- /dev/null +++ b/spec/javascripts/badges/dummy_badge.js @@ -0,0 +1,23 @@ +import { PROJECT_BADGE } from '~/badges/constants'; +import { DUMMY_IMAGE_URL, TEST_HOST } from 'spec/test_constants'; + +export const createDummyBadge = () => { + const id = Math.floor(1000 * Math.random()); + return { + id, + imageUrl: `${TEST_HOST}/badges/${id}/image/url`, + isDeleting: false, + linkUrl: `${TEST_HOST}/badges/${id}/link/url`, + kind: PROJECT_BADGE, + renderedImageUrl: `${DUMMY_IMAGE_URL}?id=${id}`, + renderedLinkUrl: `${TEST_HOST}/badges/${id}/rendered/link/url`, + }; +}; + +export const createDummyBadgeResponse = () => ({ + image_url: `${TEST_HOST}/badge/image/url`, + link_url: `${TEST_HOST}/badge/link/url`, + kind: PROJECT_BADGE, + rendered_image_url: DUMMY_IMAGE_URL, + rendered_link_url: `${TEST_HOST}/rendered/badge/link/url`, +}); diff --git a/spec/javascripts/badges/store/actions_spec.js b/spec/javascripts/badges/store/actions_spec.js new file mode 100644 index 00000000000..bb6263c6de4 --- /dev/null +++ b/spec/javascripts/badges/store/actions_spec.js @@ -0,0 +1,607 @@ +import axios from '~/lib/utils/axios_utils'; +import MockAdapter from 'axios-mock-adapter'; +import actions, { transformBackendBadge } from '~/badges/store/actions'; +import mutationTypes from '~/badges/store/mutation_types'; +import createState from '~/badges/store/state'; +import { TEST_HOST } from 'spec/test_constants'; +import testAction from 'spec/helpers/vuex_action_helper'; +import { createDummyBadge, createDummyBadgeResponse } from '../dummy_badge'; + +describe('Badges store actions', () => { + const dummyEndpointUrl = `${TEST_HOST}/badges/endpoint`; + const dummyBadges = [{ ...createDummyBadge(), id: 5 }, { ...createDummyBadge(), id: 6 }]; + + let axiosMock; + let badgeId; + let state; + + beforeEach(() => { + axiosMock = new MockAdapter(axios); + state = { + ...createState(), + apiEndpointUrl: dummyEndpointUrl, + badges: dummyBadges, + }; + badgeId = state.badges[0].id; + }); + + afterEach(() => { + axiosMock.restore(); + }); + + describe('requestNewBadge', () => { + it('commits REQUEST_NEW_BADGE', done => { + testAction( + actions.requestNewBadge, + null, + state, + [{ type: mutationTypes.REQUEST_NEW_BADGE }], + [], + done, + ); + }); + }); + + describe('receiveNewBadge', () => { + it('commits RECEIVE_NEW_BADGE', done => { + const newBadge = createDummyBadge(); + testAction( + actions.receiveNewBadge, + newBadge, + state, + [{ type: mutationTypes.RECEIVE_NEW_BADGE, payload: newBadge }], + [], + done, + ); + }); + }); + + describe('receiveNewBadgeError', () => { + it('commits RECEIVE_NEW_BADGE_ERROR', done => { + testAction( + actions.receiveNewBadgeError, + null, + state, + [{ type: mutationTypes.RECEIVE_NEW_BADGE_ERROR }], + [], + done, + ); + }); + }); + + describe('addBadge', () => { + let badgeInAddForm; + let dispatch; + let endpointMock; + + beforeEach(() => { + endpointMock = axiosMock.onPost(dummyEndpointUrl); + dispatch = jasmine.createSpy('dispatch'); + badgeInAddForm = createDummyBadge(); + state = { + ...state, + badgeInAddForm, + }; + }); + + it('dispatches requestNewBadge and receiveNewBadge for successful response', done => { + const dummyResponse = createDummyBadgeResponse(); + + endpointMock.replyOnce(req => { + expect(req.data).toBe( + JSON.stringify({ + image_url: badgeInAddForm.imageUrl, + link_url: badgeInAddForm.linkUrl, + }), + ); + expect(dispatch.calls.allArgs()).toEqual([['requestNewBadge']]); + dispatch.calls.reset(); + return [200, dummyResponse]; + }); + + const dummyBadge = transformBackendBadge(dummyResponse); + actions + .addBadge({ state, dispatch }) + .then(() => { + expect(dispatch.calls.allArgs()).toEqual([['receiveNewBadge', dummyBadge]]); + }) + .then(done) + .catch(done.fail); + }); + + it('dispatches requestNewBadge and receiveNewBadgeError for error response', done => { + endpointMock.replyOnce(req => { + expect(req.data).toBe( + JSON.stringify({ + image_url: badgeInAddForm.imageUrl, + link_url: badgeInAddForm.linkUrl, + }), + ); + expect(dispatch.calls.allArgs()).toEqual([['requestNewBadge']]); + dispatch.calls.reset(); + return [500, '']; + }); + + actions + .addBadge({ state, dispatch }) + .then(() => done.fail('Expected Ajax call to fail!')) + .catch(() => { + expect(dispatch.calls.allArgs()).toEqual([['receiveNewBadgeError']]); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('requestDeleteBadge', () => { + it('commits REQUEST_DELETE_BADGE', done => { + testAction( + actions.requestDeleteBadge, + badgeId, + state, + [{ type: mutationTypes.REQUEST_DELETE_BADGE, payload: badgeId }], + [], + done, + ); + }); + }); + + describe('receiveDeleteBadge', () => { + it('commits RECEIVE_DELETE_BADGE', done => { + testAction( + actions.receiveDeleteBadge, + badgeId, + state, + [{ type: mutationTypes.RECEIVE_DELETE_BADGE, payload: badgeId }], + [], + done, + ); + }); + }); + + describe('receiveDeleteBadgeError', () => { + it('commits RECEIVE_DELETE_BADGE_ERROR', done => { + testAction( + actions.receiveDeleteBadgeError, + badgeId, + state, + [{ type: mutationTypes.RECEIVE_DELETE_BADGE_ERROR, payload: badgeId }], + [], + done, + ); + }); + }); + + describe('deleteBadge', () => { + let dispatch; + let endpointMock; + + beforeEach(() => { + endpointMock = axiosMock.onDelete(`${dummyEndpointUrl}/${badgeId}`); + dispatch = jasmine.createSpy('dispatch'); + }); + + it('dispatches requestDeleteBadge and receiveDeleteBadge for successful response', done => { + endpointMock.replyOnce(() => { + expect(dispatch.calls.allArgs()).toEqual([['requestDeleteBadge', badgeId]]); + dispatch.calls.reset(); + return [200, '']; + }); + + actions + .deleteBadge({ state, dispatch }, { id: badgeId }) + .then(() => { + expect(dispatch.calls.allArgs()).toEqual([['receiveDeleteBadge', badgeId]]); + }) + .then(done) + .catch(done.fail); + }); + + it('dispatches requestDeleteBadge and receiveDeleteBadgeError for error response', done => { + endpointMock.replyOnce(() => { + expect(dispatch.calls.allArgs()).toEqual([['requestDeleteBadge', badgeId]]); + dispatch.calls.reset(); + return [500, '']; + }); + + actions + .deleteBadge({ state, dispatch }, { id: badgeId }) + .then(() => done.fail('Expected Ajax call to fail!')) + .catch(() => { + expect(dispatch.calls.allArgs()).toEqual([['receiveDeleteBadgeError', badgeId]]); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('editBadge', () => { + it('commits START_EDITING', done => { + const dummyBadge = createDummyBadge(); + testAction( + actions.editBadge, + dummyBadge, + state, + [{ type: mutationTypes.START_EDITING, payload: dummyBadge }], + [], + done, + ); + }); + }); + + describe('requestLoadBadges', () => { + it('commits REQUEST_LOAD_BADGES', done => { + const dummyData = 'this is not real data'; + testAction( + actions.requestLoadBadges, + dummyData, + state, + [{ type: mutationTypes.REQUEST_LOAD_BADGES, payload: dummyData }], + [], + done, + ); + }); + }); + + describe('receiveLoadBadges', () => { + it('commits RECEIVE_LOAD_BADGES', done => { + const badges = dummyBadges; + testAction( + actions.receiveLoadBadges, + badges, + state, + [{ type: mutationTypes.RECEIVE_LOAD_BADGES, payload: badges }], + [], + done, + ); + }); + }); + + describe('receiveLoadBadgesError', () => { + it('commits RECEIVE_LOAD_BADGES_ERROR', done => { + testAction( + actions.receiveLoadBadgesError, + null, + state, + [{ type: mutationTypes.RECEIVE_LOAD_BADGES_ERROR }], + [], + done, + ); + }); + }); + + describe('loadBadges', () => { + let dispatch; + let endpointMock; + + beforeEach(() => { + endpointMock = axiosMock.onGet(dummyEndpointUrl); + dispatch = jasmine.createSpy('dispatch'); + }); + + it('dispatches requestLoadBadges and receiveLoadBadges for successful response', done => { + const dummyData = 'this is just some data'; + const dummyReponse = [ + createDummyBadgeResponse(), + createDummyBadgeResponse(), + createDummyBadgeResponse(), + ]; + endpointMock.replyOnce(() => { + expect(dispatch.calls.allArgs()).toEqual([['requestLoadBadges', dummyData]]); + dispatch.calls.reset(); + return [200, dummyReponse]; + }); + + actions + .loadBadges({ state, dispatch }, dummyData) + .then(() => { + const badges = dummyReponse.map(transformBackendBadge); + expect(dispatch.calls.allArgs()).toEqual([['receiveLoadBadges', badges]]); + }) + .then(done) + .catch(done.fail); + }); + + it('dispatches requestLoadBadges and receiveLoadBadgesError for error response', done => { + const dummyData = 'this is just some data'; + endpointMock.replyOnce(() => { + expect(dispatch.calls.allArgs()).toEqual([['requestLoadBadges', dummyData]]); + dispatch.calls.reset(); + return [500, '']; + }); + + actions + .loadBadges({ state, dispatch }, dummyData) + .then(() => done.fail('Expected Ajax call to fail!')) + .catch(() => { + expect(dispatch.calls.allArgs()).toEqual([['receiveLoadBadgesError']]); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('requestRenderedBadge', () => { + it('commits REQUEST_RENDERED_BADGE', done => { + testAction( + actions.requestRenderedBadge, + null, + state, + [{ type: mutationTypes.REQUEST_RENDERED_BADGE }], + [], + done, + ); + }); + }); + + describe('receiveRenderedBadge', () => { + it('commits RECEIVE_RENDERED_BADGE', done => { + const dummyBadge = createDummyBadge(); + testAction( + actions.receiveRenderedBadge, + dummyBadge, + state, + [{ type: mutationTypes.RECEIVE_RENDERED_BADGE, payload: dummyBadge }], + [], + done, + ); + }); + }); + + describe('receiveRenderedBadgeError', () => { + it('commits RECEIVE_RENDERED_BADGE_ERROR', done => { + testAction( + actions.receiveRenderedBadgeError, + null, + state, + [{ type: mutationTypes.RECEIVE_RENDERED_BADGE_ERROR }], + [], + done, + ); + }); + }); + + describe('renderBadge', () => { + let dispatch; + let endpointMock; + let badgeInForm; + + beforeEach(() => { + badgeInForm = createDummyBadge(); + state = { + ...state, + badgeInAddForm: badgeInForm, + }; + const urlParameters = [ + `link_url=${encodeURIComponent(badgeInForm.linkUrl)}`, + `image_url=${encodeURIComponent(badgeInForm.imageUrl)}`, + ].join('&'); + endpointMock = axiosMock.onGet(`${dummyEndpointUrl}/render?${urlParameters}`); + dispatch = jasmine.createSpy('dispatch'); + }); + + it('returns immediately if imageUrl is empty', done => { + spyOn(axios, 'get'); + badgeInForm.imageUrl = ''; + + actions + .renderBadge({ state, dispatch }) + .then(() => { + expect(axios.get).not.toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + it('returns immediately if linkUrl is empty', done => { + spyOn(axios, 'get'); + badgeInForm.linkUrl = ''; + + actions + .renderBadge({ state, dispatch }) + .then(() => { + expect(axios.get).not.toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + it('escapes user input', done => { + spyOn(axios, 'get').and.callFake(() => Promise.resolve({ data: createDummyBadgeResponse() })); + badgeInForm.imageUrl = '&make-sandwhich=true'; + badgeInForm.linkUrl = '<script>I am dangerous!</script>'; + + actions + .renderBadge({ state, dispatch }) + .then(() => { + expect(axios.get.calls.count()).toBe(1); + const url = axios.get.calls.argsFor(0)[0]; + expect(url).toMatch(`^${dummyEndpointUrl}/render?`); + expect(url).toMatch('\\?link_url=%3Cscript%3EI%20am%20dangerous!%3C%2Fscript%3E&'); + expect(url).toMatch('&image_url=%26make-sandwhich%3Dtrue$'); + }) + .then(done) + .catch(done.fail); + }); + + it('dispatches requestRenderedBadge and receiveRenderedBadge for successful response', done => { + const dummyReponse = createDummyBadgeResponse(); + endpointMock.replyOnce(() => { + expect(dispatch.calls.allArgs()).toEqual([['requestRenderedBadge']]); + dispatch.calls.reset(); + return [200, dummyReponse]; + }); + + actions + .renderBadge({ state, dispatch }) + .then(() => { + const renderedBadge = transformBackendBadge(dummyReponse); + expect(dispatch.calls.allArgs()).toEqual([['receiveRenderedBadge', renderedBadge]]); + }) + .then(done) + .catch(done.fail); + }); + + it('dispatches requestRenderedBadge and receiveRenderedBadgeError for error response', done => { + endpointMock.replyOnce(() => { + expect(dispatch.calls.allArgs()).toEqual([['requestRenderedBadge']]); + dispatch.calls.reset(); + return [500, '']; + }); + + actions + .renderBadge({ state, dispatch }) + .then(() => done.fail('Expected Ajax call to fail!')) + .catch(() => { + expect(dispatch.calls.allArgs()).toEqual([['receiveRenderedBadgeError']]); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('requestUpdatedBadge', () => { + it('commits REQUEST_UPDATED_BADGE', done => { + testAction( + actions.requestUpdatedBadge, + null, + state, + [{ type: mutationTypes.REQUEST_UPDATED_BADGE }], + [], + done, + ); + }); + }); + + describe('receiveUpdatedBadge', () => { + it('commits RECEIVE_UPDATED_BADGE', done => { + const updatedBadge = createDummyBadge(); + testAction( + actions.receiveUpdatedBadge, + updatedBadge, + state, + [{ type: mutationTypes.RECEIVE_UPDATED_BADGE, payload: updatedBadge }], + [], + done, + ); + }); + }); + + describe('receiveUpdatedBadgeError', () => { + it('commits RECEIVE_UPDATED_BADGE_ERROR', done => { + testAction( + actions.receiveUpdatedBadgeError, + null, + state, + [{ type: mutationTypes.RECEIVE_UPDATED_BADGE_ERROR }], + [], + done, + ); + }); + }); + + describe('saveBadge', () => { + let badgeInEditForm; + let dispatch; + let endpointMock; + + beforeEach(() => { + badgeInEditForm = createDummyBadge(); + state = { + ...state, + badgeInEditForm, + }; + endpointMock = axiosMock.onPut(`${dummyEndpointUrl}/${badgeInEditForm.id}`); + dispatch = jasmine.createSpy('dispatch'); + }); + + it('dispatches requestUpdatedBadge and receiveUpdatedBadge for successful response', done => { + const dummyResponse = createDummyBadgeResponse(); + + endpointMock.replyOnce(req => { + expect(req.data).toBe( + JSON.stringify({ + image_url: badgeInEditForm.imageUrl, + link_url: badgeInEditForm.linkUrl, + }), + ); + expect(dispatch.calls.allArgs()).toEqual([['requestUpdatedBadge']]); + dispatch.calls.reset(); + return [200, dummyResponse]; + }); + + const updatedBadge = transformBackendBadge(dummyResponse); + actions + .saveBadge({ state, dispatch }) + .then(() => { + expect(dispatch.calls.allArgs()).toEqual([['receiveUpdatedBadge', updatedBadge]]); + }) + .then(done) + .catch(done.fail); + }); + + it('dispatches requestUpdatedBadge and receiveUpdatedBadgeError for error response', done => { + endpointMock.replyOnce(req => { + expect(req.data).toBe( + JSON.stringify({ + image_url: badgeInEditForm.imageUrl, + link_url: badgeInEditForm.linkUrl, + }), + ); + expect(dispatch.calls.allArgs()).toEqual([['requestUpdatedBadge']]); + dispatch.calls.reset(); + return [500, '']; + }); + + actions + .saveBadge({ state, dispatch }) + .then(() => done.fail('Expected Ajax call to fail!')) + .catch(() => { + expect(dispatch.calls.allArgs()).toEqual([['receiveUpdatedBadgeError']]); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('stopEditing', () => { + it('commits STOP_EDITING', done => { + testAction( + actions.stopEditing, + null, + state, + [{ type: mutationTypes.STOP_EDITING }], + [], + done, + ); + }); + }); + + describe('updateBadgeInForm', () => { + it('commits UPDATE_BADGE_IN_FORM', done => { + const dummyBadge = createDummyBadge(); + testAction( + actions.updateBadgeInForm, + dummyBadge, + state, + [{ type: mutationTypes.UPDATE_BADGE_IN_FORM, payload: dummyBadge }], + [], + done, + ); + }); + + describe('updateBadgeInModal', () => { + it('commits UPDATE_BADGE_IN_MODAL', done => { + const dummyBadge = createDummyBadge(); + testAction( + actions.updateBadgeInModal, + dummyBadge, + state, + [{ type: mutationTypes.UPDATE_BADGE_IN_MODAL, payload: dummyBadge }], + [], + done, + ); + }); + }); + }); +}); diff --git a/spec/javascripts/badges/store/mutations_spec.js b/spec/javascripts/badges/store/mutations_spec.js new file mode 100644 index 00000000000..8d26f83339d --- /dev/null +++ b/spec/javascripts/badges/store/mutations_spec.js @@ -0,0 +1,418 @@ +import { GROUP_BADGE, PROJECT_BADGE } from '~/badges/constants'; +import store from '~/badges/store'; +import types from '~/badges/store/mutation_types'; +import createState from '~/badges/store/state'; +import { createDummyBadge } from '../dummy_badge'; + +describe('Badges store mutations', () => { + let dummyBadge; + + beforeEach(() => { + dummyBadge = createDummyBadge(); + store.replaceState(createState()); + }); + + describe('RECEIVE_DELETE_BADGE', () => { + beforeEach(() => { + const badges = [ + { ...dummyBadge, id: dummyBadge.id - 1 }, + dummyBadge, + { ...dummyBadge, id: dummyBadge.id + 1 }, + ]; + + store.replaceState({ + ...store.state, + badges, + }); + }); + + it('removes deleted badge', () => { + const badgeCount = store.state.badges.length; + + store.commit(types.RECEIVE_DELETE_BADGE, dummyBadge.id); + + expect(store.state.badges.length).toBe(badgeCount - 1); + expect(store.state.badges.indexOf(dummyBadge)).toBe(-1); + }); + }); + + describe('RECEIVE_DELETE_BADGE_ERROR', () => { + beforeEach(() => { + const badges = [ + { ...dummyBadge, id: dummyBadge.id - 1, isDeleting: false }, + { ...dummyBadge, isDeleting: true }, + { ...dummyBadge, id: dummyBadge.id + 1, isDeleting: true }, + ]; + + store.replaceState({ + ...store.state, + badges, + }); + }); + + it('sets isDeleting to false', () => { + const badgeCount = store.state.badges.length; + + store.commit(types.RECEIVE_DELETE_BADGE_ERROR, dummyBadge.id); + + expect(store.state.badges.length).toBe(badgeCount); + expect(store.state.badges[0].isDeleting).toBe(false); + expect(store.state.badges[1].isDeleting).toBe(false); + expect(store.state.badges[2].isDeleting).toBe(true); + }); + }); + + describe('RECEIVE_LOAD_BADGES', () => { + beforeEach(() => { + store.replaceState({ + ...store.state, + isLoading: 'not false', + }); + }); + + it('sets badges and isLoading to false', () => { + const badges = [createDummyBadge()]; + store.commit(types.RECEIVE_LOAD_BADGES, badges); + + expect(store.state.isLoading).toBe(false); + expect(store.state.badges).toBe(badges); + }); + }); + + describe('RECEIVE_LOAD_BADGES_ERROR', () => { + beforeEach(() => { + store.replaceState({ + ...store.state, + isLoading: 'not false', + }); + }); + + it('sets isLoading to false', () => { + store.commit(types.RECEIVE_LOAD_BADGES_ERROR); + + expect(store.state.isLoading).toBe(false); + }); + }); + + describe('RECEIVE_NEW_BADGE', () => { + beforeEach(() => { + const badges = [ + { ...dummyBadge, id: dummyBadge.id - 1, kind: GROUP_BADGE }, + { ...dummyBadge, id: dummyBadge.id + 1, kind: GROUP_BADGE }, + { ...dummyBadge, id: dummyBadge.id - 1, kind: PROJECT_BADGE }, + { ...dummyBadge, id: dummyBadge.id + 1, kind: PROJECT_BADGE }, + ]; + store.replaceState({ + ...store.state, + badgeInAddForm: createDummyBadge(), + badges, + isSaving: 'dummy value', + renderedBadge: createDummyBadge(), + }); + }); + + it('resets the add form', () => { + store.commit(types.RECEIVE_NEW_BADGE, dummyBadge); + + expect(store.state.badgeInAddForm).toBe(null); + expect(store.state.isSaving).toBe(false); + expect(store.state.renderedBadge).toBe(null); + }); + + it('inserts group badge at correct position', () => { + const badgeCount = store.state.badges.length; + dummyBadge = { ...dummyBadge, kind: GROUP_BADGE }; + + store.commit(types.RECEIVE_NEW_BADGE, dummyBadge); + + expect(store.state.badges.length).toBe(badgeCount + 1); + expect(store.state.badges.indexOf(dummyBadge)).toBe(1); + }); + + it('inserts project badge at correct position', () => { + const badgeCount = store.state.badges.length; + dummyBadge = { ...dummyBadge, kind: PROJECT_BADGE }; + + store.commit(types.RECEIVE_NEW_BADGE, dummyBadge); + + expect(store.state.badges.length).toBe(badgeCount + 1); + expect(store.state.badges.indexOf(dummyBadge)).toBe(3); + }); + }); + + describe('RECEIVE_NEW_BADGE_ERROR', () => { + beforeEach(() => { + store.replaceState({ + ...store.state, + isSaving: 'dummy value', + }); + }); + + it('sets isSaving to false', () => { + store.commit(types.RECEIVE_NEW_BADGE_ERROR); + + expect(store.state.isSaving).toBe(false); + }); + }); + + describe('RECEIVE_RENDERED_BADGE', () => { + beforeEach(() => { + store.replaceState({ + ...store.state, + isRendering: 'dummy value', + renderedBadge: 'dummy value', + }); + }); + + it('sets renderedBadge', () => { + store.commit(types.RECEIVE_RENDERED_BADGE, dummyBadge); + + expect(store.state.isRendering).toBe(false); + expect(store.state.renderedBadge).toBe(dummyBadge); + }); + }); + + describe('RECEIVE_RENDERED_BADGE_ERROR', () => { + beforeEach(() => { + store.replaceState({ + ...store.state, + isRendering: 'dummy value', + }); + }); + + it('sets isRendering to false', () => { + store.commit(types.RECEIVE_RENDERED_BADGE_ERROR); + + expect(store.state.isRendering).toBe(false); + }); + }); + + describe('RECEIVE_UPDATED_BADGE', () => { + beforeEach(() => { + const badges = [ + { ...dummyBadge, id: dummyBadge.id - 1 }, + dummyBadge, + { ...dummyBadge, id: dummyBadge.id + 1 }, + ]; + store.replaceState({ + ...store.state, + badgeInEditForm: createDummyBadge(), + badges, + isEditing: 'dummy value', + isSaving: 'dummy value', + renderedBadge: createDummyBadge(), + }); + }); + + it('resets the edit form', () => { + store.commit(types.RECEIVE_UPDATED_BADGE, dummyBadge); + + expect(store.state.badgeInAddForm).toBe(null); + expect(store.state.isSaving).toBe(false); + expect(store.state.renderedBadge).toBe(null); + }); + + it('replaces the updated badge', () => { + const badgeCount = store.state.badges.length; + const badgeIndex = store.state.badges.indexOf(dummyBadge); + const newBadge = { id: dummyBadge.id, dummy: 'value' }; + + store.commit(types.RECEIVE_UPDATED_BADGE, newBadge); + + expect(store.state.badges.length).toBe(badgeCount); + expect(store.state.badges[badgeIndex]).toBe(newBadge); + }); + }); + + describe('RECEIVE_UPDATED_BADGE_ERROR', () => { + beforeEach(() => { + store.replaceState({ + ...store.state, + isSaving: 'dummy value', + }); + }); + + it('sets isSaving to false', () => { + store.commit(types.RECEIVE_NEW_BADGE_ERROR); + + expect(store.state.isSaving).toBe(false); + }); + }); + + describe('REQUEST_DELETE_BADGE', () => { + beforeEach(() => { + const badges = [ + { ...dummyBadge, id: dummyBadge.id - 1, isDeleting: false }, + { ...dummyBadge, isDeleting: false }, + { ...dummyBadge, id: dummyBadge.id + 1, isDeleting: true }, + ]; + + store.replaceState({ + ...store.state, + badges, + }); + }); + + it('sets isDeleting to true', () => { + const badgeCount = store.state.badges.length; + + store.commit(types.REQUEST_DELETE_BADGE, dummyBadge.id); + + expect(store.state.badges.length).toBe(badgeCount); + expect(store.state.badges[0].isDeleting).toBe(false); + expect(store.state.badges[1].isDeleting).toBe(true); + expect(store.state.badges[2].isDeleting).toBe(true); + }); + }); + + describe('REQUEST_LOAD_BADGES', () => { + beforeEach(() => { + store.replaceState({ + ...store.state, + apiEndpointUrl: 'some endpoint', + docsUrl: 'some url', + isLoading: 'dummy value', + kind: 'some kind', + }); + }); + + it('sets isLoading to true and initializes the store', () => { + const dummyData = { + apiEndpointUrl: 'dummy endpoint', + docsUrl: 'dummy url', + kind: 'dummy kind', + }; + + store.commit(types.REQUEST_LOAD_BADGES, dummyData); + + expect(store.state.isLoading).toBe(true); + expect(store.state.apiEndpointUrl).toBe(dummyData.apiEndpointUrl); + expect(store.state.docsUrl).toBe(dummyData.docsUrl); + expect(store.state.kind).toBe(dummyData.kind); + }); + }); + + describe('REQUEST_NEW_BADGE', () => { + beforeEach(() => { + store.replaceState({ + ...store.state, + isSaving: 'dummy value', + }); + }); + + it('sets isSaving to true', () => { + store.commit(types.REQUEST_NEW_BADGE); + + expect(store.state.isSaving).toBe(true); + }); + }); + + describe('REQUEST_RENDERED_BADGE', () => { + beforeEach(() => { + store.replaceState({ + ...store.state, + isRendering: 'dummy value', + }); + }); + + it('sets isRendering to true', () => { + store.commit(types.REQUEST_RENDERED_BADGE); + + expect(store.state.isRendering).toBe(true); + }); + }); + + describe('REQUEST_UPDATED_BADGE', () => { + beforeEach(() => { + store.replaceState({ + ...store.state, + isSaving: 'dummy value', + }); + }); + + it('sets isSaving to true', () => { + store.commit(types.REQUEST_NEW_BADGE); + + expect(store.state.isSaving).toBe(true); + }); + }); + + describe('START_EDITING', () => { + beforeEach(() => { + store.replaceState({ + ...store.state, + badgeInEditForm: 'dummy value', + isEditing: 'dummy value', + renderedBadge: 'dummy value', + }); + }); + + it('initializes the edit form', () => { + store.commit(types.START_EDITING, dummyBadge); + + expect(store.state.isEditing).toBe(true); + expect(store.state.badgeInEditForm).toEqual(dummyBadge); + expect(store.state.renderedBadge).toEqual(dummyBadge); + }); + }); + + describe('STOP_EDITING', () => { + beforeEach(() => { + store.replaceState({ + ...store.state, + badgeInEditForm: 'dummy value', + isEditing: 'dummy value', + renderedBadge: 'dummy value', + }); + }); + + it('resets the edit form', () => { + store.commit(types.STOP_EDITING); + + expect(store.state.isEditing).toBe(false); + expect(store.state.badgeInEditForm).toBe(null); + expect(store.state.renderedBadge).toBe(null); + }); + }); + + describe('UPDATE_BADGE_IN_FORM', () => { + beforeEach(() => { + store.replaceState({ + ...store.state, + badgeInAddForm: 'dummy value', + badgeInEditForm: 'dummy value', + }); + }); + + it('sets badgeInEditForm if isEditing is true', () => { + store.state.isEditing = true; + + store.commit(types.UPDATE_BADGE_IN_FORM, dummyBadge); + + expect(store.state.badgeInEditForm).toBe(dummyBadge); + }); + + it('sets badgeInAddForm if isEditing is false', () => { + store.state.isEditing = false; + + store.commit(types.UPDATE_BADGE_IN_FORM, dummyBadge); + + expect(store.state.badgeInAddForm).toBe(dummyBadge); + }); + }); + + describe('UPDATE_BADGE_IN_MODAL', () => { + beforeEach(() => { + store.replaceState({ + ...store.state, + badgeInModal: 'dummy value', + }); + }); + + it('sets badgeInModal', () => { + store.commit(types.UPDATE_BADGE_IN_MODAL, dummyBadge); + + expect(store.state.badgeInModal).toBe(dummyBadge); + }); + }); +}); diff --git a/spec/javascripts/boards/board_blank_state_spec.js b/spec/javascripts/boards/board_blank_state_spec.js index f757dadfada..664ea202e93 100644 --- a/spec/javascripts/boards/board_blank_state_spec.js +++ b/spec/javascripts/boards/board_blank_state_spec.js @@ -1,7 +1,7 @@ /* global BoardService */ import Vue from 'vue'; import '~/boards/stores/boards_store'; -import boardBlankState from '~/boards/components/board_blank_state'; +import BoardBlankState from '~/boards/components/board_blank_state.vue'; import { mockBoardService } from './mock_data'; describe('Boards blank state', () => { @@ -9,7 +9,7 @@ describe('Boards blank state', () => { let fail = false; beforeEach((done) => { - const Comp = Vue.extend(boardBlankState); + const Comp = Vue.extend(BoardBlankState); gl.issueBoards.BoardsStore.create(); gl.boardService = mockBoardService(); diff --git a/spec/javascripts/boards/modal_store_spec.js b/spec/javascripts/boards/modal_store_spec.js index e9d77f035e3..797693a21aa 100644 --- a/spec/javascripts/boards/modal_store_spec.js +++ b/spec/javascripts/boards/modal_store_spec.js @@ -4,12 +4,11 @@ import '~/vue_shared/models/label'; import '~/boards/models/issue'; import '~/boards/models/list'; import '~/boards/models/assignee'; -import '~/boards/stores/modal_store'; +import Store from '~/boards/stores/modal_store'; describe('Modal store', () => { let issue; let issue2; - const Store = gl.issueBoards.ModalStore; beforeEach(() => { // Setup default state diff --git a/spec/javascripts/fixtures/one_white_pixel.png b/spec/javascripts/fixtures/one_white_pixel.png Binary files differnew file mode 100644 index 00000000000..073fcf40a18 --- /dev/null +++ b/spec/javascripts/fixtures/one_white_pixel.png diff --git a/spec/javascripts/helpers/vue_mount_component_helper.js b/spec/javascripts/helpers/vue_mount_component_helper.js index 34acdfbfba9..effacbcff4e 100644 --- a/spec/javascripts/helpers/vue_mount_component_helper.js +++ b/spec/javascripts/helpers/vue_mount_component_helper.js @@ -3,6 +3,12 @@ export const createComponentWithStore = (Component, store, propsData = {}) => ne propsData, }); +export const mountComponentWithStore = (Component, { el, props, store }) => + new Component({ + store, + propsData: props || { }, + }).$mount(el); + export default (Component, props = {}, el = null) => new Component({ propsData: props, }).$mount(el); diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js index e57a55fa71a..ae00fb76714 100644 --- a/spec/javascripts/lib/utils/text_utility_spec.js +++ b/spec/javascripts/lib/utils/text_utility_spec.js @@ -65,11 +65,15 @@ describe('text_utility', () => { describe('stripHtml', () => { it('replaces html tag with the default replacement', () => { - expect(textUtils.stripHtml('This is a text with <p>html</p>.')).toEqual('This is a text with html.'); + expect(textUtils.stripHtml('This is a text with <p>html</p>.')).toEqual( + 'This is a text with html.', + ); }); it('replaces html tags with the provided replacement', () => { - expect(textUtils.stripHtml('This is a text with <p>html</p>.', ' ')).toEqual('This is a text with html .'); + expect(textUtils.stripHtml('This is a text with <p>html</p>.', ' ')).toEqual( + 'This is a text with html .', + ); }); }); @@ -78,4 +82,10 @@ describe('text_utility', () => { expect(textUtils.convertToCamelCase('snake_case')).toBe('snakeCase'); }); }); + + describe('convertToSentenceCase', () => { + it('converts Sentence Case to Sentence case', () => { + expect(textUtils.convertToSentenceCase('Hello World')).toBe('Hello world'); + }); + }); }); diff --git a/spec/javascripts/matchers.js b/spec/javascripts/matchers.js new file mode 100644 index 00000000000..7cc5e753c22 --- /dev/null +++ b/spec/javascripts/matchers.js @@ -0,0 +1,35 @@ +export default { + toHaveSpriteIcon: () => ({ + compare(element, iconName) { + if (!iconName) { + throw new Error('toHaveSpriteIcon is missing iconName argument!'); + } + + if (!(element instanceof HTMLElement)) { + throw new Error(`${element} is not a DOM element!`); + } + + const iconReferences = [].slice.apply(element.querySelectorAll('svg use')); + const matchingIcon = iconReferences.find(reference => reference.getAttribute('xlink:href').endsWith(`#${iconName}`)); + const result = { + pass: !!matchingIcon, + }; + + if (result.pass) { + result.message = `${element.outerHTML} contains the sprite icon "${iconName}"!`; + } else { + result.message = `${element.outerHTML} does not contain the sprite icon "${iconName}"!`; + + const existingIcons = iconReferences.map((reference) => { + const iconUrl = reference.getAttribute('xlink:href'); + return `"${iconUrl.replace(/^.+#/, '')}"`; + }); + if (existingIcons.length > 0) { + result.message += ` (only found ${existingIcons.join(',')})`; + } + } + + return result; + }, + }), +}; diff --git a/spec/javascripts/monitoring/graph/axis_spec.js b/spec/javascripts/monitoring/graph/axis_spec.js new file mode 100644 index 00000000000..c7adba00637 --- /dev/null +++ b/spec/javascripts/monitoring/graph/axis_spec.js @@ -0,0 +1,65 @@ +import Vue from 'vue'; +import GraphAxis from '~/monitoring/components/graph/axis.vue'; +import measurements from '~/monitoring/utils/measurements'; + +const createComponent = propsData => { + const Component = Vue.extend(GraphAxis); + + return new Component({ + propsData, + }).$mount(); +}; + +const defaultValuesComponent = { + graphWidth: 500, + graphHeight: 300, + graphHeightOffset: 120, + margin: measurements.large.margin, + measurements: measurements.large, + yAxisLabel: 'Values', + unitOfDisplay: 'MB', +}; + +function getTextFromNode(component, selector) { + return component.$el.querySelector(selector).firstChild.nodeValue.trim(); +} + +describe('Axis', () => { + describe('Computed props', () => { + it('textTransform', () => { + const component = createComponent(defaultValuesComponent); + + expect(component.textTransform).toContain('translate(15, 120) rotate(-90)'); + }); + + it('xPosition', () => { + const component = createComponent(defaultValuesComponent); + + expect(component.xPosition).toEqual(180); + }); + + it('yPosition', () => { + const component = createComponent(defaultValuesComponent); + + expect(component.yPosition).toEqual(240); + }); + + it('rectTransform', () => { + const component = createComponent(defaultValuesComponent); + + expect(component.rectTransform).toContain('translate(0, 120) rotate(-90)'); + }); + }); + + it('has 2 rect-axis-text rect svg elements', () => { + const component = createComponent(defaultValuesComponent); + + expect(component.$el.querySelectorAll('.rect-axis-text').length).toEqual(2); + }); + + it('contains text to signal the usage, title and time with multiple time series', () => { + const component = createComponent(defaultValuesComponent); + + expect(getTextFromNode(component, '.y-label-text')).toEqual('Values (MB)'); + }); +}); diff --git a/spec/javascripts/monitoring/graph/legend_spec.js b/spec/javascripts/monitoring/graph/legend_spec.js index 145c8db28d5..abcc51aa077 100644 --- a/spec/javascripts/monitoring/graph/legend_spec.js +++ b/spec/javascripts/monitoring/graph/legend_spec.js @@ -1,106 +1,44 @@ import Vue from 'vue'; import GraphLegend from '~/monitoring/components/graph/legend.vue'; -import measurements from '~/monitoring/utils/measurements'; import createTimeSeries from '~/monitoring/utils/multiple_time_series'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; import { singleRowMetricsMultipleSeries, convertDatesMultipleSeries } from '../mock_data'; -const createComponent = (propsData) => { - const Component = Vue.extend(GraphLegend); - - return new Component({ - propsData, - }).$mount(); -}; - const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries); -const defaultValuesComponent = { - graphWidth: 500, - graphHeight: 300, - graphHeightOffset: 120, - margin: measurements.large.margin, - measurements: measurements.large, - areaColorRgb: '#f0f0f0', - legendTitle: 'Title', - yAxisLabel: 'Values', - metricUsage: 'Value', - unitOfDisplay: 'Req/Sec', - currentDataIndex: 0, -}; +const defaultValuesComponent = {}; -const timeSeries = createTimeSeries(convertedMetrics[0].queries, - defaultValuesComponent.graphWidth, defaultValuesComponent.graphHeight, - defaultValuesComponent.graphHeightOffset); +const timeSeries = createTimeSeries(convertedMetrics[0].queries, 500, 300, 120); defaultValuesComponent.timeSeries = timeSeries; -function getTextFromNode(component, selector) { - return component.$el.querySelector(selector).firstChild.nodeValue.trim(); -} - -describe('GraphLegend', () => { - describe('Computed props', () => { - it('textTransform', () => { - const component = createComponent(defaultValuesComponent); - - expect(component.textTransform).toContain('translate(15, 120) rotate(-90)'); - }); - - it('xPosition', () => { - const component = createComponent(defaultValuesComponent); - - expect(component.xPosition).toEqual(180); - }); - - it('yPosition', () => { - const component = createComponent(defaultValuesComponent); - - expect(component.yPosition).toEqual(240); - }); - - it('rectTransform', () => { - const component = createComponent(defaultValuesComponent); +describe('Legend Component', () => { + let vm; + let Legend; - expect(component.rectTransform).toContain('translate(0, 120) rotate(-90)'); - }); + beforeEach(() => { + Legend = Vue.extend(GraphLegend); }); - describe('methods', () => { - it('translateLegendGroup should only change Y direction', () => { - const component = createComponent(defaultValuesComponent); - - const translatedCoordinate = component.translateLegendGroup(1); - expect(translatedCoordinate.indexOf('translate(0, ')).not.toEqual(-1); + describe('View', () => { + beforeEach(() => { + vm = mountComponent(Legend, { + legendTitle: 'legend', + timeSeries, + currentDataIndex: 0, + unitOfDisplay: 'Req/Sec', + }); }); - it('formatMetricUsage should contain the unit of display and the current value selected via "currentDataIndex"', () => { - const component = createComponent(defaultValuesComponent); + it('should render the usage, title and time with multiple time series', () => { + const titles = vm.$el.querySelectorAll('.legend-metric-title'); - const formattedMetricUsage = component.formatMetricUsage(timeSeries[0]); - const valueFromSeries = timeSeries[0].values[component.currentDataIndex].value; - expect(formattedMetricUsage.indexOf(component.unitOfDisplay)).not.toEqual(-1); - expect(formattedMetricUsage.indexOf(valueFromSeries)).not.toEqual(-1); + expect(titles[0].textContent.indexOf('1xx')).not.toEqual(-1); + expect(titles[1].textContent.indexOf('2xx')).not.toEqual(-1); }); - }); - - it('has 2 rect-axis-text rect svg elements', () => { - const component = createComponent(defaultValuesComponent); - - expect(component.$el.querySelectorAll('.rect-axis-text').length).toEqual(2); - }); - it('contains text to signal the usage, title and time with multiple time series', () => { - const component = createComponent(defaultValuesComponent); - const titles = component.$el.querySelectorAll('.legend-metric-title'); - - expect(titles[0].textContent.indexOf('1xx')).not.toEqual(-1); - expect(titles[1].textContent.indexOf('2xx')).not.toEqual(-1); - expect(getTextFromNode(component, '.y-label-text')).toEqual(component.yAxisLabel); - }); - - it('should contain the same number of legend groups as the timeSeries length', () => { - const component = createComponent(defaultValuesComponent); - - expect(component.$el.querySelectorAll('.legend-group').length).toEqual(component.timeSeries.length); + it('should container the same number of rows in the table as time series', () => { + expect(vm.$el.querySelectorAll('.prometheus-table tr').length).toEqual(vm.timeSeries.length); + }); }); }); diff --git a/spec/javascripts/monitoring/graph/track_info_spec.js b/spec/javascripts/monitoring/graph/track_info_spec.js new file mode 100644 index 00000000000..d3121d553f9 --- /dev/null +++ b/spec/javascripts/monitoring/graph/track_info_spec.js @@ -0,0 +1,44 @@ +import Vue from 'vue'; +import TrackInfo from '~/monitoring/components/graph/track_info.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import createTimeSeries from '~/monitoring/utils/multiple_time_series'; +import { singleRowMetricsMultipleSeries, convertDatesMultipleSeries } from '../mock_data'; + +const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries); +const timeSeries = createTimeSeries(convertedMetrics[0].queries, 500, 300, 120); + +describe('TrackInfo component', () => { + let vm; + let Component; + + beforeEach(() => { + Component = Vue.extend(TrackInfo); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('Computed props', () => { + beforeEach(() => { + vm = mountComponent(Component, { track: timeSeries[0] }); + }); + + it('summaryMetrics', () => { + expect(vm.summaryMetrics).toEqual('Avg: 0.000 · Max: 0.000'); + }); + }); + + describe('Rendered output', () => { + beforeEach(() => { + vm = mountComponent(Component, { track: timeSeries[0] }); + }); + + it('contains metric tag and the summary metrics', () => { + const metricTag = vm.$el.querySelector('strong'); + + expect(metricTag.textContent.trim()).toEqual(vm.track.metricTag); + expect(vm.$el.textContent).toContain('Avg: 0.000 · Max: 0.000'); + }); + }); +}); diff --git a/spec/javascripts/monitoring/graph/track_line_spec.js b/spec/javascripts/monitoring/graph/track_line_spec.js new file mode 100644 index 00000000000..45106830a67 --- /dev/null +++ b/spec/javascripts/monitoring/graph/track_line_spec.js @@ -0,0 +1,52 @@ +import Vue from 'vue'; +import TrackLine from '~/monitoring/components/graph/track_line.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import createTimeSeries from '~/monitoring/utils/multiple_time_series'; +import { singleRowMetricsMultipleSeries, convertDatesMultipleSeries } from '../mock_data'; + +const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries); +const timeSeries = createTimeSeries(convertedMetrics[0].queries, 500, 300, 120); + +describe('TrackLine component', () => { + let vm; + let Component; + + beforeEach(() => { + Component = Vue.extend(TrackLine); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('Computed props', () => { + it('stylizedLine for dashed lineStyles', () => { + vm = mountComponent(Component, { track: { ...timeSeries[0], lineStyle: 'dashed' } }); + + expect(vm.stylizedLine).toEqual('6, 3'); + }); + + it('stylizedLine for dotted lineStyles', () => { + vm = mountComponent(Component, { track: { ...timeSeries[0], lineStyle: 'dotted' } }); + + expect(vm.stylizedLine).toEqual('3, 3'); + }); + }); + + describe('Rendered output', () => { + it('has an svg with a line', () => { + vm = mountComponent(Component, { track: { ...timeSeries[0] } }); + const svgEl = vm.$el.querySelector('svg'); + const lineEl = vm.$el.querySelector('svg line'); + + expect(svgEl.getAttribute('width')).toEqual('15'); + expect(svgEl.getAttribute('height')).toEqual('6'); + + expect(lineEl.getAttribute('stroke-width')).toEqual('4'); + expect(lineEl.getAttribute('x1')).toEqual('0'); + expect(lineEl.getAttribute('x2')).toEqual('15'); + expect(lineEl.getAttribute('y1')).toEqual('2'); + expect(lineEl.getAttribute('y2')).toEqual('2'); + }); + }); +}); diff --git a/spec/javascripts/monitoring/graph_spec.js b/spec/javascripts/monitoring/graph_spec.js index b1d69752bad..1213c80ba3a 100644 --- a/spec/javascripts/monitoring/graph_spec.js +++ b/spec/javascripts/monitoring/graph_spec.js @@ -2,11 +2,15 @@ import Vue from 'vue'; import Graph from '~/monitoring/components/graph.vue'; import MonitoringMixins from '~/monitoring/mixins/monitoring_mixins'; import eventHub from '~/monitoring/event_hub'; -import { deploymentData, convertDatesMultipleSeries, singleRowMetricsMultipleSeries } from './mock_data'; +import { + deploymentData, + convertDatesMultipleSeries, + singleRowMetricsMultipleSeries, +} from './mock_data'; const tagsPath = 'http://test.host/frontend-fixtures/environments-project/tags'; const projectPath = 'http://test.host/frontend-fixtures/environments-project'; -const createComponent = (propsData) => { +const createComponent = propsData => { const Component = Vue.extend(Graph); return new Component({ @@ -14,7 +18,9 @@ const createComponent = (propsData) => { }).$mount(); }; -const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries); +const convertedMetrics = convertDatesMultipleSeries( + singleRowMetricsMultipleSeries, +); describe('Graph', () => { beforeEach(() => { @@ -31,7 +37,9 @@ describe('Graph', () => { projectPath, }); - expect(component.$el.querySelector('.text-center').innerText.trim()).toBe(component.graphData.title); + expect(component.$el.querySelector('.text-center').innerText.trim()).toBe( + component.graphData.title, + ); }); describe('Computed props', () => { @@ -46,8 +54,9 @@ describe('Graph', () => { }); const transformedHeight = `${component.graphHeight - 100}`; - expect(component.axisTransform.indexOf(transformedHeight)) - .not.toEqual(-1); + expect(component.axisTransform.indexOf(transformedHeight)).not.toEqual( + -1, + ); }); it('outerViewBox gets a width and height property based on the DOM size of the element', () => { @@ -63,11 +72,11 @@ describe('Graph', () => { const viewBoxArray = component.outerViewBox.split(' '); expect(typeof component.outerViewBox).toEqual('string'); expect(viewBoxArray[2]).toEqual(component.graphWidth.toString()); - expect(viewBoxArray[3]).toEqual(component.graphHeight.toString()); + expect(viewBoxArray[3]).toEqual((component.graphHeight - 50).toString()); }); }); - it('sends an event to the eventhub when it has finished resizing', (done) => { + it('sends an event to the eventhub when it has finished resizing', done => { const component = createComponent({ graphData: convertedMetrics[1], classType: 'col-md-6', diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js index f30208b27b6..50da6da2e07 100644 --- a/spec/javascripts/monitoring/mock_data.js +++ b/spec/javascripts/monitoring/mock_data.js @@ -3,2426 +3,645 @@ export const mockApiEndpoint = `${gl.TEST_HOST}/monitoring/mock`; export const metricsGroupsAPIResponse = { - 'success': true, - 'data': [ + success: true, + data: [ { - 'group': 'Kubernetes', - 'priority': 1, - 'metrics': [ - { - 'title': 'Memory usage', - 'weight': 1, - 'queries': [ + group: 'Kubernetes', + priority: 1, + metrics: [ + { + title: 'Memory usage', + weight: 1, + queries: [ + { + query_range: 'avg(container_memory_usage_bytes{%{environment_filter}}) / 2^20', + y_label: 'Memory', + unit: 'MiB', + result: [ { - 'query_range': 'avg(container_memory_usage_bytes{%{environment_filter}}) / 2^20', - 'y_label': 'Memory', - 'unit': 'MiB', - 'result': [ - { - 'metric': {}, - 'values': [ - [ - 1495700554.925, - '8.0390625' - ], - [ - 1495700614.925, - '8.0390625' - ], - [ - 1495700674.925, - '8.0390625' - ], - [ - 1495700734.925, - '8.0390625' - ], - [ - 1495700794.925, - '8.0390625' - ], - [ - 1495700854.925, - '8.0390625' - ], - [ - 1495700914.925, - '8.0390625' - ], - [ - 1495700974.925, - '8.0390625' - ], - [ - 1495701034.925, - '8.0390625' - ], - [ - 1495701094.925, - '8.0390625' - ], - [ - 1495701154.925, - '8.0390625' - ], - [ - 1495701214.925, - '8.0390625' - ], - [ - 1495701274.925, - '8.0390625' - ], - [ - 1495701334.925, - '8.0390625' - ], - [ - 1495701394.925, - '8.0390625' - ], - [ - 1495701454.925, - '8.0390625' - ], - [ - 1495701514.925, - '8.0390625' - ], - [ - 1495701574.925, - '8.0390625' - ], - [ - 1495701634.925, - '8.0390625' - ], - [ - 1495701694.925, - '8.0390625' - ], - [ - 1495701754.925, - '8.0390625' - ], - [ - 1495701814.925, - '8.0390625' - ], - [ - 1495701874.925, - '8.0390625' - ], - [ - 1495701934.925, - '8.0390625' - ], - [ - 1495701994.925, - '8.0390625' - ], - [ - 1495702054.925, - '8.0390625' - ], - [ - 1495702114.925, - '8.0390625' - ], - [ - 1495702174.925, - '8.0390625' - ], - [ - 1495702234.925, - '8.0390625' - ], - [ - 1495702294.925, - '8.0390625' - ], - [ - 1495702354.925, - '8.0390625' - ], - [ - 1495702414.925, - '8.0390625' - ], - [ - 1495702474.925, - '8.0390625' - ], - [ - 1495702534.925, - '8.0390625' - ], - [ - 1495702594.925, - '8.0390625' - ], - [ - 1495702654.925, - '8.0390625' - ], - [ - 1495702714.925, - '8.0390625' - ], - [ - 1495702774.925, - '8.0390625' - ], - [ - 1495702834.925, - '8.0390625' - ], - [ - 1495702894.925, - '8.0390625' - ], - [ - 1495702954.925, - '8.0390625' - ], - [ - 1495703014.925, - '8.0390625' - ], - [ - 1495703074.925, - '8.0390625' - ], - [ - 1495703134.925, - '8.0390625' - ], - [ - 1495703194.925, - '8.0390625' - ], - [ - 1495703254.925, - '8.03515625' - ], - [ - 1495703314.925, - '8.03515625' - ], - [ - 1495703374.925, - '8.03515625' - ], - [ - 1495703434.925, - '8.03515625' - ], - [ - 1495703494.925, - '8.03515625' - ], - [ - 1495703554.925, - '8.03515625' - ], - [ - 1495703614.925, - '8.03515625' - ], - [ - 1495703674.925, - '8.03515625' - ], - [ - 1495703734.925, - '8.03515625' - ], - [ - 1495703794.925, - '8.03515625' - ], - [ - 1495703854.925, - '8.03515625' - ], - [ - 1495703914.925, - '8.03515625' - ], - [ - 1495703974.925, - '8.03515625' - ], - [ - 1495704034.925, - '8.03515625' - ], - [ - 1495704094.925, - '8.03515625' - ], - [ - 1495704154.925, - '8.03515625' - ], - [ - 1495704214.925, - '7.9296875' - ], - [ - 1495704274.925, - '7.9296875' - ], - [ - 1495704334.925, - '7.9296875' - ], - [ - 1495704394.925, - '7.9296875' - ], - [ - 1495704454.925, - '7.9296875' - ], - [ - 1495704514.925, - '7.9296875' - ], - [ - 1495704574.925, - '7.9296875' - ], - [ - 1495704634.925, - '7.9296875' - ], - [ - 1495704694.925, - '7.9296875' - ], - [ - 1495704754.925, - '7.9296875' - ], - [ - 1495704814.925, - '7.9296875' - ], - [ - 1495704874.925, - '7.9296875' - ], - [ - 1495704934.925, - '7.9296875' - ], - [ - 1495704994.925, - '7.9296875' - ], - [ - 1495705054.925, - '7.9296875' - ], - [ - 1495705114.925, - '7.9296875' - ], - [ - 1495705174.925, - '7.9296875' - ], - [ - 1495705234.925, - '7.9296875' - ], - [ - 1495705294.925, - '7.9296875' - ], - [ - 1495705354.925, - '7.9296875' - ], - [ - 1495705414.925, - '7.9296875' - ], - [ - 1495705474.925, - '7.9296875' - ], - [ - 1495705534.925, - '7.9296875' - ], - [ - 1495705594.925, - '7.9296875' - ], - [ - 1495705654.925, - '7.9296875' - ], - [ - 1495705714.925, - '7.9296875' - ], - [ - 1495705774.925, - '7.9296875' - ], - [ - 1495705834.925, - '7.9296875' - ], - [ - 1495705894.925, - '7.9296875' - ], - [ - 1495705954.925, - '7.9296875' - ], - [ - 1495706014.925, - '7.9296875' - ], - [ - 1495706074.925, - '7.9296875' - ], - [ - 1495706134.925, - '7.9296875' - ], - [ - 1495706194.925, - '7.9296875' - ], - [ - 1495706254.925, - '7.9296875' - ], - [ - 1495706314.925, - '7.9296875' - ], - [ - 1495706374.925, - '7.9296875' - ], - [ - 1495706434.925, - '7.9296875' - ], - [ - 1495706494.925, - '7.9296875' - ], - [ - 1495706554.925, - '7.9296875' - ], - [ - 1495706614.925, - '7.9296875' - ], - [ - 1495706674.925, - '7.9296875' - ], - [ - 1495706734.925, - '7.9296875' - ], - [ - 1495706794.925, - '7.9296875' - ], - [ - 1495706854.925, - '7.9296875' - ], - [ - 1495706914.925, - '7.9296875' - ], - [ - 1495706974.925, - '7.9296875' - ], - [ - 1495707034.925, - '7.9296875' - ], - [ - 1495707094.925, - '7.9296875' - ], - [ - 1495707154.925, - '7.9296875' - ], - [ - 1495707214.925, - '7.9296875' - ], - [ - 1495707274.925, - '7.9296875' - ], - [ - 1495707334.925, - '7.9296875' - ], - [ - 1495707394.925, - '7.9296875' - ], - [ - 1495707454.925, - '7.9296875' - ], - [ - 1495707514.925, - '7.9296875' - ], - [ - 1495707574.925, - '7.9296875' - ], - [ - 1495707634.925, - '7.9296875' - ], - [ - 1495707694.925, - '7.9296875' - ], - [ - 1495707754.925, - '7.9296875' - ], - [ - 1495707814.925, - '7.9296875' - ], - [ - 1495707874.925, - '7.9296875' - ], - [ - 1495707934.925, - '7.9296875' - ], - [ - 1495707994.925, - '7.9296875' - ], - [ - 1495708054.925, - '7.9296875' - ], - [ - 1495708114.925, - '7.9296875' - ], - [ - 1495708174.925, - '7.9296875' - ], - [ - 1495708234.925, - '7.9296875' - ], - [ - 1495708294.925, - '7.9296875' - ], - [ - 1495708354.925, - '7.9296875' - ], - [ - 1495708414.925, - '7.9296875' - ], - [ - 1495708474.925, - '7.9296875' - ], - [ - 1495708534.925, - '7.9296875' - ], - [ - 1495708594.925, - '7.9296875' - ], - [ - 1495708654.925, - '7.9296875' - ], - [ - 1495708714.925, - '7.9296875' - ], - [ - 1495708774.925, - '7.9296875' - ], - [ - 1495708834.925, - '7.9296875' - ], - [ - 1495708894.925, - '7.9296875' - ], - [ - 1495708954.925, - '7.8984375' - ], - [ - 1495709014.925, - '7.8984375' - ], - [ - 1495709074.925, - '7.8984375' - ], - [ - 1495709134.925, - '7.8984375' - ], - [ - 1495709194.925, - '7.8984375' - ], - [ - 1495709254.925, - '7.89453125' - ], - [ - 1495709314.925, - '7.89453125' - ], - [ - 1495709374.925, - '7.89453125' - ], - [ - 1495709434.925, - '7.89453125' - ], - [ - 1495709494.925, - '7.89453125' - ], - [ - 1495709554.925, - '7.89453125' - ], - [ - 1495709614.925, - '7.89453125' - ], - [ - 1495709674.925, - '7.89453125' - ], - [ - 1495709734.925, - '7.89453125' - ], - [ - 1495709794.925, - '7.89453125' - ], - [ - 1495709854.925, - '7.89453125' - ], - [ - 1495709914.925, - '7.89453125' - ], - [ - 1495709974.925, - '7.89453125' - ], - [ - 1495710034.925, - '7.89453125' - ], - [ - 1495710094.925, - '7.89453125' - ], - [ - 1495710154.925, - '7.89453125' - ], - [ - 1495710214.925, - '7.89453125' - ], - [ - 1495710274.925, - '7.89453125' - ], - [ - 1495710334.925, - '7.89453125' - ], - [ - 1495710394.925, - '7.89453125' - ], - [ - 1495710454.925, - '7.89453125' - ], - [ - 1495710514.925, - '7.89453125' - ], - [ - 1495710574.925, - '7.89453125' - ], - [ - 1495710634.925, - '7.89453125' - ], - [ - 1495710694.925, - '7.89453125' - ], - [ - 1495710754.925, - '7.89453125' - ], - [ - 1495710814.925, - '7.89453125' - ], - [ - 1495710874.925, - '7.89453125' - ], - [ - 1495710934.925, - '7.89453125' - ], - [ - 1495710994.925, - '7.89453125' - ], - [ - 1495711054.925, - '7.89453125' - ], - [ - 1495711114.925, - '7.89453125' - ], - [ - 1495711174.925, - '7.8515625' - ], - [ - 1495711234.925, - '7.8515625' - ], - [ - 1495711294.925, - '7.8515625' - ], - [ - 1495711354.925, - '7.8515625' - ], - [ - 1495711414.925, - '7.8515625' - ], - [ - 1495711474.925, - '7.8515625' - ], - [ - 1495711534.925, - '7.8515625' - ], - [ - 1495711594.925, - '7.8515625' - ], - [ - 1495711654.925, - '7.8515625' - ], - [ - 1495711714.925, - '7.8515625' - ], - [ - 1495711774.925, - '7.8515625' - ], - [ - 1495711834.925, - '7.8515625' - ], - [ - 1495711894.925, - '7.8515625' - ], - [ - 1495711954.925, - '7.8515625' - ], - [ - 1495712014.925, - '7.8515625' - ], - [ - 1495712074.925, - '7.8515625' - ], - [ - 1495712134.925, - '7.8515625' - ], - [ - 1495712194.925, - '7.8515625' - ], - [ - 1495712254.925, - '7.8515625' - ], - [ - 1495712314.925, - '7.8515625' - ], - [ - 1495712374.925, - '7.8515625' - ], - [ - 1495712434.925, - '7.83203125' - ], - [ - 1495712494.925, - '7.83203125' - ], - [ - 1495712554.925, - '7.83203125' - ], - [ - 1495712614.925, - '7.83203125' - ], - [ - 1495712674.925, - '7.83203125' - ], - [ - 1495712734.925, - '7.83203125' - ], - [ - 1495712794.925, - '7.83203125' - ], - [ - 1495712854.925, - '7.83203125' - ], - [ - 1495712914.925, - '7.83203125' - ], - [ - 1495712974.925, - '7.83203125' - ], - [ - 1495713034.925, - '7.83203125' - ], - [ - 1495713094.925, - '7.83203125' - ], - [ - 1495713154.925, - '7.83203125' - ], - [ - 1495713214.925, - '7.83203125' - ], - [ - 1495713274.925, - '7.83203125' - ], - [ - 1495713334.925, - '7.83203125' - ], - [ - 1495713394.925, - '7.8125' - ], - [ - 1495713454.925, - '7.8125' - ], - [ - 1495713514.925, - '7.8125' - ], - [ - 1495713574.925, - '7.8125' - ], - [ - 1495713634.925, - '7.8125' - ], - [ - 1495713694.925, - '7.8125' - ], - [ - 1495713754.925, - '7.8125' - ], - [ - 1495713814.925, - '7.8125' - ], - [ - 1495713874.925, - '7.8125' - ], - [ - 1495713934.925, - '7.8125' - ], - [ - 1495713994.925, - '7.8125' - ], - [ - 1495714054.925, - '7.8125' - ], - [ - 1495714114.925, - '7.8125' - ], - [ - 1495714174.925, - '7.8125' - ], - [ - 1495714234.925, - '7.8125' - ], - [ - 1495714294.925, - '7.8125' - ], - [ - 1495714354.925, - '7.80859375' - ], - [ - 1495714414.925, - '7.80859375' - ], - [ - 1495714474.925, - '7.80859375' - ], - [ - 1495714534.925, - '7.80859375' - ], - [ - 1495714594.925, - '7.80859375' - ], - [ - 1495714654.925, - '7.80859375' - ], - [ - 1495714714.925, - '7.80859375' - ], - [ - 1495714774.925, - '7.80859375' - ], - [ - 1495714834.925, - '7.80859375' - ], - [ - 1495714894.925, - '7.80859375' - ], - [ - 1495714954.925, - '7.80859375' - ], - [ - 1495715014.925, - '7.80859375' - ], - [ - 1495715074.925, - '7.80859375' - ], - [ - 1495715134.925, - '7.80859375' - ], - [ - 1495715194.925, - '7.80859375' - ], - [ - 1495715254.925, - '7.80859375' - ], - [ - 1495715314.925, - '7.80859375' - ], - [ - 1495715374.925, - '7.80859375' - ], - [ - 1495715434.925, - '7.80859375' - ], - [ - 1495715494.925, - '7.80859375' - ], - [ - 1495715554.925, - '7.80859375' - ], - [ - 1495715614.925, - '7.80859375' - ], - [ - 1495715674.925, - '7.80859375' - ], - [ - 1495715734.925, - '7.80859375' - ], - [ - 1495715794.925, - '7.80859375' - ], - [ - 1495715854.925, - '7.80859375' - ], - [ - 1495715914.925, - '7.80078125' - ], - [ - 1495715974.925, - '7.80078125' - ], - [ - 1495716034.925, - '7.80078125' - ], - [ - 1495716094.925, - '7.80078125' - ], - [ - 1495716154.925, - '7.80078125' - ], - [ - 1495716214.925, - '7.796875' - ], - [ - 1495716274.925, - '7.796875' - ], - [ - 1495716334.925, - '7.796875' - ], - [ - 1495716394.925, - '7.796875' - ], - [ - 1495716454.925, - '7.796875' - ], - [ - 1495716514.925, - '7.796875' - ], - [ - 1495716574.925, - '7.796875' - ], - [ - 1495716634.925, - '7.796875' - ], - [ - 1495716694.925, - '7.796875' - ], - [ - 1495716754.925, - '7.796875' - ], - [ - 1495716814.925, - '7.796875' - ], - [ - 1495716874.925, - '7.79296875' - ], - [ - 1495716934.925, - '7.79296875' - ], - [ - 1495716994.925, - '7.79296875' - ], - [ - 1495717054.925, - '7.79296875' - ], - [ - 1495717114.925, - '7.79296875' - ], - [ - 1495717174.925, - '7.7890625' - ], - [ - 1495717234.925, - '7.7890625' - ], - [ - 1495717294.925, - '7.7890625' - ], - [ - 1495717354.925, - '7.7890625' - ], - [ - 1495717414.925, - '7.7890625' - ], - [ - 1495717474.925, - '7.7890625' - ], - [ - 1495717534.925, - '7.7890625' - ], - [ - 1495717594.925, - '7.7890625' - ], - [ - 1495717654.925, - '7.7890625' - ], - [ - 1495717714.925, - '7.7890625' - ], - [ - 1495717774.925, - '7.7890625' - ], - [ - 1495717834.925, - '7.77734375' - ], - [ - 1495717894.925, - '7.77734375' - ], - [ - 1495717954.925, - '7.77734375' - ], - [ - 1495718014.925, - '7.77734375' - ], - [ - 1495718074.925, - '7.77734375' - ], - [ - 1495718134.925, - '7.7421875' - ], - [ - 1495718194.925, - '7.7421875' - ], - [ - 1495718254.925, - '7.7421875' - ], - [ - 1495718314.925, - '7.7421875' - ] - ] - } - ] - } - ] + metric: {}, + values: [ + [1495700554.925, '8.0390625'], + [1495700614.925, '8.0390625'], + [1495700674.925, '8.0390625'], + [1495700734.925, '8.0390625'], + [1495700794.925, '8.0390625'], + [1495700854.925, '8.0390625'], + [1495700914.925, '8.0390625'], + [1495700974.925, '8.0390625'], + [1495701034.925, '8.0390625'], + [1495701094.925, '8.0390625'], + [1495701154.925, '8.0390625'], + [1495701214.925, '8.0390625'], + [1495701274.925, '8.0390625'], + [1495701334.925, '8.0390625'], + [1495701394.925, '8.0390625'], + [1495701454.925, '8.0390625'], + [1495701514.925, '8.0390625'], + [1495701574.925, '8.0390625'], + [1495701634.925, '8.0390625'], + [1495701694.925, '8.0390625'], + [1495701754.925, '8.0390625'], + [1495701814.925, '8.0390625'], + [1495701874.925, '8.0390625'], + [1495701934.925, '8.0390625'], + [1495701994.925, '8.0390625'], + [1495702054.925, '8.0390625'], + [1495702114.925, '8.0390625'], + [1495702174.925, '8.0390625'], + [1495702234.925, '8.0390625'], + [1495702294.925, '8.0390625'], + [1495702354.925, '8.0390625'], + [1495702414.925, '8.0390625'], + [1495702474.925, '8.0390625'], + [1495702534.925, '8.0390625'], + [1495702594.925, '8.0390625'], + [1495702654.925, '8.0390625'], + [1495702714.925, '8.0390625'], + [1495702774.925, '8.0390625'], + [1495702834.925, '8.0390625'], + [1495702894.925, '8.0390625'], + [1495702954.925, '8.0390625'], + [1495703014.925, '8.0390625'], + [1495703074.925, '8.0390625'], + [1495703134.925, '8.0390625'], + [1495703194.925, '8.0390625'], + [1495703254.925, '8.03515625'], + [1495703314.925, '8.03515625'], + [1495703374.925, '8.03515625'], + [1495703434.925, '8.03515625'], + [1495703494.925, '8.03515625'], + [1495703554.925, '8.03515625'], + [1495703614.925, '8.03515625'], + [1495703674.925, '8.03515625'], + [1495703734.925, '8.03515625'], + [1495703794.925, '8.03515625'], + [1495703854.925, '8.03515625'], + [1495703914.925, '8.03515625'], + [1495703974.925, '8.03515625'], + [1495704034.925, '8.03515625'], + [1495704094.925, '8.03515625'], + [1495704154.925, '8.03515625'], + [1495704214.925, '7.9296875'], + [1495704274.925, '7.9296875'], + [1495704334.925, '7.9296875'], + [1495704394.925, '7.9296875'], + [1495704454.925, '7.9296875'], + [1495704514.925, '7.9296875'], + [1495704574.925, '7.9296875'], + [1495704634.925, '7.9296875'], + [1495704694.925, '7.9296875'], + [1495704754.925, '7.9296875'], + [1495704814.925, '7.9296875'], + [1495704874.925, '7.9296875'], + [1495704934.925, '7.9296875'], + [1495704994.925, '7.9296875'], + [1495705054.925, '7.9296875'], + [1495705114.925, '7.9296875'], + [1495705174.925, '7.9296875'], + [1495705234.925, '7.9296875'], + [1495705294.925, '7.9296875'], + [1495705354.925, '7.9296875'], + [1495705414.925, '7.9296875'], + [1495705474.925, '7.9296875'], + [1495705534.925, '7.9296875'], + [1495705594.925, '7.9296875'], + [1495705654.925, '7.9296875'], + [1495705714.925, '7.9296875'], + [1495705774.925, '7.9296875'], + [1495705834.925, '7.9296875'], + [1495705894.925, '7.9296875'], + [1495705954.925, '7.9296875'], + [1495706014.925, '7.9296875'], + [1495706074.925, '7.9296875'], + [1495706134.925, '7.9296875'], + [1495706194.925, '7.9296875'], + [1495706254.925, '7.9296875'], + [1495706314.925, '7.9296875'], + [1495706374.925, '7.9296875'], + [1495706434.925, '7.9296875'], + [1495706494.925, '7.9296875'], + [1495706554.925, '7.9296875'], + [1495706614.925, '7.9296875'], + [1495706674.925, '7.9296875'], + [1495706734.925, '7.9296875'], + [1495706794.925, '7.9296875'], + [1495706854.925, '7.9296875'], + [1495706914.925, '7.9296875'], + [1495706974.925, '7.9296875'], + [1495707034.925, '7.9296875'], + [1495707094.925, '7.9296875'], + [1495707154.925, '7.9296875'], + [1495707214.925, '7.9296875'], + [1495707274.925, '7.9296875'], + [1495707334.925, '7.9296875'], + [1495707394.925, '7.9296875'], + [1495707454.925, '7.9296875'], + [1495707514.925, '7.9296875'], + [1495707574.925, '7.9296875'], + [1495707634.925, '7.9296875'], + [1495707694.925, '7.9296875'], + [1495707754.925, '7.9296875'], + [1495707814.925, '7.9296875'], + [1495707874.925, '7.9296875'], + [1495707934.925, '7.9296875'], + [1495707994.925, '7.9296875'], + [1495708054.925, '7.9296875'], + [1495708114.925, '7.9296875'], + [1495708174.925, '7.9296875'], + [1495708234.925, '7.9296875'], + [1495708294.925, '7.9296875'], + [1495708354.925, '7.9296875'], + [1495708414.925, '7.9296875'], + [1495708474.925, '7.9296875'], + [1495708534.925, '7.9296875'], + [1495708594.925, '7.9296875'], + [1495708654.925, '7.9296875'], + [1495708714.925, '7.9296875'], + [1495708774.925, '7.9296875'], + [1495708834.925, '7.9296875'], + [1495708894.925, '7.9296875'], + [1495708954.925, '7.8984375'], + [1495709014.925, '7.8984375'], + [1495709074.925, '7.8984375'], + [1495709134.925, '7.8984375'], + [1495709194.925, '7.8984375'], + [1495709254.925, '7.89453125'], + [1495709314.925, '7.89453125'], + [1495709374.925, '7.89453125'], + [1495709434.925, '7.89453125'], + [1495709494.925, '7.89453125'], + [1495709554.925, '7.89453125'], + [1495709614.925, '7.89453125'], + [1495709674.925, '7.89453125'], + [1495709734.925, '7.89453125'], + [1495709794.925, '7.89453125'], + [1495709854.925, '7.89453125'], + [1495709914.925, '7.89453125'], + [1495709974.925, '7.89453125'], + [1495710034.925, '7.89453125'], + [1495710094.925, '7.89453125'], + [1495710154.925, '7.89453125'], + [1495710214.925, '7.89453125'], + [1495710274.925, '7.89453125'], + [1495710334.925, '7.89453125'], + [1495710394.925, '7.89453125'], + [1495710454.925, '7.89453125'], + [1495710514.925, '7.89453125'], + [1495710574.925, '7.89453125'], + [1495710634.925, '7.89453125'], + [1495710694.925, '7.89453125'], + [1495710754.925, '7.89453125'], + [1495710814.925, '7.89453125'], + [1495710874.925, '7.89453125'], + [1495710934.925, '7.89453125'], + [1495710994.925, '7.89453125'], + [1495711054.925, '7.89453125'], + [1495711114.925, '7.89453125'], + [1495711174.925, '7.8515625'], + [1495711234.925, '7.8515625'], + [1495711294.925, '7.8515625'], + [1495711354.925, '7.8515625'], + [1495711414.925, '7.8515625'], + [1495711474.925, '7.8515625'], + [1495711534.925, '7.8515625'], + [1495711594.925, '7.8515625'], + [1495711654.925, '7.8515625'], + [1495711714.925, '7.8515625'], + [1495711774.925, '7.8515625'], + [1495711834.925, '7.8515625'], + [1495711894.925, '7.8515625'], + [1495711954.925, '7.8515625'], + [1495712014.925, '7.8515625'], + [1495712074.925, '7.8515625'], + [1495712134.925, '7.8515625'], + [1495712194.925, '7.8515625'], + [1495712254.925, '7.8515625'], + [1495712314.925, '7.8515625'], + [1495712374.925, '7.8515625'], + [1495712434.925, '7.83203125'], + [1495712494.925, '7.83203125'], + [1495712554.925, '7.83203125'], + [1495712614.925, '7.83203125'], + [1495712674.925, '7.83203125'], + [1495712734.925, '7.83203125'], + [1495712794.925, '7.83203125'], + [1495712854.925, '7.83203125'], + [1495712914.925, '7.83203125'], + [1495712974.925, '7.83203125'], + [1495713034.925, '7.83203125'], + [1495713094.925, '7.83203125'], + [1495713154.925, '7.83203125'], + [1495713214.925, '7.83203125'], + [1495713274.925, '7.83203125'], + [1495713334.925, '7.83203125'], + [1495713394.925, '7.8125'], + [1495713454.925, '7.8125'], + [1495713514.925, '7.8125'], + [1495713574.925, '7.8125'], + [1495713634.925, '7.8125'], + [1495713694.925, '7.8125'], + [1495713754.925, '7.8125'], + [1495713814.925, '7.8125'], + [1495713874.925, '7.8125'], + [1495713934.925, '7.8125'], + [1495713994.925, '7.8125'], + [1495714054.925, '7.8125'], + [1495714114.925, '7.8125'], + [1495714174.925, '7.8125'], + [1495714234.925, '7.8125'], + [1495714294.925, '7.8125'], + [1495714354.925, '7.80859375'], + [1495714414.925, '7.80859375'], + [1495714474.925, '7.80859375'], + [1495714534.925, '7.80859375'], + [1495714594.925, '7.80859375'], + [1495714654.925, '7.80859375'], + [1495714714.925, '7.80859375'], + [1495714774.925, '7.80859375'], + [1495714834.925, '7.80859375'], + [1495714894.925, '7.80859375'], + [1495714954.925, '7.80859375'], + [1495715014.925, '7.80859375'], + [1495715074.925, '7.80859375'], + [1495715134.925, '7.80859375'], + [1495715194.925, '7.80859375'], + [1495715254.925, '7.80859375'], + [1495715314.925, '7.80859375'], + [1495715374.925, '7.80859375'], + [1495715434.925, '7.80859375'], + [1495715494.925, '7.80859375'], + [1495715554.925, '7.80859375'], + [1495715614.925, '7.80859375'], + [1495715674.925, '7.80859375'], + [1495715734.925, '7.80859375'], + [1495715794.925, '7.80859375'], + [1495715854.925, '7.80859375'], + [1495715914.925, '7.80078125'], + [1495715974.925, '7.80078125'], + [1495716034.925, '7.80078125'], + [1495716094.925, '7.80078125'], + [1495716154.925, '7.80078125'], + [1495716214.925, '7.796875'], + [1495716274.925, '7.796875'], + [1495716334.925, '7.796875'], + [1495716394.925, '7.796875'], + [1495716454.925, '7.796875'], + [1495716514.925, '7.796875'], + [1495716574.925, '7.796875'], + [1495716634.925, '7.796875'], + [1495716694.925, '7.796875'], + [1495716754.925, '7.796875'], + [1495716814.925, '7.796875'], + [1495716874.925, '7.79296875'], + [1495716934.925, '7.79296875'], + [1495716994.925, '7.79296875'], + [1495717054.925, '7.79296875'], + [1495717114.925, '7.79296875'], + [1495717174.925, '7.7890625'], + [1495717234.925, '7.7890625'], + [1495717294.925, '7.7890625'], + [1495717354.925, '7.7890625'], + [1495717414.925, '7.7890625'], + [1495717474.925, '7.7890625'], + [1495717534.925, '7.7890625'], + [1495717594.925, '7.7890625'], + [1495717654.925, '7.7890625'], + [1495717714.925, '7.7890625'], + [1495717774.925, '7.7890625'], + [1495717834.925, '7.77734375'], + [1495717894.925, '7.77734375'], + [1495717954.925, '7.77734375'], + [1495718014.925, '7.77734375'], + [1495718074.925, '7.77734375'], + [1495718134.925, '7.7421875'], + [1495718194.925, '7.7421875'], + [1495718254.925, '7.7421875'], + [1495718314.925, '7.7421875'], + ], + }, + ], + }, + ], }, { - 'title': 'CPU usage', - 'weight': 1, - 'queries': [ + title: 'CPU usage', + weight: 1, + queries: [ + { + query_range: + 'avg(rate(container_cpu_usage_seconds_total{%{environment_filter}}[2m])) * 100', + result: [ { - 'query_range': 'avg(rate(container_cpu_usage_seconds_total{%{environment_filter}}[2m])) * 100', - 'result': [ - { - 'metric': {}, - 'values': [ - [ - 1495700554.925, - '0.0010794445585559514' - ], - [ - 1495700614.925, - '0.003927214935433527' - ], - [ - 1495700674.925, - '0.0053045219047619975' - ], - [ - 1495700734.925, - '0.0048892095238097155' - ], - [ - 1495700794.925, - '0.005827140952381137' - ], - [ - 1495700854.925, - '0.00569846906219937' - ], - [ - 1495700914.925, - '0.004972616802849382' - ], - [ - 1495700974.925, - '0.005117509523809902' - ], - [ - 1495701034.925, - '0.00512389061919564' - ], - [ - 1495701094.925, - '0.005199100501890691' - ], - [ - 1495701154.925, - '0.005415746394885837' - ], - [ - 1495701214.925, - '0.005607682788146286' - ], - [ - 1495701274.925, - '0.005641300000000118' - ], - [ - 1495701334.925, - '0.0071166279368766495' - ], - [ - 1495701394.925, - '0.0063242138095234044' - ], - [ - 1495701454.925, - '0.005793314698235304' - ], - [ - 1495701514.925, - '0.00703934942237556' - ], - [ - 1495701574.925, - '0.006357007076123191' - ], - [ - 1495701634.925, - '0.003753167300126738' - ], - [ - 1495701694.925, - '0.005018469678430698' - ], - [ - 1495701754.925, - '0.0045217153371887' - ], - [ - 1495701814.925, - '0.006140104285714119' - ], - [ - 1495701874.925, - '0.004818684285714102' - ], - [ - 1495701934.925, - '0.005079509718955242' - ], - [ - 1495701994.925, - '0.005059981142498263' - ], - [ - 1495702054.925, - '0.005269098389538773' - ], - [ - 1495702114.925, - '0.005269954285714175' - ], - [ - 1495702174.925, - '0.014199241435795856' - ], - [ - 1495702234.925, - '0.01511936843111017' - ], - [ - 1495702294.925, - '0.0060933692920682875' - ], - [ - 1495702354.925, - '0.004945682380952493' - ], - [ - 1495702414.925, - '0.005641266666666565' - ], - [ - 1495702474.925, - '0.005223752857142996' - ], - [ - 1495702534.925, - '0.005743098505699831' - ], - [ - 1495702594.925, - '0.00538493380952391' - ], - [ - 1495702654.925, - '0.005507793883751339' - ], - [ - 1495702714.925, - '0.005666705714285466' - ], - [ - 1495702774.925, - '0.006231530000000112' - ], - [ - 1495702834.925, - '0.006570768635394899' - ], - [ - 1495702894.925, - '0.005551146666666895' - ], - [ - 1495702954.925, - '0.005602604737098058' - ], - [ - 1495703014.925, - '0.00613993580402159' - ], - [ - 1495703074.925, - '0.004770258764368832' - ], - [ - 1495703134.925, - '0.005512376671364914' - ], - [ - 1495703194.925, - '0.005254436666666674' - ], - [ - 1495703254.925, - '0.0050109839141320505' - ], - [ - 1495703314.925, - '0.0049478019256960016' - ], - [ - 1495703374.925, - '0.0037666860965123463' - ], - [ - 1495703434.925, - '0.004813526061656314' - ], - [ - 1495703494.925, - '0.005047748095238278' - ], - [ - 1495703554.925, - '0.00386494081008772' - ], - [ - 1495703614.925, - '0.004304037408111405' - ], - [ - 1495703674.925, - '0.004999466661587168' - ], - [ - 1495703734.925, - '0.004689140476190834' - ], - [ - 1495703794.925, - '0.004746126153582475' - ], - [ - 1495703854.925, - '0.004482706382572302' - ], - [ - 1495703914.925, - '0.004032808931864524' - ], - [ - 1495703974.925, - '0.005728319047618988' - ], - [ - 1495704034.925, - '0.004436139179627006' - ], - [ - 1495704094.925, - '0.004553455714285617' - ], - [ - 1495704154.925, - '0.003455244285714341' - ], - [ - 1495704214.925, - '0.004742244761904621' - ], - [ - 1495704274.925, - '0.005366978571428422' - ], - [ - 1495704334.925, - '0.004257954837665058' - ], - [ - 1495704394.925, - '0.005431603259831257' - ], - [ - 1495704454.925, - '0.0052009214498621986' - ], - [ - 1495704514.925, - '0.004317201904761618' - ], - [ - 1495704574.925, - '0.004307384285714157' - ], - [ - 1495704634.925, - '0.004789801146644822' - ], - [ - 1495704694.925, - '0.0051429795906706485' - ], - [ - 1495704754.925, - '0.005322495714285479' - ], - [ - 1495704814.925, - '0.004512809333244233' - ], - [ - 1495704874.925, - '0.004953843582568726' - ], - [ - 1495704934.925, - '0.005812690120858119' - ], - [ - 1495704994.925, - '0.004997024285714838' - ], - [ - 1495705054.925, - '0.005246216154439592' - ], - [ - 1495705114.925, - '0.0063494966618726795' - ], - [ - 1495705174.925, - '0.005306004342898225' - ], - [ - 1495705234.925, - '0.005081412857142978' - ], - [ - 1495705294.925, - '0.00511409523809522' - ], - [ - 1495705354.925, - '0.0047861001481192' - ], - [ - 1495705414.925, - '0.005107688228042962' - ], - [ - 1495705474.925, - '0.005271929582294012' - ], - [ - 1495705534.925, - '0.004453254502681249' - ], - [ - 1495705594.925, - '0.005799134293959226' - ], - [ - 1495705654.925, - '0.005340865929502478' - ], - [ - 1495705714.925, - '0.004911654761904942' - ], - [ - 1495705774.925, - '0.005888234873953261' - ], - [ - 1495705834.925, - '0.005565283333332954' - ], - [ - 1495705894.925, - '0.005522869047618869' - ], - [ - 1495705954.925, - '0.005177549737621646' - ], - [ - 1495706014.925, - '0.0053145810232096465' - ], - [ - 1495706074.925, - '0.004751095238095275' - ], - [ - 1495706134.925, - '0.006242077142856976' - ], - [ - 1495706194.925, - '0.00621034406957871' - ], - [ - 1495706254.925, - '0.006887592738978596' - ], - [ - 1495706314.925, - '0.006328128779726213' - ], - [ - 1495706374.925, - '0.007488363809523927' - ], - [ - 1495706434.925, - '0.006193758571428157' - ], - [ - 1495706494.925, - '0.0068798371839706935' - ], - [ - 1495706554.925, - '0.005757034340423128' - ], - [ - 1495706614.925, - '0.004571388497294698' - ], - [ - 1495706674.925, - '0.00620283044923395' - ], - [ - 1495706734.925, - '0.005607562380952455' - ], - [ - 1495706794.925, - '0.005506969933620308' - ], - [ - 1495706854.925, - '0.005621118095238131' - ], - [ - 1495706914.925, - '0.004876606098698849' - ], - [ - 1495706974.925, - '0.0047871205988517206' - ], - [ - 1495707034.925, - '0.00526405939458784' - ], - [ - 1495707094.925, - '0.005716323800605852' - ], - [ - 1495707154.925, - '0.005301459523809575' - ], - [ - 1495707214.925, - '0.0051613042857144905' - ], - [ - 1495707274.925, - '0.005384792857142714' - ], - [ - 1495707334.925, - '0.005259719047619222' - ], - [ - 1495707394.925, - '0.00584101142857182' - ], - [ - 1495707454.925, - '0.0060066121920326326' - ], - [ - 1495707514.925, - '0.006359978571428453' - ], - [ - 1495707574.925, - '0.006315876322151109' - ], - [ - 1495707634.925, - '0.005590012517198831' - ], - [ - 1495707694.925, - '0.005517419877137072' - ], - [ - 1495707754.925, - '0.006089813430348506' - ], - [ - 1495707814.925, - '0.00466754476190479' - ], - [ - 1495707874.925, - '0.006059954380517721' - ], - [ - 1495707934.925, - '0.005085657142856972' - ], - [ - 1495707994.925, - '0.005897665238095296' - ], - [ - 1495708054.925, - '0.0062282023199555885' - ], - [ - 1495708114.925, - '0.00526214553236979' - ], - [ - 1495708174.925, - '0.0044803300000000644' - ], - [ - 1495708234.925, - '0.005421443333333592' - ], - [ - 1495708294.925, - '0.005694326244512144' - ], - [ - 1495708354.925, - '0.005527721904761457' - ], - [ - 1495708414.925, - '0.005988819523809819' - ], - [ - 1495708474.925, - '0.005484704285714448' - ], - [ - 1495708534.925, - '0.005041123649230085' - ], - [ - 1495708594.925, - '0.005717767639612059' - ], - [ - 1495708654.925, - '0.005412954417342863' - ], - [ - 1495708714.925, - '0.005833343333333254' - ], - [ - 1495708774.925, - '0.005448135238094969' - ], - [ - 1495708834.925, - '0.005117341428571432' - ], - [ - 1495708894.925, - '0.005888345825277833' - ], - [ - 1495708954.925, - '0.005398543809524135' - ], - [ - 1495709014.925, - '0.005325611428571416' - ], - [ - 1495709074.925, - '0.005848668571428527' - ], - [ - 1495709134.925, - '0.005135003105145044' - ], - [ - 1495709194.925, - '0.0054551400000003' - ], - [ - 1495709254.925, - '0.005319472937322171' - ], - [ - 1495709314.925, - '0.00585677857142792' - ], - [ - 1495709374.925, - '0.0062146261904759215' - ], - [ - 1495709434.925, - '0.0067105060904182265' - ], - [ - 1495709494.925, - '0.005829691904762108' - ], - [ - 1495709554.925, - '0.005719280952381261' - ], - [ - 1495709614.925, - '0.005682603793416407' - ], - [ - 1495709674.925, - '0.0055272846277326934' - ], - [ - 1495709734.925, - '0.0057123680952386735' - ], - [ - 1495709794.925, - '0.00520597958075818' - ], - [ - 1495709854.925, - '0.005584358957263837' - ], - [ - 1495709914.925, - '0.005601104275197466' - ], - [ - 1495709974.925, - '0.005991657142857066' - ], - [ - 1495710034.925, - '0.00553722238095218' - ], - [ - 1495710094.925, - '0.005127883122696293' - ], - [ - 1495710154.925, - '0.005498111927534584' - ], - [ - 1495710214.925, - '0.005609934069084202' - ], - [ - 1495710274.925, - '0.00459206285714307' - ], - [ - 1495710334.925, - '0.0047910828571428084' - ], - [ - 1495710394.925, - '0.0056014671288845685' - ], - [ - 1495710454.925, - '0.005686936791078528' - ], - [ - 1495710514.925, - '0.00444480476190448' - ], - [ - 1495710574.925, - '0.005780394696738921' - ], - [ - 1495710634.925, - '0.0053107227550210365' - ], - [ - 1495710694.925, - '0.005096031495761817' - ], - [ - 1495710754.925, - '0.005451377979091524' - ], - [ - 1495710814.925, - '0.005328136666667083' - ], - [ - 1495710874.925, - '0.006020612857143043' - ], - [ - 1495710934.925, - '0.0061063585714285365' - ], - [ - 1495710994.925, - '0.006018346015752312' - ], - [ - 1495711054.925, - '0.005069130952381193' - ], - [ - 1495711114.925, - '0.005458406190476052' - ], - [ - 1495711174.925, - '0.00577219190476179' - ], - [ - 1495711234.925, - '0.005760814645658314' - ], - [ - 1495711294.925, - '0.005371875716579101' - ], - [ - 1495711354.925, - '0.0064232666666665834' - ], - [ - 1495711414.925, - '0.009369806836906667' - ], - [ - 1495711474.925, - '0.008956864761904692' - ], - [ - 1495711534.925, - '0.005266849368559271' - ], - [ - 1495711594.925, - '0.005335111364934262' - ], - [ - 1495711654.925, - '0.006461778319586945' - ], - [ - 1495711714.925, - '0.004687939890762393' - ], - [ - 1495711774.925, - '0.004438831245760684' - ], - [ - 1495711834.925, - '0.005142786666666613' - ], - [ - 1495711894.925, - '0.007257734212054963' - ], - [ - 1495711954.925, - '0.005621991904761494' - ], - [ - 1495712014.925, - '0.007868689999999862' - ], - [ - 1495712074.925, - '0.00910970215275738' - ], - [ - 1495712134.925, - '0.006151004285714278' - ], - [ - 1495712194.925, - '0.005447120924961522' - ], - [ - 1495712254.925, - '0.005150705153929503' - ], - [ - 1495712314.925, - '0.006358108714969314' - ], - [ - 1495712374.925, - '0.0057725354795696475' - ], - [ - 1495712434.925, - '0.005232139047619015' - ], - [ - 1495712494.925, - '0.004932809617949037' - ], - [ - 1495712554.925, - '0.004511607508499662' - ], - [ - 1495712614.925, - '0.00440487701522666' - ], - [ - 1495712674.925, - '0.005479113333333174' - ], - [ - 1495712734.925, - '0.004726317619047547' - ], - [ - 1495712794.925, - '0.005582041102958029' - ], - [ - 1495712854.925, - '0.006381481216082099' - ], - [ - 1495712914.925, - '0.005474260014095208' - ], - [ - 1495712974.925, - '0.00567597142857188' - ], - [ - 1495713034.925, - '0.0064741233333332985' - ], - [ - 1495713094.925, - '0.005467475714285271' - ], - [ - 1495713154.925, - '0.004868648393824457' - ], - [ - 1495713214.925, - '0.005254923286444893' - ], - [ - 1495713274.925, - '0.005599217150312865' - ], - [ - 1495713334.925, - '0.005105413720618919' - ], - [ - 1495713394.925, - '0.007246073333333279' - ], - [ - 1495713454.925, - '0.005990312380952272' - ], - [ - 1495713514.925, - '0.005594601853351101' - ], - [ - 1495713574.925, - '0.004739258673727054' - ], - [ - 1495713634.925, - '0.003932121428571783' - ], - [ - 1495713694.925, - '0.005018188268459395' - ], - [ - 1495713754.925, - '0.004538238095237985' - ], - [ - 1495713814.925, - '0.00561816643265435' - ], - [ - 1495713874.925, - '0.0063132584495033586' - ], - [ - 1495713934.925, - '0.00442385238095213' - ], - [ - 1495713994.925, - '0.004181795887658453' - ], - [ - 1495714054.925, - '0.004437759047619037' - ], - [ - 1495714114.925, - '0.006421748157178241' - ], - [ - 1495714174.925, - '0.006525143809523842' - ], - [ - 1495714234.925, - '0.004715904935144247' - ], - [ - 1495714294.925, - '0.005966040152763461' - ], - [ - 1495714354.925, - '0.005614535466921674' - ], - [ - 1495714414.925, - '0.004934375119415906' - ], - [ - 1495714474.925, - '0.0054122933333327385' - ], - [ - 1495714534.925, - '0.004926540699612279' - ], - [ - 1495714594.925, - '0.006124649517134237' - ], - [ - 1495714654.925, - '0.004629427092013995' - ], - [ - 1495714714.925, - '0.005117951257607005' - ], - [ - 1495714774.925, - '0.004868774512685422' - ], - [ - 1495714834.925, - '0.005310093333333399' - ], - [ - 1495714894.925, - '0.0054907752286127345' - ], - [ - 1495714954.925, - '0.004597678117351089' - ], - [ - 1495715014.925, - '0.0059622552380952' - ], - [ - 1495715074.925, - '0.005352457072655368' - ], - [ - 1495715134.925, - '0.005491630952381143' - ], - [ - 1495715194.925, - '0.006391770078379791' - ], - [ - 1495715254.925, - '0.005933472857142518' - ], - [ - 1495715314.925, - '0.005301314285714163' - ], - [ - 1495715374.925, - '0.0058352959724814165' - ], - [ - 1495715434.925, - '0.006154755147867044' - ], - [ - 1495715494.925, - '0.009391935637482038' - ], - [ - 1495715554.925, - '0.007846462857142592' - ], - [ - 1495715614.925, - '0.00477608215316353' - ], - [ - 1495715674.925, - '0.006132865238094998' - ], - [ - 1495715734.925, - '0.006159762457649516' - ], - [ - 1495715794.925, - '0.005957307073265968' - ], - [ - 1495715854.925, - '0.006652319091792501' - ], - [ - 1495715914.925, - '0.005493557402895287' - ], - [ - 1495715974.925, - '0.0058652434829145166' - ], - [ - 1495716034.925, - '0.005627400430468021' - ], - [ - 1495716094.925, - '0.006240656190475609' - ], - [ - 1495716154.925, - '0.006305997676168624' - ], - [ - 1495716214.925, - '0.005388057732783248' - ], - [ - 1495716274.925, - '0.0052814916048421244' - ], - [ - 1495716334.925, - '0.00699498614272497' - ], - [ - 1495716394.925, - '0.00627768693035141' - ], - [ - 1495716454.925, - '0.0042411487048161145' - ], - [ - 1495716514.925, - '0.005348647473627653' - ], - [ - 1495716574.925, - '0.0047176657142853975' - ], - [ - 1495716634.925, - '0.004437898571428686' - ], - [ - 1495716694.925, - '0.004923527366927261' - ], - [ - 1495716754.925, - '0.005131935066048421' - ], - [ - 1495716814.925, - '0.005046949523809611' - ], - [ - 1495716874.925, - '0.00547184095238092' - ], - [ - 1495716934.925, - '0.005224140016380444' - ], - [ - 1495716994.925, - '0.005297991171665292' - ], - [ - 1495717054.925, - '0.005492965995623498' - ], - [ - 1495717114.925, - '0.005754660000000403' - ], - [ - 1495717174.925, - '0.005949557138639285' - ], - [ - 1495717234.925, - '0.006091816112534666' - ], - [ - 1495717294.925, - '0.005554210080192063' - ], - [ - 1495717354.925, - '0.006411504395279871' - ], - [ - 1495717414.925, - '0.006319643996609606' - ], - [ - 1495717474.925, - '0.005539174405717675' - ], - [ - 1495717534.925, - '0.0053157078842772255' - ], - [ - 1495717594.925, - '0.005247480952381066' - ], - [ - 1495717654.925, - '0.004820141620396252' - ], - [ - 1495717714.925, - '0.005906173868322844' - ], - [ - 1495717774.925, - '0.006173117219570961' - ], - [ - 1495717834.925, - '0.005963340952380661' - ], - [ - 1495717894.925, - '0.005698976627681527' - ], - [ - 1495717954.925, - '0.004751279096346378' - ], - [ - 1495718014.925, - '0.005733142379359711' - ], - [ - 1495718074.925, - '0.004831689010348035' - ], - [ - 1495718134.925, - '0.005188370476191092' - ], - [ - 1495718194.925, - '0.004793227554547938' - ], - [ - 1495718254.925, - '0.003997442857142731' - ], - [ - 1495718314.925, - '0.004386040132951264' - ] - ] - } - ] - } - ] - } - ] - } + metric: {}, + values: [ + [1495700554.925, '0.0010794445585559514'], + [1495700614.925, '0.003927214935433527'], + [1495700674.925, '0.0053045219047619975'], + [1495700734.925, '0.0048892095238097155'], + [1495700794.925, '0.005827140952381137'], + [1495700854.925, '0.00569846906219937'], + [1495700914.925, '0.004972616802849382'], + [1495700974.925, '0.005117509523809902'], + [1495701034.925, '0.00512389061919564'], + [1495701094.925, '0.005199100501890691'], + [1495701154.925, '0.005415746394885837'], + [1495701214.925, '0.005607682788146286'], + [1495701274.925, '0.005641300000000118'], + [1495701334.925, '0.0071166279368766495'], + [1495701394.925, '0.0063242138095234044'], + [1495701454.925, '0.005793314698235304'], + [1495701514.925, '0.00703934942237556'], + [1495701574.925, '0.006357007076123191'], + [1495701634.925, '0.003753167300126738'], + [1495701694.925, '0.005018469678430698'], + [1495701754.925, '0.0045217153371887'], + [1495701814.925, '0.006140104285714119'], + [1495701874.925, '0.004818684285714102'], + [1495701934.925, '0.005079509718955242'], + [1495701994.925, '0.005059981142498263'], + [1495702054.925, '0.005269098389538773'], + [1495702114.925, '0.005269954285714175'], + [1495702174.925, '0.014199241435795856'], + [1495702234.925, '0.01511936843111017'], + [1495702294.925, '0.0060933692920682875'], + [1495702354.925, '0.004945682380952493'], + [1495702414.925, '0.005641266666666565'], + [1495702474.925, '0.005223752857142996'], + [1495702534.925, '0.005743098505699831'], + [1495702594.925, '0.00538493380952391'], + [1495702654.925, '0.005507793883751339'], + [1495702714.925, '0.005666705714285466'], + [1495702774.925, '0.006231530000000112'], + [1495702834.925, '0.006570768635394899'], + [1495702894.925, '0.005551146666666895'], + [1495702954.925, '0.005602604737098058'], + [1495703014.925, '0.00613993580402159'], + [1495703074.925, '0.004770258764368832'], + [1495703134.925, '0.005512376671364914'], + [1495703194.925, '0.005254436666666674'], + [1495703254.925, '0.0050109839141320505'], + [1495703314.925, '0.0049478019256960016'], + [1495703374.925, '0.0037666860965123463'], + [1495703434.925, '0.004813526061656314'], + [1495703494.925, '0.005047748095238278'], + [1495703554.925, '0.00386494081008772'], + [1495703614.925, '0.004304037408111405'], + [1495703674.925, '0.004999466661587168'], + [1495703734.925, '0.004689140476190834'], + [1495703794.925, '0.004746126153582475'], + [1495703854.925, '0.004482706382572302'], + [1495703914.925, '0.004032808931864524'], + [1495703974.925, '0.005728319047618988'], + [1495704034.925, '0.004436139179627006'], + [1495704094.925, '0.004553455714285617'], + [1495704154.925, '0.003455244285714341'], + [1495704214.925, '0.004742244761904621'], + [1495704274.925, '0.005366978571428422'], + [1495704334.925, '0.004257954837665058'], + [1495704394.925, '0.005431603259831257'], + [1495704454.925, '0.0052009214498621986'], + [1495704514.925, '0.004317201904761618'], + [1495704574.925, '0.004307384285714157'], + [1495704634.925, '0.004789801146644822'], + [1495704694.925, '0.0051429795906706485'], + [1495704754.925, '0.005322495714285479'], + [1495704814.925, '0.004512809333244233'], + [1495704874.925, '0.004953843582568726'], + [1495704934.925, '0.005812690120858119'], + [1495704994.925, '0.004997024285714838'], + [1495705054.925, '0.005246216154439592'], + [1495705114.925, '0.0063494966618726795'], + [1495705174.925, '0.005306004342898225'], + [1495705234.925, '0.005081412857142978'], + [1495705294.925, '0.00511409523809522'], + [1495705354.925, '0.0047861001481192'], + [1495705414.925, '0.005107688228042962'], + [1495705474.925, '0.005271929582294012'], + [1495705534.925, '0.004453254502681249'], + [1495705594.925, '0.005799134293959226'], + [1495705654.925, '0.005340865929502478'], + [1495705714.925, '0.004911654761904942'], + [1495705774.925, '0.005888234873953261'], + [1495705834.925, '0.005565283333332954'], + [1495705894.925, '0.005522869047618869'], + [1495705954.925, '0.005177549737621646'], + [1495706014.925, '0.0053145810232096465'], + [1495706074.925, '0.004751095238095275'], + [1495706134.925, '0.006242077142856976'], + [1495706194.925, '0.00621034406957871'], + [1495706254.925, '0.006887592738978596'], + [1495706314.925, '0.006328128779726213'], + [1495706374.925, '0.007488363809523927'], + [1495706434.925, '0.006193758571428157'], + [1495706494.925, '0.0068798371839706935'], + [1495706554.925, '0.005757034340423128'], + [1495706614.925, '0.004571388497294698'], + [1495706674.925, '0.00620283044923395'], + [1495706734.925, '0.005607562380952455'], + [1495706794.925, '0.005506969933620308'], + [1495706854.925, '0.005621118095238131'], + [1495706914.925, '0.004876606098698849'], + [1495706974.925, '0.0047871205988517206'], + [1495707034.925, '0.00526405939458784'], + [1495707094.925, '0.005716323800605852'], + [1495707154.925, '0.005301459523809575'], + [1495707214.925, '0.0051613042857144905'], + [1495707274.925, '0.005384792857142714'], + [1495707334.925, '0.005259719047619222'], + [1495707394.925, '0.00584101142857182'], + [1495707454.925, '0.0060066121920326326'], + [1495707514.925, '0.006359978571428453'], + [1495707574.925, '0.006315876322151109'], + [1495707634.925, '0.005590012517198831'], + [1495707694.925, '0.005517419877137072'], + [1495707754.925, '0.006089813430348506'], + [1495707814.925, '0.00466754476190479'], + [1495707874.925, '0.006059954380517721'], + [1495707934.925, '0.005085657142856972'], + [1495707994.925, '0.005897665238095296'], + [1495708054.925, '0.0062282023199555885'], + [1495708114.925, '0.00526214553236979'], + [1495708174.925, '0.0044803300000000644'], + [1495708234.925, '0.005421443333333592'], + [1495708294.925, '0.005694326244512144'], + [1495708354.925, '0.005527721904761457'], + [1495708414.925, '0.005988819523809819'], + [1495708474.925, '0.005484704285714448'], + [1495708534.925, '0.005041123649230085'], + [1495708594.925, '0.005717767639612059'], + [1495708654.925, '0.005412954417342863'], + [1495708714.925, '0.005833343333333254'], + [1495708774.925, '0.005448135238094969'], + [1495708834.925, '0.005117341428571432'], + [1495708894.925, '0.005888345825277833'], + [1495708954.925, '0.005398543809524135'], + [1495709014.925, '0.005325611428571416'], + [1495709074.925, '0.005848668571428527'], + [1495709134.925, '0.005135003105145044'], + [1495709194.925, '0.0054551400000003'], + [1495709254.925, '0.005319472937322171'], + [1495709314.925, '0.00585677857142792'], + [1495709374.925, '0.0062146261904759215'], + [1495709434.925, '0.0067105060904182265'], + [1495709494.925, '0.005829691904762108'], + [1495709554.925, '0.005719280952381261'], + [1495709614.925, '0.005682603793416407'], + [1495709674.925, '0.0055272846277326934'], + [1495709734.925, '0.0057123680952386735'], + [1495709794.925, '0.00520597958075818'], + [1495709854.925, '0.005584358957263837'], + [1495709914.925, '0.005601104275197466'], + [1495709974.925, '0.005991657142857066'], + [1495710034.925, '0.00553722238095218'], + [1495710094.925, '0.005127883122696293'], + [1495710154.925, '0.005498111927534584'], + [1495710214.925, '0.005609934069084202'], + [1495710274.925, '0.00459206285714307'], + [1495710334.925, '0.0047910828571428084'], + [1495710394.925, '0.0056014671288845685'], + [1495710454.925, '0.005686936791078528'], + [1495710514.925, '0.00444480476190448'], + [1495710574.925, '0.005780394696738921'], + [1495710634.925, '0.0053107227550210365'], + [1495710694.925, '0.005096031495761817'], + [1495710754.925, '0.005451377979091524'], + [1495710814.925, '0.005328136666667083'], + [1495710874.925, '0.006020612857143043'], + [1495710934.925, '0.0061063585714285365'], + [1495710994.925, '0.006018346015752312'], + [1495711054.925, '0.005069130952381193'], + [1495711114.925, '0.005458406190476052'], + [1495711174.925, '0.00577219190476179'], + [1495711234.925, '0.005760814645658314'], + [1495711294.925, '0.005371875716579101'], + [1495711354.925, '0.0064232666666665834'], + [1495711414.925, '0.009369806836906667'], + [1495711474.925, '0.008956864761904692'], + [1495711534.925, '0.005266849368559271'], + [1495711594.925, '0.005335111364934262'], + [1495711654.925, '0.006461778319586945'], + [1495711714.925, '0.004687939890762393'], + [1495711774.925, '0.004438831245760684'], + [1495711834.925, '0.005142786666666613'], + [1495711894.925, '0.007257734212054963'], + [1495711954.925, '0.005621991904761494'], + [1495712014.925, '0.007868689999999862'], + [1495712074.925, '0.00910970215275738'], + [1495712134.925, '0.006151004285714278'], + [1495712194.925, '0.005447120924961522'], + [1495712254.925, '0.005150705153929503'], + [1495712314.925, '0.006358108714969314'], + [1495712374.925, '0.0057725354795696475'], + [1495712434.925, '0.005232139047619015'], + [1495712494.925, '0.004932809617949037'], + [1495712554.925, '0.004511607508499662'], + [1495712614.925, '0.00440487701522666'], + [1495712674.925, '0.005479113333333174'], + [1495712734.925, '0.004726317619047547'], + [1495712794.925, '0.005582041102958029'], + [1495712854.925, '0.006381481216082099'], + [1495712914.925, '0.005474260014095208'], + [1495712974.925, '0.00567597142857188'], + [1495713034.925, '0.0064741233333332985'], + [1495713094.925, '0.005467475714285271'], + [1495713154.925, '0.004868648393824457'], + [1495713214.925, '0.005254923286444893'], + [1495713274.925, '0.005599217150312865'], + [1495713334.925, '0.005105413720618919'], + [1495713394.925, '0.007246073333333279'], + [1495713454.925, '0.005990312380952272'], + [1495713514.925, '0.005594601853351101'], + [1495713574.925, '0.004739258673727054'], + [1495713634.925, '0.003932121428571783'], + [1495713694.925, '0.005018188268459395'], + [1495713754.925, '0.004538238095237985'], + [1495713814.925, '0.00561816643265435'], + [1495713874.925, '0.0063132584495033586'], + [1495713934.925, '0.00442385238095213'], + [1495713994.925, '0.004181795887658453'], + [1495714054.925, '0.004437759047619037'], + [1495714114.925, '0.006421748157178241'], + [1495714174.925, '0.006525143809523842'], + [1495714234.925, '0.004715904935144247'], + [1495714294.925, '0.005966040152763461'], + [1495714354.925, '0.005614535466921674'], + [1495714414.925, '0.004934375119415906'], + [1495714474.925, '0.0054122933333327385'], + [1495714534.925, '0.004926540699612279'], + [1495714594.925, '0.006124649517134237'], + [1495714654.925, '0.004629427092013995'], + [1495714714.925, '0.005117951257607005'], + [1495714774.925, '0.004868774512685422'], + [1495714834.925, '0.005310093333333399'], + [1495714894.925, '0.0054907752286127345'], + [1495714954.925, '0.004597678117351089'], + [1495715014.925, '0.0059622552380952'], + [1495715074.925, '0.005352457072655368'], + [1495715134.925, '0.005491630952381143'], + [1495715194.925, '0.006391770078379791'], + [1495715254.925, '0.005933472857142518'], + [1495715314.925, '0.005301314285714163'], + [1495715374.925, '0.0058352959724814165'], + [1495715434.925, '0.006154755147867044'], + [1495715494.925, '0.009391935637482038'], + [1495715554.925, '0.007846462857142592'], + [1495715614.925, '0.00477608215316353'], + [1495715674.925, '0.006132865238094998'], + [1495715734.925, '0.006159762457649516'], + [1495715794.925, '0.005957307073265968'], + [1495715854.925, '0.006652319091792501'], + [1495715914.925, '0.005493557402895287'], + [1495715974.925, '0.0058652434829145166'], + [1495716034.925, '0.005627400430468021'], + [1495716094.925, '0.006240656190475609'], + [1495716154.925, '0.006305997676168624'], + [1495716214.925, '0.005388057732783248'], + [1495716274.925, '0.0052814916048421244'], + [1495716334.925, '0.00699498614272497'], + [1495716394.925, '0.00627768693035141'], + [1495716454.925, '0.0042411487048161145'], + [1495716514.925, '0.005348647473627653'], + [1495716574.925, '0.0047176657142853975'], + [1495716634.925, '0.004437898571428686'], + [1495716694.925, '0.004923527366927261'], + [1495716754.925, '0.005131935066048421'], + [1495716814.925, '0.005046949523809611'], + [1495716874.925, '0.00547184095238092'], + [1495716934.925, '0.005224140016380444'], + [1495716994.925, '0.005297991171665292'], + [1495717054.925, '0.005492965995623498'], + [1495717114.925, '0.005754660000000403'], + [1495717174.925, '0.005949557138639285'], + [1495717234.925, '0.006091816112534666'], + [1495717294.925, '0.005554210080192063'], + [1495717354.925, '0.006411504395279871'], + [1495717414.925, '0.006319643996609606'], + [1495717474.925, '0.005539174405717675'], + [1495717534.925, '0.0053157078842772255'], + [1495717594.925, '0.005247480952381066'], + [1495717654.925, '0.004820141620396252'], + [1495717714.925, '0.005906173868322844'], + [1495717774.925, '0.006173117219570961'], + [1495717834.925, '0.005963340952380661'], + [1495717894.925, '0.005698976627681527'], + [1495717954.925, '0.004751279096346378'], + [1495718014.925, '0.005733142379359711'], + [1495718074.925, '0.004831689010348035'], + [1495718134.925, '0.005188370476191092'], + [1495718194.925, '0.004793227554547938'], + [1495718254.925, '0.003997442857142731'], + [1495718314.925, '0.004386040132951264'], + ], + }, + ], + }, + ], + }, + ], + }, ], - 'last_update': '2017-05-25T13:18:34.949Z' + last_update: '2017-05-25T13:18:34.949Z', }; export default metricsGroupsAPIResponse; @@ -2432,41 +651,44 @@ export const deploymentData = [ id: 111, iid: 3, sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187', - commitUrl: 'http://test.host/frontend-fixtures/environments-project/commit/f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187', + commitUrl: + 'http://test.host/frontend-fixtures/environments-project/commit/f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187', ref: { - name: 'master' + name: 'master', }, created_at: '2017-05-31T21:23:37.881Z', tag: false, tagUrl: 'http://test.host/frontend-fixtures/environments-project/tags/false', - 'last?': true + 'last?': true, }, { id: 110, iid: 2, sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187', - commitUrl: 'http://test.host/frontend-fixtures/environments-project/commit/f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187', + commitUrl: + 'http://test.host/frontend-fixtures/environments-project/commit/f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187', ref: { - name: 'master' + name: 'master', }, created_at: '2017-05-30T20:08:04.629Z', tag: false, - tagUrl: 'http://test.host/frontend-fixtures/environments-project/tags/false', - 'last?': false + tagUrl: 'http://test.host/frontend-fixtures/environments-project/tags/false', + 'last?': false, }, { id: 109, iid: 1, sha: '6511e58faafaa7ad2228990ec57f19d66f7db7c2', - commitUrl: 'http://test.host/frontend-fixtures/environments-project/commit/6511e58faafaa7ad2228990ec57f19d66f7db7c2', + commitUrl: + 'http://test.host/frontend-fixtures/environments-project/commit/6511e58faafaa7ad2228990ec57f19d66f7db7c2', ref: { - name: 'update2-readme' + name: 'update2-readme', }, created_at: '2017-05-30T17:42:38.409Z', tag: false, tagUrl: 'http://test.host/frontend-fixtures/environments-project/tags/false', - 'last?': false - } + 'last?': false, + }, ]; export const statePaths = { @@ -2476,5844 +698,5844 @@ export const statePaths = { }; export const singleRowMetricsMultipleSeries = [ - { - 'title': 'Multiple Time Series', - 'weight': 1, - 'y_label': 'Request Rates', - 'queries': [ - { - 'query_range': 'sum(rate(nginx_responses_total{environment="production"}[2m])) by (status_code)', - 'label': 'Requests', - 'unit': 'Req/sec', - 'result': [ - { - 'metric': { - 'status_code': '1xx' - }, - 'values': [ - { - 'time': '2017-08-27T11:01:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:02:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:03:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:04:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:05:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:06:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:07:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:08:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:09:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:10:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:11:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:12:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:13:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:14:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:15:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:16:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:17:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:18:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:19:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:20:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:21:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:22:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:23:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:24:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:25:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:26:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:27:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:28:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:29:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:30:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:31:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:32:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:33:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:34:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:35:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:36:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:37:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:38:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:39:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:40:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:41:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:42:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:43:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:44:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:45:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:46:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:47:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:48:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:49:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:50:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:51:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:52:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:53:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:54:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:55:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:56:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:57:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:58:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T11:59:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:00:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:01:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:02:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:03:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:04:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:05:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:06:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:07:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:08:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:09:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:10:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:11:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:12:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:13:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:14:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:15:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:16:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:17:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:18:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:19:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:20:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:21:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:22:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:23:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:24:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:25:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:26:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:27:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:28:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:29:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:30:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:31:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:32:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:33:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:34:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:35:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:36:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:37:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:38:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:39:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:40:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:41:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:42:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:43:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:44:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:45:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:46:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:47:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:48:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:49:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:50:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:51:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:52:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:53:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:54:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:55:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:56:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:57:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:58:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T12:59:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:00:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:01:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:02:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:03:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:04:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:05:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:06:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:07:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:08:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:09:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:10:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:11:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:12:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:13:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:14:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:15:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:16:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:17:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:18:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:19:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:20:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:21:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:22:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:23:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:24:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:25:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:26:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:27:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:28:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:29:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:30:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:31:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:32:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:33:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:34:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:35:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:36:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:37:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:38:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:39:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:40:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:41:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:42:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:43:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:44:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:45:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:46:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:47:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:48:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:49:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:50:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:51:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:52:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:53:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:54:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:55:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:56:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:57:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:58:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T13:59:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:00:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:01:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:02:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:03:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:04:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:05:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:06:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:07:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:08:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:09:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:10:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:11:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:12:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:13:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:14:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:15:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:16:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:17:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:18:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:19:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:20:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:21:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:22:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:23:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:24:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:25:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:26:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:27:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:28:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:29:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:30:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:31:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:32:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:33:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:34:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:35:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:36:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:37:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:38:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:39:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:40:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:41:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:42:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:43:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:44:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:45:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:46:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:47:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:48:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:49:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:50:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:51:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:52:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:53:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:54:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:55:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:56:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:57:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:58:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T14:59:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:00:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:01:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:02:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:03:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:04:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:05:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:06:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:07:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:08:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:09:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:10:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:11:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:12:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:13:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:14:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:15:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:16:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:17:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:18:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:19:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:20:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:21:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:22:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:23:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:24:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:25:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:26:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:27:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:28:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:29:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:30:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:31:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:32:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:33:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:34:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:35:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:36:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:37:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:38:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:39:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:40:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:41:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:42:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:43:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:44:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:45:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:46:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:47:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:48:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:49:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:50:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:51:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:52:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:53:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:54:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:55:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:56:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:57:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:58:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T15:59:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:00:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:01:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:02:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:03:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:04:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:05:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:06:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:07:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:08:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:09:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:10:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:11:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:12:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:13:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:14:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:15:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:16:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:17:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:18:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:19:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:20:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:21:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:22:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:23:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:24:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:25:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:26:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:27:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:28:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:29:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:30:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:31:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:32:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:33:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:34:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:35:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:36:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:37:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:38:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:39:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:40:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:41:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:42:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:43:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:44:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:45:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:46:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:47:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:48:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:49:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:50:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:51:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:52:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:53:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:54:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:55:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:56:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:57:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:58:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T16:59:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:00:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:01:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:02:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:03:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:04:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:05:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:06:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:07:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:08:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:09:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:10:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:11:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:12:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:13:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:14:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:15:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:16:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:17:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:18:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:19:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:20:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:21:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:22:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:23:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:24:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:25:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:26:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:27:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:28:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:29:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:30:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:31:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:32:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:33:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:34:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:35:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:36:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:37:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:38:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:39:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:40:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:41:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:42:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:43:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:44:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:45:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:46:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:47:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:48:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:49:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:50:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:51:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:52:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:53:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:54:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:55:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:56:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:57:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:58:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T17:59:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:00:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:01:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:02:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:03:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:04:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:05:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:06:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:07:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:08:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:09:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:10:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:11:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:12:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:13:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:14:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:15:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:16:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:17:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:18:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:19:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:20:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:21:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:22:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:23:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:24:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:25:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:26:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:27:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:28:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:29:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:30:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:31:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:32:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:33:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:34:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:35:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:36:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:37:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:38:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:39:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:40:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:41:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:42:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:43:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:44:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:45:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:46:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:47:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:48:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:49:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:50:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:51:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:52:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:53:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:54:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:55:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:56:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:57:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:58:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T18:59:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T19:00:51.462Z', - 'value': '0' - }, - { - 'time': '2017-08-27T19:01:51.462Z', - 'value': '0' - } - ] - }, - { - 'metric': { - 'status_code': '2xx' - }, - 'values': [ - { - 'time': '2017-08-27T11:01:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T11:02:51.462Z', - 'value': '1.2571428571428571' - }, - { - 'time': '2017-08-27T11:03:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T11:04:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T11:05:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T11:06:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T11:07:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T11:08:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T11:09:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T11:10:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T11:11:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T11:12:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T11:13:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T11:14:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T11:15:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T11:16:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T11:17:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T11:18:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T11:19:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T11:20:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T11:21:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T11:22:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T11:23:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T11:24:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T11:25:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T11:26:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T11:27:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T11:28:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T11:29:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T11:30:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T11:31:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T11:32:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T11:33:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T11:34:51.462Z', - 'value': '1.333320635041571' - }, - { - 'time': '2017-08-27T11:35:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T11:36:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T11:37:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T11:38:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T11:39:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T11:40:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T11:41:51.462Z', - 'value': '1.3333587306424883' - }, - { - 'time': '2017-08-27T11:42:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T11:43:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T11:44:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T11:45:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T11:46:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T11:47:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T11:48:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T11:49:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T11:50:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T11:51:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T11:52:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T11:53:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T11:54:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T11:55:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T11:56:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T11:57:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T11:58:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T11:59:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T12:00:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T12:01:51.462Z', - 'value': '1.3333460318669703' - }, - { - 'time': '2017-08-27T12:02:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T12:03:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T12:04:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T12:05:51.462Z', - 'value': '1.31427319739812' - }, - { - 'time': '2017-08-27T12:06:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T12:07:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T12:08:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T12:09:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T12:10:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T12:11:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T12:12:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T12:13:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T12:14:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T12:15:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T12:16:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T12:17:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T12:18:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T12:19:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T12:20:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T12:21:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T12:22:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T12:23:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T12:24:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T12:25:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T12:26:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T12:27:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T12:28:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T12:29:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T12:30:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T12:31:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T12:32:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T12:33:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T12:34:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T12:35:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T12:36:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T12:37:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T12:38:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T12:39:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T12:40:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T12:41:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T12:42:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T12:43:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T12:44:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T12:45:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T12:46:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T12:47:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T12:48:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T12:49:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T12:50:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T12:51:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T12:52:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T12:53:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T12:54:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T12:55:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T12:56:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T12:57:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T12:58:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T12:59:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T13:00:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T13:01:51.462Z', - 'value': '1.295225759754669' - }, - { - 'time': '2017-08-27T13:02:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T13:03:51.462Z', - 'value': '1.2952627669098458' - }, - { - 'time': '2017-08-27T13:04:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T13:05:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T13:06:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T13:07:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T13:08:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T13:09:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T13:10:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T13:11:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T13:12:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T13:13:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T13:14:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T13:15:51.462Z', - 'value': '1.2571428571428571' - }, - { - 'time': '2017-08-27T13:16:51.462Z', - 'value': '1.3333587306424883' - }, - { - 'time': '2017-08-27T13:17:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T13:18:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T13:19:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T13:20:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T13:21:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T13:22:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T13:23:51.462Z', - 'value': '1.276190476190476' - }, - { - 'time': '2017-08-27T13:24:51.462Z', - 'value': '1.2571428571428571' - }, - { - 'time': '2017-08-27T13:25:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T13:26:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T13:27:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T13:28:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T13:29:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T13:30:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T13:31:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T13:32:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T13:33:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T13:34:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T13:35:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T13:36:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T13:37:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T13:38:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T13:39:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T13:40:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T13:41:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T13:42:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T13:43:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T13:44:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T13:45:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T13:46:51.462Z', - 'value': '1.2571428571428571' - }, - { - 'time': '2017-08-27T13:47:51.462Z', - 'value': '1.276190476190476' - }, - { - 'time': '2017-08-27T13:48:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T13:49:51.462Z', - 'value': '1.295225759754669' - }, - { - 'time': '2017-08-27T13:50:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T13:51:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T13:52:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T13:53:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T13:54:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T13:55:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T13:56:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T13:57:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T13:58:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T13:59:51.462Z', - 'value': '1.295225759754669' - }, - { - 'time': '2017-08-27T14:00:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T14:01:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T14:02:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T14:03:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T14:04:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T14:05:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T14:06:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T14:07:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T14:08:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T14:09:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T14:10:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T14:11:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T14:12:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T14:13:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T14:14:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T14:15:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T14:16:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T14:17:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T14:18:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T14:19:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T14:20:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T14:21:51.462Z', - 'value': '1.3333079369916765' - }, - { - 'time': '2017-08-27T14:22:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T14:23:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T14:24:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T14:25:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T14:26:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T14:27:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T14:28:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T14:29:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T14:30:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T14:31:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T14:32:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T14:33:51.462Z', - 'value': '1.2571428571428571' - }, - { - 'time': '2017-08-27T14:34:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T14:35:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T14:36:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T14:37:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T14:38:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T14:39:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T14:40:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T14:41:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T14:42:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T14:43:51.462Z', - 'value': '1.276190476190476' - }, - { - 'time': '2017-08-27T14:44:51.462Z', - 'value': '1.2571428571428571' - }, - { - 'time': '2017-08-27T14:45:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T14:46:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T14:47:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T14:48:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T14:49:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T14:50:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T14:51:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T14:52:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T14:53:51.462Z', - 'value': '1.333320635041571' - }, - { - 'time': '2017-08-27T14:54:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T14:55:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T14:56:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T14:57:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T14:58:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T14:59:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T15:00:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T15:01:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T15:02:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T15:03:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T15:04:51.462Z', - 'value': '1.2571428571428571' - }, - { - 'time': '2017-08-27T15:05:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T15:06:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T15:07:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T15:08:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T15:09:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T15:10:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T15:11:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T15:12:51.462Z', - 'value': '1.31427319739812' - }, - { - 'time': '2017-08-27T15:13:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T15:14:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T15:15:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T15:16:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T15:17:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T15:18:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T15:19:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T15:20:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T15:21:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T15:22:51.462Z', - 'value': '1.3333460318669703' - }, - { - 'time': '2017-08-27T15:23:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T15:24:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T15:25:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T15:26:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T15:27:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T15:28:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T15:29:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T15:30:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T15:31:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T15:32:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T15:33:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T15:34:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T15:35:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T15:36:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T15:37:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T15:38:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T15:39:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T15:40:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T15:41:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T15:42:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T15:43:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T15:44:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T15:45:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T15:46:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T15:47:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T15:48:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T15:49:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T15:50:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T15:51:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T15:52:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T15:53:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T15:54:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T15:55:51.462Z', - 'value': '1.3333587306424883' - }, - { - 'time': '2017-08-27T15:56:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T15:57:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T15:58:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T15:59:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T16:00:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T16:01:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T16:02:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T16:03:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T16:04:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T16:05:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T16:06:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T16:07:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T16:08:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T16:09:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T16:10:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T16:11:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T16:12:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T16:13:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T16:14:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T16:15:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T16:16:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T16:17:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T16:18:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T16:19:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T16:20:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T16:21:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T16:22:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T16:23:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T16:24:51.462Z', - 'value': '1.295225759754669' - }, - { - 'time': '2017-08-27T16:25:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T16:26:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T16:27:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T16:28:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T16:29:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T16:30:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T16:31:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T16:32:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T16:33:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T16:34:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T16:35:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T16:36:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T16:37:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T16:38:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T16:39:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T16:40:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T16:41:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T16:42:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T16:43:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T16:44:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T16:45:51.462Z', - 'value': '1.3142982314117277' - }, - { - 'time': '2017-08-27T16:46:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T16:47:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T16:48:51.462Z', - 'value': '1.333320635041571' - }, - { - 'time': '2017-08-27T16:49:51.462Z', - 'value': '1.31427319739812' - }, - { - 'time': '2017-08-27T16:50:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T16:51:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T16:52:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T16:53:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T16:54:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T16:55:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T16:56:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T16:57:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T16:58:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T16:59:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T17:00:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T17:01:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T17:02:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T17:03:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T17:04:51.462Z', - 'value': '1.2952504309564854' - }, - { - 'time': '2017-08-27T17:05:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T17:06:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T17:07:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T17:08:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T17:09:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T17:10:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T17:11:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T17:12:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T17:13:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T17:14:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T17:15:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T17:16:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T17:17:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T17:18:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T17:19:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T17:20:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T17:21:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T17:22:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T17:23:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T17:24:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T17:25:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T17:26:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T17:27:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T17:28:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T17:29:51.462Z', - 'value': '1.295225759754669' - }, - { - 'time': '2017-08-27T17:30:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T17:31:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T17:32:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T17:33:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T17:34:51.462Z', - 'value': '1.295225759754669' - }, - { - 'time': '2017-08-27T17:35:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T17:36:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T17:37:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T17:38:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T17:39:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T17:40:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T17:41:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T17:42:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T17:43:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T17:44:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T17:45:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T17:46:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T17:47:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T17:48:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T17:49:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T17:50:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T17:51:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T17:52:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T17:53:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T17:54:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T17:55:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T17:56:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T17:57:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T17:58:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T17:59:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T18:00:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T18:01:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T18:02:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T18:03:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T18:04:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T18:05:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T18:06:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T18:07:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T18:08:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T18:09:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T18:10:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T18:11:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T18:12:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T18:13:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T18:14:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T18:15:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T18:16:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T18:17:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T18:18:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T18:19:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T18:20:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T18:21:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T18:22:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T18:23:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T18:24:51.462Z', - 'value': '1.2571428571428571' - }, - { - 'time': '2017-08-27T18:25:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T18:26:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T18:27:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T18:28:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T18:29:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T18:30:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T18:31:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T18:32:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T18:33:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T18:34:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T18:35:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T18:36:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T18:37:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T18:38:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T18:39:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T18:40:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T18:41:51.462Z', - 'value': '1.580952380952381' - }, - { - 'time': '2017-08-27T18:42:51.462Z', - 'value': '1.7333333333333334' - }, - { - 'time': '2017-08-27T18:43:51.462Z', - 'value': '2.057142857142857' - }, - { - 'time': '2017-08-27T18:44:51.462Z', - 'value': '2.1904761904761902' - }, - { - 'time': '2017-08-27T18:45:51.462Z', - 'value': '1.8285714285714287' - }, - { - 'time': '2017-08-27T18:46:51.462Z', - 'value': '2.1142857142857143' - }, - { - 'time': '2017-08-27T18:47:51.462Z', - 'value': '1.619047619047619' - }, - { - 'time': '2017-08-27T18:48:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T18:49:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T18:50:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T18:51:51.462Z', - 'value': '1.2952504309564854' - }, - { - 'time': '2017-08-27T18:52:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T18:53:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T18:54:51.462Z', - 'value': '1.3333333333333333' - }, - { - 'time': '2017-08-27T18:55:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T18:56:51.462Z', - 'value': '1.314285714285714' - }, - { - 'time': '2017-08-27T18:57:51.462Z', - 'value': '1.295238095238095' - }, - { - 'time': '2017-08-27T18:58:51.462Z', - 'value': '1.7142857142857142' - }, - { - 'time': '2017-08-27T18:59:51.462Z', - 'value': '1.7333333333333334' - }, - { - 'time': '2017-08-27T19:00:51.462Z', - 'value': '1.3904761904761904' - }, - { - 'time': '2017-08-27T19:01:51.462Z', - 'value': '1.5047619047619047' - } - ] - }, - ], - 'when': [ - { - 'value': 'hundred(s)', - 'color': 'green', - }, - ], - } - ] - }, - { - 'title': 'Throughput', - 'weight': 1, - 'y_label': 'Requests / Sec', - 'queries': [ - { - 'query_range': 'sum(rate(nginx_requests_total{server_zone!=\'*\', server_zone!=\'_\', container_name!=\'POD\',environment=\'production\'}[2m]))', - 'label': 'Total', - 'unit': 'req / sec', - 'result': [ - { - 'metric': { - - }, - 'values': [ - { - 'time': '2017-08-27T11:01:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T11:02:51.462Z', - 'value': '0.45714285714285713' - }, - { - 'time': '2017-08-27T11:03:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T11:04:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T11:05:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T11:06:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T11:07:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T11:08:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T11:09:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T11:10:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T11:11:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T11:12:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T11:13:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T11:14:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T11:15:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T11:16:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T11:17:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T11:18:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T11:19:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T11:20:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T11:21:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T11:22:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T11:23:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T11:24:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T11:25:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T11:26:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T11:27:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T11:28:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T11:29:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T11:30:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T11:31:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T11:32:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T11:33:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T11:34:51.462Z', - 'value': '0.4952333787297264' - }, - { - 'time': '2017-08-27T11:35:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T11:36:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T11:37:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T11:38:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T11:39:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T11:40:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T11:41:51.462Z', - 'value': '0.49524752852435283' - }, - { - 'time': '2017-08-27T11:42:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T11:43:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T11:44:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T11:45:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T11:46:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T11:47:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T11:48:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T11:49:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T11:50:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T11:51:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T11:52:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T11:53:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T11:54:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T11:55:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T11:56:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T11:57:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T11:58:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T11:59:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T12:00:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T12:01:51.462Z', - 'value': '0.49524281183630325' - }, - { - 'time': '2017-08-27T12:02:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T12:03:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T12:04:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T12:05:51.462Z', - 'value': '0.4857096599080009' - }, - { - 'time': '2017-08-27T12:06:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T12:07:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T12:08:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T12:09:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T12:10:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T12:11:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T12:12:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T12:13:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T12:14:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T12:15:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T12:16:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T12:17:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T12:18:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T12:19:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T12:20:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T12:21:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T12:22:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T12:23:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T12:24:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T12:25:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T12:26:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T12:27:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T12:28:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T12:29:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T12:30:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T12:31:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T12:32:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T12:33:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T12:34:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T12:35:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T12:36:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T12:37:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T12:38:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T12:39:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T12:40:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T12:41:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T12:42:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T12:43:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T12:44:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T12:45:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T12:46:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T12:47:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T12:48:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T12:49:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T12:50:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T12:51:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T12:52:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T12:53:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T12:54:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T12:55:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T12:56:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T12:57:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T12:58:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T12:59:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T13:00:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T13:01:51.462Z', - 'value': '0.4761859410862754' - }, - { - 'time': '2017-08-27T13:02:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T13:03:51.462Z', - 'value': '0.4761995466580315' - }, - { - 'time': '2017-08-27T13:04:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T13:05:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T13:06:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T13:07:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T13:08:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T13:09:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T13:10:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T13:11:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T13:12:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T13:13:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T13:14:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T13:15:51.462Z', - 'value': '0.45714285714285713' - }, - { - 'time': '2017-08-27T13:16:51.462Z', - 'value': '0.49524752852435283' - }, - { - 'time': '2017-08-27T13:17:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T13:18:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T13:19:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T13:20:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T13:21:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T13:22:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T13:23:51.462Z', - 'value': '0.4666666666666667' - }, - { - 'time': '2017-08-27T13:24:51.462Z', - 'value': '0.45714285714285713' - }, - { - 'time': '2017-08-27T13:25:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T13:26:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T13:27:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T13:28:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T13:29:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T13:30:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T13:31:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T13:32:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T13:33:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T13:34:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T13:35:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T13:36:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T13:37:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T13:38:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T13:39:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T13:40:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T13:41:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T13:42:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T13:43:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T13:44:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T13:45:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T13:46:51.462Z', - 'value': '0.45714285714285713' - }, - { - 'time': '2017-08-27T13:47:51.462Z', - 'value': '0.4666666666666667' - }, - { - 'time': '2017-08-27T13:48:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T13:49:51.462Z', - 'value': '0.4761859410862754' - }, - { - 'time': '2017-08-27T13:50:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T13:51:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T13:52:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T13:53:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T13:54:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T13:55:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T13:56:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T13:57:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T13:58:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T13:59:51.462Z', - 'value': '0.4761859410862754' - }, - { - 'time': '2017-08-27T14:00:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T14:01:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T14:02:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T14:03:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T14:04:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T14:05:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T14:06:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T14:07:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T14:08:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T14:09:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T14:10:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T14:11:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T14:12:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T14:13:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T14:14:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T14:15:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T14:16:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T14:17:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T14:18:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T14:19:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T14:20:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T14:21:51.462Z', - 'value': '0.4952286623111941' - }, - { - 'time': '2017-08-27T14:22:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T14:23:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T14:24:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T14:25:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T14:26:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T14:27:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T14:28:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T14:29:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T14:30:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T14:31:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T14:32:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T14:33:51.462Z', - 'value': '0.45714285714285713' - }, - { - 'time': '2017-08-27T14:34:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T14:35:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T14:36:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T14:37:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T14:38:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T14:39:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T14:40:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T14:41:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T14:42:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T14:43:51.462Z', - 'value': '0.4666666666666667' - }, - { - 'time': '2017-08-27T14:44:51.462Z', - 'value': '0.45714285714285713' - }, - { - 'time': '2017-08-27T14:45:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T14:46:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T14:47:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T14:48:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T14:49:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T14:50:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T14:51:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T14:52:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T14:53:51.462Z', - 'value': '0.4952333787297264' - }, - { - 'time': '2017-08-27T14:54:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T14:55:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T14:56:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T14:57:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T14:58:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T14:59:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T15:00:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T15:01:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T15:02:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T15:03:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T15:04:51.462Z', - 'value': '0.45714285714285713' - }, - { - 'time': '2017-08-27T15:05:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T15:06:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T15:07:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T15:08:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T15:09:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T15:10:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T15:11:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T15:12:51.462Z', - 'value': '0.4857096599080009' - }, - { - 'time': '2017-08-27T15:13:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T15:14:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T15:15:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T15:16:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T15:17:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T15:18:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T15:19:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T15:20:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T15:21:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T15:22:51.462Z', - 'value': '0.49524281183630325' - }, - { - 'time': '2017-08-27T15:23:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T15:24:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T15:25:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T15:26:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T15:27:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T15:28:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T15:29:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T15:30:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T15:31:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T15:32:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T15:33:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T15:34:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T15:35:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T15:36:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T15:37:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T15:38:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T15:39:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T15:40:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T15:41:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T15:42:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T15:43:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T15:44:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T15:45:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T15:46:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T15:47:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T15:48:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T15:49:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T15:50:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T15:51:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T15:52:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T15:53:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T15:54:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T15:55:51.462Z', - 'value': '0.49524752852435283' - }, - { - 'time': '2017-08-27T15:56:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T15:57:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T15:58:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T15:59:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T16:00:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T16:01:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T16:02:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T16:03:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T16:04:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T16:05:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T16:06:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T16:07:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T16:08:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T16:09:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T16:10:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T16:11:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T16:12:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T16:13:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T16:14:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T16:15:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T16:16:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T16:17:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T16:18:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T16:19:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T16:20:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T16:21:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T16:22:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T16:23:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T16:24:51.462Z', - 'value': '0.4761859410862754' - }, - { - 'time': '2017-08-27T16:25:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T16:26:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T16:27:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T16:28:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T16:29:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T16:30:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T16:31:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T16:32:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T16:33:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T16:34:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T16:35:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T16:36:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T16:37:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T16:38:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T16:39:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T16:40:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T16:41:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T16:42:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T16:43:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T16:44:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T16:45:51.462Z', - 'value': '0.485718911608682' - }, - { - 'time': '2017-08-27T16:46:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T16:47:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T16:48:51.462Z', - 'value': '0.4952333787297264' - }, - { - 'time': '2017-08-27T16:49:51.462Z', - 'value': '0.4857096599080009' - }, - { - 'time': '2017-08-27T16:50:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T16:51:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T16:52:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T16:53:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T16:54:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T16:55:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T16:56:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T16:57:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T16:58:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T16:59:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T17:00:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T17:01:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T17:02:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T17:03:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T17:04:51.462Z', - 'value': '0.47619501138106085' - }, - { - 'time': '2017-08-27T17:05:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T17:06:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T17:07:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T17:08:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T17:09:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T17:10:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T17:11:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T17:12:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T17:13:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T17:14:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T17:15:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T17:16:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T17:17:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T17:18:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T17:19:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T17:20:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T17:21:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T17:22:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T17:23:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T17:24:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T17:25:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T17:26:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T17:27:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T17:28:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T17:29:51.462Z', - 'value': '0.4761859410862754' - }, - { - 'time': '2017-08-27T17:30:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T17:31:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T17:32:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T17:33:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T17:34:51.462Z', - 'value': '0.4761859410862754' - }, - { - 'time': '2017-08-27T17:35:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T17:36:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T17:37:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T17:38:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T17:39:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T17:40:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T17:41:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T17:42:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T17:43:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T17:44:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T17:45:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T17:46:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T17:47:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T17:48:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T17:49:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T17:50:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T17:51:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T17:52:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T17:53:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T17:54:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T17:55:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T17:56:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T17:57:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T17:58:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T17:59:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T18:00:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T18:01:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T18:02:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T18:03:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T18:04:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T18:05:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T18:06:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T18:07:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T18:08:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T18:09:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T18:10:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T18:11:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T18:12:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T18:13:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T18:14:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T18:15:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T18:16:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T18:17:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T18:18:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T18:19:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T18:20:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T18:21:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T18:22:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T18:23:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T18:24:51.462Z', - 'value': '0.45714285714285713' - }, - { - 'time': '2017-08-27T18:25:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T18:26:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T18:27:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T18:28:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T18:29:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T18:30:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T18:31:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T18:32:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T18:33:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T18:34:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T18:35:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T18:36:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T18:37:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T18:38:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T18:39:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T18:40:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T18:41:51.462Z', - 'value': '0.6190476190476191' - }, - { - 'time': '2017-08-27T18:42:51.462Z', - 'value': '0.6952380952380952' - }, - { - 'time': '2017-08-27T18:43:51.462Z', - 'value': '0.857142857142857' - }, - { - 'time': '2017-08-27T18:44:51.462Z', - 'value': '0.9238095238095239' - }, - { - 'time': '2017-08-27T18:45:51.462Z', - 'value': '0.7428571428571429' - }, - { - 'time': '2017-08-27T18:46:51.462Z', - 'value': '0.8857142857142857' - }, - { - 'time': '2017-08-27T18:47:51.462Z', - 'value': '0.638095238095238' - }, - { - 'time': '2017-08-27T18:48:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T18:49:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T18:50:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T18:51:51.462Z', - 'value': '0.47619501138106085' - }, - { - 'time': '2017-08-27T18:52:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T18:53:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T18:54:51.462Z', - 'value': '0.4952380952380952' - }, - { - 'time': '2017-08-27T18:55:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T18:56:51.462Z', - 'value': '0.4857142857142857' - }, - { - 'time': '2017-08-27T18:57:51.462Z', - 'value': '0.47619047619047616' - }, - { - 'time': '2017-08-27T18:58:51.462Z', - 'value': '0.6857142857142856' - }, - { - 'time': '2017-08-27T18:59:51.462Z', - 'value': '0.6952380952380952' - }, - { - 'time': '2017-08-27T19:00:51.462Z', - 'value': '0.5238095238095237' - }, - { - 'time': '2017-08-27T19:01:51.462Z', - 'value': '0.5904761904761905' - } - ] - } - ] - } - ] - } + { + title: 'Multiple Time Series', + weight: 1, + y_label: 'Request Rates', + queries: [ + { + query_range: + 'sum(rate(nginx_responses_total{environment="production"}[2m])) by (status_code)', + label: 'Requests', + unit: 'Req/sec', + result: [ + { + metric: { + status_code: '1xx', + }, + values: [ + { + time: '2017-08-27T11:01:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:02:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:03:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:04:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:05:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:06:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:07:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:08:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:09:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:10:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:11:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:12:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:13:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:14:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:15:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:16:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:17:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:18:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:19:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:20:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:21:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:22:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:23:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:24:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:25:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:26:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:27:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:28:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:29:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:30:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:31:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:32:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:33:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:34:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:35:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:36:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:37:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:38:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:39:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:40:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:41:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:42:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:43:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:44:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:45:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:46:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:47:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:48:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:49:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:50:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:51:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:52:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:53:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:54:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:55:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:56:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:57:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:58:51.462Z', + value: '0', + }, + { + time: '2017-08-27T11:59:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:00:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:01:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:02:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:03:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:04:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:05:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:06:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:07:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:08:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:09:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:10:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:11:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:12:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:13:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:14:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:15:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:16:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:17:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:18:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:19:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:20:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:21:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:22:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:23:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:24:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:25:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:26:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:27:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:28:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:29:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:30:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:31:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:32:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:33:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:34:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:35:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:36:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:37:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:38:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:39:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:40:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:41:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:42:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:43:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:44:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:45:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:46:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:47:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:48:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:49:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:50:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:51:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:52:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:53:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:54:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:55:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:56:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:57:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:58:51.462Z', + value: '0', + }, + { + time: '2017-08-27T12:59:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:00:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:01:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:02:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:03:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:04:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:05:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:06:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:07:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:08:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:09:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:10:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:11:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:12:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:13:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:14:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:15:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:16:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:17:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:18:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:19:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:20:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:21:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:22:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:23:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:24:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:25:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:26:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:27:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:28:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:29:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:30:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:31:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:32:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:33:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:34:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:35:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:36:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:37:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:38:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:39:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:40:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:41:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:42:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:43:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:44:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:45:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:46:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:47:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:48:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:49:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:50:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:51:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:52:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:53:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:54:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:55:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:56:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:57:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:58:51.462Z', + value: '0', + }, + { + time: '2017-08-27T13:59:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:00:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:01:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:02:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:03:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:04:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:05:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:06:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:07:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:08:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:09:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:10:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:11:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:12:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:13:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:14:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:15:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:16:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:17:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:18:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:19:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:20:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:21:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:22:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:23:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:24:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:25:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:26:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:27:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:28:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:29:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:30:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:31:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:32:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:33:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:34:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:35:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:36:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:37:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:38:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:39:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:40:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:41:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:42:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:43:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:44:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:45:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:46:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:47:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:48:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:49:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:50:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:51:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:52:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:53:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:54:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:55:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:56:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:57:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:58:51.462Z', + value: '0', + }, + { + time: '2017-08-27T14:59:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:00:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:01:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:02:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:03:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:04:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:05:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:06:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:07:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:08:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:09:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:10:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:11:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:12:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:13:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:14:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:15:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:16:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:17:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:18:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:19:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:20:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:21:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:22:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:23:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:24:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:25:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:26:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:27:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:28:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:29:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:30:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:31:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:32:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:33:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:34:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:35:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:36:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:37:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:38:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:39:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:40:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:41:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:42:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:43:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:44:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:45:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:46:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:47:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:48:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:49:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:50:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:51:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:52:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:53:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:54:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:55:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:56:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:57:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:58:51.462Z', + value: '0', + }, + { + time: '2017-08-27T15:59:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:00:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:01:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:02:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:03:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:04:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:05:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:06:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:07:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:08:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:09:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:10:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:11:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:12:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:13:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:14:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:15:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:16:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:17:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:18:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:19:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:20:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:21:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:22:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:23:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:24:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:25:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:26:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:27:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:28:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:29:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:30:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:31:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:32:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:33:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:34:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:35:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:36:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:37:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:38:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:39:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:40:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:41:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:42:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:43:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:44:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:45:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:46:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:47:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:48:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:49:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:50:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:51:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:52:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:53:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:54:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:55:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:56:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:57:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:58:51.462Z', + value: '0', + }, + { + time: '2017-08-27T16:59:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:00:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:01:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:02:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:03:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:04:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:05:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:06:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:07:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:08:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:09:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:10:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:11:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:12:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:13:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:14:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:15:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:16:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:17:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:18:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:19:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:20:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:21:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:22:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:23:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:24:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:25:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:26:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:27:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:28:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:29:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:30:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:31:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:32:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:33:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:34:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:35:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:36:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:37:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:38:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:39:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:40:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:41:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:42:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:43:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:44:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:45:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:46:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:47:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:48:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:49:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:50:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:51:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:52:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:53:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:54:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:55:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:56:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:57:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:58:51.462Z', + value: '0', + }, + { + time: '2017-08-27T17:59:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:00:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:01:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:02:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:03:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:04:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:05:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:06:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:07:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:08:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:09:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:10:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:11:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:12:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:13:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:14:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:15:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:16:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:17:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:18:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:19:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:20:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:21:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:22:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:23:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:24:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:25:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:26:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:27:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:28:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:29:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:30:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:31:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:32:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:33:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:34:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:35:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:36:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:37:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:38:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:39:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:40:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:41:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:42:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:43:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:44:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:45:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:46:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:47:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:48:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:49:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:50:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:51:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:52:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:53:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:54:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:55:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:56:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:57:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:58:51.462Z', + value: '0', + }, + { + time: '2017-08-27T18:59:51.462Z', + value: '0', + }, + { + time: '2017-08-27T19:00:51.462Z', + value: '0', + }, + { + time: '2017-08-27T19:01:51.462Z', + value: '0', + }, + ], + }, + { + metric: { + status_code: '2xx', + }, + values: [ + { + time: '2017-08-27T11:01:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T11:02:51.462Z', + value: '1.2571428571428571', + }, + { + time: '2017-08-27T11:03:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T11:04:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T11:05:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T11:06:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T11:07:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T11:08:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T11:09:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T11:10:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T11:11:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T11:12:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T11:13:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T11:14:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T11:15:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T11:16:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T11:17:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T11:18:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T11:19:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T11:20:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T11:21:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T11:22:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T11:23:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T11:24:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T11:25:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T11:26:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T11:27:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T11:28:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T11:29:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T11:30:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T11:31:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T11:32:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T11:33:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T11:34:51.462Z', + value: '1.333320635041571', + }, + { + time: '2017-08-27T11:35:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T11:36:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T11:37:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T11:38:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T11:39:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T11:40:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T11:41:51.462Z', + value: '1.3333587306424883', + }, + { + time: '2017-08-27T11:42:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T11:43:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T11:44:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T11:45:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T11:46:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T11:47:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T11:48:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T11:49:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T11:50:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T11:51:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T11:52:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T11:53:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T11:54:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T11:55:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T11:56:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T11:57:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T11:58:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T11:59:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T12:00:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T12:01:51.462Z', + value: '1.3333460318669703', + }, + { + time: '2017-08-27T12:02:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T12:03:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T12:04:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T12:05:51.462Z', + value: '1.31427319739812', + }, + { + time: '2017-08-27T12:06:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T12:07:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T12:08:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T12:09:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T12:10:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T12:11:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T12:12:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T12:13:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T12:14:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T12:15:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T12:16:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T12:17:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T12:18:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T12:19:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T12:20:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T12:21:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T12:22:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T12:23:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T12:24:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T12:25:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T12:26:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T12:27:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T12:28:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T12:29:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T12:30:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T12:31:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T12:32:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T12:33:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T12:34:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T12:35:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T12:36:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T12:37:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T12:38:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T12:39:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T12:40:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T12:41:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T12:42:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T12:43:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T12:44:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T12:45:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T12:46:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T12:47:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T12:48:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T12:49:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T12:50:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T12:51:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T12:52:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T12:53:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T12:54:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T12:55:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T12:56:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T12:57:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T12:58:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T12:59:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T13:00:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T13:01:51.462Z', + value: '1.295225759754669', + }, + { + time: '2017-08-27T13:02:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T13:03:51.462Z', + value: '1.2952627669098458', + }, + { + time: '2017-08-27T13:04:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T13:05:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T13:06:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T13:07:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T13:08:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T13:09:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T13:10:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T13:11:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T13:12:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T13:13:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T13:14:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T13:15:51.462Z', + value: '1.2571428571428571', + }, + { + time: '2017-08-27T13:16:51.462Z', + value: '1.3333587306424883', + }, + { + time: '2017-08-27T13:17:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T13:18:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T13:19:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T13:20:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T13:21:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T13:22:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T13:23:51.462Z', + value: '1.276190476190476', + }, + { + time: '2017-08-27T13:24:51.462Z', + value: '1.2571428571428571', + }, + { + time: '2017-08-27T13:25:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T13:26:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T13:27:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T13:28:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T13:29:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T13:30:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T13:31:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T13:32:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T13:33:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T13:34:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T13:35:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T13:36:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T13:37:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T13:38:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T13:39:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T13:40:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T13:41:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T13:42:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T13:43:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T13:44:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T13:45:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T13:46:51.462Z', + value: '1.2571428571428571', + }, + { + time: '2017-08-27T13:47:51.462Z', + value: '1.276190476190476', + }, + { + time: '2017-08-27T13:48:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T13:49:51.462Z', + value: '1.295225759754669', + }, + { + time: '2017-08-27T13:50:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T13:51:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T13:52:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T13:53:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T13:54:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T13:55:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T13:56:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T13:57:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T13:58:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T13:59:51.462Z', + value: '1.295225759754669', + }, + { + time: '2017-08-27T14:00:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T14:01:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T14:02:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T14:03:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T14:04:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T14:05:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T14:06:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T14:07:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T14:08:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T14:09:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T14:10:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T14:11:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T14:12:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T14:13:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T14:14:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T14:15:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T14:16:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T14:17:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T14:18:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T14:19:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T14:20:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T14:21:51.462Z', + value: '1.3333079369916765', + }, + { + time: '2017-08-27T14:22:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T14:23:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T14:24:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T14:25:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T14:26:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T14:27:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T14:28:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T14:29:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T14:30:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T14:31:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T14:32:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T14:33:51.462Z', + value: '1.2571428571428571', + }, + { + time: '2017-08-27T14:34:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T14:35:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T14:36:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T14:37:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T14:38:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T14:39:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T14:40:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T14:41:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T14:42:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T14:43:51.462Z', + value: '1.276190476190476', + }, + { + time: '2017-08-27T14:44:51.462Z', + value: '1.2571428571428571', + }, + { + time: '2017-08-27T14:45:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T14:46:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T14:47:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T14:48:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T14:49:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T14:50:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T14:51:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T14:52:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T14:53:51.462Z', + value: '1.333320635041571', + }, + { + time: '2017-08-27T14:54:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T14:55:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T14:56:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T14:57:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T14:58:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T14:59:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T15:00:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T15:01:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T15:02:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T15:03:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T15:04:51.462Z', + value: '1.2571428571428571', + }, + { + time: '2017-08-27T15:05:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T15:06:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T15:07:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T15:08:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T15:09:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T15:10:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T15:11:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T15:12:51.462Z', + value: '1.31427319739812', + }, + { + time: '2017-08-27T15:13:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T15:14:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T15:15:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T15:16:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T15:17:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T15:18:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T15:19:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T15:20:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T15:21:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T15:22:51.462Z', + value: '1.3333460318669703', + }, + { + time: '2017-08-27T15:23:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T15:24:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T15:25:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T15:26:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T15:27:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T15:28:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T15:29:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T15:30:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T15:31:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T15:32:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T15:33:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T15:34:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T15:35:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T15:36:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T15:37:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T15:38:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T15:39:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T15:40:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T15:41:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T15:42:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T15:43:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T15:44:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T15:45:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T15:46:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T15:47:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T15:48:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T15:49:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T15:50:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T15:51:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T15:52:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T15:53:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T15:54:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T15:55:51.462Z', + value: '1.3333587306424883', + }, + { + time: '2017-08-27T15:56:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T15:57:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T15:58:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T15:59:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T16:00:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T16:01:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T16:02:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T16:03:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T16:04:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T16:05:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T16:06:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T16:07:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T16:08:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T16:09:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T16:10:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T16:11:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T16:12:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T16:13:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T16:14:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T16:15:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T16:16:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T16:17:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T16:18:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T16:19:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T16:20:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T16:21:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T16:22:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T16:23:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T16:24:51.462Z', + value: '1.295225759754669', + }, + { + time: '2017-08-27T16:25:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T16:26:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T16:27:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T16:28:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T16:29:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T16:30:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T16:31:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T16:32:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T16:33:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T16:34:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T16:35:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T16:36:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T16:37:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T16:38:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T16:39:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T16:40:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T16:41:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T16:42:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T16:43:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T16:44:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T16:45:51.462Z', + value: '1.3142982314117277', + }, + { + time: '2017-08-27T16:46:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T16:47:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T16:48:51.462Z', + value: '1.333320635041571', + }, + { + time: '2017-08-27T16:49:51.462Z', + value: '1.31427319739812', + }, + { + time: '2017-08-27T16:50:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T16:51:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T16:52:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T16:53:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T16:54:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T16:55:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T16:56:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T16:57:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T16:58:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T16:59:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T17:00:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T17:01:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T17:02:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T17:03:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T17:04:51.462Z', + value: '1.2952504309564854', + }, + { + time: '2017-08-27T17:05:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T17:06:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T17:07:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T17:08:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T17:09:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T17:10:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T17:11:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T17:12:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T17:13:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T17:14:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T17:15:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T17:16:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T17:17:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T17:18:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T17:19:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T17:20:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T17:21:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T17:22:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T17:23:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T17:24:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T17:25:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T17:26:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T17:27:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T17:28:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T17:29:51.462Z', + value: '1.295225759754669', + }, + { + time: '2017-08-27T17:30:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T17:31:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T17:32:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T17:33:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T17:34:51.462Z', + value: '1.295225759754669', + }, + { + time: '2017-08-27T17:35:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T17:36:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T17:37:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T17:38:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T17:39:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T17:40:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T17:41:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T17:42:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T17:43:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T17:44:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T17:45:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T17:46:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T17:47:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T17:48:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T17:49:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T17:50:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T17:51:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T17:52:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T17:53:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T17:54:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T17:55:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T17:56:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T17:57:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T17:58:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T17:59:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T18:00:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T18:01:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T18:02:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T18:03:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T18:04:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T18:05:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T18:06:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T18:07:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T18:08:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T18:09:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T18:10:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T18:11:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T18:12:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T18:13:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T18:14:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T18:15:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T18:16:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T18:17:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T18:18:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T18:19:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T18:20:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T18:21:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T18:22:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T18:23:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T18:24:51.462Z', + value: '1.2571428571428571', + }, + { + time: '2017-08-27T18:25:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T18:26:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T18:27:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T18:28:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T18:29:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T18:30:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T18:31:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T18:32:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T18:33:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T18:34:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T18:35:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T18:36:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T18:37:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T18:38:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T18:39:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T18:40:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T18:41:51.462Z', + value: '1.580952380952381', + }, + { + time: '2017-08-27T18:42:51.462Z', + value: '1.7333333333333334', + }, + { + time: '2017-08-27T18:43:51.462Z', + value: '2.057142857142857', + }, + { + time: '2017-08-27T18:44:51.462Z', + value: '2.1904761904761902', + }, + { + time: '2017-08-27T18:45:51.462Z', + value: '1.8285714285714287', + }, + { + time: '2017-08-27T18:46:51.462Z', + value: '2.1142857142857143', + }, + { + time: '2017-08-27T18:47:51.462Z', + value: '1.619047619047619', + }, + { + time: '2017-08-27T18:48:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T18:49:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T18:50:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T18:51:51.462Z', + value: '1.2952504309564854', + }, + { + time: '2017-08-27T18:52:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T18:53:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T18:54:51.462Z', + value: '1.3333333333333333', + }, + { + time: '2017-08-27T18:55:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T18:56:51.462Z', + value: '1.314285714285714', + }, + { + time: '2017-08-27T18:57:51.462Z', + value: '1.295238095238095', + }, + { + time: '2017-08-27T18:58:51.462Z', + value: '1.7142857142857142', + }, + { + time: '2017-08-27T18:59:51.462Z', + value: '1.7333333333333334', + }, + { + time: '2017-08-27T19:00:51.462Z', + value: '1.3904761904761904', + }, + { + time: '2017-08-27T19:01:51.462Z', + value: '1.5047619047619047', + }, + ], + }, + ], + when: [ + { + value: 'hundred(s)', + color: 'green', + }, + ], + }, + ], + }, + { + title: 'Throughput', + weight: 1, + y_label: 'Requests / Sec', + queries: [ + { + query_range: + "sum(rate(nginx_requests_total{server_zone!='*', server_zone!='_', container_name!='POD',environment='production'}[2m]))", + label: 'Total', + unit: 'req / sec', + result: [ + { + metric: {}, + values: [ + { + time: '2017-08-27T11:01:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T11:02:51.462Z', + value: '0.45714285714285713', + }, + { + time: '2017-08-27T11:03:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T11:04:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T11:05:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T11:06:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T11:07:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T11:08:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T11:09:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T11:10:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T11:11:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T11:12:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T11:13:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T11:14:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T11:15:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T11:16:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T11:17:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T11:18:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T11:19:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T11:20:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T11:21:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T11:22:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T11:23:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T11:24:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T11:25:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T11:26:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T11:27:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T11:28:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T11:29:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T11:30:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T11:31:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T11:32:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T11:33:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T11:34:51.462Z', + value: '0.4952333787297264', + }, + { + time: '2017-08-27T11:35:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T11:36:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T11:37:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T11:38:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T11:39:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T11:40:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T11:41:51.462Z', + value: '0.49524752852435283', + }, + { + time: '2017-08-27T11:42:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T11:43:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T11:44:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T11:45:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T11:46:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T11:47:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T11:48:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T11:49:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T11:50:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T11:51:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T11:52:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T11:53:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T11:54:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T11:55:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T11:56:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T11:57:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T11:58:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T11:59:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T12:00:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T12:01:51.462Z', + value: '0.49524281183630325', + }, + { + time: '2017-08-27T12:02:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T12:03:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T12:04:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T12:05:51.462Z', + value: '0.4857096599080009', + }, + { + time: '2017-08-27T12:06:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T12:07:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T12:08:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T12:09:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T12:10:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T12:11:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T12:12:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T12:13:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T12:14:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T12:15:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T12:16:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T12:17:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T12:18:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T12:19:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T12:20:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T12:21:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T12:22:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T12:23:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T12:24:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T12:25:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T12:26:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T12:27:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T12:28:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T12:29:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T12:30:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T12:31:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T12:32:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T12:33:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T12:34:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T12:35:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T12:36:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T12:37:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T12:38:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T12:39:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T12:40:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T12:41:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T12:42:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T12:43:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T12:44:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T12:45:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T12:46:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T12:47:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T12:48:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T12:49:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T12:50:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T12:51:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T12:52:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T12:53:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T12:54:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T12:55:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T12:56:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T12:57:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T12:58:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T12:59:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T13:00:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T13:01:51.462Z', + value: '0.4761859410862754', + }, + { + time: '2017-08-27T13:02:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T13:03:51.462Z', + value: '0.4761995466580315', + }, + { + time: '2017-08-27T13:04:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T13:05:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T13:06:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T13:07:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T13:08:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T13:09:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T13:10:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T13:11:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T13:12:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T13:13:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T13:14:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T13:15:51.462Z', + value: '0.45714285714285713', + }, + { + time: '2017-08-27T13:16:51.462Z', + value: '0.49524752852435283', + }, + { + time: '2017-08-27T13:17:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T13:18:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T13:19:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T13:20:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T13:21:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T13:22:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T13:23:51.462Z', + value: '0.4666666666666667', + }, + { + time: '2017-08-27T13:24:51.462Z', + value: '0.45714285714285713', + }, + { + time: '2017-08-27T13:25:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T13:26:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T13:27:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T13:28:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T13:29:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T13:30:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T13:31:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T13:32:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T13:33:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T13:34:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T13:35:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T13:36:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T13:37:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T13:38:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T13:39:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T13:40:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T13:41:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T13:42:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T13:43:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T13:44:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T13:45:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T13:46:51.462Z', + value: '0.45714285714285713', + }, + { + time: '2017-08-27T13:47:51.462Z', + value: '0.4666666666666667', + }, + { + time: '2017-08-27T13:48:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T13:49:51.462Z', + value: '0.4761859410862754', + }, + { + time: '2017-08-27T13:50:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T13:51:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T13:52:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T13:53:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T13:54:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T13:55:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T13:56:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T13:57:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T13:58:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T13:59:51.462Z', + value: '0.4761859410862754', + }, + { + time: '2017-08-27T14:00:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T14:01:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T14:02:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T14:03:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T14:04:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T14:05:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T14:06:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T14:07:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T14:08:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T14:09:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T14:10:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T14:11:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T14:12:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T14:13:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T14:14:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T14:15:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T14:16:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T14:17:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T14:18:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T14:19:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T14:20:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T14:21:51.462Z', + value: '0.4952286623111941', + }, + { + time: '2017-08-27T14:22:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T14:23:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T14:24:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T14:25:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T14:26:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T14:27:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T14:28:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T14:29:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T14:30:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T14:31:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T14:32:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T14:33:51.462Z', + value: '0.45714285714285713', + }, + { + time: '2017-08-27T14:34:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T14:35:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T14:36:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T14:37:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T14:38:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T14:39:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T14:40:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T14:41:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T14:42:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T14:43:51.462Z', + value: '0.4666666666666667', + }, + { + time: '2017-08-27T14:44:51.462Z', + value: '0.45714285714285713', + }, + { + time: '2017-08-27T14:45:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T14:46:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T14:47:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T14:48:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T14:49:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T14:50:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T14:51:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T14:52:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T14:53:51.462Z', + value: '0.4952333787297264', + }, + { + time: '2017-08-27T14:54:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T14:55:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T14:56:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T14:57:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T14:58:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T14:59:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T15:00:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T15:01:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T15:02:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T15:03:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T15:04:51.462Z', + value: '0.45714285714285713', + }, + { + time: '2017-08-27T15:05:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T15:06:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T15:07:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T15:08:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T15:09:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T15:10:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T15:11:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T15:12:51.462Z', + value: '0.4857096599080009', + }, + { + time: '2017-08-27T15:13:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T15:14:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T15:15:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T15:16:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T15:17:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T15:18:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T15:19:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T15:20:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T15:21:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T15:22:51.462Z', + value: '0.49524281183630325', + }, + { + time: '2017-08-27T15:23:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T15:24:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T15:25:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T15:26:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T15:27:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T15:28:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T15:29:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T15:30:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T15:31:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T15:32:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T15:33:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T15:34:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T15:35:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T15:36:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T15:37:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T15:38:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T15:39:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T15:40:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T15:41:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T15:42:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T15:43:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T15:44:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T15:45:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T15:46:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T15:47:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T15:48:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T15:49:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T15:50:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T15:51:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T15:52:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T15:53:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T15:54:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T15:55:51.462Z', + value: '0.49524752852435283', + }, + { + time: '2017-08-27T15:56:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T15:57:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T15:58:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T15:59:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T16:00:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T16:01:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T16:02:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T16:03:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T16:04:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T16:05:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T16:06:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T16:07:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T16:08:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T16:09:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T16:10:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T16:11:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T16:12:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T16:13:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T16:14:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T16:15:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T16:16:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T16:17:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T16:18:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T16:19:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T16:20:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T16:21:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T16:22:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T16:23:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T16:24:51.462Z', + value: '0.4761859410862754', + }, + { + time: '2017-08-27T16:25:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T16:26:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T16:27:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T16:28:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T16:29:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T16:30:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T16:31:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T16:32:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T16:33:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T16:34:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T16:35:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T16:36:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T16:37:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T16:38:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T16:39:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T16:40:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T16:41:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T16:42:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T16:43:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T16:44:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T16:45:51.462Z', + value: '0.485718911608682', + }, + { + time: '2017-08-27T16:46:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T16:47:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T16:48:51.462Z', + value: '0.4952333787297264', + }, + { + time: '2017-08-27T16:49:51.462Z', + value: '0.4857096599080009', + }, + { + time: '2017-08-27T16:50:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T16:51:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T16:52:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T16:53:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T16:54:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T16:55:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T16:56:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T16:57:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T16:58:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T16:59:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T17:00:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T17:01:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T17:02:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T17:03:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T17:04:51.462Z', + value: '0.47619501138106085', + }, + { + time: '2017-08-27T17:05:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T17:06:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T17:07:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T17:08:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T17:09:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T17:10:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T17:11:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T17:12:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T17:13:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T17:14:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T17:15:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T17:16:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T17:17:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T17:18:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T17:19:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T17:20:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T17:21:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T17:22:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T17:23:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T17:24:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T17:25:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T17:26:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T17:27:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T17:28:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T17:29:51.462Z', + value: '0.4761859410862754', + }, + { + time: '2017-08-27T17:30:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T17:31:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T17:32:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T17:33:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T17:34:51.462Z', + value: '0.4761859410862754', + }, + { + time: '2017-08-27T17:35:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T17:36:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T17:37:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T17:38:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T17:39:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T17:40:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T17:41:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T17:42:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T17:43:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T17:44:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T17:45:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T17:46:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T17:47:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T17:48:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T17:49:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T17:50:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T17:51:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T17:52:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T17:53:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T17:54:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T17:55:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T17:56:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T17:57:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T17:58:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T17:59:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T18:00:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T18:01:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T18:02:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T18:03:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T18:04:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T18:05:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T18:06:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T18:07:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T18:08:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T18:09:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T18:10:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T18:11:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T18:12:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T18:13:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T18:14:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T18:15:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T18:16:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T18:17:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T18:18:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T18:19:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T18:20:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T18:21:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T18:22:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T18:23:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T18:24:51.462Z', + value: '0.45714285714285713', + }, + { + time: '2017-08-27T18:25:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T18:26:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T18:27:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T18:28:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T18:29:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T18:30:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T18:31:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T18:32:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T18:33:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T18:34:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T18:35:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T18:36:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T18:37:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T18:38:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T18:39:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T18:40:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T18:41:51.462Z', + value: '0.6190476190476191', + }, + { + time: '2017-08-27T18:42:51.462Z', + value: '0.6952380952380952', + }, + { + time: '2017-08-27T18:43:51.462Z', + value: '0.857142857142857', + }, + { + time: '2017-08-27T18:44:51.462Z', + value: '0.9238095238095239', + }, + { + time: '2017-08-27T18:45:51.462Z', + value: '0.7428571428571429', + }, + { + time: '2017-08-27T18:46:51.462Z', + value: '0.8857142857142857', + }, + { + time: '2017-08-27T18:47:51.462Z', + value: '0.638095238095238', + }, + { + time: '2017-08-27T18:48:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T18:49:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T18:50:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T18:51:51.462Z', + value: '0.47619501138106085', + }, + { + time: '2017-08-27T18:52:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T18:53:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T18:54:51.462Z', + value: '0.4952380952380952', + }, + { + time: '2017-08-27T18:55:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T18:56:51.462Z', + value: '0.4857142857142857', + }, + { + time: '2017-08-27T18:57:51.462Z', + value: '0.47619047619047616', + }, + { + time: '2017-08-27T18:58:51.462Z', + value: '0.6857142857142856', + }, + { + time: '2017-08-27T18:59:51.462Z', + value: '0.6952380952380952', + }, + { + time: '2017-08-27T19:00:51.462Z', + value: '0.5238095238095237', + }, + { + time: '2017-08-27T19:01:51.462Z', + value: '0.5904761904761905', + }, + ], + }, + ], + }, + ], + }, ]; export function convertDatesMultipleSeries(multipleSeries) { const convertedMultiple = multipleSeries; multipleSeries.forEach((column, index) => { let convertedResult = []; - convertedResult = column.queries[0].result.map((resultObj) => { + convertedResult = column.queries[0].result.map(resultObj => { const convertedMetrics = {}; convertedMetrics.values = resultObj.values.map(val => ({ - time: new Date(val.time), - value: val.value, + time: new Date(val.time), + value: val.value, })); convertedMetrics.metric = resultObj.metric; return convertedMetrics; diff --git a/spec/javascripts/pipelines/graph/action_component_spec.js b/spec/javascripts/pipelines/graph/action_component_spec.js index e8fcd4b1a36..581209f215d 100644 --- a/spec/javascripts/pipelines/graph/action_component_spec.js +++ b/spec/javascripts/pipelines/graph/action_component_spec.js @@ -1,25 +1,30 @@ import Vue from 'vue'; import actionComponent from '~/pipelines/components/graph/action_component.vue'; +import eventHub from '~/pipelines/event_hub'; +import mountComponent from '../../helpers/vue_mount_component_helper'; describe('pipeline graph action component', () => { let component; beforeEach((done) => { const ActionComponent = Vue.extend(actionComponent); - component = new ActionComponent({ - propsData: { - tooltipText: 'bar', - link: 'foo', - actionMethod: 'post', - actionIcon: 'cancel', - }, - }).$mount(); + component = mountComponent(ActionComponent, { + tooltipText: 'bar', + link: 'foo', + actionIcon: 'cancel', + }); Vue.nextTick(done); }); - it('should render a link', () => { - expect(component.$el.getAttribute('href')).toEqual('foo'); + afterEach(() => { + component.$destroy(); + }); + + it('should emit an event with the provided link', () => { + eventHub.$on('graphAction', (link) => { + expect(link).toEqual('foo'); + }); }); it('should render the provided title as a bootstrap tooltip', () => { diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index 40115792652..1a27955983d 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -6,8 +6,21 @@ import SearchAutocomplete from '~/search_autocomplete'; import '~/lib/utils/common_utils'; import * as urlUtils from '~/lib/utils/url_utility'; -(function() { - var assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget; +describe('Search autocomplete dropdown', () => { + var assertLinks, + dashboardIssuesPath, + dashboardMRsPath, + groupIssuesPath, + groupMRsPath, + groupName, + mockDashboardOptions, + mockGroupOptions, + mockProjectOptions, + projectIssuesPath, + projectMRsPath, + projectName, + userId, + widget; var userName = 'root'; widget = null; @@ -66,133 +79,126 @@ import * as urlUtils from '~/lib/utils/url_utility'; // Mock `gl` object in window for dashboard specific page. App code will need it. mockDashboardOptions = function() { window.gl || (window.gl = {}); - return window.gl.dashboardOptions = { + return (window.gl.dashboardOptions = { issuesPath: dashboardIssuesPath, - mrPath: dashboardMRsPath - }; + mrPath: dashboardMRsPath, + }); }; // Mock `gl` object in window for project specific page. App code will need it. mockProjectOptions = function() { window.gl || (window.gl = {}); - return window.gl.projectOptions = { + return (window.gl.projectOptions = { 'gitlab-ce': { issuesPath: projectIssuesPath, mrPath: projectMRsPath, - projectName: projectName - } - }; + projectName: projectName, + }, + }); }; mockGroupOptions = function() { window.gl || (window.gl = {}); - return window.gl.groupOptions = { + return (window.gl.groupOptions = { 'gitlab-org': { issuesPath: groupIssuesPath, mrPath: groupMRsPath, - projectName: groupName - } - }; + projectName: groupName, + }, + }); }; assertLinks = function(list, issuesPath, mrsPath) { - var a1, a2, a3, a4, issuesAssignedToMeLink, issuesIHaveCreatedLink, mrsAssignedToMeLink, mrsIHaveCreatedLink; if (issuesPath) { - issuesAssignedToMeLink = issuesPath + "/?assignee_username=" + userName; - issuesIHaveCreatedLink = issuesPath + "/?author_username=" + userName; - a1 = "a[href='" + issuesAssignedToMeLink + "']"; - a2 = "a[href='" + issuesIHaveCreatedLink + "']"; - expect(list.find(a1).length).toBe(1); - expect(list.find(a1).text()).toBe('Issues assigned to me'); - expect(list.find(a2).length).toBe(1); - expect(list.find(a2).text()).toBe("Issues I've created"); + const issuesAssignedToMeLink = `a[href="${issuesPath}/?assignee_id=${userId}"]`; + const issuesIHaveCreatedLink = `a[href="${issuesPath}/?author_id=${userId}"]`; + expect(list.find(issuesAssignedToMeLink).length).toBe(1); + expect(list.find(issuesAssignedToMeLink).text()).toBe('Issues assigned to me'); + expect(list.find(issuesIHaveCreatedLink).length).toBe(1); + expect(list.find(issuesIHaveCreatedLink).text()).toBe("Issues I've created"); } - mrsAssignedToMeLink = mrsPath + "/?assignee_username=" + userName; - mrsIHaveCreatedLink = mrsPath + "/?author_username=" + userName; - a3 = "a[href='" + mrsAssignedToMeLink + "']"; - a4 = "a[href='" + mrsIHaveCreatedLink + "']"; - expect(list.find(a3).length).toBe(1); - expect(list.find(a3).text()).toBe('Merge requests assigned to me'); - expect(list.find(a4).length).toBe(1); - return expect(list.find(a4).text()).toBe("Merge requests I've created"); + const mrsAssignedToMeLink = `a[href="${mrsPath}/?assignee_id=${userId}"]`; + const mrsIHaveCreatedLink = `a[href="${mrsPath}/?author_id=${userId}"]`; + expect(list.find(mrsAssignedToMeLink).length).toBe(1); + expect(list.find(mrsAssignedToMeLink).text()).toBe('Merge requests assigned to me'); + expect(list.find(mrsIHaveCreatedLink).length).toBe(1); + expect(list.find(mrsIHaveCreatedLink).text()).toBe("Merge requests I've created"); }; - describe('Search autocomplete dropdown', function() { - preloadFixtures('static/search_autocomplete.html.raw'); - beforeEach(function() { - loadFixtures('static/search_autocomplete.html.raw'); + preloadFixtures('static/search_autocomplete.html.raw'); + beforeEach(function() { + loadFixtures('static/search_autocomplete.html.raw'); - // Prevent turbolinks from triggering within gl_dropdown - spyOn(urlUtils, 'visitUrl').and.returnValue(true); + // Prevent turbolinks from triggering within gl_dropdown + spyOn(urlUtils, 'visitUrl').and.returnValue(true); - window.gon = {}; - window.gon.current_user_id = userId; - window.gon.current_username = userName; + window.gon = {}; + window.gon.current_user_id = userId; + window.gon.current_username = userName; - return widget = new SearchAutocomplete(); - }); + return (widget = new SearchAutocomplete()); + }); - afterEach(function() { - // Undo what we did to the shared <body> - removeBodyAttributes(); - window.gon = {}; - }); - it('should show Dashboard specific dropdown menu', function() { - var list; - addBodyAttributes(); - mockDashboardOptions(); - widget.searchInput.triggerHandler('focus'); - list = widget.wrap.find('.dropdown-menu').find('ul'); - return assertLinks(list, dashboardIssuesPath, dashboardMRsPath); - }); - it('should show Group specific dropdown menu', function() { - var list; - addBodyAttributes('group'); - mockGroupOptions(); - widget.searchInput.triggerHandler('focus'); - list = widget.wrap.find('.dropdown-menu').find('ul'); - return assertLinks(list, groupIssuesPath, groupMRsPath); - }); - it('should show Project specific dropdown menu', function() { - var list; - addBodyAttributes('project'); - mockProjectOptions(); - widget.searchInput.triggerHandler('focus'); - list = widget.wrap.find('.dropdown-menu').find('ul'); - return assertLinks(list, projectIssuesPath, projectMRsPath); - }); - it('should show only Project mergeRequest dropdown menu items when project issues are disabled', function() { - addBodyAttributes('project'); - disableProjectIssues(); - mockProjectOptions(); - widget.searchInput.triggerHandler('focus'); - const list = widget.wrap.find('.dropdown-menu').find('ul'); - assertLinks(list, null, projectMRsPath); - }); - it('should not show category related menu if there is text in the input', function() { - var link, list; - addBodyAttributes('project'); - mockProjectOptions(); - widget.searchInput.val('help'); - widget.searchInput.triggerHandler('focus'); - list = widget.wrap.find('.dropdown-menu').find('ul'); - link = "a[href='" + projectIssuesPath + "/?assignee_id=" + userId + "']"; - return expect(list.find(link).length).toBe(0); - }); - return it('should not submit the search form when selecting an autocomplete row with the keyboard', function() { - var ENTER = 13; - var DOWN = 40; - addBodyAttributes(); - mockDashboardOptions(true); - var submitSpy = spyOnEvent('form', 'submit'); - widget.searchInput.triggerHandler('focus'); - widget.wrap.trigger($.Event('keydown', { which: DOWN })); - var enterKeyEvent = $.Event('keydown', { which: ENTER }); - widget.searchInput.trigger(enterKeyEvent); - // This does not currently catch failing behavior. For security reasons, - // browsers will not trigger default behavior (form submit, in this - // example) on JavaScript-created keypresses. - expect(submitSpy).not.toHaveBeenTriggered(); - }); + afterEach(function() { + // Undo what we did to the shared <body> + removeBodyAttributes(); + window.gon = {}; + }); + it('should show Dashboard specific dropdown menu', function() { + var list; + addBodyAttributes(); + mockDashboardOptions(); + widget.searchInput.triggerHandler('focus'); + list = widget.wrap.find('.dropdown-menu').find('ul'); + return assertLinks(list, dashboardIssuesPath, dashboardMRsPath); + }); + it('should show Group specific dropdown menu', function() { + var list; + addBodyAttributes('group'); + mockGroupOptions(); + widget.searchInput.triggerHandler('focus'); + list = widget.wrap.find('.dropdown-menu').find('ul'); + return assertLinks(list, groupIssuesPath, groupMRsPath); + }); + it('should show Project specific dropdown menu', function() { + var list; + addBodyAttributes('project'); + mockProjectOptions(); + widget.searchInput.triggerHandler('focus'); + list = widget.wrap.find('.dropdown-menu').find('ul'); + return assertLinks(list, projectIssuesPath, projectMRsPath); + }); + it('should show only Project mergeRequest dropdown menu items when project issues are disabled', function() { + addBodyAttributes('project'); + disableProjectIssues(); + mockProjectOptions(); + widget.searchInput.triggerHandler('focus'); + const list = widget.wrap.find('.dropdown-menu').find('ul'); + assertLinks(list, null, projectMRsPath); + }); + it('should not show category related menu if there is text in the input', function() { + var link, list; + addBodyAttributes('project'); + mockProjectOptions(); + widget.searchInput.val('help'); + widget.searchInput.triggerHandler('focus'); + list = widget.wrap.find('.dropdown-menu').find('ul'); + link = "a[href='" + projectIssuesPath + '/?assignee_id=' + userId + "']"; + return expect(list.find(link).length).toBe(0); + }); + it('should not submit the search form when selecting an autocomplete row with the keyboard', function() { + var ENTER = 13; + var DOWN = 40; + addBodyAttributes(); + mockDashboardOptions(true); + var submitSpy = spyOnEvent('form', 'submit'); + widget.searchInput.triggerHandler('focus'); + widget.wrap.trigger($.Event('keydown', { which: DOWN })); + var enterKeyEvent = $.Event('keydown', { which: ENTER }); + widget.searchInput.trigger(enterKeyEvent); + // This does not currently catch failing behavior. For security reasons, + // browsers will not trigger default behavior (form submit, in this + // example) on JavaScript-created keypresses. + expect(submitSpy).not.toHaveBeenTriggered(); }); -}).call(window); +}); diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index 1bcfdfe72b6..d158786e484 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -7,6 +7,9 @@ import Vue from 'vue'; import VueResource from 'vue-resource'; import { getDefaultAdapter } from '~/lib/utils/axios_utils'; +import { FIXTURES_PATH, TEST_HOST } from './test_constants'; + +import customMatchers from './matchers'; const isHeadlessChrome = /\bHeadlessChrome\//.test(navigator.userAgent); Vue.config.devtools = !isHeadlessChrome; @@ -27,15 +30,17 @@ Vue.config.errorHandler = function (err) { Vue.use(VueResource); // enable test fixtures -jasmine.getFixtures().fixturesPath = '/base/spec/javascripts/fixtures'; -jasmine.getJSONFixtures().fixturesPath = '/base/spec/javascripts/fixtures'; +jasmine.getFixtures().fixturesPath = FIXTURES_PATH; +jasmine.getJSONFixtures().fixturesPath = FIXTURES_PATH; + +beforeAll(() => jasmine.addMatchers(customMatchers)); // globalize common libraries window.$ = window.jQuery = $; // stub expected globals window.gl = window.gl || {}; -window.gl.TEST_HOST = 'http://test.host'; +window.gl.TEST_HOST = TEST_HOST; window.gon = window.gon || {}; window.gon.test_env = true; diff --git a/spec/javascripts/test_constants.js b/spec/javascripts/test_constants.js new file mode 100644 index 00000000000..df59195e9f6 --- /dev/null +++ b/spec/javascripts/test_constants.js @@ -0,0 +1,4 @@ +export const FIXTURES_PATH = '/base/spec/javascripts/fixtures'; +export const TEST_HOST = 'http://test.host'; + +export const DUMMY_IMAGE_URL = `${FIXTURES_PATH}/one_white_pixel.png`; diff --git a/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js b/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js index c7c454a0b45..383f0cd29ea 100644 --- a/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js +++ b/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js @@ -38,4 +38,33 @@ describe('ContentViewer', () => { done(); }); }); + + it('renders image preview', done => { + createComponent({ + path: 'test.jpg', + fileSize: 1024, + }); + + setTimeout(() => { + expect(vm.$el.querySelector('.image_file img').getAttribute('src')).toBe('test.jpg'); + + done(); + }); + }); + + it('renders fallback download control', done => { + createComponent({ + path: 'test.abc', + fileSize: 1024, + }); + + setTimeout(() => { + expect(vm.$el.querySelector('.file-info').textContent.trim()).toContain( + 'test.abc (1.00 KiB)', + ); + expect(vm.$el.querySelector('.btn.btn-default').textContent.trim()).toContain('Download'); + + done(); + }); + }); }); diff --git a/spec/lib/banzai/filter/commit_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_reference_filter_spec.rb index 35f8792ff35..b18af806118 100644 --- a/spec/lib/banzai/filter/commit_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/commit_reference_filter_spec.rb @@ -207,4 +207,35 @@ describe Banzai::Filter::CommitReferenceFilter do expect(reference_filter(act).to_html).to match(%r{<a.+>#{Regexp.escape(invalidate_reference(reference))}</a>}) end end + + context 'URL reference for a commit patch' do + let(:namespace) { create(:namespace) } + let(:project2) { create(:project, :public, :repository, namespace: namespace) } + let(:commit) { project2.commit } + let(:link) { urls.project_commit_url(project2, commit.id) } + let(:extension) { '.patch' } + let(:reference) { link + extension } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')) + .to eq reference + end + + it 'has valid text' do + doc = reference_filter("See #{reference}") + + expect(doc.text).to eq("See #{commit.reference_link_text(project)} (patch)") + end + + it 'does not link to patch when extension match is after the path' do + invalidate_commit_reference = reference_filter("#{link}/builds.patch") + + doc = reference_filter("See (#{invalidate_commit_reference})") + + expect(doc.css('a').first.attr('href')).to eq "#{link}/builds" + expect(doc.text).to eq("See (#{commit.reference_link_text(project)} (builds).patch)") + end + end end diff --git a/spec/lib/forever_spec.rb b/spec/lib/forever_spec.rb new file mode 100644 index 00000000000..cf40c467c72 --- /dev/null +++ b/spec/lib/forever_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Forever do + describe '.date' do + subject { described_class.date } + + context 'when using PostgreSQL' do + it 'should return Postgresql future date' do + allow(Gitlab::Database).to receive(:postgresql?).and_return(true) + expect(subject).to eq(described_class::POSTGRESQL_DATE) + end + end + + context 'when using MySQL' do + it 'should return MySQL future date' do + allow(Gitlab::Database).to receive(:postgresql?).and_return(false) + expect(subject).to eq(described_class::MYSQL_DATE) + end + end + end +end diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index 18cef8ec996..9ccd0b206cc 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::Auth do describe 'constants' do it 'API_SCOPES contains all scopes for API access' do - expect(subject::API_SCOPES).to eq %i[api read_user sudo] + expect(subject::API_SCOPES).to eq %i[api read_user sudo read_repository] end it 'OPENID_SCOPES contains all scopes for OpenID Connect' do @@ -19,7 +19,7 @@ describe Gitlab::Auth do it 'optional_scopes contains all non-default scopes' do stub_container_registry_config(enabled: true) - expect(subject.optional_scopes).to eq %i[read_user sudo read_registry openid] + expect(subject.optional_scopes).to eq %i[read_user sudo read_repository read_registry openid] end context 'registry_scopes' do @@ -231,7 +231,7 @@ describe Gitlab::Auth do .to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)) end - it 'falls through oauth authentication when the username is oauth2' do + it 'fails through oauth authentication when the username is oauth2' do user = create( :user, username: 'oauth2', @@ -255,6 +255,122 @@ describe Gitlab::Auth do expect { gl_auth.find_for_git_client('foo', 'bar', project: nil, ip: 'ip') }.to raise_error(Gitlab::Auth::MissingPersonalAccessTokenError) end + + context 'while using deploy tokens' do + let(:project) { create(:project) } + let(:auth_failure) { Gitlab::Auth::Result.new(nil, nil) } + + context 'when the deploy token has read_repository as scope' do + let(:deploy_token) { create(:deploy_token, read_registry: false, projects: [project]) } + let(:login) { deploy_token.username } + + it 'succeeds when login and token are valid' do + auth_success = Gitlab::Auth::Result.new(deploy_token, project, :deploy_token, [:download_code]) + + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: login) + expect(gl_auth.find_for_git_client(login, deploy_token.token, project: project, ip: 'ip')) + .to eq(auth_success) + end + + it 'fails when login is not valid' do + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'random_login') + expect(gl_auth.find_for_git_client('random_login', deploy_token.token, project: project, ip: 'ip')) + .to eq(auth_failure) + end + + it 'fails when token is not valid' do + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: login) + expect(gl_auth.find_for_git_client(login, '123123', project: project, ip: 'ip')) + .to eq(auth_failure) + end + + it 'fails if token is nil' do + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: login) + expect(gl_auth.find_for_git_client(login, nil, project: project, ip: 'ip')) + .to eq(auth_failure) + end + + it 'fails if token is not related to project' do + another_deploy_token = create(:deploy_token) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: login) + expect(gl_auth.find_for_git_client(login, another_deploy_token.token, project: project, ip: 'ip')) + .to eq(auth_failure) + end + + it 'fails if token has been revoked' do + deploy_token.revoke! + + expect(deploy_token.revoked?).to be_truthy + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'deploy-token') + expect(gl_auth.find_for_git_client('deploy-token', deploy_token.token, project: project, ip: 'ip')) + .to eq(auth_failure) + end + end + + context 'when the deploy token has read_registry as a scope' do + let(:deploy_token) { create(:deploy_token, read_repository: false, projects: [project]) } + let(:login) { deploy_token.username } + + context 'when registry enabled' do + before do + stub_container_registry_config(enabled: true) + end + + it 'succeeds when login and token are valid' do + auth_success = Gitlab::Auth::Result.new(deploy_token, project, :deploy_token, [:read_container_image]) + + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: login) + expect(gl_auth.find_for_git_client(login, deploy_token.token, project: nil, ip: 'ip')) + .to eq(auth_success) + end + + it 'fails when login is not valid' do + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'random_login') + expect(gl_auth.find_for_git_client('random_login', deploy_token.token, project: project, ip: 'ip')) + .to eq(auth_failure) + end + + it 'fails when token is not valid' do + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: login) + expect(gl_auth.find_for_git_client(login, '123123', project: project, ip: 'ip')) + .to eq(auth_failure) + end + + it 'fails if token is nil' do + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: login) + expect(gl_auth.find_for_git_client(login, nil, project: nil, ip: 'ip')) + .to eq(auth_failure) + end + + it 'fails if token is not related to project' do + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: login) + expect(gl_auth.find_for_git_client(login, 'abcdef', project: nil, ip: 'ip')) + .to eq(auth_failure) + end + + it 'fails if token has been revoked' do + deploy_token.revoke! + + expect(deploy_token.revoked?).to be_truthy + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'deploy-token') + expect(gl_auth.find_for_git_client('deploy-token', deploy_token.token, project: nil, ip: 'ip')) + .to eq(auth_failure) + end + end + + context 'when registry disabled' do + before do + stub_container_registry_config(enabled: false) + end + + it 'fails when login and token are valid' do + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: login) + expect(gl_auth.find_for_git_client(login, deploy_token.token, project: nil, ip: 'ip')) + .to eq(auth_failure) + end + end + end + end end describe 'find_with_user_password' do diff --git a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb index 3ef0b6817e9..78d6fa65b5a 100644 --- a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb +++ b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb @@ -90,6 +90,10 @@ describe Gitlab::Ci::Status::Build::Cancelable do describe '#action_title' do it { expect(subject.action_title).to eq 'Cancel' } end + + describe '#action_button_title' do + it { expect(subject.action_button_title).to eq 'Cancel this job' } + end end describe '.matches?' do diff --git a/spec/lib/gitlab/ci/status/build/canceled_spec.rb b/spec/lib/gitlab/ci/status/build/canceled_spec.rb new file mode 100644 index 00000000000..c6b5cc68770 --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/canceled_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::Canceled do + let(:user) { create(:user) } + + subject do + described_class.new(double('subject')) + end + + describe '#illustration' do + it { expect(subject.illustration).to include(:image, :size, :title) } + end + + describe '.matches?' do + subject {described_class.matches?(build, user) } + + context 'when build is canceled' do + let(:build) { create(:ci_build, :canceled) } + + it 'is a correct match' do + expect(subject).to be true + end + end + + context 'when build is not canceled' do + let(:build) { create(:ci_build) } + + it 'does not match' do + expect(subject).to be false + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/build/created_spec.rb b/spec/lib/gitlab/ci/status/build/created_spec.rb new file mode 100644 index 00000000000..8bdfe6ef7a2 --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/created_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::Created do + let(:user) { create(:user) } + + subject do + described_class.new(double('subject')) + end + + describe '#illustration' do + it { expect(subject.illustration).to include(:image, :size, :title, :content) } + end + + describe '.matches?' do + subject {described_class.matches?(build, user) } + + context 'when build is created' do + let(:build) { create(:ci_build, :created) } + + it 'is a correct match' do + expect(subject).to be true + end + end + + context 'when build is not created' do + let(:build) { create(:ci_build) } + + it 'does not match' do + expect(subject).to be false + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/build/erased_spec.rb b/spec/lib/gitlab/ci/status/build/erased_spec.rb new file mode 100644 index 00000000000..0acd271e375 --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/erased_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::Erased do + let(:user) { create(:user) } + + subject do + described_class.new(double('subject')) + end + + describe '#illustration' do + it { expect(subject.illustration).to include(:image, :size, :title) } + end + + describe '.matches?' do + subject { described_class.matches?(build, user) } + + context 'when build is erased' do + let(:build) { create(:ci_build, :success, :erased) } + + it 'is a correct match' do + expect(subject).to be true + end + end + + context 'when build is not erased' do + let(:build) { create(:ci_build, :success, :trace_artifact) } + + it 'does not match' do + expect(subject).to be false + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb index bbfa60169a1..6d5b73bb01b 100644 --- a/spec/lib/gitlab/ci/status/build/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb @@ -13,7 +13,7 @@ describe Gitlab::Ci::Status::Build::Factory do end context 'when build is successful' do - let(:build) { create(:ci_build, :success) } + let(:build) { create(:ci_build, :success, :trace_artifact) } it 'matches correct core status' do expect(factory.core_status).to be_a Gitlab::Ci::Status::Success @@ -38,6 +38,33 @@ describe Gitlab::Ci::Status::Build::Factory do end end + context 'when build is erased' do + let(:build) { create(:ci_build, :success, :erased) } + + it 'matches correct core status' do + expect(factory.core_status).to be_a Gitlab::Ci::Status::Success + end + + it 'matches correct extended statuses' do + expect(factory.extended_statuses) + .to eq [Gitlab::Ci::Status::Build::Erased, + Gitlab::Ci::Status::Build::Retryable] + end + + it 'fabricates a retryable build status' do + expect(status).to be_a Gitlab::Ci::Status::Build::Retryable + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'passed' + expect(status.icon).to eq 'status_success' + expect(status.favicon).to eq 'favicon_status_success' + expect(status.label).to eq 'passed' + expect(status).to have_details + expect(status).to have_action + end + end + context 'when build is failed' do context 'when build is not allowed to fail' do let(:build) { create(:ci_build, :failed) } @@ -106,7 +133,7 @@ describe Gitlab::Ci::Status::Build::Factory do it 'matches correct extended statuses' do expect(factory.extended_statuses) - .to eq [Gitlab::Ci::Status::Build::Retryable] + .to eq [Gitlab::Ci::Status::Build::Canceled, Gitlab::Ci::Status::Build::Retryable] end it 'fabricates a retryable build status' do @@ -117,6 +144,7 @@ describe Gitlab::Ci::Status::Build::Factory do expect(status.text).to eq 'canceled' expect(status.icon).to eq 'status_canceled' expect(status.favicon).to eq 'favicon_status_canceled' + expect(status.illustration).to include(:image, :size, :title) expect(status.label).to eq 'canceled' expect(status).to have_details expect(status).to have_action @@ -158,7 +186,7 @@ describe Gitlab::Ci::Status::Build::Factory do it 'matches correct extended statuses' do expect(factory.extended_statuses) - .to eq [Gitlab::Ci::Status::Build::Cancelable] + .to eq [Gitlab::Ci::Status::Build::Pending, Gitlab::Ci::Status::Build::Cancelable] end it 'fabricates a cancelable build status' do @@ -169,6 +197,7 @@ describe Gitlab::Ci::Status::Build::Factory do expect(status.text).to eq 'pending' expect(status.icon).to eq 'status_pending' expect(status.favicon).to eq 'favicon_status_pending' + expect(status.illustration).to include(:image, :size, :title, :content) expect(status.label).to eq 'pending' expect(status).to have_details expect(status).to have_action @@ -182,18 +211,19 @@ describe Gitlab::Ci::Status::Build::Factory do expect(factory.core_status).to be_a Gitlab::Ci::Status::Skipped end - it 'does not match extended statuses' do - expect(factory.extended_statuses).to be_empty + it 'matches correct extended statuses' do + expect(factory.extended_statuses).to eq [Gitlab::Ci::Status::Build::Skipped] end - it 'fabricates a core skipped status' do - expect(status).to be_a Gitlab::Ci::Status::Skipped + it 'fabricates a skipped build status' do + expect(status).to be_a Gitlab::Ci::Status::Build::Skipped end it 'fabricates status with correct details' do expect(status.text).to eq 'skipped' expect(status.icon).to eq 'status_skipped' expect(status.favicon).to eq 'favicon_status_skipped' + expect(status.illustration).to include(:image, :size, :title) expect(status.label).to eq 'skipped' expect(status).to have_details expect(status).not_to have_action @@ -210,7 +240,8 @@ describe Gitlab::Ci::Status::Build::Factory do it 'matches correct extended statuses' do expect(factory.extended_statuses) - .to eq [Gitlab::Ci::Status::Build::Play, + .to eq [Gitlab::Ci::Status::Build::Manual, + Gitlab::Ci::Status::Build::Play, Gitlab::Ci::Status::Build::Action] end @@ -223,6 +254,7 @@ describe Gitlab::Ci::Status::Build::Factory do expect(status.group).to eq 'manual' expect(status.icon).to eq 'status_manual' expect(status.favicon).to eq 'favicon_status_manual' + expect(status.illustration).to include(:image, :size, :title, :content) expect(status.label).to include 'manual play action' expect(status).to have_details expect(status.action_path).to include 'play' @@ -257,7 +289,8 @@ describe Gitlab::Ci::Status::Build::Factory do it 'matches correct extended statuses' do expect(factory.extended_statuses) - .to eq [Gitlab::Ci::Status::Build::Stop, + .to eq [Gitlab::Ci::Status::Build::Manual, + Gitlab::Ci::Status::Build::Stop, Gitlab::Ci::Status::Build::Action] end diff --git a/spec/lib/gitlab/ci/status/build/manual_spec.rb b/spec/lib/gitlab/ci/status/build/manual_spec.rb new file mode 100644 index 00000000000..6386296f992 --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/manual_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::Manual do + let(:user) { create(:user) } + + subject do + build = create(:ci_build, :manual) + described_class.new(Gitlab::Ci::Status::Core.new(build, user)) + end + + describe '#illustration' do + it { expect(subject.illustration).to include(:image, :size, :title, :content) } + end + + describe '.matches?' do + subject {described_class.matches?(build, user) } + + context 'when build is manual' do + let(:build) { create(:ci_build, :manual) } + + it 'is a correct match' do + expect(subject).to be true + end + end + + context 'when build is not manual' do + let(:build) { create(:ci_build) } + + it 'does not match' do + expect(subject).to be false + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/build/pending_spec.rb b/spec/lib/gitlab/ci/status/build/pending_spec.rb new file mode 100644 index 00000000000..4cf70828e53 --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/pending_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::Pending do + let(:user) { create(:user) } + + subject do + described_class.new(double('subject')) + end + + describe '#illustration' do + it { expect(subject.illustration).to include(:image, :size, :title, :content) } + end + + describe '.matches?' do + subject {described_class.matches?(build, user) } + + context 'when build is pending' do + let(:build) { create(:ci_build, :pending) } + + it 'is a correct match' do + expect(subject).to be true + end + end + + context 'when build is not pending' do + let(:build) { create(:ci_build, :success) } + + it 'does not match' do + expect(subject).to be false + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb index 35e47cd2526..f128c1d4ca4 100644 --- a/spec/lib/gitlab/ci/status/build/play_spec.rb +++ b/spec/lib/gitlab/ci/status/build/play_spec.rb @@ -69,6 +69,10 @@ describe Gitlab::Ci::Status::Build::Play do it { expect(subject.action_title).to eq 'Play' } end + describe '#action_button_title' do + it { expect(subject.action_button_title).to eq 'Trigger this manual action' } + end + describe '.matches?' do subject { described_class.matches?(build, user) } diff --git a/spec/lib/gitlab/ci/status/build/retryable_spec.rb b/spec/lib/gitlab/ci/status/build/retryable_spec.rb index 0c5099b7da5..84d98588f2d 100644 --- a/spec/lib/gitlab/ci/status/build/retryable_spec.rb +++ b/spec/lib/gitlab/ci/status/build/retryable_spec.rb @@ -90,6 +90,10 @@ describe Gitlab::Ci::Status::Build::Retryable do describe '#action_title' do it { expect(subject.action_title).to eq 'Retry' } end + + describe '#action_button_title' do + it { expect(subject.action_button_title).to eq 'Retry this job' } + end end describe '.matches?' do diff --git a/spec/lib/gitlab/ci/status/build/skipped_spec.rb b/spec/lib/gitlab/ci/status/build/skipped_spec.rb new file mode 100644 index 00000000000..46f6933025a --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/skipped_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::Skipped do + let(:user) { create(:user) } + + subject do + described_class.new(double('subject')) + end + + describe '#illustration' do + it { expect(subject.illustration).to include(:image, :size, :title) } + end + + describe '.matches?' do + subject {described_class.matches?(build, user) } + + context 'when build is skipped' do + let(:build) { create(:ci_build, :skipped) } + + it 'is a correct match' do + expect(subject).to be true + end + end + + context 'when build is not skipped' do + let(:build) { create(:ci_build) } + + it 'does not match' do + expect(subject).to be false + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/build/stop_spec.rb b/spec/lib/gitlab/ci/status/build/stop_spec.rb index f16fc5c9205..5b7534c96c1 100644 --- a/spec/lib/gitlab/ci/status/build/stop_spec.rb +++ b/spec/lib/gitlab/ci/status/build/stop_spec.rb @@ -44,6 +44,10 @@ describe Gitlab::Ci::Status::Build::Stop do describe '#action_title' do it { expect(subject.action_title).to eq 'Stop' } end + + describe '#action_button_title' do + it { expect(subject.action_button_title).to eq 'Stop this environment' } + end end describe '.matches?' do diff --git a/spec/lib/gitlab/email/handler_spec.rb b/spec/lib/gitlab/email/handler_spec.rb index 650b01c4df4..386d73e6115 100644 --- a/spec/lib/gitlab/email/handler_spec.rb +++ b/spec/lib/gitlab/email/handler_spec.rb @@ -14,4 +14,28 @@ describe Gitlab::Email::Handler do expect(described_class.for('email', '')).to be_nil end end + + describe 'regexps are set properly' do + let(:addresses) do + %W(sent_notification_key#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX} sent_notification_key path/to/project+merge-request+user_email_token path/to/project+user_email_token) + end + + it 'picks each handler at least once' do + matched_handlers = addresses.map do |address| + described_class.for('email', address).class + end + + expect(matched_handlers.uniq).to match_array(Gitlab::Email::Handler::HANDLERS) + end + + it 'can pick exactly one handler for each address' do + addresses.each do |address| + matched_handlers = Gitlab::Email::Handler::HANDLERS.select do |handler| + handler.new('email', address).can_handle? + end + + expect(matched_handlers.count).to eq(1), "#{address} matches #{matched_handlers.count} handlers: #{matched_handlers}" + end + end + end end diff --git a/spec/lib/gitlab/git/checksum_spec.rb b/spec/lib/gitlab/git/checksum_spec.rb deleted file mode 100644 index 8ff310905bf..00000000000 --- a/spec/lib/gitlab/git/checksum_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Git::Checksum, seed_helper: true do - let(:storage) { 'default' } - - it 'raises Gitlab::Git::Repository::NoRepository when there is no repo' do - checksum = described_class.new(storage, 'nonexistent-repo') - - expect { checksum.calculate }.to raise_error Gitlab::Git::Repository::NoRepository - end - - it 'pretends that checksum is 000000... when the repo is empty' do - FileUtils.rm_rf(File.join(SEED_STORAGE_PATH, 'empty-repo.git')) - - system(git_env, *%W(#{Gitlab.config.git.bin_path} init --bare empty-repo.git), - chdir: SEED_STORAGE_PATH, - out: '/dev/null', - err: '/dev/null') - - checksum = described_class.new(storage, 'empty-repo') - - expect(checksum.calculate).to eq '0000000000000000000000000000000000000000' - end - - it 'raises Gitlab::Git::Repository::Failure when shelling out to git return non-zero status' do - checksum = described_class.new(storage, 'gitlab-git-test') - - allow(checksum).to receive(:popen).and_return(['output', nil]) - - expect { checksum.calculate }.to raise_error Gitlab::Git::Checksum::Failure - end - - it 'calculates the checksum when there is a repo' do - checksum = described_class.new(storage, 'gitlab-git-test') - - expect(checksum.calculate).to eq '54f21be4c32c02f6788d72207fa03ad3bce725e4' - end -end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 5cbe2808d0b..d3ab61746f4 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -247,38 +247,44 @@ describe Gitlab::Git::Repository, seed_helper: true do end it 'returns parameterised string for a ref containing slashes' do - prefix = repository.archive_prefix('test/branch', 'SHA') + prefix = repository.archive_prefix('test/branch', 'SHA', append_sha: nil) expect(prefix).to eq("#{project_name}-test-branch-SHA") end it 'returns correct string for a ref containing dots' do - prefix = repository.archive_prefix('test.branch', 'SHA') + prefix = repository.archive_prefix('test.branch', 'SHA', append_sha: nil) expect(prefix).to eq("#{project_name}-test.branch-SHA") end + + it 'returns string with sha when append_sha is false' do + prefix = repository.archive_prefix('test.branch', 'SHA', append_sha: false) + + expect(prefix).to eq("#{project_name}-test.branch") + end end describe '#archive' do - let(:metadata) { repository.archive_metadata('master', '/tmp') } + let(:metadata) { repository.archive_metadata('master', '/tmp', append_sha: true) } it_should_behave_like 'archive check', '.tar.gz' end describe '#archive_zip' do - let(:metadata) { repository.archive_metadata('master', '/tmp', 'zip') } + let(:metadata) { repository.archive_metadata('master', '/tmp', 'zip', append_sha: true) } it_should_behave_like 'archive check', '.zip' end describe '#archive_bz2' do - let(:metadata) { repository.archive_metadata('master', '/tmp', 'tbz2') } + let(:metadata) { repository.archive_metadata('master', '/tmp', 'tbz2', append_sha: true) } it_should_behave_like 'archive check', '.tar.bz2' end describe '#archive_fallback' do - let(:metadata) { repository.archive_metadata('master', '/tmp', 'madeup') } + let(:metadata) { repository.archive_metadata('master', '/tmp', 'madeup', append_sha: true) } it_should_behave_like 'archive check', '.tar.gz' end @@ -2178,6 +2184,55 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + describe '#checksum' do + shared_examples 'calculating checksum' do + it 'calculates the checksum for non-empty repo' do + expect(repository.checksum).to eq '54f21be4c32c02f6788d72207fa03ad3bce725e4' + end + + it 'returns 0000000000000000000000000000000000000000 for an empty repo' do + FileUtils.rm_rf(File.join(storage_path, 'empty-repo.git')) + + system(git_env, *%W(#{Gitlab.config.git.bin_path} init --bare empty-repo.git), + chdir: storage_path, + out: '/dev/null', + err: '/dev/null') + + empty_repo = described_class.new('default', 'empty-repo.git', '') + + expect(empty_repo.checksum).to eq '0000000000000000000000000000000000000000' + end + + it 'raises a no repository exception when there is no repo' do + broken_repo = described_class.new('default', 'a/path.git', '') + + expect { broken_repo.checksum }.to raise_error(Gitlab::Git::Repository::NoRepository) + end + end + + context 'when calculate_checksum Gitaly feature is enabled' do + it_behaves_like 'calculating checksum' + end + + context 'when calculate_checksum Gitaly feature is disabled', :disable_gitaly do + it_behaves_like 'calculating checksum' + + describe 'when storage is broken', :broken_storage do + it 'raises a storage exception when storage is not available' do + broken_repo = described_class.new('broken', 'a/path.git', '') + + expect { broken_repo.rugged }.to raise_error(Gitlab::Git::Storage::Inaccessible) + end + end + + it "raises a Gitlab::Git::Repository::Failure error if the `popen` call to git returns a non-zero exit code" do + allow(repository).to receive(:popen).and_return(['output', nil]) + + expect { repository.checksum }.to raise_error Gitlab::Git::Repository::ChecksumError + end + end + end + context 'gitlab_projects commands' do let(:gitlab_projects) { repository.gitlab_projects } let(:timeout) { Gitlab.config.gitlab_shell.git_timeout } @@ -2251,6 +2306,39 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + describe '#clean_stale_repository_files' do + let(:worktree_path) { File.join(repository.path, 'worktrees', 'delete-me') } + + it 'cleans up the files' do + repository.with_worktree(worktree_path, 'master', env: ENV) do + FileUtils.touch(worktree_path, mtime: Time.now - 8.hours) + # git rev-list --all will fail in git 2.16 if HEAD is pointing to a non-existent object, + # but the HEAD must be 40 characters long or git will ignore it. + File.write(File.join(worktree_path, 'HEAD'), Gitlab::Git::BLANK_SHA) + + # git 2.16 fails with "fatal: bad object HEAD" + expect { repository.rev_list(including: :all) }.to raise_error(Gitlab::Git::Repository::GitError) + + repository.clean_stale_repository_files + + expect { repository.rev_list(including: :all) }.not_to raise_error + expect(File.exist?(worktree_path)).to be_falsey + end + end + + it 'increments a counter upon an error' do + expect(repository.gitaly_repository_client).to receive(:cleanup).and_raise(Gitlab::Git::CommandError) + + counter = double(:counter) + + expect(counter).to receive(:increment) + expect(Gitlab::Metrics).to receive(:counter).with(:failed_repository_cleanup_total, + 'Number of failed repository cleanup events').and_return(counter) + + repository.clean_stale_repository_files + end + end + describe '#delete_remote_branches' do subject do repository.delete_remote_branches('downstream-remote', ['master']) diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index b845abab5ef..6c625596605 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -145,6 +145,33 @@ describe Gitlab::GitAccess do expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_upload]) end end + + context 'when actor is DeployToken' do + let(:actor) { create(:deploy_token, projects: [project]) } + + context 'when DeployToken is active and belongs to project' do + it 'allows pull access' do + expect { pull_access_check }.not_to raise_error + end + + it 'blocks the push' do + expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:upload]) + end + end + + context 'when DeployToken does not belong to project' do + let(:another_project) { create(:project) } + let(:actor) { create(:deploy_token, projects: [another_project]) } + + it 'blocks pull access' do + expect { pull_access_check }.to raise_not_found + end + + it 'blocks the push' do + expect { push_access_check }.to raise_not_found + end + end + end end context 'when actor is nil' do @@ -594,6 +621,41 @@ describe Gitlab::GitAccess do end end + describe 'deploy token permissions' do + let(:deploy_token) { create(:deploy_token) } + let(:actor) { deploy_token } + + context 'pull code' do + context 'when project is authorized' do + before do + deploy_token.projects << project + end + + it { expect { pull_access_check }.not_to raise_error } + end + + context 'when unauthorized' do + context 'from public project' do + let(:project) { create(:project, :public, :repository) } + + it { expect { pull_access_check }.not_to raise_error } + end + + context 'from internal project' do + let(:project) { create(:project, :internal, :repository) } + + it { expect { pull_access_check }.to raise_not_found } + end + + context 'from private project' do + let(:project) { create(:project, :private, :repository) } + + it { expect { pull_access_check }.to raise_not_found } + end + end + end + end + describe 'build authentication_abilities permissions' do let(:authentication_abilities) { build_authentication_abilities } @@ -855,6 +917,20 @@ describe Gitlab::GitAccess do admin: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false })) end end + + context 'when pushing to a project' do + let(:project) { create(:project, :public, :repository) } + let(:changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2ab refs/heads/wow" } + + before do + project.add_developer(user) + end + + it 'cleans up the files' do + expect(project.repository).to receive(:clean_stale_repository_files).and_call_original + expect { push_access_check }.not_to raise_error + end + end end describe 'build authentication abilities' do diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb index 1c41dbcb9ef..21592688bf0 100644 --- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb @@ -17,6 +17,16 @@ describe Gitlab::GitalyClient::RepositoryService do end end + describe '#cleanup' do + it 'sends a cleanup message' do + expect_any_instance_of(Gitaly::RepositoryService::Stub) + .to receive(:cleanup) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + + client.cleanup + end + end + describe '#garbage_collect' do it 'sends a garbage_collect message' do expect_any_instance_of(Gitaly::RepositoryService::Stub) @@ -124,4 +134,15 @@ describe Gitlab::GitalyClient::RepositoryService do client.squash_in_progress?(squash_id) end end + + describe '#calculate_checksum' do + it 'sends a calculate_checksum message' do + expect_any_instance_of(Gitaly::RepositoryService::Stub) + .to receive(:calculate_checksum) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + .and_return(double(checksum: 0)) + + client.calculate_checksum + end + end end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index b675d5dc031..897a5984782 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -145,6 +145,9 @@ pipeline_schedule: - pipelines pipeline_schedule_variables: - pipeline_schedule +deploy_tokens: +- project_deploy_tokens +- projects deploy_keys: - user - deploy_keys_projects @@ -281,6 +284,8 @@ project: - project_badges - source_of_merge_requests - internal_ids +- project_deploy_tokens +- deploy_tokens award_emoji: - awardable - user diff --git a/spec/lib/gitlab/import_export/importer_spec.rb b/spec/lib/gitlab/import_export/importer_spec.rb index d75416f2a62..991e354f499 100644 --- a/spec/lib/gitlab/import_export/importer_spec.rb +++ b/spec/lib/gitlab/import_export/importer_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe Gitlab::ImportExport::Importer do + let(:user) { create(:user) } let(:test_path) { "#{Dir.tmpdir}/importer_spec" } let(:shared) { project.import_export_shared } let(:project) { create(:project, import_source: File.join(test_path, 'exported-project.gz')) } @@ -11,6 +12,7 @@ describe Gitlab::ImportExport::Importer do allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(test_path) FileUtils.mkdir_p(shared.export_path) FileUtils.cp(Rails.root.join('spec', 'fixtures', 'exported-project.gz'), test_path) + allow(subject).to receive(:remove_import_file) end after do @@ -42,7 +44,8 @@ describe Gitlab::ImportExport::Importer do Gitlab::ImportExport::RepoRestorer, Gitlab::ImportExport::WikiRestorer, Gitlab::ImportExport::UploadsRestorer, - Gitlab::ImportExport::LfsRestorer + Gitlab::ImportExport::LfsRestorer, + Gitlab::ImportExport::StatisticsRestorer ].each do |restorer| it "calls the #{restorer}" do fake_restorer = double(restorer.to_s) @@ -60,5 +63,42 @@ describe Gitlab::ImportExport::Importer do importer.execute end end + + context 'when project successfully restored' do + let!(:existing_project) { create(:project, namespace: user.namespace) } + let(:project) { create(:project, namespace: user.namespace, name: 'whatever', path: 'whatever') } + + before do + restorers = double + + allow(subject).to receive(:import_file).and_return(true) + allow(subject).to receive(:check_version!).and_return(true) + allow(subject).to receive(:restorers).and_return(restorers) + allow(restorers).to receive(:all?).and_return(true) + allow(project).to receive(:import_data).and_return(double(data: { 'original_path' => existing_project.path })) + end + + context 'when import_data' do + context 'has original_path' do + it 'overwrites existing project' do + expect_any_instance_of(::Projects::OverwriteProjectService).to receive(:execute).with(existing_project) + + subject.execute + end + end + + context 'has not original_path' do + before do + allow(project).to receive(:import_data).and_return(double(data: {})) + end + + it 'does not call the overwrite service' do + expect_any_instance_of(::Projects::OverwriteProjectService).not_to receive(:execute).with(existing_project) + + subject.execute + end + end + end + end end end diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index 2b3ffb2d7c0..d64ea72e346 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -16,7 +16,7 @@ describe Gitlab::Workhorse do let(:ref) { 'master' } let(:format) { 'zip' } let(:storage_path) { Gitlab.config.gitlab.repository_downloads_path } - let(:base_params) { repository.archive_metadata(ref, storage_path, format) } + let(:base_params) { repository.archive_metadata(ref, storage_path, format, append_sha: nil) } let(:gitaly_params) do base_params.merge( 'GitalyServer' => { @@ -29,7 +29,7 @@ describe Gitlab::Workhorse do let(:cache_disabled) { false } subject do - described_class.send_git_archive(repository, ref: ref, format: format) + described_class.send_git_archive(repository, ref: ref, format: format, append_sha: nil) end before do diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 971a88e9ee9..43e419cd7de 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -390,11 +390,11 @@ describe Notify do end end - describe 'that have new commits' do + shared_examples 'a push to an existing merge request' do let(:push_user) { create(:user) } subject do - described_class.push_to_merge_request_email(recipient.id, merge_request.id, push_user.id, new_commits: merge_request.commits) + described_class.push_to_merge_request_email(recipient.id, merge_request.id, push_user.id, new_commits: merge_request.commits, existing_commits: existing_commits) end it_behaves_like 'a multiple recipients email' @@ -419,6 +419,18 @@ describe Notify do end end end + + describe 'that have new commits' do + let(:existing_commits) { [] } + + it_behaves_like 'a push to an existing merge request' + end + + describe 'that have new commits on top of an existing one' do + let(:existing_commits) { [merge_request.commits.first] } + + it_behaves_like 'a push to an existing merge request' + end end context 'for issue notes' do diff --git a/spec/migrations/add_foreign_keys_to_todos_spec.rb b/spec/migrations/add_foreign_keys_to_todos_spec.rb index 4a22bd6f342..bf2fa5c0f56 100644 --- a/spec/migrations/add_foreign_keys_to_todos_spec.rb +++ b/spec/migrations/add_foreign_keys_to_todos_spec.rb @@ -4,8 +4,8 @@ require Rails.root.join('db', 'migrate', '20180201110056_add_foreign_keys_to_tod describe AddForeignKeysToTodos, :migration do let(:todos) { table(:todos) } - let(:project) { create(:project) } - let(:user) { create(:user) } + let(:project) { create(:project) } # rubocop:disable RSpec/FactoriesInMigrationSpecs + let(:user) { create(:user) } # rubocop:disable RSpec/FactoriesInMigrationSpecs context 'add foreign key on user_id' do let!(:todo_with_user) { create_todo(user_id: user.id) } @@ -34,7 +34,7 @@ describe AddForeignKeysToTodos, :migration do end context 'add foreign key on note_id' do - let(:note) { create(:note) } + let(:note) { create(:note) } # rubocop:disable RSpec/FactoriesInMigrationSpecs let!(:todo_with_note) { create_todo(note_id: note.id) } let!(:todo_with_invalid_note) { create_todo(note_id: 4711) } let!(:todo_without_note) { create_todo(note_id: nil) } diff --git a/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb b/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb index 63defcb39bf..d8dd7a2fb83 100644 --- a/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb +++ b/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb @@ -6,18 +6,18 @@ describe AddHeadPipelineForEachMergeRequest, :delete do let(:migration) { described_class.new } - let!(:project) { create(:project) } + let!(:project) { create(:project) } # rubocop:disable RSpec/FactoriesInMigrationSpecs let!(:other_project) { fork_project(project) } - let!(:pipeline_1) { create(:ci_pipeline, project: project, ref: "branch_1") } - let!(:pipeline_2) { create(:ci_pipeline, project: other_project, ref: "branch_1") } - let!(:pipeline_3) { create(:ci_pipeline, project: other_project, ref: "branch_1") } - let!(:pipeline_4) { create(:ci_pipeline, project: project, ref: "branch_2") } + let!(:pipeline_1) { create(:ci_pipeline, project: project, ref: "branch_1") } # rubocop:disable RSpec/FactoriesInMigrationSpecs + let!(:pipeline_2) { create(:ci_pipeline, project: other_project, ref: "branch_1") } # rubocop:disable RSpec/FactoriesInMigrationSpecs + let!(:pipeline_3) { create(:ci_pipeline, project: other_project, ref: "branch_1") } # rubocop:disable RSpec/FactoriesInMigrationSpecs + let!(:pipeline_4) { create(:ci_pipeline, project: project, ref: "branch_2") } # rubocop:disable RSpec/FactoriesInMigrationSpecs - let!(:mr_1) { create(:merge_request, source_project: project, target_project: project, source_branch: "branch_1", target_branch: "target_1") } - let!(:mr_2) { create(:merge_request, source_project: other_project, target_project: project, source_branch: "branch_1", target_branch: "target_2") } - let!(:mr_3) { create(:merge_request, source_project: project, target_project: project, source_branch: "branch_2", target_branch: "master") } - let!(:mr_4) { create(:merge_request, source_project: project, target_project: project, source_branch: "branch_3", target_branch: "master") } + let!(:mr_1) { create(:merge_request, source_project: project, target_project: project, source_branch: "branch_1", target_branch: "target_1") } # rubocop:disable RSpec/FactoriesInMigrationSpecs + let!(:mr_2) { create(:merge_request, source_project: other_project, target_project: project, source_branch: "branch_1", target_branch: "target_2") } # rubocop:disable RSpec/FactoriesInMigrationSpecs + let!(:mr_3) { create(:merge_request, source_project: project, target_project: project, source_branch: "branch_2", target_branch: "master") } # rubocop:disable RSpec/FactoriesInMigrationSpecs + let!(:mr_4) { create(:merge_request, source_project: project, target_project: project, source_branch: "branch_3", target_branch: "master") } # rubocop:disable RSpec/FactoriesInMigrationSpecs context "#up" do context "when source_project and source_branch of pipeline are the same of merge request" do diff --git a/spec/migrations/calculate_conv_dev_index_percentages_spec.rb b/spec/migrations/calculate_conv_dev_index_percentages_spec.rb index f3a46025376..19f06810e54 100644 --- a/spec/migrations/calculate_conv_dev_index_percentages_spec.rb +++ b/spec/migrations/calculate_conv_dev_index_percentages_spec.rb @@ -6,7 +6,7 @@ require Rails.root.join('db', 'post_migrate', '20170803090603_calculate_conv_dev describe CalculateConvDevIndexPercentages, :delete do let(:migration) { described_class.new } let!(:conv_dev_index) do - create(:conversational_development_index_metric, + create(:conversational_development_index_metric, # rubocop:disable RSpec/FactoriesInMigrationSpecs leader_notes: 0, instance_milestones: 0, percentage_issues: 0, diff --git a/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb b/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb index 033d0e7584d..b5980cb9ddb 100644 --- a/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb +++ b/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb @@ -10,9 +10,9 @@ describe CleanupNamespacelessPendingDeleteProjects, :migration, schema: 20180222 describe '#up' do it 'only cleans up pending delete projects' do - create(:project) - create(:project, pending_delete: true) - project = build(:project, pending_delete: true, namespace_id: nil) + create(:project) # rubocop:disable RSpec/FactoriesInMigrationSpecs + create(:project, pending_delete: true) # rubocop:disable RSpec/FactoriesInMigrationSpecs + project = build(:project, pending_delete: true, namespace_id: nil) # rubocop:disable RSpec/FactoriesInMigrationSpecs project.save(validate: false) expect(NamespacelessProjectDestroyWorker).to receive(:bulk_perform_async).with([[project.id]]) @@ -21,8 +21,8 @@ describe CleanupNamespacelessPendingDeleteProjects, :migration, schema: 20180222 end it 'does nothing when no pending delete projects without namespace found' do - create(:project) - create(:project, pending_delete: true) + create(:project) # rubocop:disable RSpec/FactoriesInMigrationSpecs + create(:project, pending_delete: true) # rubocop:disable RSpec/FactoriesInMigrationSpecs expect(NamespacelessProjectDestroyWorker).not_to receive(:bulk_perform_async) diff --git a/spec/migrations/cleanup_nonexisting_namespace_pending_delete_projects_spec.rb b/spec/migrations/cleanup_nonexisting_namespace_pending_delete_projects_spec.rb index 7879105a334..8f40ac3e38b 100644 --- a/spec/migrations/cleanup_nonexisting_namespace_pending_delete_projects_spec.rb +++ b/spec/migrations/cleanup_nonexisting_namespace_pending_delete_projects_spec.rb @@ -9,11 +9,11 @@ describe CleanupNonexistingNamespacePendingDeleteProjects do end describe '#up' do - set(:some_project) { create(:project) } + set(:some_project) { create(:project) } # rubocop:disable RSpec/FactoriesInMigrationSpecs it 'only cleans up when namespace does not exist' do - create(:project, pending_delete: true) - project = build(:project, pending_delete: true, namespace: nil, namespace_id: Namespace.maximum(:id).to_i.succ) + create(:project, pending_delete: true) # rubocop:disable RSpec/FactoriesInMigrationSpecs + project = build(:project, pending_delete: true, namespace: nil, namespace_id: Namespace.maximum(:id).to_i.succ) # rubocop:disable RSpec/FactoriesInMigrationSpecs project.save(validate: false) expect(NamespacelessProjectDestroyWorker).to receive(:bulk_perform_async).with([[project.id]]) @@ -22,7 +22,7 @@ describe CleanupNonexistingNamespacePendingDeleteProjects do end it 'does nothing when no pending delete projects without namespace found' do - create(:project, pending_delete: true, namespace: create(:namespace)) + create(:project, pending_delete: true, namespace: create(:namespace)) # rubocop:disable RSpec/FactoriesInMigrationSpecs expect(NamespacelessProjectDestroyWorker).not_to receive(:bulk_perform_async) diff --git a/spec/migrations/issues_moved_to_id_foreign_key_spec.rb b/spec/migrations/issues_moved_to_id_foreign_key_spec.rb index d2eef81f396..dd2b08099f2 100644 --- a/spec/migrations/issues_moved_to_id_foreign_key_spec.rb +++ b/spec/migrations/issues_moved_to_id_foreign_key_spec.rb @@ -5,9 +5,9 @@ require Rails.root.join('db', 'migrate', '20171106151218_issues_moved_to_id_fore # only_mirror_protected_branches column in the projects table to create a # project via FactoryBot. describe IssuesMovedToIdForeignKey, :migration, schema: 20171114150259 do - let!(:issue_first) { create(:issue, moved_to_id: issue_second.id) } - let!(:issue_second) { create(:issue, moved_to_id: issue_third.id) } - let!(:issue_third) { create(:issue) } + let!(:issue_first) { create(:issue, moved_to_id: issue_second.id) } # rubocop:disable RSpec/FactoriesInMigrationSpecs + let!(:issue_second) { create(:issue, moved_to_id: issue_third.id) } # rubocop:disable RSpec/FactoriesInMigrationSpecs + let!(:issue_third) { create(:issue) } # rubocop:disable RSpec/FactoriesInMigrationSpecs subject { described_class.new } diff --git a/spec/migrations/migrate_old_artifacts_spec.rb b/spec/migrations/migrate_old_artifacts_spec.rb index 638b2853374..4187ab149a5 100644 --- a/spec/migrations/migrate_old_artifacts_spec.rb +++ b/spec/migrations/migrate_old_artifacts_spec.rb @@ -16,18 +16,18 @@ describe MigrateOldArtifacts do end context 'with migratable data' do - set(:project1) { create(:project, ci_id: 2) } - set(:project2) { create(:project, ci_id: 3) } - set(:project3) { create(:project) } - - set(:pipeline1) { create(:ci_empty_pipeline, project: project1) } - set(:pipeline2) { create(:ci_empty_pipeline, project: project2) } - set(:pipeline3) { create(:ci_empty_pipeline, project: project3) } - - let!(:build_with_legacy_artifacts) { create(:ci_build, pipeline: pipeline1) } - let!(:build_without_artifacts) { create(:ci_build, pipeline: pipeline1) } - let!(:build2) { create(:ci_build, pipeline: pipeline2) } - let!(:build3) { create(:ci_build, pipeline: pipeline3) } + set(:project1) { create(:project, ci_id: 2) } # rubocop:disable RSpec/FactoriesInMigrationSpecs + set(:project2) { create(:project, ci_id: 3) } # rubocop:disable RSpec/FactoriesInMigrationSpecs + set(:project3) { create(:project) } # rubocop:disable RSpec/FactoriesInMigrationSpecs + + set(:pipeline1) { create(:ci_empty_pipeline, project: project1) } # rubocop:disable RSpec/FactoriesInMigrationSpecs + set(:pipeline2) { create(:ci_empty_pipeline, project: project2) } # rubocop:disable RSpec/FactoriesInMigrationSpecs + set(:pipeline3) { create(:ci_empty_pipeline, project: project3) } # rubocop:disable RSpec/FactoriesInMigrationSpecs + + let!(:build_with_legacy_artifacts) { create(:ci_build, pipeline: pipeline1) } # rubocop:disable RSpec/FactoriesInMigrationSpecs + let!(:build_without_artifacts) { create(:ci_build, pipeline: pipeline1) } # rubocop:disable RSpec/FactoriesInMigrationSpecs + let!(:build2) { create(:ci_build, pipeline: pipeline2) } # rubocop:disable RSpec/FactoriesInMigrationSpecs + let!(:build3) { create(:ci_build, pipeline: pipeline3) } # rubocop:disable RSpec/FactoriesInMigrationSpecs before do setup_builds(build2, build3) diff --git a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb index 657113812bd..4ee1d255fbd 100644 --- a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb +++ b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb @@ -4,8 +4,8 @@ require 'spec_helper' require Rails.root.join('db', 'migrate', '20161124141322_migrate_process_commit_worker_jobs.rb') describe MigrateProcessCommitWorkerJobs do - let(:project) { create(:project, :legacy_storage, :repository) } - let(:user) { create(:user) } + let(:project) { create(:project, :legacy_storage, :repository) } # rubocop:disable RSpec/FactoriesInMigrationSpecs + let(:user) { create(:user) } # rubocop:disable RSpec/FactoriesInMigrationSpecs let(:commit) { project.commit.raw.rugged_commit } describe 'Project' do diff --git a/spec/migrations/migrate_user_activities_to_users_last_activity_on_spec.rb b/spec/migrations/migrate_user_activities_to_users_last_activity_on_spec.rb index a17c9c72bde..99173708190 100644 --- a/spec/migrations/migrate_user_activities_to_users_last_activity_on_spec.rb +++ b/spec/migrations/migrate_user_activities_to_users_last_activity_on_spec.rb @@ -5,8 +5,8 @@ require Rails.root.join('db', 'post_migrate', '20170324160416_migrate_user_activ describe MigrateUserActivitiesToUsersLastActivityOn, :clean_gitlab_redis_shared_state, :delete do let(:migration) { described_class.new } - let!(:user_active_1) { create(:user) } - let!(:user_active_2) { create(:user) } + let!(:user_active_1) { create(:user) } # rubocop:disable RSpec/FactoriesInMigrationSpecs + let!(:user_active_2) { create(:user) } # rubocop:disable RSpec/FactoriesInMigrationSpecs def record_activity(user, time) Gitlab::Redis::SharedState.with do |redis| diff --git a/spec/migrations/migrate_user_project_view_spec.rb b/spec/migrations/migrate_user_project_view_spec.rb index 31d16e17d7b..80468b9d01e 100644 --- a/spec/migrations/migrate_user_project_view_spec.rb +++ b/spec/migrations/migrate_user_project_view_spec.rb @@ -5,7 +5,7 @@ require Rails.root.join('db', 'post_migrate', '20170406142253_migrate_user_proje describe MigrateUserProjectView, :delete do let(:migration) { described_class.new } - let!(:user) { create(:user, project_view: 'readme') } + let!(:user) { create(:user, project_view: 'readme') } # rubocop:disable RSpec/FactoriesInMigrationSpecs describe '#up' do it 'updates project view setting with new value' do diff --git a/spec/migrations/move_personal_snippets_files_spec.rb b/spec/migrations/move_personal_snippets_files_spec.rb index 1a319eccc0d..1f39ad98fb8 100644 --- a/spec/migrations/move_personal_snippets_files_spec.rb +++ b/spec/migrations/move_personal_snippets_files_spec.rb @@ -16,14 +16,14 @@ describe MovePersonalSnippetsFiles do describe "#up" do let(:snippet) do - snippet = create(:personal_snippet) + snippet = create(:personal_snippet) # rubocop:disable RSpec/FactoriesInMigrationSpecs create_upload('picture.jpg', snippet) snippet.update(description: markdown_linking_file('picture.jpg', snippet)) snippet end let(:snippet_with_missing_file) do - snippet = create(:snippet) + snippet = create(:snippet) # rubocop:disable RSpec/FactoriesInMigrationSpecs create_upload('picture.jpg', snippet, create_file: false) snippet.update(description: markdown_linking_file('picture.jpg', snippet)) snippet @@ -62,7 +62,7 @@ describe MovePersonalSnippetsFiles do secret = "secret#{snippet.id}" file_location = "/uploads/-/system/personal_snippet/#{snippet.id}/#{secret}/picture.jpg" markdown = markdown_linking_file('picture.jpg', snippet) - note = create(:note_on_personal_snippet, noteable: snippet, note: "with #{markdown}") + note = create(:note_on_personal_snippet, noteable: snippet, note: "with #{markdown}") # rubocop:disable RSpec/FactoriesInMigrationSpecs migration.up @@ -73,14 +73,14 @@ describe MovePersonalSnippetsFiles do describe "#down" do let(:snippet) do - snippet = create(:personal_snippet) + snippet = create(:personal_snippet) # rubocop:disable RSpec/FactoriesInMigrationSpecs create_upload('picture.jpg', snippet, in_new_path: true) snippet.update(description: markdown_linking_file('picture.jpg', snippet, in_new_path: true)) snippet end let(:snippet_with_missing_file) do - snippet = create(:personal_snippet) + snippet = create(:personal_snippet) # rubocop:disable RSpec/FactoriesInMigrationSpecs create_upload('picture.jpg', snippet, create_file: false, in_new_path: true) snippet.update(description: markdown_linking_file('picture.jpg', snippet, in_new_path: true)) snippet @@ -119,7 +119,7 @@ describe MovePersonalSnippetsFiles do markdown = markdown_linking_file('picture.jpg', snippet, in_new_path: true) secret = "secret#{snippet.id}" file_location = "/uploads/personal_snippet/#{snippet.id}/#{secret}/picture.jpg" - note = create(:note_on_personal_snippet, noteable: snippet, note: "with #{markdown}") + note = create(:note_on_personal_snippet, noteable: snippet, note: "with #{markdown}") # rubocop:disable RSpec/FactoriesInMigrationSpecs migration.down @@ -135,7 +135,7 @@ describe MovePersonalSnippetsFiles do secret = '123456789' filename = 'hello.jpg' - snippet = create(:personal_snippet) + snippet = create(:personal_snippet) # rubocop:disable RSpec/FactoriesInMigrationSpecs path_before = "/uploads/personal_snippet/#{snippet.id}/#{secret}/#{filename}" path_after = "/uploads/system/personal_snippet/#{snippet.id}/#{secret}/#{filename}" @@ -161,7 +161,7 @@ describe MovePersonalSnippetsFiles do FileUtils.touch(absolute_path) end - create(:upload, model: snippet, path: "#{secret}/#{filename}", uploader: PersonalFileUploader) + create(:upload, model: snippet, path: "#{secret}/#{filename}", uploader: PersonalFileUploader) # rubocop:disable RSpec/FactoriesInMigrationSpecs end def markdown_linking_file(filename, snippet, in_new_path: false) diff --git a/spec/migrations/remove_dot_git_from_usernames_spec.rb b/spec/migrations/remove_dot_git_from_usernames_spec.rb index 3a88a66a476..f11880a83e9 100644 --- a/spec/migrations/remove_dot_git_from_usernames_spec.rb +++ b/spec/migrations/remove_dot_git_from_usernames_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' require Rails.root.join('db', 'migrate', '20161226122833_remove_dot_git_from_usernames.rb') describe RemoveDotGitFromUsernames do - let(:user) { create(:user) } + let(:user) { create(:user) } # rubocop:disable RSpec/FactoriesInMigrationSpecs let(:migration) { described_class.new } describe '#up' do @@ -23,7 +23,7 @@ describe RemoveDotGitFromUsernames do context 'when new path exists already' do describe '#up' do - let(:user2) { create(:user) } + let(:user2) { create(:user) } # rubocop:disable RSpec/FactoriesInMigrationSpecs before do update_namespace(user, 'test.git') diff --git a/spec/migrations/remove_duplicate_mr_events_spec.rb b/spec/migrations/remove_duplicate_mr_events_spec.rb index e51872239ad..2509ac6afd6 100644 --- a/spec/migrations/remove_duplicate_mr_events_spec.rb +++ b/spec/migrations/remove_duplicate_mr_events_spec.rb @@ -5,17 +5,17 @@ describe RemoveDuplicateMrEvents, :delete do let(:migration) { described_class.new } describe '#up' do - let(:user) { create(:user) } - let(:merge_requests) { create_list(:merge_request, 2) } - let(:issue) { create(:issue) } + let(:user) { create(:user) } # rubocop:disable RSpec/FactoriesInMigrationSpecs + let(:merge_requests) { create_list(:merge_request, 2) } # rubocop:disable RSpec/FactoriesInMigrationSpecs + let(:issue) { create(:issue) } # rubocop:disable RSpec/FactoriesInMigrationSpecs let!(:events) do [ - create(:event, :created, author: user, target: merge_requests.first), - create(:event, :created, author: user, target: merge_requests.first), - create(:event, :updated, author: user, target: merge_requests.first), - create(:event, :created, author: user, target: merge_requests.second), - create(:event, :created, author: user, target: issue), - create(:event, :created, author: user, target: issue) + create(:event, :created, author: user, target: merge_requests.first), # rubocop:disable RSpec/FactoriesInMigrationSpecs + create(:event, :created, author: user, target: merge_requests.first), # rubocop:disable RSpec/FactoriesInMigrationSpecs + create(:event, :updated, author: user, target: merge_requests.first), # rubocop:disable RSpec/FactoriesInMigrationSpecs + create(:event, :created, author: user, target: merge_requests.second), # rubocop:disable RSpec/FactoriesInMigrationSpecs + create(:event, :created, author: user, target: issue), # rubocop:disable RSpec/FactoriesInMigrationSpecs + create(:event, :created, author: user, target: issue) # rubocop:disable RSpec/FactoriesInMigrationSpecs ] end diff --git a/spec/migrations/remove_project_labels_group_id_spec.rb b/spec/migrations/remove_project_labels_group_id_spec.rb index d80d61af20b..01b09e71d83 100644 --- a/spec/migrations/remove_project_labels_group_id_spec.rb +++ b/spec/migrations/remove_project_labels_group_id_spec.rb @@ -5,9 +5,9 @@ require Rails.root.join('db', 'post_migrate', '20180202111106_remove_project_lab describe RemoveProjectLabelsGroupId, :delete do let(:migration) { described_class.new } - let(:group) { create(:group) } - let!(:project_label) { create(:label, group_id: group.id) } - let!(:group_label) { create(:group_label) } + let(:group) { create(:group) } # rubocop:disable RSpec/FactoriesInMigrationSpecs + let!(:project_label) { create(:label, group_id: group.id) } # rubocop:disable RSpec/FactoriesInMigrationSpecs + let!(:group_label) { create(:group_label) } # rubocop:disable RSpec/FactoriesInMigrationSpecs describe '#up' do it 'updates the project labels group ID' do diff --git a/spec/migrations/remove_soft_removed_objects_spec.rb b/spec/migrations/remove_soft_removed_objects_spec.rb index ec089f9106d..fb70c284f5e 100644 --- a/spec/migrations/remove_soft_removed_objects_spec.rb +++ b/spec/migrations/remove_soft_removed_objects_spec.rb @@ -8,7 +8,7 @@ describe RemoveSoftRemovedObjects, :migration do create_with_deleted_at(:issue) end - regular_issue = create(:issue) + regular_issue = create(:issue) # rubocop:disable RSpec/FactoriesInMigrationSpecs run_migration @@ -28,7 +28,7 @@ describe RemoveSoftRemovedObjects, :migration do it 'removes routes of soft removed personal namespaces' do namespace = create_with_deleted_at(:namespace) - group = create(:group) + group = create(:group) # rubocop:disable RSpec/FactoriesInMigrationSpecs expect(Route.where(source: namespace).exists?).to eq(true) expect(Route.where(source: group).exists?).to eq(true) @@ -41,7 +41,7 @@ describe RemoveSoftRemovedObjects, :migration do it 'schedules the removal of soft removed groups' do group = create_with_deleted_at(:group) - admin = create(:user, admin: true) + admin = create(:user, admin: true) # rubocop:disable RSpec/FactoriesInMigrationSpecs expect_any_instance_of(GroupDestroyWorker) .to receive(:perform) @@ -67,7 +67,7 @@ describe RemoveSoftRemovedObjects, :migration do end def create_with_deleted_at(*args) - row = create(*args) + row = create(*args) # rubocop:disable RSpec/FactoriesInMigrationSpecs # We set "deleted_at" this way so we don't run into any column cache issues. row.class.where(id: row.id).update_all(deleted_at: 1.year.ago) diff --git a/spec/migrations/rename_more_reserved_project_names_spec.rb b/spec/migrations/rename_more_reserved_project_names_spec.rb index 75310075cc5..034e8a6a4e5 100644 --- a/spec/migrations/rename_more_reserved_project_names_spec.rb +++ b/spec/migrations/rename_more_reserved_project_names_spec.rb @@ -8,7 +8,7 @@ require Rails.root.join('db', 'post_migrate', '20170313133418_rename_more_reserv # around this we use the DELETE cleaning strategy. describe RenameMoreReservedProjectNames, :delete do let(:migration) { described_class.new } - let!(:project) { create(:project) } + let!(:project) { create(:project) } # rubocop:disable RSpec/FactoriesInMigrationSpecs before do project.path = 'artifacts' diff --git a/spec/migrations/rename_reserved_project_names_spec.rb b/spec/migrations/rename_reserved_project_names_spec.rb index 34336d705b1..592ac2b5fb9 100644 --- a/spec/migrations/rename_reserved_project_names_spec.rb +++ b/spec/migrations/rename_reserved_project_names_spec.rb @@ -12,7 +12,7 @@ require Rails.root.join('db', 'post_migrate', '20161221153951_rename_reserved_pr # Ideally, the test should not use factories and rely on the `table` helper instead. describe RenameReservedProjectNames, :migration, schema: :latest do let(:migration) { described_class.new } - let!(:project) { create(:project) } + let!(:project) { create(:project) } # rubocop:disable RSpec/FactoriesInMigrationSpecs before do project.path = 'projects' diff --git a/spec/migrations/rename_users_with_renamed_namespace_spec.rb b/spec/migrations/rename_users_with_renamed_namespace_spec.rb index e2994103ed7..b8a4dc2b2c0 100644 --- a/spec/migrations/rename_users_with_renamed_namespace_spec.rb +++ b/spec/migrations/rename_users_with_renamed_namespace_spec.rb @@ -3,12 +3,12 @@ require Rails.root.join('db', 'post_migrate', '20170518200835_rename_users_with_ describe RenameUsersWithRenamedNamespace, :delete do it 'renames a user that had their namespace renamed to the namespace path' do - other_user = create(:user, username: 'kodingu') - other_user1 = create(:user, username: 'api0') + other_user = create(:user, username: 'kodingu') # rubocop:disable RSpec/FactoriesInMigrationSpecs + other_user1 = create(:user, username: 'api0') # rubocop:disable RSpec/FactoriesInMigrationSpecs - user = create(:user, username: "Users0") + user = create(:user, username: "Users0") # rubocop:disable RSpec/FactoriesInMigrationSpecs user.update_column(:username, 'Users') - user1 = create(:user, username: "import0") + user1 = create(:user, username: "import0") # rubocop:disable RSpec/FactoriesInMigrationSpecs user1.update_column(:username, 'import') described_class.new.up diff --git a/spec/migrations/schedule_create_gpg_key_subkeys_from_gpg_keys_spec.rb b/spec/migrations/schedule_create_gpg_key_subkeys_from_gpg_keys_spec.rb index 65ec07da31c..ed306fb3d62 100644 --- a/spec/migrations/schedule_create_gpg_key_subkeys_from_gpg_keys_spec.rb +++ b/spec/migrations/schedule_create_gpg_key_subkeys_from_gpg_keys_spec.rb @@ -3,8 +3,8 @@ require Rails.root.join('db', 'post_migrate', '20171005130944_schedule_create_gp describe ScheduleCreateGpgKeySubkeysFromGpgKeys, :migration, :sidekiq do before do - create(:gpg_key, id: 1, key: GpgHelpers::User1.public_key) - create(:gpg_key, id: 2, key: GpgHelpers::User3.public_key) + create(:gpg_key, id: 1, key: GpgHelpers::User1.public_key) # rubocop:disable RSpec/FactoriesInMigrationSpecs + create(:gpg_key, id: 2, key: GpgHelpers::User3.public_key) # rubocop:disable RSpec/FactoriesInMigrationSpecs # Delete all subkeys so they can be recreated GpgKeySubkey.destroy_all end diff --git a/spec/migrations/schedule_populate_merge_request_metrics_with_events_data_spec.rb b/spec/migrations/schedule_populate_merge_request_metrics_with_events_data_spec.rb index 7494624066a..578440cba20 100644 --- a/spec/migrations/schedule_populate_merge_request_metrics_with_events_data_spec.rb +++ b/spec/migrations/schedule_populate_merge_request_metrics_with_events_data_spec.rb @@ -8,7 +8,7 @@ describe SchedulePopulateMergeRequestMetricsWithEventsData, :migration, :sidekiq .to receive(:commits_count=).and_return(nil) end - let!(:mrs) { create_list(:merge_request, 3) } + let!(:mrs) { create_list(:merge_request, 3) } # rubocop:disable RSpec/FactoriesInMigrationSpecs it 'correctly schedules background migrations' do stub_const("#{described_class.name}::BATCH_SIZE", 2) diff --git a/spec/migrations/turn_nested_groups_into_regular_groups_for_mysql_spec.rb b/spec/migrations/turn_nested_groups_into_regular_groups_for_mysql_spec.rb index 528dc54781d..560409f08de 100644 --- a/spec/migrations/turn_nested_groups_into_regular_groups_for_mysql_spec.rb +++ b/spec/migrations/turn_nested_groups_into_regular_groups_for_mysql_spec.rb @@ -2,10 +2,10 @@ require 'spec_helper' require Rails.root.join('db', 'migrate', '20170503140202_turn_nested_groups_into_regular_groups_for_mysql.rb') describe TurnNestedGroupsIntoRegularGroupsForMysql do - let!(:parent_group) { create(:group) } - let!(:child_group) { create(:group, parent: parent_group) } - let!(:project) { create(:project, :legacy_storage, :empty_repo, namespace: child_group) } - let!(:member) { create(:user) } + let!(:parent_group) { create(:group) } # rubocop:disable RSpec/FactoriesInMigrationSpecs + let!(:child_group) { create(:group, parent: parent_group) } # rubocop:disable RSpec/FactoriesInMigrationSpecs + let!(:project) { create(:project, :legacy_storage, :empty_repo, namespace: child_group) } # rubocop:disable RSpec/FactoriesInMigrationSpecs + let!(:member) { create(:user) } # rubocop:disable RSpec/FactoriesInMigrationSpecs let(:migration) { described_class.new } before do diff --git a/spec/migrations/update_retried_for_ci_build_spec.rb b/spec/migrations/update_retried_for_ci_build_spec.rb index ccb77766b84..637dcbb8e01 100644 --- a/spec/migrations/update_retried_for_ci_build_spec.rb +++ b/spec/migrations/update_retried_for_ci_build_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170503004427_update_retried_for_ci_build.rb') describe UpdateRetriedForCiBuild, :delete do - let(:pipeline) { create(:ci_pipeline) } - let!(:build_old) { create(:ci_build, pipeline: pipeline, name: 'test') } - let!(:build_new) { create(:ci_build, pipeline: pipeline, name: 'test') } + let(:pipeline) { create(:ci_pipeline) } # rubocop:disable RSpec/FactoriesInMigrationSpecs + let!(:build_old) { create(:ci_build, pipeline: pipeline, name: 'test') } # rubocop:disable RSpec/FactoriesInMigrationSpecs + let!(:build_new) { create(:ci_build, pipeline: pipeline, name: 'test') } # rubocop:disable RSpec/FactoriesInMigrationSpecs before do described_class.new.up diff --git a/spec/models/deploy_token_spec.rb b/spec/models/deploy_token_spec.rb new file mode 100644 index 00000000000..5a15c23def4 --- /dev/null +++ b/spec/models/deploy_token_spec.rb @@ -0,0 +1,134 @@ +require 'spec_helper' + +describe DeployToken do + subject(:deploy_token) { create(:deploy_token) } + + it { is_expected.to have_many :project_deploy_tokens } + it { is_expected.to have_many(:projects).through(:project_deploy_tokens) } + + describe '#ensure_token' do + it 'should ensure a token' do + deploy_token.token = nil + deploy_token.save + + expect(deploy_token.token).not_to be_empty + end + end + + describe '#ensure_at_least_one_scope' do + context 'with at least one scope' do + it 'should be valid' do + is_expected.to be_valid + end + end + + context 'with no scopes' do + it 'should be invalid' do + deploy_token = build(:deploy_token, read_repository: false, read_registry: false) + + expect(deploy_token).not_to be_valid + expect(deploy_token.errors[:base].first).to eq("Scopes can't be blank") + end + end + end + + describe '#scopes' do + context 'with all the scopes' do + it 'should return scopes assigned to DeployToken' do + expect(deploy_token.scopes).to eq([:read_repository, :read_registry]) + end + end + + context 'with only one scope' do + it 'should return scopes assigned to DeployToken' do + deploy_token = create(:deploy_token, read_registry: false) + expect(deploy_token.scopes).to eq([:read_repository]) + end + end + end + + describe '#revoke!' do + it 'should update revoke attribute' do + deploy_token.revoke! + expect(deploy_token.revoked?).to be_truthy + end + end + + describe "#active?" do + context "when it has been revoked" do + it 'should return false' do + deploy_token.revoke! + expect(deploy_token.active?).to be_falsy + end + end + + context "when it hasn't been revoked" do + it 'should return true' do + expect(deploy_token.active?).to be_truthy + end + end + end + + describe '#username' do + it 'returns a harcoded username' do + expect(deploy_token.username).to eq("gitlab+deploy-token-#{deploy_token.id}") + end + end + + describe '#has_access_to?' do + let(:project) { create(:project) } + + subject(:deploy_token) { create(:deploy_token, projects: [project]) } + + context 'when the deploy token has access to the project' do + it 'should return true' do + expect(deploy_token.has_access_to?(project)).to be_truthy + end + end + + context 'when the deploy token does not have access to the project' do + it 'should return false' do + another_project = create(:project) + expect(deploy_token.has_access_to?(another_project)).to be_falsy + end + end + end + + describe '#expires_at' do + context 'when using Forever.date' do + let(:deploy_token) { create(:deploy_token, expires_at: nil) } + + it 'should return nil' do + expect(deploy_token.expires_at).to be_nil + end + end + + context 'when using a personalized date' do + let(:expires_at) { Date.today + 5.months } + let(:deploy_token) { create(:deploy_token, expires_at: expires_at) } + + it 'should return the personalized date' do + expect(deploy_token.expires_at).to eq(expires_at) + end + end + end + + describe '#expires_at=' do + context 'when passing nil' do + let(:deploy_token) { create(:deploy_token, expires_at: nil) } + + it 'should assign Forever.date' do + expect(deploy_token.read_attribute(:expires_at)).to eq(Forever.date) + end + end + + context 'when passign a value' do + let(:expires_at) { Date.today + 5.months } + let(:deploy_token) { create(:deploy_token, expires_at: expires_at) } + + it 'should respect the value' do + expect(deploy_token.read_attribute(:expires_at)).to eq(expires_at) + end + end + end +end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 412eca4a56b..56161bfcc28 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -368,6 +368,32 @@ describe Environment do end end + describe '#deployment_platform' do + context 'when there is a deployment platform for environment' do + let!(:cluster) do + create(:cluster, :provided_by_gcp, + environment_scope: '*', projects: [project]) + end + + it 'finds a deployment platform' do + expect(environment.deployment_platform).to eq cluster.platform + end + end + + context 'when there is no deployment platform for environment' do + it 'returns nil' do + expect(environment.deployment_platform).to be_nil + end + end + + it 'checks deployment platforms associated with a project' do + expect(project).to receive(:deployment_platform) + .with(environment: environment.name) + + environment.deployment_platform + end + end + describe '#terminals' do subject { environment.terminals } diff --git a/spec/models/project_deploy_token_spec.rb b/spec/models/project_deploy_token_spec.rb new file mode 100644 index 00000000000..9e2e40c2e8f --- /dev/null +++ b/spec/models/project_deploy_token_spec.rb @@ -0,0 +1,14 @@ +require 'rails_helper' + +RSpec.describe ProjectDeployToken, type: :model do + let(:project) { create(:project) } + let(:deploy_token) { create(:deploy_token) } + subject(:project_deploy_token) { create(:project_deploy_token, project: project, deploy_token: deploy_token) } + + it { is_expected.to belong_to :project } + it { is_expected.to belong_to :deploy_token } + + it { is_expected.to validate_presence_of :deploy_token } + it { is_expected.to validate_presence_of :project } + it { is_expected.to validate_uniqueness_of(:deploy_token_id).scoped_to(:project_id) } +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 7007f78e702..2675c2f52c1 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -84,6 +84,8 @@ describe Project do it { is_expected.to have_many(:custom_attributes).class_name('ProjectCustomAttribute') } it { is_expected.to have_many(:project_badges).class_name('ProjectBadge') } it { is_expected.to have_many(:lfs_file_locks) } + it { is_expected.to have_many(:project_deploy_tokens) } + it { is_expected.to have_many(:deploy_tokens).through(:project_deploy_tokens) } context 'after initialized' do it "has a project_feature" do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index a600987d0bf..35db7616efb 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1850,6 +1850,21 @@ describe User do it_behaves_like :member end + + context 'with subgroup with different owner for project runner', :nested_groups do + let(:group) { create(:group) } + let(:another_user) { create(:user) } + let(:subgroup) { create(:group, parent: group) } + let(:project) { create(:project, group: subgroup) } + + def add_user(access) + group.add_user(user, access) + group.add_user(another_user, :owner) + subgroup.add_user(another_user, :owner) + end + + it_behaves_like :member + end end describe '#projects_with_reporter_access_limited_to' do @@ -2234,6 +2249,20 @@ describe User do end end + context '#invalidate_personal_projects_count' do + let(:user) { build_stubbed(:user) } + + it 'invalidates cache for personal projects counter' do + cache_mock = double + + expect(cache_mock).to receive(:delete).with(['users', user.id, 'personal_projects_count']) + + allow(Rails).to receive(:cache).and_return(cache_mock) + + user.invalidate_personal_projects_count + end + end + describe '#allow_password_authentication_for_web?' do context 'regular user' do let(:user) { build(:user) } @@ -2283,11 +2312,9 @@ describe User do user = build(:user) projects = double(:projects, count: 1) - expect(user).to receive(:personal_projects).once.and_return(projects) + expect(user).to receive(:personal_projects).and_return(projects) - 2.times do - expect(user.personal_projects_count).to eq(1) - end + expect(user.personal_projects_count).to eq(1) end end diff --git a/spec/policies/deploy_token_policy_spec.rb b/spec/policies/deploy_token_policy_spec.rb new file mode 100644 index 00000000000..eea287d895e --- /dev/null +++ b/spec/policies/deploy_token_policy_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +describe DeployTokenPolicy do + let(:current_user) { create(:user) } + let(:project) { create(:project) } + let(:deploy_token) { create(:deploy_token, projects: [project]) } + + subject { described_class.new(current_user, deploy_token) } + + describe 'creating a deploy key' do + context 'when user is master' do + before do + project.add_master(current_user) + end + + it { is_expected.to be_allowed(:create_deploy_token) } + end + + context 'when user is not master' do + before do + project.add_developer(current_user) + end + + it { is_expected.to be_disallowed(:create_deploy_token) } + end + end + + describe 'updating a deploy key' do + context 'when user is master' do + before do + project.add_master(current_user) + end + + it { is_expected.to be_allowed(:update_deploy_token) } + end + + context 'when user is not master' do + before do + project.add_developer(current_user) + end + + it { is_expected.to be_disallowed(:update_deploy_token) } + end + end +end diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb index 5d13e6de741..f68057a92a1 100644 --- a/spec/requests/api/project_import_spec.rb +++ b/spec/requests/api/project_import_spec.rb @@ -114,6 +114,29 @@ describe API::ProjectImport do expect(import_project.description).to eq('Hello world') end + context 'when target path already exists in namespace' do + let(:existing_project) { create(:project, namespace: user.namespace) } + + it 'does not schedule an import' do + expect_any_instance_of(Project).not_to receive(:import_schedule) + + post api('/projects/import', user), path: existing_project.path, file: fixture_file_upload(file) + + expect(response).to have_gitlab_http_status(400) + expect(json_response['message']).to eq('Name has already been taken') + end + + context 'when param overwrite is true' do + it 'schedules an import' do + stub_import(user.namespace) + + post api('/projects/import', user), path: existing_project.path, file: fixture_file_upload(file), overwrite: true + + expect(response).to have_gitlab_http_status(201) + end + end + end + def stub_import(namespace) expect_any_instance_of(Project).to receive(:import_schedule) expect(::Projects::CreateService).to receive(:new).with(user, hash_including(namespace_id: namespace.id)).and_call_original diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index fb1281a6b42..e1b4e618092 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -164,20 +164,36 @@ describe 'project routing' do # archive_project_repository GET /:project_id/repository/archive(.:format) projects/repositories#archive # edit_project_repository GET /:project_id/repository/edit(.:format) projects/repositories#edit describe Projects::RepositoriesController, 'routing' do - it 'to #archive' do - expect(get('/gitlab/gitlabhq/repository/master/archive')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', ref: 'master') - end - it 'to #archive format:zip' do - expect(get('/gitlab/gitlabhq/repository/master/archive.zip')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'zip', ref: 'master') + expect(get('/gitlab/gitlabhq/-/archive/master/archive.zip')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'zip', id: 'master/archive') end it 'to #archive format:tar.bz2' do - expect(get('/gitlab/gitlabhq/repository/master/archive.tar.bz2')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'tar.bz2', ref: 'master') + expect(get('/gitlab/gitlabhq/-/archive/master/archive.tar.bz2')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'tar.bz2', id: 'master/archive') end it 'to #archive with "/" in route' do - expect(get('/gitlab/gitlabhq/repository/improve/awesome/archive')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', ref: 'improve/awesome') + expect(get('/gitlab/gitlabhq/-/archive/improve/awesome/gitlabhq-improve-awesome.tar.gz')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'tar.gz', id: 'improve/awesome/gitlabhq-improve-awesome') + end + + it 'to #archive_alternative' do + expect(get('/gitlab/gitlabhq/repository/archive')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', append_sha: true) + end + + it 'to #archive_deprecated' do + expect(get('/gitlab/gitlabhq/repository/master/archive')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', append_sha: true) + end + + it 'to #archive_deprecated format:zip' do + expect(get('/gitlab/gitlabhq/repository/master/archive.zip')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'zip', id: 'master', append_sha: true) + end + + it 'to #archive_deprecated format:tar.bz2' do + expect(get('/gitlab/gitlabhq/repository/master/archive.tar.bz2')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'tar.bz2', id: 'master', append_sha: true) + end + + it 'to #archive_deprecated with "/" in route' do + expect(get('/gitlab/gitlabhq/repository/improve/awesome/archive')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'improve/awesome', append_sha: true) end end diff --git a/spec/rubocop/cop/rspec/factories_in_migration_specs_spec.rb b/spec/rubocop/cop/rspec/factories_in_migration_specs_spec.rb new file mode 100644 index 00000000000..2763f2bda21 --- /dev/null +++ b/spec/rubocop/cop/rspec/factories_in_migration_specs_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +require 'rubocop' +require 'rubocop/rspec/support' + +require_relative '../../../../rubocop/cop/rspec/factories_in_migration_specs' + +describe RuboCop::Cop::RSpec::FactoriesInMigrationSpecs do + include CopHelper + + let(:source_file) { 'spec/migrations/foo_spec.rb' } + + subject(:cop) { described_class.new } + + shared_examples 'an offensive factory call' do |namespace| + %i[build build_list create create_list].each do |forbidden_method| + namespaced_forbidden_method = "#{namespace}#{forbidden_method}(:user)" + + it "registers an offense for #{namespaced_forbidden_method}" do + expect_offense(<<-RUBY) + describe 'foo' do + let(:user) { #{namespaced_forbidden_method} } + #{'^' * namespaced_forbidden_method.size} Don't use FactoryBot.#{forbidden_method} in migration specs, use `table` instead. + end + RUBY + end + end + end + + context 'in a migration spec file' do + before do + allow(cop).to receive(:in_migration_spec?).and_return(true) + end + + it_behaves_like 'an offensive factory call', '' + it_behaves_like 'an offensive factory call', 'FactoryBot.' + end + + context 'outside of a migration spec file' do + it "does not register an offense" do + expect_no_offenses(<<-RUBY) + describe 'foo' do + let(:user) { create(:user) } + end + RUBY + end + end +end diff --git a/spec/services/deploy_tokens/create_service_spec.rb b/spec/services/deploy_tokens/create_service_spec.rb new file mode 100644 index 00000000000..3a2bbf1ecd1 --- /dev/null +++ b/spec/services/deploy_tokens/create_service_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +describe DeployTokens::CreateService do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:deploy_token_params) { attributes_for(:deploy_token) } + + describe '#execute' do + subject { described_class.new(project, user, deploy_token_params).execute } + + context 'when the deploy token is valid' do + it 'should create a new DeployToken' do + expect { subject }.to change { DeployToken.count }.by(1) + end + + it 'should create a new ProjectDeployToken' do + expect { subject }.to change { ProjectDeployToken.count }.by(1) + end + + it 'returns a DeployToken' do + expect(subject).to be_an_instance_of DeployToken + end + end + + context 'when expires at date is not passed' do + let(:deploy_token_params) { attributes_for(:deploy_token, expires_at: '') } + + it 'should set Forever.date' do + expect(subject.read_attribute(:expires_at)).to eq(Forever.date) + end + end + + context 'when the deploy token is invalid' do + let(:deploy_token_params) { attributes_for(:deploy_token, read_repository: false, read_registry: false) } + + it 'should not create a new DeployToken' do + expect { subject }.not_to change { DeployToken.count } + end + + it 'should not create a new ProjectDeployToken' do + expect { subject }.not_to change { ProjectDeployToken.count } + end + end + end +end diff --git a/spec/services/merge_requests/conflicts/list_service_spec.rb b/spec/services/merge_requests/conflicts/list_service_spec.rb index 6cadcd438c3..837b8a56d12 100644 --- a/spec/services/merge_requests/conflicts/list_service_spec.rb +++ b/spec/services/merge_requests/conflicts/list_service_spec.rb @@ -77,6 +77,14 @@ describe MergeRequests::Conflicts::ListService do expect(service.can_be_resolved_in_ui?).to be_falsey end + it 'returns a falsey value when the MR has a missing revision after a force push' do + merge_request = create_merge_request('conflict-resolvable') + service = conflicts_service(merge_request) + allow(merge_request).to receive_message_chain(:target_branch_head, :raw, :id).and_return(Gitlab::Git::BLANK_SHA) + + expect(service.can_be_resolved_in_ui?).to be_falsey + end + context 'with gitaly disabled', :skip_gitaly_mock do it 'returns a falsey value when the MR has a missing ref after a force push' do merge_request = create_merge_request('conflict-resolvable') @@ -85,6 +93,14 @@ describe MergeRequests::Conflicts::ListService do expect(service.can_be_resolved_in_ui?).to be_falsey end + + it 'returns a falsey value when the MR has a missing revision after a force push' do + merge_request = create_merge_request('conflict-resolvable') + service = conflicts_service(merge_request) + allow(merge_request).to receive_message_chain(:target_branch_head, :raw, :id).and_return(Gitlab::Git::BLANK_SHA) + + expect(service.can_be_resolved_in_ui?).to be_falsey + end end end end diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index 2cacb97a293..e35f0f6337a 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -28,6 +28,14 @@ describe Projects::CreateService, '#execute' do end end + describe 'after create actions' do + it 'invalidate personal_projects_count caches' do + expect(user).to receive(:invalidate_personal_projects_count) + + create_project(user, opts) + end + end + context "admin creates project with other user's namespace_id" do it 'sets the correct permissions' do admin = create(:admin) diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index 0bec2054f50..a66e3c5e995 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -66,6 +66,12 @@ describe Projects::DestroyService do end it_behaves_like 'deleting the project' + + it 'invalidates personal_project_count cache' do + expect(user).to receive(:invalidate_personal_projects_count) + + destroy_project(project, user) + end end context 'Sidekiq fake' do @@ -242,6 +248,28 @@ describe Projects::DestroyService do end end + context '#attempt_restore_repositories' do + let(:path) { project.disk_path + '.git' } + + before do + expect(project.gitlab_shell.exists?(project.repository_storage_path, path)).to be_truthy + expect(project.gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey + + # Dont run sidekiq to check if renamed repository exists + Sidekiq::Testing.fake! { destroy_project(project, user, {}) } + + expect(project.gitlab_shell.exists?(project.repository_storage_path, path)).to be_falsey + expect(project.gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_truthy + end + + it 'restores the repositories' do + Sidekiq::Testing.fake! { described_class.new(project, user).attempt_repositories_rollback } + + expect(project.gitlab_shell.exists?(project.repository_storage_path, path)).to be_truthy + expect(project.gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey + end + end + def destroy_project(project, user, params = {}) if async Projects::DestroyService.new(project, user, params).async_execute diff --git a/spec/services/projects/gitlab_projects_import_service_spec.rb b/spec/services/projects/gitlab_projects_import_service_spec.rb index 880b2aae66a..ee1a886f5d6 100644 --- a/spec/services/projects/gitlab_projects_import_service_spec.rb +++ b/spec/services/projects/gitlab_projects_import_service_spec.rb @@ -4,7 +4,8 @@ describe Projects::GitlabProjectsImportService do set(:namespace) { create(:namespace) } let(:path) { 'test-path' } let(:file) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') } - let(:import_params) { { namespace_id: namespace.id, path: path, file: file } } + let(:overwrite) { false } + let(:import_params) { { namespace_id: namespace.id, path: path, file: file, overwrite: overwrite } } subject { described_class.new(namespace.owner, import_params) } describe '#execute' do @@ -37,5 +38,28 @@ describe Projects::GitlabProjectsImportService do expect(project.import_data.data['override_params']['description']).to eq('Hello') end end + + context 'when there is a project with the same path' do + let(:existing_project) { create(:project, namespace: namespace) } + let(:path) { existing_project.path} + + it 'does not create the project' do + project = subject.execute + + expect(project).to be_invalid + expect(project).not_to be_persisted + end + + context 'when overwrite param is set' do + let(:overwrite) { true } + + it 'creates a project in a temporary full_path' do + project = subject.execute + + expect(project).to be_valid + expect(project).to be_persisted + end + end + end end end diff --git a/spec/services/projects/move_access_service_spec.rb b/spec/services/projects/move_access_service_spec.rb new file mode 100644 index 00000000000..a820ebd91f4 --- /dev/null +++ b/spec/services/projects/move_access_service_spec.rb @@ -0,0 +1,114 @@ +require 'spec_helper' + +describe Projects::MoveAccessService do + let(:user) { create(:user) } + let(:group) { create(:group) } + let(:project_with_access) { create(:project, namespace: user.namespace) } + let(:master_user) { create(:user) } + let(:reporter_user) { create(:user) } + let(:developer_user) { create(:user) } + let(:master_group) { create(:group) } + let(:reporter_group) { create(:group) } + let(:developer_group) { create(:group) } + + before do + project_with_access.add_master(master_user) + project_with_access.add_developer(developer_user) + project_with_access.add_reporter(reporter_user) + project_with_access.project_group_links.create(group: master_group, group_access: Gitlab::Access::MASTER) + project_with_access.project_group_links.create(group: developer_group, group_access: Gitlab::Access::DEVELOPER) + project_with_access.project_group_links.create(group: reporter_group, group_access: Gitlab::Access::REPORTER) + end + + subject { described_class.new(target_project, user) } + + describe '#execute' do + shared_examples 'move the accesses' do + it do + expect(project_with_access.project_members.count).to eq 4 + expect(project_with_access.project_group_links.count).to eq 3 + expect(project_with_access.authorized_users.count).to eq 4 + + subject.execute(project_with_access) + + expect(project_with_access.project_members.count).to eq 0 + expect(project_with_access.project_group_links.count).to eq 0 + expect(project_with_access.authorized_users.count).to eq 1 + expect(target_project.project_members.count).to eq 4 + expect(target_project.project_group_links.count).to eq 3 + expect(target_project.authorized_users.count).to eq 4 + end + + it 'rollbacks if an exception is raised' do + allow(subject).to receive(:success).and_raise(StandardError) + + expect { subject.execute(project_with_groups) }.to raise_error(StandardError) + + expect(project_with_access.project_members.count).to eq 4 + expect(project_with_access.project_group_links.count).to eq 3 + expect(project_with_access.authorized_users.count).to eq 4 + end + end + + context 'when both projects are in the same namespace' do + let(:target_project) { create(:project, namespace: user.namespace) } + + it 'does not refresh project owner authorized projects' do + allow(project_with_access).to receive(:namespace).and_return(user.namespace) + expect(project_with_access.namespace).not_to receive(:refresh_project_authorizations) + expect(target_project.namespace).not_to receive(:refresh_project_authorizations) + + subject.execute(project_with_access) + end + + it_behaves_like 'move the accesses' + end + + context 'when projects are in different namespaces' do + let(:target_project) { create(:project, namespace: group) } + + before do + group.add_owner(user) + end + + it 'refreshes both project owner authorized projects' do + allow(project_with_access).to receive(:namespace).and_return(user.namespace) + expect(user.namespace).to receive(:refresh_project_authorizations).once + expect(group).to receive(:refresh_project_authorizations).once + + subject.execute(project_with_access) + end + + it_behaves_like 'move the accesses' + end + + context 'when remove_remaining_elements is false' do + let(:target_project) { create(:project, namespace: user.namespace) } + let(:options) { { remove_remaining_elements: false } } + + it 'does not remove remaining memberships' do + target_project.add_master(master_user) + + subject.execute(project_with_access, options) + + expect(project_with_access.project_members.count).not_to eq 0 + end + + it 'does not remove remaining group links' do + target_project.project_group_links.create(group: master_group, group_access: Gitlab::Access::MASTER) + + subject.execute(project_with_access, options) + + expect(project_with_access.project_group_links.count).not_to eq 0 + end + + it 'does not remove remaining authorizations' do + target_project.add_developer(developer_user) + + subject.execute(project_with_access, options) + + expect(project_with_access.project_authorizations.count).not_to eq 0 + end + end + end +end diff --git a/spec/services/projects/move_deploy_keys_projects_service_spec.rb b/spec/services/projects/move_deploy_keys_projects_service_spec.rb new file mode 100644 index 00000000000..c548edf39a8 --- /dev/null +++ b/spec/services/projects/move_deploy_keys_projects_service_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' + +describe Projects::MoveDeployKeysProjectsService do + let!(:user) { create(:user) } + let!(:project_with_deploy_keys) { create(:project, namespace: user.namespace) } + let!(:target_project) { create(:project, namespace: user.namespace) } + + subject { described_class.new(target_project, user) } + + describe '#execute' do + before do + create_list(:deploy_keys_project, 2, project: project_with_deploy_keys) + end + + it 'moves the user\'s deploy keys from one project to another' do + expect(project_with_deploy_keys.deploy_keys_projects.count).to eq 2 + expect(target_project.deploy_keys_projects.count).to eq 0 + + subject.execute(project_with_deploy_keys) + + expect(project_with_deploy_keys.deploy_keys_projects.count).to eq 0 + expect(target_project.deploy_keys_projects.count).to eq 2 + end + + it 'does not link existent deploy_keys in the current project' do + target_project.deploy_keys << project_with_deploy_keys.deploy_keys.first + + expect(project_with_deploy_keys.deploy_keys_projects.count).to eq 2 + expect(target_project.deploy_keys_projects.count).to eq 1 + + subject.execute(project_with_deploy_keys) + + expect(project_with_deploy_keys.deploy_keys_projects.count).to eq 0 + expect(target_project.deploy_keys_projects.count).to eq 2 + end + + it 'rollbacks changes if transaction fails' do + allow(subject).to receive(:success).and_raise(StandardError) + + expect { subject.execute(project_with_deploy_keys) }.to raise_error(StandardError) + + expect(project_with_deploy_keys.deploy_keys_projects.count).to eq 2 + expect(target_project.deploy_keys_projects.count).to eq 0 + end + + context 'when remove_remaining_elements is false' do + let(:options) { { remove_remaining_elements: false } } + + it 'does not remove remaining deploy keys projects' do + target_project.deploy_keys << project_with_deploy_keys.deploy_keys.first + + subject.execute(project_with_deploy_keys, options) + + expect(project_with_deploy_keys.deploy_keys_projects.count).not_to eq 0 + end + end + end +end diff --git a/spec/services/projects/move_forks_service_spec.rb b/spec/services/projects/move_forks_service_spec.rb new file mode 100644 index 00000000000..f4a5a7f9fc2 --- /dev/null +++ b/spec/services/projects/move_forks_service_spec.rb @@ -0,0 +1,96 @@ +require 'spec_helper' + +describe Projects::MoveForksService do + include ProjectForksHelper + + let!(:user) { create(:user) } + let!(:project_with_forks) { create(:project, namespace: user.namespace) } + let!(:target_project) { create(:project, namespace: user.namespace) } + let!(:lvl1_forked_project_1) { fork_project(project_with_forks, user) } + let!(:lvl1_forked_project_2) { fork_project(project_with_forks, user) } + let!(:lvl2_forked_project_1_1) { fork_project(lvl1_forked_project_1, user) } + let!(:lvl2_forked_project_1_2) { fork_project(lvl1_forked_project_1, user) } + + subject { described_class.new(target_project, user) } + + describe '#execute' do + context 'when moving a root forked project' do + it 'moves the descendant forks' do + expect(project_with_forks.forks.count).to eq 2 + expect(target_project.forks.count).to eq 0 + + subject.execute(project_with_forks) + + expect(project_with_forks.forks.count).to eq 0 + expect(target_project.forks.count).to eq 2 + expect(lvl1_forked_project_1.forked_from_project).to eq target_project + expect(lvl1_forked_project_1.fork_network_member.forked_from_project).to eq target_project + expect(lvl1_forked_project_2.forked_from_project).to eq target_project + expect(lvl1_forked_project_2.fork_network_member.forked_from_project).to eq target_project + end + + it 'updates the fork network' do + expect(project_with_forks.fork_network.root_project).to eq project_with_forks + expect(project_with_forks.fork_network.fork_network_members.map(&:project)).to include project_with_forks + + subject.execute(project_with_forks) + + expect(target_project.reload.fork_network.root_project).to eq target_project + expect(target_project.fork_network.fork_network_members.map(&:project)).not_to include project_with_forks + end + end + + context 'when moving a intermediate forked project' do + it 'moves the descendant forks' do + expect(lvl1_forked_project_1.forks.count).to eq 2 + expect(target_project.forks.count).to eq 0 + + subject.execute(lvl1_forked_project_1) + + expect(lvl1_forked_project_1.forks.count).to eq 0 + expect(target_project.forks.count).to eq 2 + expect(lvl2_forked_project_1_1.forked_from_project).to eq target_project + expect(lvl2_forked_project_1_1.fork_network_member.forked_from_project).to eq target_project + expect(lvl2_forked_project_1_2.forked_from_project).to eq target_project + expect(lvl2_forked_project_1_2.fork_network_member.forked_from_project).to eq target_project + end + + it 'moves the ascendant fork' do + subject.execute(lvl1_forked_project_1) + + expect(target_project.forked_from_project).to eq project_with_forks + expect(target_project.fork_network_member.forked_from_project).to eq project_with_forks + end + + it 'does not update fork network' do + subject.execute(lvl1_forked_project_1) + + expect(target_project.reload.fork_network.root_project).to eq project_with_forks + end + end + + context 'when moving a leaf forked project' do + it 'moves the ascendant fork' do + subject.execute(lvl2_forked_project_1_1) + + expect(target_project.forked_from_project).to eq lvl1_forked_project_1 + expect(target_project.fork_network_member.forked_from_project).to eq lvl1_forked_project_1 + end + + it 'does not update fork network' do + subject.execute(lvl2_forked_project_1_1) + + expect(target_project.reload.fork_network.root_project).to eq project_with_forks + end + end + + it 'rollbacks changes if transaction fails' do + allow(subject).to receive(:success).and_raise(StandardError) + + expect { subject.execute(project_with_forks) }.to raise_error(StandardError) + + expect(project_with_forks.forks.count).to eq 2 + expect(target_project.forks.count).to eq 0 + end + end +end diff --git a/spec/services/projects/move_lfs_objects_projects_service_spec.rb b/spec/services/projects/move_lfs_objects_projects_service_spec.rb new file mode 100644 index 00000000000..517a24a982a --- /dev/null +++ b/spec/services/projects/move_lfs_objects_projects_service_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe Projects::MoveLfsObjectsProjectsService do + let!(:user) { create(:user) } + let!(:project_with_lfs_objects) { create(:project, namespace: user.namespace) } + let!(:target_project) { create(:project, namespace: user.namespace) } + + subject { described_class.new(target_project, user) } + + before do + create_list(:lfs_objects_project, 3, project: project_with_lfs_objects) + end + + describe '#execute' do + it 'links the lfs objects from existent in source project' do + expect(target_project.lfs_objects.count).to eq 0 + + subject.execute(project_with_lfs_objects) + + expect(project_with_lfs_objects.reload.lfs_objects.count).to eq 0 + expect(target_project.reload.lfs_objects.count).to eq 3 + end + + it 'does not link existent lfs_object in the current project' do + target_project.lfs_objects << project_with_lfs_objects.lfs_objects.first(2) + + expect(target_project.lfs_objects.count).to eq 2 + + subject.execute(project_with_lfs_objects) + + expect(target_project.lfs_objects.count).to eq 3 + end + + it 'rollbacks changes if transaction fails' do + allow(subject).to receive(:success).and_raise(StandardError) + + expect { subject.execute(project_with_lfs_objects) }.to raise_error(StandardError) + + expect(project_with_lfs_objects.lfs_objects.count).to eq 3 + expect(target_project.lfs_objects.count).to eq 0 + end + + context 'when remove_remaining_elements is false' do + let(:options) { { remove_remaining_elements: false } } + + it 'does not remove remaining lfs objects' do + target_project.lfs_objects << project_with_lfs_objects.lfs_objects.first(2) + + subject.execute(project_with_lfs_objects, options) + + expect(project_with_lfs_objects.lfs_objects.count).not_to eq 0 + end + end + end +end diff --git a/spec/services/projects/move_notification_settings_service_spec.rb b/spec/services/projects/move_notification_settings_service_spec.rb new file mode 100644 index 00000000000..24d69eef86a --- /dev/null +++ b/spec/services/projects/move_notification_settings_service_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +describe Projects::MoveNotificationSettingsService do + let(:user) { create(:user) } + let(:project_with_notifications) { create(:project, namespace: user.namespace) } + let(:target_project) { create(:project, namespace: user.namespace) } + + subject { described_class.new(target_project, user) } + + describe '#execute' do + context 'with notification settings' do + before do + create_list(:notification_setting, 2, source: project_with_notifications) + end + + it 'moves the user\'s notification settings from one project to another' do + expect(project_with_notifications.notification_settings.count).to eq 3 + expect(target_project.notification_settings.count).to eq 1 + + subject.execute(project_with_notifications) + + expect(project_with_notifications.notification_settings.count).to eq 0 + expect(target_project.notification_settings.count).to eq 3 + end + + it 'rollbacks changes if transaction fails' do + allow(subject).to receive(:success).and_raise(StandardError) + + expect { subject.execute(project_with_notifications) }.to raise_error(StandardError) + + expect(project_with_notifications.notification_settings.count).to eq 3 + expect(target_project.notification_settings.count).to eq 1 + end + end + + it 'does not move existent notification settings in the current project' do + expect(project_with_notifications.notification_settings.count).to eq 1 + expect(target_project.notification_settings.count).to eq 1 + expect(user.notification_settings.count).to eq 2 + + subject.execute(project_with_notifications) + + expect(user.notification_settings.count).to eq 1 + end + + context 'when remove_remaining_elements is false' do + let(:options) { { remove_remaining_elements: false } } + + it 'does not remove remaining notification settings' do + subject.execute(project_with_notifications, options) + + expect(project_with_notifications.notification_settings.count).not_to eq 0 + end + end + end +end diff --git a/spec/services/projects/move_project_authorizations_service_spec.rb b/spec/services/projects/move_project_authorizations_service_spec.rb new file mode 100644 index 00000000000..f7262b9b887 --- /dev/null +++ b/spec/services/projects/move_project_authorizations_service_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +describe Projects::MoveProjectAuthorizationsService do + let!(:user) { create(:user) } + let(:project_with_users) { create(:project, namespace: user.namespace) } + let(:target_project) { create(:project, namespace: user.namespace) } + let(:master_user) { create(:user) } + let(:reporter_user) { create(:user) } + let(:developer_user) { create(:user) } + + subject { described_class.new(target_project, user) } + + describe '#execute' do + before do + project_with_users.add_master(master_user) + project_with_users.add_developer(developer_user) + project_with_users.add_reporter(reporter_user) + end + + it 'moves the authorizations from one project to another' do + expect(project_with_users.authorized_users.count).to eq 4 + expect(target_project.authorized_users.count).to eq 1 + + subject.execute(project_with_users) + + expect(project_with_users.authorized_users.count).to eq 0 + expect(target_project.authorized_users.count).to eq 4 + end + + it 'does not move existent authorizations to the current project' do + target_project.add_master(developer_user) + target_project.add_developer(reporter_user) + + expect(project_with_users.authorized_users.count).to eq 4 + expect(target_project.authorized_users.count).to eq 3 + + subject.execute(project_with_users) + + expect(project_with_users.authorized_users.count).to eq 0 + expect(target_project.authorized_users.count).to eq 4 + end + + context 'when remove_remaining_elements is false' do + let(:options) { { remove_remaining_elements: false } } + + it 'does not remove remaining project authorizations' do + target_project.add_master(developer_user) + target_project.add_developer(reporter_user) + + subject.execute(project_with_users, options) + + expect(project_with_users.project_authorizations.count).not_to eq 0 + end + end + end +end diff --git a/spec/services/projects/move_project_group_links_service_spec.rb b/spec/services/projects/move_project_group_links_service_spec.rb new file mode 100644 index 00000000000..e3d06e6d3d7 --- /dev/null +++ b/spec/services/projects/move_project_group_links_service_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +describe Projects::MoveProjectGroupLinksService do + let!(:user) { create(:user) } + let(:project_with_groups) { create(:project, namespace: user.namespace) } + let(:target_project) { create(:project, namespace: user.namespace) } + let(:master_group) { create(:group) } + let(:reporter_group) { create(:group) } + let(:developer_group) { create(:group) } + + subject { described_class.new(target_project, user) } + + describe '#execute' do + before do + project_with_groups.project_group_links.create(group: master_group, group_access: Gitlab::Access::MASTER) + project_with_groups.project_group_links.create(group: developer_group, group_access: Gitlab::Access::DEVELOPER) + project_with_groups.project_group_links.create(group: reporter_group, group_access: Gitlab::Access::REPORTER) + end + + it 'moves the group links from one project to another' do + expect(project_with_groups.project_group_links.count).to eq 3 + expect(target_project.project_group_links.count).to eq 0 + + subject.execute(project_with_groups) + + expect(project_with_groups.project_group_links.count).to eq 0 + expect(target_project.project_group_links.count).to eq 3 + end + + it 'does not move existent group links in the current project' do + target_project.project_group_links.create(group: master_group, group_access: Gitlab::Access::MASTER) + target_project.project_group_links.create(group: developer_group, group_access: Gitlab::Access::DEVELOPER) + + expect(project_with_groups.project_group_links.count).to eq 3 + expect(target_project.project_group_links.count).to eq 2 + + subject.execute(project_with_groups) + + expect(project_with_groups.project_group_links.count).to eq 0 + expect(target_project.project_group_links.count).to eq 3 + end + + it 'rollbacks changes if transaction fails' do + allow(subject).to receive(:success).and_raise(StandardError) + + expect { subject.execute(project_with_groups) }.to raise_error(StandardError) + + expect(project_with_groups.project_group_links.count).to eq 3 + expect(target_project.project_group_links.count).to eq 0 + end + + context 'when remove_remaining_elements is false' do + let(:options) { { remove_remaining_elements: false } } + + it 'does not remove remaining project group links' do + target_project.project_group_links.create(group: master_group, group_access: Gitlab::Access::MASTER) + target_project.project_group_links.create(group: developer_group, group_access: Gitlab::Access::DEVELOPER) + + subject.execute(project_with_groups, options) + + expect(project_with_groups.project_group_links.count).not_to eq 0 + end + end + end +end diff --git a/spec/services/projects/move_project_members_service_spec.rb b/spec/services/projects/move_project_members_service_spec.rb new file mode 100644 index 00000000000..9c9a2d2fde1 --- /dev/null +++ b/spec/services/projects/move_project_members_service_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +describe Projects::MoveProjectMembersService do + let!(:user) { create(:user) } + let(:project_with_users) { create(:project, namespace: user.namespace) } + let(:target_project) { create(:project, namespace: user.namespace) } + let(:master_user) { create(:user) } + let(:reporter_user) { create(:user) } + let(:developer_user) { create(:user) } + + subject { described_class.new(target_project, user) } + + describe '#execute' do + before do + project_with_users.add_master(master_user) + project_with_users.add_developer(developer_user) + project_with_users.add_reporter(reporter_user) + end + + it 'moves the members from one project to another' do + expect(project_with_users.project_members.count).to eq 4 + expect(target_project.project_members.count).to eq 1 + + subject.execute(project_with_users) + + expect(project_with_users.project_members.count).to eq 0 + expect(target_project.project_members.count).to eq 4 + end + + it 'does not move existent members to the current project' do + target_project.add_master(developer_user) + target_project.add_developer(reporter_user) + + expect(project_with_users.project_members.count).to eq 4 + expect(target_project.project_members.count).to eq 3 + + subject.execute(project_with_users) + + expect(project_with_users.project_members.count).to eq 0 + expect(target_project.project_members.count).to eq 4 + end + + it 'rollbacks changes if transaction fails' do + allow(subject).to receive(:success).and_raise(StandardError) + + expect { subject.execute(project_with_users) }.to raise_error(StandardError) + + expect(project_with_users.project_members.count).to eq 4 + expect(target_project.project_members.count).to eq 1 + end + + context 'when remove_remaining_elements is false' do + let(:options) { { remove_remaining_elements: false } } + + it 'does not remove remaining project members' do + target_project.add_master(developer_user) + target_project.add_developer(reporter_user) + + subject.execute(project_with_users, options) + + expect(project_with_users.project_members.count).not_to eq 0 + end + end + end +end diff --git a/spec/services/projects/move_users_star_projects_service_spec.rb b/spec/services/projects/move_users_star_projects_service_spec.rb new file mode 100644 index 00000000000..e0545c5a21b --- /dev/null +++ b/spec/services/projects/move_users_star_projects_service_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +describe Projects::MoveUsersStarProjectsService do + let!(:user) { create(:user) } + let!(:project_with_stars) { create(:project, namespace: user.namespace) } + let!(:target_project) { create(:project, namespace: user.namespace) } + + subject { described_class.new(target_project, user) } + + describe '#execute' do + before do + create_list(:users_star_project, 2, project: project_with_stars) + end + + it 'moves the user\'s stars from one project to another' do + expect(project_with_stars.users_star_projects.count).to eq 2 + expect(project_with_stars.star_count).to eq 2 + expect(target_project.users_star_projects.count).to eq 0 + expect(target_project.star_count).to eq 0 + + subject.execute(project_with_stars) + project_with_stars.reload + target_project.reload + + expect(project_with_stars.users_star_projects.count).to eq 0 + expect(project_with_stars.star_count).to eq 0 + expect(target_project.users_star_projects.count).to eq 2 + expect(target_project.star_count).to eq 2 + end + + it 'rollbacks changes if transaction fails' do + allow(subject).to receive(:success).and_raise(StandardError) + + expect { subject.execute(project_with_stars) }.to raise_error(StandardError) + + expect(project_with_stars.users_star_projects.count).to eq 2 + expect(project_with_stars.star_count).to eq 2 + expect(target_project.users_star_projects.count).to eq 0 + expect(target_project.star_count).to eq 0 + end + end +end diff --git a/spec/services/projects/overwrite_project_service_spec.rb b/spec/services/projects/overwrite_project_service_spec.rb new file mode 100644 index 00000000000..252c61f4224 --- /dev/null +++ b/spec/services/projects/overwrite_project_service_spec.rb @@ -0,0 +1,198 @@ +require 'spec_helper' + +describe Projects::OverwriteProjectService do + include ProjectForksHelper + + let(:user) { create(:user) } + let(:project_from) { create(:project, namespace: user.namespace) } + let(:project_to) { create(:project, namespace: user.namespace) } + let!(:lvl1_forked_project_1) { fork_project(project_from, user) } + let!(:lvl1_forked_project_2) { fork_project(project_from, user) } + let!(:lvl2_forked_project_1_1) { fork_project(lvl1_forked_project_1, user) } + let!(:lvl2_forked_project_1_2) { fork_project(lvl1_forked_project_1, user) } + + subject { described_class.new(project_to, user) } + + before do + allow(project_to).to receive(:import_data).and_return(double(data: { 'original_path' => project_from.path })) + end + + describe '#execute' do + shared_examples 'overwrite actions' do + it 'moves deploy keys' do + deploy_keys_count = project_from.deploy_keys_projects.count + + subject.execute(project_from) + + expect(project_to.deploy_keys_projects.count).to eq deploy_keys_count + end + + it 'moves notification settings' do + notification_count = project_from.notification_settings.count + + subject.execute(project_from) + + expect(project_to.notification_settings.count).to eq notification_count + end + + it 'moves users stars' do + stars_count = project_from.users_star_projects.count + + subject.execute(project_from) + project_to.reload + + expect(project_to.users_star_projects.count).to eq stars_count + expect(project_to.star_count).to eq stars_count + end + + it 'moves project group links' do + group_links_count = project_from.project_group_links.count + + subject.execute(project_from) + + expect(project_to.project_group_links.count).to eq group_links_count + end + + it 'moves memberships and authorizations' do + members_count = project_from.project_members.count + project_authorizations = project_from.project_authorizations.count + + subject.execute(project_from) + + expect(project_to.project_members.count).to eq members_count + expect(project_to.project_authorizations.count).to eq project_authorizations + end + + context 'moves lfs objects relationships' do + before do + create_list(:lfs_objects_project, 3, project: project_from) + end + + it do + lfs_objects_count = project_from.lfs_objects.count + + subject.execute(project_from) + + expect(project_to.lfs_objects.count).to eq lfs_objects_count + end + end + + it 'removes the original project' do + subject.execute(project_from) + + expect { Project.find(project_from.id) }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'renames the project' do + subject.execute(project_from) + + expect(project_to.full_path).to eq project_from.full_path + end + end + + context 'when project does not have any relation' do + it_behaves_like 'overwrite actions' + end + + context 'when project with elements' do + it_behaves_like 'overwrite actions' do + let(:master_user) { create(:user) } + let(:reporter_user) { create(:user) } + let(:developer_user) { create(:user) } + let(:master_group) { create(:group) } + let(:reporter_group) { create(:group) } + let(:developer_group) { create(:group) } + + before do + create_list(:deploy_keys_project, 2, project: project_from) + create_list(:notification_setting, 2, source: project_from) + create_list(:users_star_project, 2, project: project_from) + project_from.project_group_links.create(group: master_group, group_access: Gitlab::Access::MASTER) + project_from.project_group_links.create(group: developer_group, group_access: Gitlab::Access::DEVELOPER) + project_from.project_group_links.create(group: reporter_group, group_access: Gitlab::Access::REPORTER) + project_from.add_master(master_user) + project_from.add_developer(developer_user) + project_from.add_reporter(reporter_user) + end + end + end + + context 'forks' do + context 'when moving a root forked project' do + it 'moves the descendant forks' do + expect(project_from.forks.count).to eq 2 + expect(project_to.forks.count).to eq 0 + + subject.execute(project_from) + + expect(project_from.forks.count).to eq 0 + expect(project_to.forks.count).to eq 2 + expect(lvl1_forked_project_1.forked_from_project).to eq project_to + expect(lvl1_forked_project_1.fork_network_member.forked_from_project).to eq project_to + expect(lvl1_forked_project_2.forked_from_project).to eq project_to + expect(lvl1_forked_project_2.fork_network_member.forked_from_project).to eq project_to + end + + it 'updates the fork network' do + expect(project_from.fork_network.root_project).to eq project_from + expect(project_from.fork_network.fork_network_members.map(&:project)).to include project_from + + subject.execute(project_from) + + expect(project_to.reload.fork_network.root_project).to eq project_to + expect(project_to.fork_network.fork_network_members.map(&:project)).not_to include project_from + end + end + context 'when moving a intermediate forked project' do + let(:project_to) { create(:project, namespace: lvl1_forked_project_1.namespace) } + + it 'moves the descendant forks' do + expect(lvl1_forked_project_1.forks.count).to eq 2 + expect(project_to.forks.count).to eq 0 + + subject.execute(lvl1_forked_project_1) + + expect(lvl1_forked_project_1.forks.count).to eq 0 + expect(project_to.forks.count).to eq 2 + expect(lvl2_forked_project_1_1.forked_from_project).to eq project_to + expect(lvl2_forked_project_1_1.fork_network_member.forked_from_project).to eq project_to + expect(lvl2_forked_project_1_2.forked_from_project).to eq project_to + expect(lvl2_forked_project_1_2.fork_network_member.forked_from_project).to eq project_to + end + + it 'moves the ascendant fork' do + subject.execute(lvl1_forked_project_1) + + expect(project_to.reload.forked_from_project).to eq project_from + expect(project_to.fork_network_member.forked_from_project).to eq project_from + end + + it 'does not update fork network' do + subject.execute(lvl1_forked_project_1) + + expect(project_to.reload.fork_network.root_project).to eq project_from + end + end + end + + context 'if an exception is raised' do + it 'rollbacks changes' do + updated_at = project_from.updated_at + + allow(subject).to receive(:rename_project).and_raise(StandardError) + + expect { subject.execute(project_from) }.to raise_error(StandardError) + expect(Project.find(project_from.id)).not_to be_nil + expect(project_from.reload.updated_at.change(usec: 0)).to eq updated_at.change(usec: 0) + end + + it 'tries to restore the original project repositories' do + allow(subject).to receive(:rename_project).and_raise(StandardError) + + expect(subject).to receive(:attempt_restore_repositories).with(project_from) + + expect { subject.execute(project_from) }.to raise_error(StandardError) + end + end + end +end diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 95a6771c59d..ff9b2372a35 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -37,6 +37,12 @@ describe Projects::TransferService do transfer_project(project, user, group) end + it 'invalidates the user\'s personal_project_count cache' do + expect(user).to receive(:invalidate_personal_projects_count) + + transfer_project(project, user, group) + end + it 'executes system hooks' do transfer_project(project, user, group) do |service| expect(service).to receive(:execute_system_hooks) diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb index dd31a677dfe..1b6caeab15d 100644 --- a/spec/services/projects/update_pages_service_spec.rb +++ b/spec/services/projects/update_pages_service_spec.rb @@ -21,76 +21,72 @@ describe Projects::UpdatePagesService do end context 'legacy artifacts' do - %w(tar.gz zip).each do |format| - let(:extension) { format } + let(:extension) { 'zip' } - context "for valid #{format}" do + before do + build.update_attributes(legacy_artifacts_file: file) + build.update_attributes(legacy_artifacts_metadata: metadata) + end + + describe 'pages artifacts' do + context 'with expiry date' do before do - build.update_attributes(legacy_artifacts_file: file) - build.update_attributes(legacy_artifacts_metadata: metadata) + build.artifacts_expire_in = "2 days" + build.save! end - describe 'pages artifacts' do - context 'with expiry date' do - before do - build.artifacts_expire_in = "2 days" - build.save! - end - - it "doesn't delete artifacts" do - expect(execute).to eq(:success) - - expect(build.reload.artifacts?).to eq(true) - end - end - - context 'without expiry date' do - it "does delete artifacts" do - expect(execute).to eq(:success) + it "doesn't delete artifacts" do + expect(execute).to eq(:success) - expect(build.reload.artifacts?).to eq(false) - end - end + expect(build.reload.artifacts?).to eq(true) end + end - it 'succeeds' do - expect(project.pages_deployed?).to be_falsey + context 'without expiry date' do + it "does delete artifacts" do expect(execute).to eq(:success) - expect(project.pages_deployed?).to be_truthy - # Check that all expected files are extracted - %w[index.html zero .hidden/file].each do |filename| - expect(File.exist?(File.join(project.public_pages_path, filename))).to be_truthy - end + expect(build.reload.artifacts?).to eq(false) end + end + end - it 'limits pages size' do - stub_application_setting(max_pages_size: 1) - expect(execute).not_to eq(:success) - end + it 'succeeds' do + expect(project.pages_deployed?).to be_falsey + expect(execute).to eq(:success) + expect(project.pages_deployed?).to be_truthy - it 'removes pages after destroy' do - expect(PagesWorker).to receive(:perform_in) - expect(project.pages_deployed?).to be_falsey - expect(execute).to eq(:success) - expect(project.pages_deployed?).to be_truthy - project.destroy - expect(project.pages_deployed?).to be_falsey - end + # Check that all expected files are extracted + %w[index.html zero .hidden/file].each do |filename| + expect(File.exist?(File.join(project.public_pages_path, filename))).to be_truthy + end + end - it 'fails if sha on branch is not latest' do - build.update_attributes(ref: 'feature') + it 'limits pages size' do + stub_application_setting(max_pages_size: 1) + expect(execute).not_to eq(:success) + end - expect(execute).not_to eq(:success) - end + it 'removes pages after destroy' do + expect(PagesWorker).to receive(:perform_in) + expect(project.pages_deployed?).to be_falsey + expect(execute).to eq(:success) + expect(project.pages_deployed?).to be_truthy + project.destroy + expect(project.pages_deployed?).to be_falsey + end - it 'fails for empty file fails' do - build.update_attributes(legacy_artifacts_file: empty_file) + it 'fails if sha on branch is not latest' do + build.update_attributes(ref: 'feature') - expect { execute } - .to raise_error(Projects::UpdatePagesService::FailedToExtractError) - end - end + expect(execute).not_to eq(:success) + end + + it 'fails for empty file fails' do + build.update_attributes(legacy_artifacts_file: empty_file) + + expect { execute } + .to raise_error(Projects::UpdatePagesService::FailedToExtractError) end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index beabba99cf5..83664bae046 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -66,6 +66,7 @@ RSpec.configure do |config| config.include MigrationsHelpers, :migration config.include StubFeatureFlags config.include StubENV + config.include ExpectOffense config.infer_spec_type_from_file_location! diff --git a/spec/support/helpers/expect_offense.rb b/spec/support/helpers/expect_offense.rb new file mode 100644 index 00000000000..35718ba90c5 --- /dev/null +++ b/spec/support/helpers/expect_offense.rb @@ -0,0 +1,20 @@ +require 'rubocop/rspec/support' + +# https://github.com/backus/rubocop-rspec/blob/master/spec/support/expect_offense.rb +# rubocop-rspec gem extension of RuboCop's ExpectOffense module. +# +# This mixin is the same as rubocop's ExpectOffense except the default +# filename ends with `_spec.rb` +module ExpectOffense + include RuboCop::RSpec::ExpectOffense + + DEFAULT_FILENAME = 'example_spec.rb'.freeze + + def expect_offense(source, filename = DEFAULT_FILENAME) + super + end + + def expect_no_offenses(source, filename = DEFAULT_FILENAME) + super + end +end diff --git a/spec/support/issuables_list_metadata_shared_examples.rb b/spec/support/issuables_list_metadata_shared_examples.rb index 75982432ab4..e61983c60b4 100644 --- a/spec/support/issuables_list_metadata_shared_examples.rb +++ b/spec/support/issuables_list_metadata_shared_examples.rb @@ -5,9 +5,9 @@ shared_examples 'issuables list meta-data' do |issuable_type, action = nil| %w[fix improve/awesome].each do |source_branch| issuable = if issuable_type == :issue - create(issuable_type, project: project) + create(issuable_type, project: project, author: project.creator) else - create(issuable_type, source_project: project, source_branch: source_branch) + create(issuable_type, source_project: project, source_branch: source_branch, author: project.creator) end @issuable_ids << issuable.id @@ -16,7 +16,7 @@ shared_examples 'issuables list meta-data' do |issuable_type, action = nil| it "creates indexed meta-data object for issuable notes and votes count" do if action - get action + get action, author_id: project.creator.id else get :index, namespace_id: project.namespace, project_id: project end @@ -35,7 +35,7 @@ shared_examples 'issuables list meta-data' do |issuable_type, action = nil| it "doesn't execute any queries with false conditions" do get_action = if action - proc { get action } + proc { get action, author_id: project.creator.id } else proc { get :index, namespace_id: project2.namespace, project_id: project2 } end diff --git a/spec/support/issuables_requiring_filter_shared_examples.rb b/spec/support/issuables_requiring_filter_shared_examples.rb new file mode 100644 index 00000000000..439ef5ed92e --- /dev/null +++ b/spec/support/issuables_requiring_filter_shared_examples.rb @@ -0,0 +1,15 @@ +shared_examples 'issuables requiring filter' do |action| + it "doesn't load any issuables if no filter is set" do + expect_any_instance_of(described_class).not_to receive(:issuables_collection) + + get action + + expect(response).to render_template(action) + end + + it "loads issuables if at least one filter is set" do + expect_any_instance_of(described_class).to receive(:issuables_collection).and_call_original + + get action, author_id: user.id + end +end diff --git a/spec/views/projects/jobs/show.html.haml_spec.rb b/spec/views/projects/jobs/show.html.haml_spec.rb index 9e692159bd0..c93152b88e3 100644 --- a/spec/views/projects/jobs/show.html.haml_spec.rb +++ b/spec/views/projects/jobs/show.html.haml_spec.rb @@ -21,7 +21,7 @@ describe 'projects/jobs/show' do describe 'environment info in job view' do context 'job with latest deployment' do let(:build) do - create(:ci_build, :success, environment: 'staging') + create(:ci_build, :success, :trace_artifact, environment: 'staging') end before do @@ -40,11 +40,11 @@ describe 'projects/jobs/show' do context 'job with outdated deployment' do let(:build) do - create(:ci_build, :success, environment: 'staging', pipeline: pipeline) + create(:ci_build, :success, :trace_artifact, environment: 'staging', pipeline: pipeline) end let(:second_build) do - create(:ci_build, :success, environment: 'staging', pipeline: pipeline) + create(:ci_build, :success, :trace_artifact, environment: 'staging', pipeline: pipeline) end let(:environment) do @@ -70,7 +70,7 @@ describe 'projects/jobs/show' do context 'job failed to deploy' do let(:build) do - create(:ci_build, :failed, environment: 'staging', pipeline: pipeline) + create(:ci_build, :failed, :trace_artifact, environment: 'staging', pipeline: pipeline) end let!(:environment) do @@ -88,7 +88,7 @@ describe 'projects/jobs/show' do context 'job will deploy' do let(:build) do - create(:ci_build, :running, environment: 'staging', pipeline: pipeline) + create(:ci_build, :running, :trace_live, environment: 'staging', pipeline: pipeline) end context 'when environment exists' do @@ -136,7 +136,7 @@ describe 'projects/jobs/show' do context 'job that failed to deploy and environment has not been created' do let(:build) do - create(:ci_build, :failed, environment: 'staging', pipeline: pipeline) + create(:ci_build, :failed, :trace_artifact, environment: 'staging', pipeline: pipeline) end let!(:environment) do @@ -154,7 +154,7 @@ describe 'projects/jobs/show' do context 'job that will deploy and environment has not been created' do let(:build) do - create(:ci_build, :running, environment: 'staging', pipeline: pipeline) + create(:ci_build, :running, :trace_live, environment: 'staging', pipeline: pipeline) end let!(:environment) do @@ -174,8 +174,9 @@ describe 'projects/jobs/show' do end context 'when job is running' do + let(:build) { create(:ci_build, :trace_live, :running, pipeline: pipeline) } + before do - build.run! render end diff --git a/spec/views/projects/merge_requests/_commits.html.haml_spec.rb b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb index 3ca67114558..b1c6565c08a 100644 --- a/spec/views/projects/merge_requests/_commits.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb @@ -28,6 +28,6 @@ describe 'projects/merge_requests/_commits.html.haml' do commit = merge_request.commits.first # HEAD href = diffs_project_merge_request_path(target_project, merge_request, commit_id: commit) - expect(rendered).to have_link(Commit.truncate_sha(commit.sha), href: href) + expect(rendered).to have_link(href: href) end end diff --git a/yarn.lock b/yarn.lock index af7bda5d562..243b83f4471 100644 --- a/yarn.lock +++ b/yarn.lock @@ -54,9 +54,9 @@ lodash "^4.2.0" to-fast-properties "^2.0.0" -"@gitlab-org/gitlab-svgs@^1.16.0": - version "1.16.0" - resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.16.0.tgz#6c88a1bd9f5b3d3e5bf6a6d89d61724022185667" +"@gitlab-org/gitlab-svgs@^1.17.0": + version "1.17.0" + resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.17.0.tgz#d0c74d9e44c127ccfad16941f352088b86f86c89" "@types/jquery@^2.0.40": version "2.0.48" |