diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-06 12:09:17 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-06 12:09:17 +0000 |
commit | 4ff56b118438f4fa6191b691fd968c75d8e94d5a (patch) | |
tree | 5b1e6ce71ee1c40a755daad006cefc3ff02bcb5e | |
parent | 0dce1c285f8d6487daf4b83be1ca9585e3a084e6 (diff) | |
download | gitlab-ce-4ff56b118438f4fa6191b691fd968c75d8e94d5a.tar.gz |
Add latest changes from gitlab-org/gitlab@master
47 files changed, 1158 insertions, 29 deletions
diff --git a/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue b/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue new file mode 100644 index 00000000000..fd65d29a0f5 --- /dev/null +++ b/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue @@ -0,0 +1,188 @@ +<script> +import Vue from 'vue'; +import { + GlIcon, + GlFormInput, + GlDropdown, + GlDropdownItem, + GlSearchBoxByType, + GlTooltipDirective as GlTooltip, +} from '@gitlab/ui'; +import { s__, __ } from '~/locale'; +// Mocks will be removed when integrating with BE is ready +// data format most likely will differ but UI will not +// feature rollout plan - https://gitlab.com/gitlab-org/gitlab/-/issues/262707#note_442529171 +import gitlabFields from './mocks/gitlabFields.json'; +import parsedMapping from './mocks/parsedMapping.json'; + +export const i18n = { + columns: { + gitlabKeyTitle: s__('AlertMappingBuilder|GitLab alert key'), + payloadKeyTitle: s__('AlertMappingBuilder|Payload alert key'), + fallbackKeyTitle: s__('AlertMappingBuilder|Define fallback'), + }, + selectMappingKey: s__('AlertMappingBuilder|Select key'), + makeSelection: s__('AlertMappingBuilder|Make selection'), + fallbackTooltip: s__( + 'AlertMappingBuilder|Title is a required field for alerts in GitLab. Should the payload field you specified not be available, specifiy which field we should use instead. ', + ), + noResults: __('No matching results'), +}; + +export default { + i18n, + components: { + GlIcon, + GlFormInput, + GlDropdown, + GlDropdownItem, + GlSearchBoxByType, + }, + directives: { + GlTooltip, + }, + data() { + return { + gitlabFields, + }; + }, + computed: { + mappingData() { + return this.gitlabFields.map(gitlabField => { + const mappingFields = parsedMapping.filter(field => field.type === gitlabField.type); + + return { + mapping: null, + fallback: null, + searchTerm: '', + fallbackSearchTerm: '', + mappingFields, + ...gitlabField, + }; + }); + }, + }, + methods: { + setMapping(gitlabKey, mappingKey, valueKey) { + const fieldIndex = this.gitlabFields.findIndex(field => field.key === gitlabKey); + const updatedField = { ...this.gitlabFields[fieldIndex], ...{ [valueKey]: mappingKey } }; + Vue.set(this.gitlabFields, fieldIndex, updatedField); + }, + setSearchTerm(search = '', searchFieldKey, gitlabKey) { + const fieldIndex = this.gitlabFields.findIndex(field => field.key === gitlabKey); + const updatedField = { ...this.gitlabFields[fieldIndex], ...{ [searchFieldKey]: search } }; + Vue.set(this.gitlabFields, fieldIndex, updatedField); + }, + filterFields(searchTerm = '', fields) { + const search = searchTerm.toLowerCase(); + + return fields.filter(field => field.label.toLowerCase().includes(search)); + }, + isSelected(fieldValue, mapping) { + return fieldValue === mapping; + }, + selectedValue(key) { + return ( + parsedMapping.find(item => item.key === key)?.label || this.$options.i18n.makeSelection + ); + }, + getFieldValue({ label, type }) { + return `${label} (${type})`; + }, + noResults(searchTerm, fields) { + return !this.filterFields(searchTerm, fields).length; + }, + }, +}; +</script> + +<template> + <div class="gl-display-table gl-w-full gl-mt-5"> + <div class="gl-display-table-row"> + <h5 class="gl-display-table-cell gl-py-3 gl-pr-3"> + {{ $options.i18n.columns.gitlabKeyTitle }} + </h5> + <h5 class="gl-display-table-cell gl-py-3 gl-pr-3"> </h5> + <h5 class="gl-display-table-cell gl-py-3 gl-pr-3"> + {{ $options.i18n.columns.payloadKeyTitle }} + </h5> + <h5 class="gl-display-table-cell gl-py-3 gl-pr-3"> + {{ $options.i18n.columns.fallbackKeyTitle }} + <gl-icon + v-gl-tooltip + name="question" + class="gl-text-gray-500" + :title="$options.i18n.fallbackTooltip" + /> + </h5> + </div> + <div v-for="gitlabField in mappingData" :key="gitlabField.key" class="gl-display-table-row"> + <div class="gl-display-table-cell gl-py-3 gl-pr-3 w-30p"> + <gl-form-input + disabled + :value="getFieldValue(gitlabField)" + class="gl-bg-transparent! gl-text-gray-900!" + /> + </div> + + <div class="gl-display-table-cell gl-py-3 gl-pr-3"> + <div class="right-arrow"> + <i class="right-arrow-head"></i> + </div> + </div> + + <div class="gl-display-table-cell gl-py-3 gl-pr-3 w-30p"> + <gl-dropdown + :text="selectedValue(gitlabField.mapping)" + class="gl-w-full" + :header-text="$options.i18n.selectMappingKey" + > + <gl-search-box-by-type @input="setSearchTerm($event, 'searchTerm', gitlabField.key)" /> + <gl-dropdown-item + v-for="mappingField in filterFields(gitlabField.searchTerm, gitlabField.mappingFields)" + :key="`${mappingField.key}__mapping`" + :is-checked="isSelected(gitlabField.mapping, mappingField.key)" + is-check-item + @click="setMapping(gitlabField.key, mappingField.key, 'mapping')" + > + {{ mappingField.label }} + </gl-dropdown-item> + <gl-dropdown-item v-if="noResults(gitlabField.searchTerm, gitlabField.mappingFields)"> + > + {{ $options.i18n.noResults }} + </gl-dropdown-item> + </gl-dropdown> + </div> + + <div class="gl-display-table-cell gl-py-3 w-30p"> + <gl-dropdown + v-if="gitlabField.hasFallback" + :text="selectedValue(gitlabField.fallback)" + class="gl-w-full" + :header-text="$options.i18n.selectMappingKey" + > + <gl-search-box-by-type + @input="setSearchTerm($event, 'fallbackSearchTerm', gitlabField.key)" + /> + <gl-dropdown-item + v-for="mappingField in filterFields( + gitlabField.fallbackSearchTerm, + gitlabField.mappingFields, + )" + :key="`${mappingField.key}__fallback`" + :is-checked="isSelected(gitlabField.fallback, mappingField.key)" + is-check-item + @click="setMapping(gitlabField.key, mappingField.key, 'fallback')" + > + {{ mappingField.label }} + </gl-dropdown-item> + <gl-dropdown-item + v-if="noResults(gitlabField.fallbackSearchTerm, gitlabField.mappingFields)" + > + {{ $options.i18n.noResults }} + </gl-dropdown-item> + </gl-dropdown> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_form_new.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_form_new.vue index c5322c9865e..059623ba11c 100644 --- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form_new.vue +++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form_new.vue @@ -12,6 +12,7 @@ import { GlModalDirective, GlToggle, } from '@gitlab/ui'; +import MappingBuilder from './alert_mapping_builder.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { s__ } from '~/locale'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; @@ -84,6 +85,7 @@ export default { GlModal, GlToggle, AlertSettingsFormHelpBlock, + MappingBuilder, }, directives: { 'gl-modal': GlModalDirective, @@ -344,7 +346,7 @@ export default { label-for="mapping-builder" > <span class="gl-text-gray-500">{{ $options.i18n.integrationFormSteps.step5.intro }}</span> - <!--mapping builder will be added here--> + <mapping-builder /> </gl-form-group> <div class="gl-display-flex gl-justify-content-end"> <gl-button type="reset" class="gl-mr-3 js-no-auto-disable">{{ __('Cancel') }}</gl-button> diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_form_old.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_form_old.vue index e4ba87c4e14..0246315bdc5 100644 --- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form_old.vue +++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form_old.vue @@ -477,6 +477,7 @@ export default { max-rows="10" /> </gl-form-group> + <gl-button :disabled="!canTestAlert" @click="validateTestAlert">{{ $options.i18n.testAlertInfo }}</gl-button> diff --git a/app/assets/javascripts/alerts_settings/components/mocks/gitlabFields.json b/app/assets/javascripts/alerts_settings/components/mocks/gitlabFields.json new file mode 100644 index 00000000000..42810b101be --- /dev/null +++ b/app/assets/javascripts/alerts_settings/components/mocks/gitlabFields.json @@ -0,0 +1,48 @@ +[ + { + "key":"title", + "label":"Title", + "type":"String", + "hasFallback": true + }, + { + "key":"description", + "label":"Description", + "type":"String" + }, + { + "key":"startTime", + "label":"Start time", + "type":"DateTime" + }, + { + "key":"service", + "label":"Service", + "type":"String" + }, + { + "key":"monitoringTool", + "label":"Monitoring tool", + "type":"String" + }, + { + "key":"hosts", + "label":"Hosts", + "type":"String or Array" + }, + { + "key":"severity", + "label":"Severity", + "type":"String" + }, + { + "key":"fingerprint", + "label":"Fingerprint", + "type":"String" + }, + { + "key":"environment", + "label":"Environment", + "type":"String" + } +] diff --git a/app/assets/javascripts/alerts_settings/components/mocks/parsedMapping.json b/app/assets/javascripts/alerts_settings/components/mocks/parsedMapping.json new file mode 100644 index 00000000000..3041e7d64cf --- /dev/null +++ b/app/assets/javascripts/alerts_settings/components/mocks/parsedMapping.json @@ -0,0 +1,47 @@ +[ + { + "key":"title", + "label":"Title", + "type":"String" + }, + { + "key":"description", + "label":"Description", + "type":"String" + }, + { + "key":"startTime", + "label":"Start time", + "type":"DateTime" + }, + { + "key":"service", + "label":"Service", + "type":"String" + }, + { + "key":"monitoringTool", + "label":"Monitoring tool", + "type":"String" + }, + { + "key":"hosts", + "label":"Hosts", + "type":"String or Array" + }, + { + "key":"severity", + "label":"Severity", + "type":"String" + }, + { + "key":"fingerprint", + "label":"Fingerprint", + "type":"String" + }, + { + "key":"environment", + "label":"Environment", + "type":"String" + } +] diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue index f36fe87ccfa..9d2deb1d4d0 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue @@ -1,8 +1,7 @@ <script> import { mapState, mapActions, mapGetters } from 'vuex'; -import { GlModal, GlSafeHtmlDirective } from '@gitlab/ui'; +import { GlModal, GlSafeHtmlDirective, GlButton } from '@gitlab/ui'; import { n__, __ } from '~/locale'; -import LoadingButton from '~/vue_shared/components/loading_button.vue'; import CommitMessageField from './message_field.vue'; import Actions from './actions.vue'; import SuccessMessage from './success_message.vue'; @@ -12,10 +11,10 @@ import { createUnexpectedCommitError } from '../../lib/errors'; export default { components: { Actions, - LoadingButton, CommitMessageField, SuccessMessage, GlModal, + GlButton, }, directives: { SafeHtml: GlSafeHtmlDirective, @@ -156,12 +155,16 @@ export default { /> <div class="clearfix gl-mt-5"> <actions /> - <loading-button + <gl-button :loading="submitCommitLoading" - :label="commitButtonText" - container-class="btn btn-success btn-sm float-left qa-commit-button" + class="float-left qa-commit-button" + size="small" + category="primary" + variant="success" @click="commit" - /> + > + {{ __('Commit') }} + </gl-button> <button v-if="!discardDraftButtonDisabled" type="button" @@ -170,14 +173,17 @@ export default { > {{ __('Discard draft') }} </button> - <button + <gl-button v-else type="button" - class="btn btn-default btn-sm float-right" + class="float-right" + category="secondary" + variant="default" + size="small" @click="toggleIsCompact" > {{ __('Collapse') }} - </button> + </gl-button> </div> <gl-modal ref="commitErrorModal" diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js index 2f27814a692..5317093c4cf 100644 --- a/app/assets/javascripts/pages/projects/project.js +++ b/app/assets/javascripts/pages/projects/project.js @@ -57,7 +57,7 @@ export default class Project { $('.project-refs-select').on('change', function() { return $(this) .parents('form') - .submit(); + .trigger('submit'); }); } @@ -156,11 +156,32 @@ export default class Project { }, clicked(options) { const { e } = options; - if (!shouldVisit) { - e.preventDefault(); + e.preventDefault(); + + // Since this page does not reload when changing directories in a repo + // the rendered links do not have the path to the current directory. + // This updates the path based on the current url and then opens + // the the url with the updated path parameter. + if (shouldVisit) { + const selectedUrl = new URL(e.target.href); + const loc = window.location.href; + + if (loc.includes('/-/')) { + const refs = this.fullData.Branches.concat(this.fullData.Tags); + const currentRef = refs.find(ref => loc.indexOf(ref) > -1); + if (currentRef) { + const targetPath = loc.split(currentRef)[1].slice(1); + selectedUrl.searchParams.set('path', targetPath); + } + } + + // Open in new window if "meta" key is pressed + if (e.metaKey) { + window.open(selectedUrl.href, '_blank'); + } else { + window.location.href = selectedUrl.href; + } } - /* The actual process is removed since `link.href` in `RenderRow` contains the full target. - * It makes the visitable link can be visited when opening on a new tab of browser */ }, }); }); diff --git a/app/assets/javascripts/pages/projects/terraform/index/index.js b/app/assets/javascripts/pages/projects/terraform/index/index.js new file mode 100644 index 00000000000..6f9f820f8e1 --- /dev/null +++ b/app/assets/javascripts/pages/projects/terraform/index/index.js @@ -0,0 +1,3 @@ +import loadTerraformVues from '~/terraform'; + +loadTerraformVues(); diff --git a/app/assets/javascripts/terraform/components/empty_state.vue b/app/assets/javascripts/terraform/components/empty_state.vue new file mode 100644 index 00000000000..d86ba3af2b1 --- /dev/null +++ b/app/assets/javascripts/terraform/components/empty_state.vue @@ -0,0 +1,44 @@ +<script> +import { GlEmptyState, GlIcon, GlLink, GlSprintf } from '@gitlab/ui'; + +export default { + components: { + GlEmptyState, + GlIcon, + GlLink, + GlSprintf, + }, + props: { + image: { + type: String, + required: true, + }, + }, +}; +</script> + +<template> + <gl-empty-state :svg-path="image" :title="s__('Terraform|Get started with Terraform')"> + <template #description> + <p> + <gl-sprintf + :message=" + s__( + 'Terraform|Find out how to use the %{linkStart}GitLab managed Terraform State%{linkEnd}', + ) + " + > + <template #link="{ content }"> + <gl-link + href="https://docs.gitlab.com/ee/user/infrastructure/index.html" + target="_blank" + > + {{ content }} + <gl-icon name="external-link" /> + </gl-link> + </template> + </gl-sprintf> + </p> + </template> + </gl-empty-state> +</template> diff --git a/app/assets/javascripts/terraform/components/states_table.vue b/app/assets/javascripts/terraform/components/states_table.vue new file mode 100644 index 00000000000..1fed5158027 --- /dev/null +++ b/app/assets/javascripts/terraform/components/states_table.vue @@ -0,0 +1,63 @@ +<script> +import { GlBadge, GlIcon, GlSprintf, GlTable } from '@gitlab/ui'; +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; + +export default { + components: { + GlBadge, + GlIcon, + GlSprintf, + GlTable, + TimeAgoTooltip, + }, + props: { + states: { + required: true, + type: Array, + }, + }, + computed: { + fields() { + return [ + { + key: 'name', + thClass: 'gl-display-none', + }, + { + key: 'updated', + thClass: 'gl-display-none', + tdClass: 'gl-text-right', + }, + ]; + }, + }, +}; +</script> + +<template> + <gl-table :items="states" :fields="fields" data-testid="terraform-states-table"> + <template #cell(name)="{ item }"> + <p + class="gl-font-weight-bold gl-m-0 gl-text-gray-900" + data-testid="terraform-states-table-name" + > + {{ item.name }} + + <gl-badge v-if="item.lockedAt"> + <gl-icon name="lock" /> + {{ s__('Terraform|Locked') }} + </gl-badge> + </p> + </template> + + <template #cell(updated)="{ item }"> + <p class="gl-m-0" data-testid="terraform-states-table-updated"> + <gl-sprintf :message="s__('Terraform|updated %{timeStart}time%{timeEnd}')"> + <template #time> + <time-ago-tooltip :time="item.updatedAt" /> + </template> + </gl-sprintf> + </p> + </template> + </gl-table> +</template> diff --git a/app/assets/javascripts/terraform/components/terraform_list.vue b/app/assets/javascripts/terraform/components/terraform_list.vue new file mode 100644 index 00000000000..bd15156334f --- /dev/null +++ b/app/assets/javascripts/terraform/components/terraform_list.vue @@ -0,0 +1,85 @@ +<script> +import { GlAlert, GlBadge, GlLoadingIcon, GlTab, GlTabs } from '@gitlab/ui'; +import getStatesQuery from '../graphql/queries/get_states.query.graphql'; +import EmptyState from './empty_state.vue'; +import StatesTable from './states_table.vue'; + +export default { + apollo: { + states: { + query: getStatesQuery, + variables() { + return { + projectPath: this.projectPath, + }; + }, + update: data => { + return { + count: data?.project?.terraformStates?.count, + list: data?.project?.terraformStates?.nodes, + }; + }, + error() { + this.states = null; + }, + }, + }, + components: { + EmptyState, + GlAlert, + GlBadge, + GlLoadingIcon, + GlTab, + GlTabs, + StatesTable, + }, + props: { + emptyStateImage: { + required: true, + type: String, + }, + projectPath: { + required: true, + type: String, + }, + }, + computed: { + isLoading() { + return this.$apollo.queries.states.loading; + }, + statesCount() { + return this.states?.count; + }, + statesList() { + return this.states?.list; + }, + }, +}; +</script> + +<template> + <section> + <gl-tabs> + <gl-tab> + <template slot="title"> + <p class="gl-m-0"> + {{ s__('Terraform|States') }} + <gl-badge v-if="statesCount">{{ statesCount }}</gl-badge> + </p> + </template> + + <gl-loading-icon v-if="isLoading" size="md" class="gl-mt-3" /> + + <div v-else-if="statesList"> + <states-table v-if="statesCount" :states="statesList" /> + + <empty-state v-else :image="emptyStateImage" /> + </div> + + <gl-alert v-else variant="danger" :dismissible="false"> + {{ s__('Terraform|An error occurred while loading your Terraform States') }} + </gl-alert> + </gl-tab> + </gl-tabs> + </section> +</template> diff --git a/app/assets/javascripts/terraform/graphql/fragments/state.fragment.graphql b/app/assets/javascripts/terraform/graphql/fragments/state.fragment.graphql new file mode 100644 index 00000000000..ea9a569b1ee --- /dev/null +++ b/app/assets/javascripts/terraform/graphql/fragments/state.fragment.graphql @@ -0,0 +1,6 @@ +fragment State on TerraformState { + id + name + lockedAt + updatedAt +} diff --git a/app/assets/javascripts/terraform/graphql/queries/get_states.query.graphql b/app/assets/javascripts/terraform/graphql/queries/get_states.query.graphql new file mode 100644 index 00000000000..0ffcbc4e900 --- /dev/null +++ b/app/assets/javascripts/terraform/graphql/queries/get_states.query.graphql @@ -0,0 +1,12 @@ +#import "../fragments/state.fragment.graphql" + +query getStates($projectPath: ID!) { + project(fullPath: $projectPath) { + terraformStates { + count + nodes { + ...State + } + } + } +} diff --git a/app/assets/javascripts/terraform/index.js b/app/assets/javascripts/terraform/index.js new file mode 100644 index 00000000000..579d2d14023 --- /dev/null +++ b/app/assets/javascripts/terraform/index.js @@ -0,0 +1,31 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import TerraformList from './components/terraform_list.vue'; +import createDefaultClient from '~/lib/graphql'; + +Vue.use(VueApollo); + +export default () => { + const el = document.querySelector('#js-terraform-list'); + + if (!el) { + return null; + } + + const defaultClient = createDefaultClient(); + + const { emptyStateImage, projectPath } = el.dataset; + + return new Vue({ + el, + apolloProvider: new VueApollo({ defaultClient }), + render(createElement) { + return createElement(TerraformList, { + props: { + emptyStateImage, + projectPath, + }, + }); + }, + }); +}; diff --git a/app/assets/stylesheets/page_bundles/alert_management_settings.scss b/app/assets/stylesheets/page_bundles/alert_management_settings.scss new file mode 100644 index 00000000000..fb7c1602cba --- /dev/null +++ b/app/assets/stylesheets/page_bundles/alert_management_settings.scss @@ -0,0 +1,24 @@ +@import 'mixins_and_variables_and_functions'; + +$stroke-size: 1px; + +.right-arrow { + @include gl-relative; + @include gl-w-full; + height: $stroke-size; + @include gl-display-inline-block; + background-color: var(--gray-400, $gray-400); + min-width: $gl-spacing-scale-5; + + &-head { + @include gl-absolute; + top: -$gl-spacing-scale-2; + left: calc(100% - #{$gl-spacing-scale-3} - #{2 * $stroke-size}); + border-color: var(--gray-400, $gray-400); + @include gl-border-solid; + border-width: 0 $stroke-size $stroke-size 0; + @include gl-display-inline-block; + @include gl-p-2; + transform: rotate(-45deg); + } +} diff --git a/app/controllers/projects/terraform_controller.rb b/app/controllers/projects/terraform_controller.rb new file mode 100644 index 00000000000..aef163c98c5 --- /dev/null +++ b/app/controllers/projects/terraform_controller.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class Projects::TerraformController < Projects::ApplicationController + before_action :authorize_can_read_terraform_state! + + feature_category :infrastructure_as_code + + def index + end + + private + + def authorize_can_read_terraform_state! + access_denied! unless can?(current_user, :read_terraform_state, project) + end +end diff --git a/app/helpers/projects/terraform_helper.rb b/app/helpers/projects/terraform_helper.rb new file mode 100644 index 00000000000..b286bc4d7a5 --- /dev/null +++ b/app/helpers/projects/terraform_helper.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Projects::TerraformHelper + def js_terraform_list_data(project) + { + empty_state_image: image_path('illustrations/empty-state/empty-serverless-lg.svg'), + project_path: project.full_path + } + end +end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index ae46135e890..9d1f685960f 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -465,6 +465,7 @@ module ProjectsHelper builds: :read_build, clusters: :read_cluster, serverless: :read_cluster, + terraform: :read_terraform_state, error_tracking: :read_sentry_issue, alert_management: :read_alert_management_alert, incidents: :read_issue, @@ -484,7 +485,8 @@ module ProjectsHelper :read_issue, :read_sentry_issue, :read_cluster, - :read_feature_flag + :read_feature_flag, + :read_terraform_state ].any? do |ability| can?(current_user, ability, project) end @@ -762,6 +764,7 @@ module ProjectsHelper metrics_dashboard feature_flags tracings + terraform ] end diff --git a/app/presenters/release_presenter.rb b/app/presenters/release_presenter.rb index 2e85ab2bb94..9d7e183aaf3 100644 --- a/app/presenters/release_presenter.rb +++ b/app/presenters/release_presenter.rb @@ -82,7 +82,7 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated end def release_mr_issue_urls_available? - ::Feature.enabled?(:release_mr_issue_urls, project) + ::Feature.enabled?(:release_mr_issue_urls, project, default_enabled: true) end def release_edit_page_available? diff --git a/app/views/admin/dev_ops_report/show.html.haml b/app/views/admin/dev_ops_report/show.html.haml index ad9f0c6f776..dc3bda3a994 100644 --- a/app/views/admin/dev_ops_report/show.html.haml +++ b/app/views/admin/dev_ops_report/show.html.haml @@ -3,7 +3,7 @@ .container .gl-mt-3 - - if Feature.enabled?(:devops_adoption) + - if Gitlab.ee? && Feature.enabled?(:devops_adoption_feature) && License.feature_available?(:devops_adoption) = render_if_exists 'admin/dev_ops_report/devops_tabs' - else = render 'report' diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index 95f9dfb8bbe..d1168437c5d 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -268,6 +268,12 @@ %span = _('Serverless') + - if project_nav_tab? :terraform + = nav_link(controller: :terraform) do + = link_to project_terraform_index_path(@project), title: _('Terraform') do + %span + = _('Terraform') + - if project_nav_tab? :clusters - show_cluster_hint = show_gke_cluster_integration_callout?(@project) = nav_link(controller: [:clusters, :user, :gcp]) do diff --git a/app/views/projects/settings/operations/_alert_management.html.haml b/app/views/projects/settings/operations/_alert_management.html.haml index 5c16a5e2758..9e76ad52ecb 100644 --- a/app/views/projects/settings/operations/_alert_management.html.haml +++ b/app/views/projects/settings/operations/_alert_management.html.haml @@ -1,5 +1,6 @@ - return unless can?(current_user, :admin_operations, @project) - expanded = expanded_by_default? +- add_page_specific_style 'page_bundles/alert_management_settings' %section.settings.no-animate#js-alert-management-settings{ class: ('expanded' if expanded) } .settings-header diff --git a/app/views/projects/terraform/index.html.haml b/app/views/projects/terraform/index.html.haml new file mode 100644 index 00000000000..136e7ded224 --- /dev/null +++ b/app/views/projects/terraform/index.html.haml @@ -0,0 +1,4 @@ +- breadcrumb_title _('Terraform') +- page_title _('Terraform') + +#js-terraform-list{ data: js_terraform_list_data(@project) } diff --git a/changelogs/unreleased/231777-switching-branches-in-repo-tree-view-navigates-backwards.yml b/changelogs/unreleased/231777-switching-branches-in-repo-tree-view-navigates-backwards.yml new file mode 100644 index 00000000000..9212cb18331 --- /dev/null +++ b/changelogs/unreleased/231777-switching-branches-in-repo-tree-view-navigates-backwards.yml @@ -0,0 +1,5 @@ +--- +title: Fix loading current directory when changing branches +merge_request: 46479 +author: +type: fixed diff --git a/changelogs/unreleased/263106-user-admin-approval-enable-disable-toggle-require_admin_approval_a.yml b/changelogs/unreleased/263106-user-admin-approval-enable-disable-toggle-require_admin_approval_a.yml new file mode 100644 index 00000000000..1532833cfed --- /dev/null +++ b/changelogs/unreleased/263106-user-admin-approval-enable-disable-toggle-require_admin_approval_a.yml @@ -0,0 +1,6 @@ +--- +title: Allow setting the value of 'require_admin_approval_after_user_signup' via Settings + API +merge_request: 46851 +author: +type: added diff --git a/changelogs/unreleased/267147-terraform-list.yml b/changelogs/unreleased/267147-terraform-list.yml new file mode 100644 index 00000000000..22db29c8bb0 --- /dev/null +++ b/changelogs/unreleased/267147-terraform-list.yml @@ -0,0 +1,5 @@ +--- +title: Add new Terraform state list page +merge_request: 45700 +author: +type: added diff --git a/changelogs/unreleased/nfriend-make-release_mr_issue_urls-enabled-by-default.yml b/changelogs/unreleased/nfriend-make-release_mr_issue_urls-enabled-by-default.yml new file mode 100644 index 00000000000..820cc5a7af0 --- /dev/null +++ b/changelogs/unreleased/nfriend-make-release_mr_issue_urls-enabled-by-default.yml @@ -0,0 +1,5 @@ +--- +title: Enable issue and MR stat links on release progress review +merge_request: 46910 +author: +type: added diff --git a/config/application.rb b/config/application.rb index b01bd92e5e1..22259b95cdc 100644 --- a/config/application.rb +++ b/config/application.rb @@ -200,6 +200,7 @@ module Gitlab config.assets.precompile << "page_bundles/reports.css" config.assets.precompile << "page_bundles/wiki.css" config.assets.precompile << "page_bundles/xterm.css" + config.assets.precompile << "page_bundles/alert_management_settings.css" config.assets.precompile << "lazy_bundles/cropper.css" config.assets.precompile << "performance_bar.css" config.assets.precompile << "disable_animations.css" diff --git a/config/feature_flags/development/devops_adoption.yml b/config/feature_flags/development/devops_adoption_feature.yml index af9e9fae9f5..060e87d5a16 100644 --- a/config/feature_flags/development/devops_adoption.yml +++ b/config/feature_flags/development/devops_adoption_feature.yml @@ -1,5 +1,5 @@ --- -name: devops_adoption +name: devops_adoption_feature introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46005 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/271568 type: development diff --git a/config/feature_flags/development/release_mr_issue_urls.yml b/config/feature_flags/development/release_mr_issue_urls.yml index 3f9c81b8fe9..0f68ec1946a 100644 --- a/config/feature_flags/development/release_mr_issue_urls.yml +++ b/config/feature_flags/development/release_mr_issue_urls.yml @@ -1,7 +1,7 @@ --- name: release_mr_issue_urls introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/18727 -rollout_issue_url: +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/276619 group: group::release management type: development -default_enabled: false +default_enabled: true diff --git a/config/initializers/oj.rb b/config/initializers_before_autoloader/oj.rb index 3fa26259fc6..3fa26259fc6 100644 --- a/config/initializers/oj.rb +++ b/config/initializers_before_autoloader/oj.rb diff --git a/config/routes/project.rb b/config/routes/project.rb index 33a96345a93..30e9bfb4bfd 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -265,6 +265,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do resources :functions, only: [:index] end + resources :terraform, only: [:index] + resources :environments, except: [:destroy] do member do post :stop diff --git a/doc/api/settings.md b/doc/api/settings.md index 3885d236a72..fdce87aec78 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -79,7 +79,8 @@ Example response: "snippet_size_limit": 52428800, "issues_create_limit": 300, "raw_blob_request_limit": 300, - "wiki_page_max_content_bytes": 52428800 + "wiki_page_max_content_bytes": 52428800, + "require_admin_approval_after_user_signup": false } ``` @@ -170,7 +171,8 @@ Example response: "snippet_size_limit": 52428800, "issues_create_limit": 300, "raw_blob_request_limit": 300, - "wiki_page_max_content_bytes": 52428800 + "wiki_page_max_content_bytes": 52428800, + "require_admin_approval_after_user_signup": false } ``` @@ -331,6 +333,7 @@ listed in the descriptions of the relevant settings. | `repository_size_limit` | integer | no | **(PREMIUM)** Size limit per repository (MB) | | `repository_storages_weighted` | hash of strings to integers | no | (GitLab 13.1 and later) Hash of names of taken from `gitlab.yml` to [weights](../administration/repository_storage_paths.md#choose-where-new-repositories-will-be-stored). New projects are created in one of these stores, chosen by a weighted random selection. | | `repository_storages` | array of strings | no | (GitLab 13.0 and earlier) List of names of enabled storage paths, taken from `gitlab.yml`. New projects are created in one of these stores, chosen at random. | +| `require_admin_approval_after_user_signup` | boolean | no | When enabled, any user that signs up for an account using the registration form is placed under a **Pending approval** state and has to be explicitly [approved](../user/admin_area/approving_users.md) by an administrator. | | `require_two_factor_authentication` | boolean | no | (**If enabled, requires:** `two_factor_grace_period`) Require all users to set up Two-factor authentication. | | `restricted_visibility_levels` | array of strings | no | Selected levels cannot be used by non-admin users for groups, projects or snippets. Can take `private`, `internal` and `public` as a parameter. Default is `null` which means there is no restriction. | | `rsa_key_restriction` | integer | no | The minimum allowed bit length of an uploaded RSA key. Default is `0` (no restriction). `-1` disables RSA keys. | diff --git a/doc/user/project/merge_requests/code_quality.md b/doc/user/project/merge_requests/code_quality.md index 2299e9d5eab..f3a676e2b48 100644 --- a/doc/user/project/merge_requests/code_quality.md +++ b/doc/user/project/merge_requests/code_quality.md @@ -284,6 +284,21 @@ code_quality: paths: [gl-code-quality-report.html] ``` +It's also possible to generate both JSON and HTML report files by defining +another job and using `extends: code_quality`: + +```yaml +include: + - template: Code-Quality.gitlab-ci.yml + +code_quality_html: + extends: code_quality + variables: + REPORT_FORMAT: html + artifacts: + paths: [gl-code-quality-report.html] +``` + ## Extending functionality ### Using Analysis Plugins diff --git a/lib/api/settings.rb b/lib/api/settings.rb index b40af368f36..16ac4ec4ef2 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -159,6 +159,7 @@ module API optional :issues_create_limit, type: Integer, desc: "Maximum number of issue creation requests allowed per minute per user. Set to 0 for unlimited requests per minute." optional :raw_blob_request_limit, type: Integer, desc: "Maximum number of requests per minute for each raw path. Set to 0 for unlimited requests per minute." optional :wiki_page_max_content_bytes, type: Integer, desc: "Maximum wiki page content size in bytes" + optional :require_admin_approval_after_user_signup, type: Boolean, desc: 'Require explicit admin approval for new signups' ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type| optional :"#{type}_key_restriction", diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 30f4b38412b..eb546397436 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2488,6 +2488,24 @@ msgstr "" msgid "AlertManagement|You have enabled the Opsgenie integration. Your alerts will be visible directly in Opsgenie." msgstr "" +msgid "AlertMappingBuilder|Define fallback" +msgstr "" + +msgid "AlertMappingBuilder|GitLab alert key" +msgstr "" + +msgid "AlertMappingBuilder|Make selection" +msgstr "" + +msgid "AlertMappingBuilder|Payload alert key" +msgstr "" + +msgid "AlertMappingBuilder|Select key" +msgstr "" + +msgid "AlertMappingBuilder|Title is a required field for alerts in GitLab. Should the payload field you specified not be available, specifiy which field we should use instead. " +msgstr "" + msgid "AlertService|Review your external service's documentation to learn where to provide this information to your external service, and the %{linkStart}GitLab documentation%{linkEnd} to learn more about configuring your endpoint." msgstr "" @@ -26324,6 +26342,9 @@ msgstr "" msgid "Terms of Service and Privacy Policy" msgstr "" +msgid "Terraform" +msgstr "" + msgid "Terraform|%{number} Terraform report failed to generate" msgid_plural "Terraform|%{number} Terraform reports failed to generate" msgstr[0] "" @@ -26340,18 +26361,36 @@ msgstr "" msgid "Terraform|A Terraform report was generated in your pipelines." msgstr "" +msgid "Terraform|An error occurred while loading your Terraform States" +msgstr "" + +msgid "Terraform|Find out how to use the %{linkStart}GitLab managed Terraform State%{linkEnd}" +msgstr "" + msgid "Terraform|Generating the report caused an error." msgstr "" +msgid "Terraform|Get started with Terraform" +msgstr "" + +msgid "Terraform|Locked" +msgstr "" + msgid "Terraform|Reported Resource Changes: %{addNum} to add, %{changeNum} to change, %{deleteNum} to delete" msgstr "" +msgid "Terraform|States" +msgstr "" + msgid "Terraform|The Terraform report %{name} failed to generate." msgstr "" msgid "Terraform|The Terraform report %{name} was generated in your pipelines." msgstr "" +msgid "Terraform|updated %{timeStart}time%{timeEnd}" +msgstr "" + msgid "Test" msgstr "" diff --git a/spec/controllers/projects/terraform_controller_spec.rb b/spec/controllers/projects/terraform_controller_spec.rb new file mode 100644 index 00000000000..1978b9494fa --- /dev/null +++ b/spec/controllers/projects/terraform_controller_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::TerraformController do + let_it_be(:project) { create(:project) } + + describe 'GET index' do + subject { get :index, params: { namespace_id: project.namespace, project_id: project } } + + context 'when user is authorized' do + let(:user) { project.creator } + + before do + sign_in(user) + subject + end + + it 'renders content' do + expect(response).to be_successful + end + end + + context 'when user is unauthorized' do + let(:user) { create(:user) } + + before do + project.add_guest(user) + sign_in(user) + subject + end + + it 'shows 404' do + expect(response).to have_gitlab_http_status(:not_found) + end + end + end +end diff --git a/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb b/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb index fda2992af8d..6b9fd41059d 100644 --- a/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb +++ b/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb @@ -23,8 +23,6 @@ RSpec.describe 'User creates new blob', :js do ide_commit - click_button('Commit') - expect(page).to have_content('All changes are committed') expect(project.repository.blob_at('master', 'dummy-file').data).to eql("Hello world\n") end diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb index d28e31c08dc..42f8daf9d5e 100644 --- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb +++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb @@ -27,9 +27,7 @@ RSpec.describe 'Projects > Files > Project owner sees a link to create a license ide_commit - click_button('Commit') - - expect(current_path).to eq("/-/ide/project/#{project.full_path}/tree/master/-/") + expect(current_path).to eq("/-/ide/project/#{project.full_path}/tree/master/-/LICENSE/") expect(page).to have_content('All changes are committed') diff --git a/spec/features/projects/terraform_spec.rb b/spec/features/projects/terraform_spec.rb new file mode 100644 index 00000000000..2680dfb2b13 --- /dev/null +++ b/spec/features/projects/terraform_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Terraform', :js do + let_it_be(:project) { create(:project) } + + let(:user) { project.creator } + + before do + gitlab_sign_in(user) + end + + context 'when user does not have any terraform states and visits index page' do + before do + visit project_terraform_index_path(project) + end + + it 'sees an empty state' do + expect(page).to have_content('Get started with Terraform') + end + end + + context 'when user has a terraform state' do + let_it_be(:terraform_state) { create(:terraform_state, :locked, project: project) } + + context 'when user visits the index page' do + before do + visit project_terraform_index_path(project) + end + + it 'displays a tab with states count' do + expect(page).to have_content("States #{project.terraform_states.size}") + end + + it 'displays a table with terraform states' do + expect(page).to have_selector( + '[data-testid="terraform-states-table"] tbody tr', + count: project.terraform_states.size + ) + end + + it 'displays terraform information' do + expect(page).to have_content(terraform_state.name) + end + end + end +end diff --git a/spec/frontend/alerts_settings/alert_mapping_builder_spec.js b/spec/frontend/alerts_settings/alert_mapping_builder_spec.js new file mode 100644 index 00000000000..a75422b8c48 --- /dev/null +++ b/spec/frontend/alerts_settings/alert_mapping_builder_spec.js @@ -0,0 +1,88 @@ +import { GlIcon, GlFormInput, GlDropdown, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import AlertMappingBuilder, { i18n } from '~/alerts_settings/components/alert_mapping_builder.vue'; +import gitlabFields from '~/alerts_settings/components/mocks/gitlabFields.json'; +import parsedMapping from '~/alerts_settings/components/mocks/parsedMapping.json'; + +describe('AlertMappingBuilder', () => { + let wrapper; + + function mountComponent() { + wrapper = shallowMount(AlertMappingBuilder); + } + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + wrapper = null; + } + }); + + beforeEach(() => { + mountComponent(); + }); + + const findColumnInRow = (row, column) => + wrapper + .findAll('.gl-display-table-row') + .at(row) + .findAll('.gl-display-table-cell ') + .at(column); + + const fieldsByTypeCount = parsedMapping.reduce((acc, { type }) => { + acc[type] = (acc[type] || 0) + 1; + return acc; + }, {}); + + it('renders column captions', () => { + expect(findColumnInRow(0, 0).text()).toContain(i18n.columns.gitlabKeyTitle); + expect(findColumnInRow(0, 2).text()).toContain(i18n.columns.payloadKeyTitle); + expect(findColumnInRow(0, 3).text()).toContain(i18n.columns.fallbackKeyTitle); + + const fallbackColumnIcon = findColumnInRow(0, 3).find(GlIcon); + expect(fallbackColumnIcon.exists()).toBe(true); + expect(fallbackColumnIcon.attributes('name')).toBe('question'); + expect(fallbackColumnIcon.attributes('title')).toBe(i18n.fallbackTooltip); + }); + + it('renders disabled form input for each mapped field', () => { + gitlabFields.forEach((field, index) => { + const input = findColumnInRow(index + 1, 0).find(GlFormInput); + expect(input.attributes('value')).toBe(`${field.label} (${field.type})`); + expect(input.attributes('disabled')).toBe(''); + }); + }); + + it('renders right arrow next to each input', () => { + gitlabFields.forEach((field, index) => { + const arrow = findColumnInRow(index + 1, 1).find('.right-arrow'); + expect(arrow.exists()).toBe(true); + }); + }); + + it('renders mapping dropdown for each field', () => { + gitlabFields.forEach(({ type }, index) => { + const dropdown = findColumnInRow(index + 1, 2).find(GlDropdown); + const searchBox = dropdown.find(GlSearchBoxByType); + const dropdownItems = dropdown.findAll(GlDropdownItem); + + expect(dropdown.exists()).toBe(true); + expect(searchBox.exists()).toBe(true); + expect(dropdownItems.length).toBe(fieldsByTypeCount[type]); + }); + }); + + it('renders fallback dropdown only for the fields that have fallback', () => { + gitlabFields.forEach(({ type, hasFallback }, index) => { + const dropdown = findColumnInRow(index + 1, 3).find(GlDropdown); + expect(dropdown.exists()).toBe(Boolean(hasFallback)); + + if (hasFallback) { + const searchBox = dropdown.find(GlSearchBoxByType); + const dropdownItems = dropdown.findAll(GlDropdownItem); + expect(searchBox.exists()).toBe(hasFallback); + expect(dropdownItems.length).toBe(fieldsByTypeCount[type]); + } + }); + }); +}); diff --git a/spec/frontend/terraform/components/empty_state_spec.js b/spec/frontend/terraform/components/empty_state_spec.js new file mode 100644 index 00000000000..c86160e18f3 --- /dev/null +++ b/spec/frontend/terraform/components/empty_state_spec.js @@ -0,0 +1,26 @@ +import { GlEmptyState, GlSprintf } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import EmptyState from '~/terraform/components/empty_state.vue'; + +describe('EmptyStateComponent', () => { + let wrapper; + + const propsData = { + image: '/image/path', + }; + + beforeEach(() => { + wrapper = shallowMount(EmptyState, { propsData, stubs: { GlEmptyState, GlSprintf } }); + return wrapper.vm.$nextTick(); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('should render content', () => { + expect(wrapper.find(GlEmptyState).exists()).toBe(true); + expect(wrapper.text()).toContain('Get started with Terraform'); + }); +}); diff --git a/spec/frontend/terraform/components/states_table_spec.js b/spec/frontend/terraform/components/states_table_spec.js new file mode 100644 index 00000000000..e50969b2c83 --- /dev/null +++ b/spec/frontend/terraform/components/states_table_spec.js @@ -0,0 +1,62 @@ +import { GlIcon } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import { useFakeDate } from 'helpers/fake_date'; +import StatesTable from '~/terraform/components/states_table.vue'; + +describe('StatesTable', () => { + let wrapper; + useFakeDate([2020, 10, 15]); + + const propsData = { + states: [ + { + name: 'state-1', + lockedAt: '2020-10-13T00:00:00Z', + updatedAt: '2020-10-13T00:00:00Z', + }, + { + name: 'state-2', + lockedAt: null, + updatedAt: '2020-10-10T00:00:00Z', + }, + ], + }; + + beforeEach(() => { + wrapper = mount(StatesTable, { propsData }); + return wrapper.vm.$nextTick(); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it.each` + stateName | locked | lineNumber + ${'state-1'} | ${true} | ${0} + ${'state-2'} | ${false} | ${1} + `( + 'displays the name "$stateName" for line "$lineNumber"', + ({ stateName, locked, lineNumber }) => { + const states = wrapper.findAll('[data-testid="terraform-states-table-name"]'); + + const state = states.at(lineNumber); + + expect(state.text()).toContain(stateName); + expect(state.find(GlIcon).exists()).toBe(locked); + }, + ); + + it.each` + updateTime | lineNumber + ${'updated 2 days ago'} | ${0} + ${'updated 5 days ago'} | ${1} + `('displays the time "$updateTime" for line "$lineNumber"', ({ updateTime, lineNumber }) => { + const states = wrapper.findAll('[data-testid="terraform-states-table-updated"]'); + + const state = states.at(lineNumber); + + expect(state.text()).toBe(updateTime); + }); +}); diff --git a/spec/frontend/terraform/components/terraform_list_spec.js b/spec/frontend/terraform/components/terraform_list_spec.js new file mode 100644 index 00000000000..7eeaf31f3f7 --- /dev/null +++ b/spec/frontend/terraform/components/terraform_list_spec.js @@ -0,0 +1,135 @@ +import { GlAlert, GlBadge, GlLoadingIcon, GlTab } from '@gitlab/ui'; +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import createMockApollo from 'jest/helpers/mock_apollo_helper'; +import VueApollo from 'vue-apollo'; +import EmptyState from '~/terraform/components/empty_state.vue'; +import StatesTable from '~/terraform/components/states_table.vue'; +import TerraformList from '~/terraform/components/terraform_list.vue'; +import getStatesQuery from '~/terraform/graphql/queries/get_states.query.graphql'; + +const localVue = createLocalVue(); +localVue.use(VueApollo); + +describe('TerraformList', () => { + let wrapper; + + const propsData = { + emptyStateImage: '/path/to/image', + projectPath: 'path/to/project', + }; + + const createWrapper = ({ terraformStates, queryResponse = null }) => { + const apolloQueryResponse = { + data: { + project: { + terraformStates, + }, + }, + }; + + const statsQueryResponse = queryResponse || jest.fn().mockResolvedValue(apolloQueryResponse); + const apolloProvider = createMockApollo([[getStatesQuery, statsQueryResponse]]); + + wrapper = shallowMount(TerraformList, { + localVue, + apolloProvider, + propsData, + }); + }; + + const findBadge = () => wrapper.find(GlBadge); + const findEmptyState = () => wrapper.find(EmptyState); + const findStatesTable = () => wrapper.find(StatesTable); + const findTab = () => wrapper.find(GlTab); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('when the terraform query has succeeded', () => { + describe('when there is a list of terraform states', () => { + const states = [ + { + id: 'gid://gitlab/Terraform::State/1', + name: 'state-1', + lockedAt: null, + updatedAt: null, + }, + { + id: 'gid://gitlab/Terraform::State/2', + name: 'state-2', + lockedAt: null, + updatedAt: null, + }, + ]; + + beforeEach(() => { + createWrapper({ + terraformStates: { + nodes: states, + count: states.length, + }, + }); + + return wrapper.vm.$nextTick(); + }); + + it('displays a states tab and count', () => { + expect(findTab().text()).toContain('States'); + expect(findBadge().text()).toBe('2'); + }); + + it('renders the states table', () => { + expect(findStatesTable().exists()).toBe(true); + }); + }); + + describe('when the list of terraform states is empty', () => { + beforeEach(() => { + createWrapper({ + terraformStates: { + nodes: [], + count: 0, + }, + }); + + return wrapper.vm.$nextTick(); + }); + + it('displays a states tab with no count', () => { + expect(findTab().text()).toContain('States'); + expect(findBadge().exists()).toBe(false); + }); + + it('renders the empty state', () => { + expect(findEmptyState().exists()).toBe(true); + }); + }); + }); + + describe('when the terraform query has errored', () => { + beforeEach(() => { + createWrapper({ terraformStates: null, queryResponse: jest.fn().mockRejectedValue() }); + + return wrapper.vm.$nextTick(); + }); + + it('displays an alert message', () => { + expect(wrapper.find(GlAlert).exists()).toBe(true); + }); + }); + + describe('when the terraform query is loading', () => { + beforeEach(() => { + createWrapper({ + terraformStates: null, + queryResponse: jest.fn().mockReturnValue(new Promise(() => {})), + }); + }); + + it('displays a loading icon', () => { + expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + }); + }); +}); diff --git a/spec/helpers/projects/terraform_helper_spec.rb b/spec/helpers/projects/terraform_helper_spec.rb new file mode 100644 index 00000000000..de363c42d21 --- /dev/null +++ b/spec/helpers/projects/terraform_helper_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::TerraformHelper do + describe '#js_terraform_list_data' do + let_it_be(:project) { create(:project) } + + subject { helper.js_terraform_list_data(project) } + + it 'displays image path' do + image_path = ActionController::Base.helpers.image_path( + 'illustrations/empty-state/empty-serverless-lg.svg' + ) + + expect(subject[:empty_state_image]).to eq(image_path) + end + + it 'displays project path' do + expect(subject[:project_path]).to eq(project.full_path) + end + end +end diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index e551db3552e..916d8dec8c2 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -40,6 +40,7 @@ RSpec.describe API::Settings, 'Settings' do expect(json_response['spam_check_endpoint_enabled']).to be_falsey expect(json_response['spam_check_endpoint_url']).to be_nil expect(json_response['wiki_page_max_content_bytes']).to be_a(Integer) + expect(json_response['require_admin_approval_after_user_signup']).to eq(false) end end @@ -423,6 +424,14 @@ RSpec.describe API::Settings, 'Settings' do expect(json_response['abuse_notification_email']).to eq('test@example.com') end + it 'supports setting require_admin_approval_after_user_signup' do + put api('/application/settings', admin), + params: { require_admin_approval_after_user_signup: true } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['require_admin_approval_after_user_signup']).to eq(true) + end + context "missing sourcegraph_url value when sourcegraph_enabled is true" do it "returns a blank parameter error message" do put api("/application/settings", admin), params: { sourcegraph_enabled: true } diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb index d304a3e9a0d..ed74c3f179f 100644 --- a/spec/support/shared_contexts/navbar_structure_context.rb +++ b/spec/support/shared_contexts/navbar_structure_context.rb @@ -72,6 +72,7 @@ RSpec.shared_context 'project navbar structure' do _('Alerts'), _('Incidents'), _('Serverless'), + _('Terraform'), _('Kubernetes'), _('Environments'), _('Feature Flags'), |