Commit 874daf71 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents d7096bd9 7a3a4c0b
...@@ -30,6 +30,7 @@ class CopyCodeButton extends HTMLElement { ...@@ -30,6 +30,7 @@ class CopyCodeButton extends HTMLElement {
function addCodeButton() { function addCodeButton() {
[...document.querySelectorAll('pre.code.js-syntax-highlight')] [...document.querySelectorAll('pre.code.js-syntax-highlight')]
.filter((el) => el.attr('lang') !== 'mermaid')
.filter((el) => !el.closest('.js-markdown-code')) .filter((el) => !el.closest('.js-markdown-code'))
.forEach((el) => { .forEach((el) => {
const copyCodeEl = document.createElement('copy-code'); const copyCodeEl = document.createElement('copy-code');
......
...@@ -23,4 +23,39 @@ class Projects::GoogleCloud::BaseController < Projects::ApplicationController ...@@ -23,4 +23,39 @@ class Projects::GoogleCloud::BaseController < Projects::ApplicationController
def feature_flag_enabled! def feature_flag_enabled!
access_denied! unless Feature.enabled?(:incubation_5mp_google_cloud, project) access_denied! unless Feature.enabled?(:incubation_5mp_google_cloud, project)
end end
def validate_gcp_token!
is_token_valid = GoogleApi::CloudPlatform::Client.new(token_in_session, nil)
.validate_token(expires_at_in_session)
return if is_token_valid
return_url = project_google_cloud_index_path(project)
state = generate_session_key_redirect(request.url, return_url)
@authorize_url = GoogleApi::CloudPlatform::Client.new(nil,
callback_google_api_auth_url,
state: state).authorize_url
redirect_to @authorize_url
end
def generate_session_key_redirect(uri, error_uri)
GoogleApi::CloudPlatform::Client.new_session_key_for_redirect_uri do |key|
session[key] = uri
session[:error_uri] = error_uri
end
end
def token_in_session
session[GoogleApi::CloudPlatform::Client.session_key_for_token]
end
def expires_at_in_session
session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at]
end
def handle_gcp_error(error, project)
Gitlab::ErrorTracking.track_exception(error, project_id: project.id)
@js_data = { screen: 'gcp_error', error: error.to_s }.to_json
render status: :unauthorized, template: 'projects/google_cloud/errors/gcp_error'
end
end end
# frozen_string_literal: true
class Projects::GoogleCloud::DeploymentsController < Projects::GoogleCloud::BaseController
before_action :validate_gcp_token!
def cloud_run
render json: "Placeholder"
end
def cloud_storage
render json: "Placeholder"
end
end
...@@ -45,41 +45,4 @@ class Projects::GoogleCloud::ServiceAccountsController < Projects::GoogleCloud:: ...@@ -45,41 +45,4 @@ class Projects::GoogleCloud::ServiceAccountsController < Projects::GoogleCloud::
rescue Google::Apis::ClientError, Google::Apis::ServerError, Google::Apis::AuthorizationError => error rescue Google::Apis::ClientError, Google::Apis::ServerError, Google::Apis::AuthorizationError => error
handle_gcp_error(error, project) handle_gcp_error(error, project)
end end
private
def validate_gcp_token!
is_token_valid = GoogleApi::CloudPlatform::Client.new(token_in_session, nil)
.validate_token(expires_at_in_session)
return if is_token_valid
return_url = project_google_cloud_index_path(project)
state = generate_session_key_redirect(request.url, return_url)
@authorize_url = GoogleApi::CloudPlatform::Client.new(nil,
callback_google_api_auth_url,
state: state).authorize_url
redirect_to @authorize_url
end
def generate_session_key_redirect(uri, error_uri)
GoogleApi::CloudPlatform::Client.new_session_key_for_redirect_uri do |key|
session[key] = uri
session[:error_uri] = error_uri
end
end
def token_in_session
session[GoogleApi::CloudPlatform::Client.session_key_for_token]
end
def expires_at_in_session
session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at]
end
def handle_gcp_error(error, project)
Gitlab::ErrorTracking.track_exception(error, project_id: project.id)
@js_data = { screen: 'gcp_error', error: error.to_s }.to_json
render status: :unauthorized, template: 'projects/google_cloud/errors/gcp_error'
end
end end
...@@ -19,6 +19,7 @@ class Projects::TreeController < Projects::ApplicationController ...@@ -19,6 +19,7 @@ class Projects::TreeController < Projects::ApplicationController
push_frontend_feature_flag(:lazy_load_commits, @project, default_enabled: :yaml) push_frontend_feature_flag(:lazy_load_commits, @project, default_enabled: :yaml)
push_frontend_feature_flag(:new_dir_modal, @project, default_enabled: :yaml) push_frontend_feature_flag(:new_dir_modal, @project, default_enabled: :yaml)
push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml) push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml)
push_frontend_feature_flag(:highlight_js, @project, default_enabled: :yaml)
end end
feature_category :source_code_management feature_category :source_code_management
......
...@@ -4,6 +4,7 @@ module Types ...@@ -4,6 +4,7 @@ module Types
module Ci module Ci
class RunnerType < BaseObject class RunnerType < BaseObject
edge_type_class(RunnerWebUrlEdge) edge_type_class(RunnerWebUrlEdge)
connection_type_class(Types::CountableConnectionType)
graphql_name 'CiRunner' graphql_name 'CiRunner'
authorize :read_runner authorize :read_runner
present_using ::Ci::RunnerPresenter present_using ::Ci::RunnerPresenter
......
...@@ -427,6 +427,10 @@ module Ci ...@@ -427,6 +427,10 @@ module Ci
action? && !archived? && (manual? || scheduled? || retryable?) action? && !archived? && (manual? || scheduled? || retryable?)
end end
def waiting_for_deployment_approval?
manual? && starts_environment? && deployment&.blocked?
end
def schedulable? def schedulable?
self.when == 'delayed' && options[:start_in].present? self.when == 'delayed' && options[:start_in].present?
end end
......
...@@ -123,8 +123,6 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy ...@@ -123,8 +123,6 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
enable :read_group_member enable :read_group_member
enable :read_custom_emoji enable :read_custom_emoji
enable :read_counts enable :read_counts
enable :read_crm_organization
enable :read_crm_contact
end end
rule { ~public_group & ~has_access }.prevent :read_counts rule { ~public_group & ~has_access }.prevent :read_counts
...@@ -159,6 +157,8 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy ...@@ -159,6 +157,8 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
enable :read_prometheus enable :read_prometheus
enable :read_package enable :read_package
enable :read_package_settings enable :read_package_settings
enable :read_crm_organization
enable :read_crm_contact
end end
rule { maintainer }.policy do rule { maintainer }.policy do
......
...@@ -28,18 +28,16 @@ module Ci ...@@ -28,18 +28,16 @@ module Ci
return if events.empty? return if events.empty?
first = events.first processed_events = []
last_processed = nil
begin begin
events.each do |event| events.each do |event|
@sync_class.sync!(event) @sync_class.sync!(event)
last_processed = event processed_events << event
end end
ensure ensure
# remove events till the one that was last succesfully processed @sync_event_class.id_in(processed_events).delete_all
@sync_event_class.id_in(first.id..last_processed.id).delete_all if last_processed
end end
end end
......
...@@ -319,6 +319,9 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -319,6 +319,9 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
namespace :google_cloud do namespace :google_cloud do
resources :service_accounts, only: [:index, :create] resources :service_accounts, only: [:index, :create]
get '/deployments/cloud_run', to: 'deployments#cloud_run'
get '/deployments/cloud_storage', to: 'deployments#cloud_storage'
end end
resources :environments, except: [:destroy] do resources :environments, except: [:destroy] do
......
# frozen_string_literal: true
class AddCiRunnersIndexOnActiveState < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
INDEX_NAME = 'index_ci_runners_on_active'
def up
add_concurrent_index :ci_runners, [:active, :id], name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :ci_runners, INDEX_NAME
end
end
5e5e41ee4c8dc9c3fe791470862d15b8d213fcc931ef8b80937bdb5f5db20aed
\ No newline at end of file
...@@ -25593,6 +25593,8 @@ CREATE INDEX index_ci_runner_projects_on_project_id ON ci_runner_projects USING ...@@ -25593,6 +25593,8 @@ CREATE INDEX index_ci_runner_projects_on_project_id ON ci_runner_projects USING
CREATE INDEX index_ci_runner_projects_on_runner_id ON ci_runner_projects USING btree (runner_id); CREATE INDEX index_ci_runner_projects_on_runner_id ON ci_runner_projects USING btree (runner_id);
CREATE INDEX index_ci_runners_on_active ON ci_runners USING btree (active, id);
CREATE INDEX index_ci_runners_on_contacted_at_and_id_desc ON ci_runners USING btree (contacted_at, id DESC); CREATE INDEX index_ci_runners_on_contacted_at_and_id_desc ON ci_runners USING btree (contacted_at, id DESC);
CREATE INDEX index_ci_runners_on_contacted_at_and_id_where_inactive ON ci_runners USING btree (contacted_at DESC, id DESC) WHERE (active = false); CREATE INDEX index_ci_runners_on_contacted_at_and_id_where_inactive ON ci_runners USING btree (contacted_at DESC, id DESC) WHERE (active = false);
...@@ -5503,6 +5503,7 @@ The connection type for [`CiRunner`](#cirunner). ...@@ -5503,6 +5503,7 @@ The connection type for [`CiRunner`](#cirunner).
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <a id="cirunnerconnectioncount"></a>`count` | [`Int!`](#int) | Total count of collection. |
| <a id="cirunnerconnectionedges"></a>`edges` | [`[CiRunnerEdge]`](#cirunneredge) | A list of edges. | | <a id="cirunnerconnectionedges"></a>`edges` | [`[CiRunnerEdge]`](#cirunneredge) | A list of edges. |
| <a id="cirunnerconnectionnodes"></a>`nodes` | [`[CiRunner]`](#cirunner) | A list of nodes. | | <a id="cirunnerconnectionnodes"></a>`nodes` | [`[CiRunner]`](#cirunner) | A list of nodes. |
| <a id="cirunnerconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. | | <a id="cirunnerconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
...@@ -589,6 +589,87 @@ LIMIT 20 ...@@ -589,6 +589,87 @@ LIMIT 20
NOTE: NOTE:
To make the query efficient, the following columns need to be covered with an index: `project_id`, `issue_type`, `created_at`, and `id`. To make the query efficient, the following columns need to be covered with an index: `project_id`, `issue_type`, `created_at`, and `id`.
#### Using calculated ORDER BY expression
The following example orders epic records by the duration between the creation time and closed
time. It is calculated with the following formula:
```sql
SELECT EXTRACT('epoch' FROM epics.closed_at - epics.created_at) FROM epics
```
The query above returns the duration in seconds (`double precision`) between the two timestamp
columns in seconds. To order the records by this expression, you must reference it
in the `ORDER BY` clause:
```sql
SELECT EXTRACT('epoch' FROM epics.closed_at - epics.created_at)
FROM epics
ORDER BY EXTRACT('epoch' FROM epics.closed_at - epics.created_at) DESC
```
To make this ordering efficient on the group-level with the in-operator optimization, use a
custom `ORDER BY` configuration. Since the duration is not a distinct value (no unique index
present), you must add a tie-breaker column (`id`).
The following example shows the final `ORDER BY` clause:
```sql
ORDER BY extract('epoch' FROM epics.closed_at - epics.created_at) DESC, epics.id DESC
```
Snippet for loading records ordered by the calcualted duration:
```ruby
arel_table = Epic.arel_table
order = Gitlab::Pagination::Keyset::Order.build([
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'duration_in_seconds',
order_expression: Arel.sql('EXTRACT(EPOCH FROM epics.closed_at - epics.created_at)').desc,
distinct: false,
sql_type: 'double precision' # important for calculated SQL expressions
),
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'id',
order_expression: arel_table[:id].desc
)
])
records = Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder.new(
scope: Epic.where.not(closed_at: nil).reorder(order), # filter out NULL values
array_scope: Group.find(9970).self_and_descendants.select(:id),
array_mapping_scope: -> (id_expression) { Epic.where(Epic.arel_table[:group_id].eq(id_expression)) }
).execute.limit(20)
puts records.pluck(:duration_in_seconds, :id) # other columnns are not available
```
Building the query requires quite a bit of configuration. For the order configuration you
can find more information within the
[complex order configuration](keyset_pagination.md#complex-order-configuration)
section for keyset paginated database queries.
The query requires a specialized database index:
```sql
CREATE INDEX index_epics_on_duration ON epics USING btree (group_id, EXTRACT(EPOCH FROM epics.closed_at - epics.created_at) DESC, id DESC) WHERE (closed_at IS NOT NULL);
```
Notice that the `finder_query` parameter is not used. The query only returns the `ORDER BY` columns
which are the `duration_in_seconds` (calculated column) and the `id` columns. This is a limitation
of the feature, defining the `finder_query` with calculated `ORDER BY` expressions is not supported.
To get the complete database records, an extra query can be invoked by the returned `id` column:
```ruby
records_by_id = records.index_by(&:id)
complete_records = Epic.where(id: records_by_id.keys).index_by(&:id)
# Printing the complete records according to the `ORDER BY` clause
records_by_id.each do |id, _|
puts complete_records[id].attributes
end
```
#### Batch iteration #### Batch iteration
Batch iteration over the records is possible via the keyset `Iterator` class. Batch iteration over the records is possible via the keyset `Iterator` class.
......
...@@ -14,6 +14,15 @@ With customer relations management (CRM) you can create a record of contacts ...@@ -14,6 +14,15 @@ With customer relations management (CRM) you can create a record of contacts
You can use contacts and organizations to tie work to customers for billing and reporting purposes. You can use contacts and organizations to tie work to customers for billing and reporting purposes.
To read more about what is planned for the future, see [issue 2256](https://gitlab.com/gitlab-org/gitlab/-/issues/2256). To read more about what is planned for the future, see [issue 2256](https://gitlab.com/gitlab-org/gitlab/-/issues/2256).
## Permissions
| Permission | Guest | Reporter | Developer, Maintainer, and Owner |
| ---------- | ---------------- | -------- | -------------------------------- |
| View contacts/organizations | | ✓ | ✓ |
| View issue contacts | | ✓ | ✓ |
| Add/remove issue contacts | | ✓ | ✓ |
| Create/edit contacts/organizations | | | ✓ |
## Enable customer relations management (CRM) ## Enable customer relations management (CRM)
To enable customer relations management in a group: To enable customer relations management in a group:
...@@ -122,10 +131,6 @@ API. ...@@ -122,10 +131,6 @@ API.
### Add or remove issue contacts ### Add or remove issue contacts
Prerequisites:
- You must have at least the [Developer role](../permissions.md#project-members-permissions) for a group.
### Add contacts to an issue ### Add contacts to an issue
To add contacts to an issue use the `/add_contacts` To add contacts to an issue use the `/add_contacts`
......
<script> <script>
import emptySvg from '@gitlab/svgs/dist/illustrations/security-dashboard-empty-state.svg'; import emptySvg from '@gitlab/svgs/dist/illustrations/security-dashboard-empty-state.svg';
import { GlEmptyState, GlIcon, GlTooltipDirective } from '@gitlab/ui'; import { GlEmptyState, GlIcon, GlAlert, GlTooltipDirective } from '@gitlab/ui';
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import OrderSummary from 'ee/subscriptions/buy_addons_shared/components/order_summary.vue'; import OrderSummary from 'ee/subscriptions/buy_addons_shared/components/order_summary.vue';
import { ERROR_FETCHING_DATA_HEADER, ERROR_FETCHING_DATA_DESCRIPTION } from '~/ensure_data'; import { ERROR_FETCHING_DATA_HEADER, ERROR_FETCHING_DATA_DESCRIPTION } from '~/ensure_data';
...@@ -18,6 +18,7 @@ export default { ...@@ -18,6 +18,7 @@ export default {
Checkout, Checkout,
GlEmptyState, GlEmptyState,
GlIcon, GlIcon,
GlAlert,
OrderSummary, OrderSummary,
}, },
directives: { directives: {
...@@ -36,6 +37,7 @@ export default { ...@@ -36,6 +37,7 @@ export default {
data() { data() {
return { return {
hasError: false, hasError: false,
alertMessage: '',
}; };
}, },
computed: { computed: {
...@@ -88,6 +90,9 @@ export default { ...@@ -88,6 +90,9 @@ export default {
selectedPlanPrice: price, selectedPlanPrice: price,
}); });
}, },
alertError(errorMessage) {
this.alertMessage = errorMessage;
},
}, },
apollo: { apollo: {
plans: { plans: {
...@@ -130,8 +135,11 @@ export default { ...@@ -130,8 +135,11 @@ export default {
class="row gl-flex-grow-1 gl-flex-direction-column gl-flex-nowrap gl-lg-flex-direction-row gl-xl-flex-direction-row gl-lg-flex-wrap gl-xl-flex-wrap" class="row gl-flex-grow-1 gl-flex-direction-column gl-flex-nowrap gl-lg-flex-direction-row gl-xl-flex-direction-row gl-lg-flex-wrap gl-xl-flex-wrap"
> >
<div <div
class="checkout-pane gl-px-3 gl-align-items-center gl-bg-gray-10 col-lg-7 gl-display-flex gl-flex-direction-column gl-flex-grow-1" class="checkout-pane gl-px-3 gl-pt-5 gl-align-items-center gl-bg-gray-10 col-lg-7 gl-display-flex gl-flex-direction-column gl-flex-grow-1"
> >
<gl-alert v-if="alertMessage" class="checkout-alert" variant="danger" :dismissible="false">
{{ alertMessage }}
</gl-alert>
<checkout :plan="plan"> <checkout :plan="plan">
<template #purchase-details> <template #purchase-details>
<addon-purchase-details <addon-purchase-details
...@@ -162,6 +170,7 @@ export default { ...@@ -162,6 +170,7 @@ export default {
:plan="plan" :plan="plan"
:title="config.title" :title="config.title"
:purchase-has-expiration="config.hasExpiration" :purchase-has-expiration="config.hasExpiration"
@alertError="alertError"
> >
<template #price-per-unit="{ price }"> <template #price-per-unit="{ price }">
{{ pricePerUnitLabel(price) }} {{ pricePerUnitLabel(price) }}
......
...@@ -4,7 +4,7 @@ import find from 'lodash/find'; ...@@ -4,7 +4,7 @@ import find from 'lodash/find';
import { logError } from '~/lib/logger'; import { logError } from '~/lib/logger';
import { TAX_RATE } from 'ee/subscriptions/new/constants'; import { TAX_RATE } from 'ee/subscriptions/new/constants';
import { CUSTOMERSDOT_CLIENT } from 'ee/subscriptions/buy_addons_shared/constants'; import { CUSTOMERSDOT_CLIENT, I18N_API_ERROR } from 'ee/subscriptions/buy_addons_shared/constants';
import formattingMixins from 'ee/subscriptions/new/formatting_mixins'; import formattingMixins from 'ee/subscriptions/new/formatting_mixins';
import { sprintf } from '~/locale'; import { sprintf } from '~/locale';
...@@ -62,13 +62,16 @@ export default { ...@@ -62,13 +62,16 @@ export default {
}, },
manual: true, manual: true,
result({ data }) { result({ data }) {
if (data.orderPreview) { if (data.errors) {
this.hasError = true;
} else if (data.orderPreview) {
this.endDate = data.orderPreview.targetDate; this.endDate = data.orderPreview.targetDate;
this.proratedAmount = data.orderPreview.amount; this.proratedAmount = data.orderPreview.amount;
} }
}, },
error(error) { error(error) {
this.hasError = true; this.hasError = true;
this.$emit('alertError', I18N_API_ERROR);
logError(error); logError(error);
}, },
skip() { skip() {
...@@ -91,7 +94,7 @@ export default { ...@@ -91,7 +94,7 @@ export default {
return this.plan.pricePerYear; return this.plan.pricePerYear;
}, },
totalExVat() { totalExVat() {
return this.isLoading return this.hideAmount
? 0 ? 0
: this.proratedAmount || this.subscription.quantity * this.selectedPlanPrice; : this.proratedAmount || this.subscription.quantity * this.selectedPlanPrice;
}, },
...@@ -99,7 +102,7 @@ export default { ...@@ -99,7 +102,7 @@ export default {
return TAX_RATE * this.totalExVat; return TAX_RATE * this.totalExVat;
}, },
totalAmount() { totalAmount() {
return this.isLoading ? 0 : this.proratedAmount || this.totalExVat + this.vat; return this.hideAmount ? 0 : this.proratedAmount || this.totalExVat + this.vat;
}, },
quantityPresent() { quantityPresent() {
return this.subscription.quantity > 0; return this.subscription.quantity > 0;
...@@ -116,6 +119,9 @@ export default { ...@@ -116,6 +119,9 @@ export default {
isLoading() { isLoading() {
return this.$apollo.loading; return this.$apollo.loading;
}, },
hideAmount() {
return this.isLoading || this.hasError;
},
}, },
taxRate: TAX_RATE, taxRate: TAX_RATE,
}; };
......
...@@ -63,3 +63,7 @@ export const I18N_SUMMARY_TAX_NOTE = s__( ...@@ -63,3 +63,7 @@ export const I18N_SUMMARY_TAX_NOTE = s__(
'Checkout|(may be %{linkStart}charged upon purchase%{linkEnd})', 'Checkout|(may be %{linkStart}charged upon purchase%{linkEnd})',
); );
export const I18N_SUMMARY_TOTAL = s__('Checkout|Total'); export const I18N_SUMMARY_TOTAL = s__('Checkout|Total');
export const I18N_API_ERROR = s__(
'Checkout|An unknown error has occurred. Please try again by refreshing this page.',
);
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
class="row gl-flex-grow-1 gl-flex-direction-column gl-flex-nowrap gl-lg-flex-direction-row gl-xl-flex-direction-row gl-lg-flex-wrap gl-xl-flex-wrap" class="row gl-flex-grow-1 gl-flex-direction-column gl-flex-nowrap gl-lg-flex-direction-row gl-xl-flex-direction-row gl-lg-flex-wrap gl-xl-flex-wrap"
> >
<div <div
class="checkout-pane gl-px-3 gl-align-items-center gl-bg-gray-10 col-lg-7 gl-display-flex gl-flex-direction-column gl-flex-grow-1" class="checkout-pane gl-px-3 gl-pt-5 gl-align-items-center gl-bg-gray-10 col-lg-7 gl-display-flex gl-flex-direction-column gl-flex-grow-1"
> >
<slot name="checkout"></slot> <slot name="checkout"></slot>
</div> </div>
......
...@@ -20,15 +20,22 @@ $subscriptions-full-width-lg: 541px; ...@@ -20,15 +20,22 @@ $subscriptions-full-width-lg: 541px;
} }
} }
.checkout-alert {
width: 100%;
max-width: $subscriptions-full-width-md;
@media(min-width: map-get($grid-breakpoints, lg)) {
max-width: $subscriptions-full-width-lg;
}
}
.checkout { .checkout {
align-items: center; align-items: center;
flex-grow: 1; flex-grow: 1;
margin-top: $gl-padding;
max-width: $subscriptions-full-width-md; max-width: $subscriptions-full-width-md;
@media(min-width: map-get($grid-breakpoints, lg)) { @media(min-width: map-get($grid-breakpoints, lg)) {
justify-content: inherit !important; justify-content: inherit !important;
margin-top: $gl-padding-32;
max-width: none; max-width: none;
} }
......
import { GlEmptyState } from '@gitlab/ui'; import { GlEmptyState, GlAlert } from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils'; import { createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import { pick } from 'lodash'; import { pick } from 'lodash';
...@@ -13,6 +13,7 @@ import { ...@@ -13,6 +13,7 @@ import {
I18N_STORAGE_TITLE, I18N_STORAGE_TITLE,
I18N_STORAGE_PRICE_PER_UNIT, I18N_STORAGE_PRICE_PER_UNIT,
I18N_STORAGE_TOOLTIP_NOTE, I18N_STORAGE_TOOLTIP_NOTE,
I18N_API_ERROR,
planTags, planTags,
STORAGE_PER_PACK, STORAGE_PER_PACK,
} from 'ee/subscriptions/buy_addons_shared/constants'; } from 'ee/subscriptions/buy_addons_shared/constants';
...@@ -67,6 +68,7 @@ describe('Buy Storage App', () => { ...@@ -67,6 +68,7 @@ describe('Buy Storage App', () => {
const getStoragePlan = () => pick(mockStoragePlans[0], ['id', 'code', 'pricePerYear', 'name']); const getStoragePlan = () => pick(mockStoragePlans[0], ['id', 'code', 'pricePerYear', 'name']);
const findCheckout = () => wrapper.findComponent(Checkout); const findCheckout = () => wrapper.findComponent(Checkout);
const findOrderSummary = () => wrapper.findComponent(OrderSummary); const findOrderSummary = () => wrapper.findComponent(OrderSummary);
const findAlert = () => wrapper.findComponent(GlAlert);
const findPriceLabel = () => wrapper.findByTestId('price-per-unit'); const findPriceLabel = () => wrapper.findByTestId('price-per-unit');
const findQuantityText = () => wrapper.findByTestId('addon-quantity-text'); const findQuantityText = () => wrapper.findByTestId('addon-quantity-text');
const findRootElement = () => wrapper.findByTestId('buy-addons-shared'); const findRootElement = () => wrapper.findByTestId('buy-addons-shared');
...@@ -100,6 +102,20 @@ describe('Buy Storage App', () => { ...@@ -100,6 +102,20 @@ describe('Buy Storage App', () => {
title: I18N_STORAGE_TITLE, title: I18N_STORAGE_TITLE,
}); });
}); });
describe('and an error occurred', () => {
beforeEach(() => {
findOrderSummary().vm.$emit('alertError', I18N_API_ERROR);
});
it('shows the alert', () => {
expect(findAlert().props()).toMatchObject({
dismissible: false,
variant: 'danger',
});
expect(findAlert().text()).toBe(I18N_API_ERROR);
});
});
}); });
describe('when data is not received', () => { describe('when data is not received', () => {
......
...@@ -15,7 +15,7 @@ import { ...@@ -15,7 +15,7 @@ import {
import createMockApollo, { createMockClient } from 'helpers/mock_apollo_helper'; import createMockApollo, { createMockClient } from 'helpers/mock_apollo_helper';
import orderPreviewQuery from 'ee/subscriptions/graphql/queries/order_preview.customer.query.graphql'; import orderPreviewQuery from 'ee/subscriptions/graphql/queries/order_preview.customer.query.graphql';
import { CUSTOMERSDOT_CLIENT } from 'ee/subscriptions/buy_addons_shared/constants'; import { CUSTOMERSDOT_CLIENT, I18N_API_ERROR } from 'ee/subscriptions/buy_addons_shared/constants';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(VueApollo); localVue.use(VueApollo);
...@@ -123,7 +123,7 @@ describe('Order Summary', () => { ...@@ -123,7 +123,7 @@ describe('Order Summary', () => {
createComponent(apolloProvider, { purchaseHasExpiration: true }); createComponent(apolloProvider, { purchaseHasExpiration: true });
}); });
it('renders amount from the state', () => { it('renders default amount without proration from the state', () => {
expect(findAmount().text()).toBe('$60'); expect(findAmount().text()).toBe('$60');
}); });
}); });
...@@ -139,8 +139,32 @@ describe('Order Summary', () => { ...@@ -139,8 +139,32 @@ describe('Order Summary', () => {
createComponent(apolloProvider, { purchaseHasExpiration: true }); createComponent(apolloProvider, { purchaseHasExpiration: true });
}); });
it('renders amount from the state', () => { it('does not render amount', () => {
expect(findAmount().text()).toBe('$60'); expect(findAmount().text()).toBe('-');
});
});
describe('calls api that returns an error', () => {
beforeEach(() => {
jest.spyOn(console, 'error').mockImplementation(() => {});
const orderPreviewQueryMock = jest.fn().mockRejectedValue(new Error('An error happened!'));
const apolloProvider = createMockApolloProvider(
{ subscription: { quantity: 1 } },
orderPreviewQueryMock,
);
createComponent(apolloProvider, { purchaseHasExpiration: true });
});
it('does not render amount', () => {
expect(findAmount().text()).toBe('-');
});
it('should emit `alertError` event', async () => {
jest.spyOn(wrapper.vm, '$emit');
await wrapper.vm.$nextTick();
expect(wrapper.vm.$emit).toHaveBeenCalledWith('alertError', I18N_API_ERROR);
}); });
}); });
......
...@@ -14,7 +14,8 @@ module Gitlab ...@@ -14,7 +14,8 @@ module Gitlab
Status::Build::WaitingForResource, Status::Build::WaitingForResource,
Status::Build::Preparing, Status::Build::Preparing,
Status::Build::Pending, Status::Build::Pending,
Status::Build::Skipped], Status::Build::Skipped,
Status::Build::WaitingForApproval],
[Status::Build::Cancelable, [Status::Build::Cancelable,
Status::Build::Retryable], Status::Build::Retryable],
[Status::Build::FailedUnmetPrerequisites, [Status::Build::FailedUnmetPrerequisites,
......
# frozen_string_literal: true
module Gitlab
module Ci
module Status
module Build
class WaitingForApproval < Status::Extended
def illustration
{
image: 'illustrations/manual_action.svg',
size: 'svg-394',
title: 'Waiting for approval',
content: "This job deploys to the protected environment \"#{subject.deployment&.environment&.name}\" which requires approvals. Use the Deployments API to approve or reject the deployment."
}
end
def self.matches?(build, user)
build.waiting_for_deployment_approval?
end
end
end
end
end
end
...@@ -114,6 +114,20 @@ module Gitlab ...@@ -114,6 +114,20 @@ module Gitlab
# - When the order is a calculated expression or the column is in another table (JOIN-ed) # - When the order is a calculated expression or the column is in another table (JOIN-ed)
# #
# If the add_to_projections is true, the query builder will automatically add the column to the SELECT values # If the add_to_projections is true, the query builder will automatically add the column to the SELECT values
#
# **sql_type**
#
# The SQL type of the column or SQL expression. This is an optional field which is only required when using the
# column with the InOperatorOptimization class.
#
# Example: When the order expression is a calculated SQL expression.
#
# {
# attribute_name: 'id_times_count',
# order_expression: Arel.sql('(id * count)').asc,
# sql_type: 'integer' # the SQL type here must match with the type of the produced data by the order_expression. Putting 'text' here would be incorrect.
# }
#
class ColumnOrderDefinition class ColumnOrderDefinition
REVERSED_ORDER_DIRECTIONS = { asc: :desc, desc: :asc }.freeze REVERSED_ORDER_DIRECTIONS = { asc: :desc, desc: :asc }.freeze
REVERSED_NULL_POSITIONS = { nulls_first: :nulls_last, nulls_last: :nulls_first }.freeze REVERSED_NULL_POSITIONS = { nulls_first: :nulls_last, nulls_last: :nulls_first }.freeze
...@@ -122,7 +136,8 @@ module Gitlab ...@@ -122,7 +136,8 @@ module Gitlab
attr_reader :attribute_name, :column_expression, :order_expression, :add_to_projections, :order_direction attr_reader :attribute_name, :column_expression, :order_expression, :add_to_projections, :order_direction
def initialize(attribute_name:, order_expression:, column_expression: nil, reversed_order_expression: nil, nullable: :not_nullable, distinct: true, order_direction: nil, add_to_projections: false) # rubocop: disable Metrics/ParameterLists
def initialize(attribute_name:, order_expression:, column_expression: nil, reversed_order_expression: nil, nullable: :not_nullable, distinct: true, order_direction: nil, sql_type: nil, add_to_projections: false)
@attribute_name = attribute_name @attribute_name = attribute_name
@order_expression = order_expression @order_expression = order_expression
@column_expression = column_expression || calculate_column_expression(order_expression) @column_expression = column_expression || calculate_column_expression(order_expression)
...@@ -130,8 +145,10 @@ module Gitlab ...@@ -130,8 +145,10 @@ module Gitlab
@reversed_order_expression = reversed_order_expression || calculate_reversed_order(order_expression) @reversed_order_expression = reversed_order_expression || calculate_reversed_order(order_expression)
@nullable = parse_nullable(nullable, distinct) @nullable = parse_nullable(nullable, distinct)
@order_direction = parse_order_direction(order_expression, order_direction) @order_direction = parse_order_direction(order_expression, order_direction)
@sql_type = sql_type
@add_to_projections = add_to_projections @add_to_projections = add_to_projections
end end
# rubocop: enable Metrics/ParameterLists
def reverse def reverse
self.class.new( self.class.new(
...@@ -185,6 +202,12 @@ module Gitlab ...@@ -185,6 +202,12 @@ module Gitlab
sql_string sql_string
end end
def sql_type
raise Gitlab::Pagination::Keyset::SqlTypeMissingError.for_column(self) if @sql_type.nil?
@sql_type
end
private private
attr_reader :reversed_order_expression, :nullable, :distinct attr_reader :reversed_order_expression, :nullable, :distinct
......
...@@ -4,23 +4,35 @@ module Gitlab ...@@ -4,23 +4,35 @@ module Gitlab
module Pagination module Pagination
module Keyset module Keyset
module InOperatorOptimization module InOperatorOptimization
# This class is used for wrapping an Arel column with
# convenient helper methods in order to make the query
# building for the InOperatorOptimization a bit cleaner.
class ColumnData class ColumnData
attr_reader :original_column_name, :as, :arel_table attr_reader :original_column_name, :as, :arel_table
def initialize(original_column_name, as, arel_table) # column - name of the DB column
@original_column_name = original_column_name.to_s # as - custom alias for the column
# arel_table - relation where the column is located
def initialize(column, as, arel_table)
@original_column_name = column
@as = as.to_s @as = as.to_s
@arel_table = arel_table @arel_table = arel_table
end end
# Generates: `issues.name AS my_alias`
def projection def projection
arel_column.as(as) arel_column.as(as)
end end
# Generates: issues.name`
def arel_column def arel_column
arel_table[original_column_name] arel_table[original_column_name]
end end
# overridden in OrderByColumnData class
alias_method :column_expression, :arel_column
# Generates: `issues.my_alias`
def arel_column_as def arel_column_as
arel_table[as] arel_table[as]
end end
...@@ -29,8 +41,9 @@ module Gitlab ...@@ -29,8 +41,9 @@ module Gitlab
"#{arel_table.name}_#{original_column_name}_array" "#{arel_table.name}_#{original_column_name}_array"
end end
# Generates: SELECT ARRAY_AGG(...) AS issues_name_array
def array_aggregated_column def array_aggregated_column
Arel::Nodes::NamedFunction.new('ARRAY_AGG', [arel_column]).as(array_aggregated_column_name) Arel::Nodes::NamedFunction.new('ARRAY_AGG', [column_expression]).as(array_aggregated_column_name)
end end
end end
end end
......
# frozen_string_literal: true
module Gitlab
module Pagination
module Keyset
module InOperatorOptimization
class OrderByColumnData < ColumnData
extend ::Gitlab::Utils::Override
attr_reader :column
# column - a ColumnOrderDefinition object
# as - custom alias for the column
# arel_table - relation where the column is located
def initialize(column, as, arel_table)
super(column.attribute_name.to_s, as, arel_table)
@column = column
end
override :arel_column
def arel_column
column.column_expression
end
override :column_expression
def column_expression
arel_table[original_column_name]
end
def column_for_projection
column.column_expression.as(original_column_name)
end
end
end
end
end
end
...@@ -9,16 +9,16 @@ module Gitlab ...@@ -9,16 +9,16 @@ module Gitlab
# This class exposes collection methods for the order by columns # This class exposes collection methods for the order by columns
# #
# Example: by modelling the `issues.created_at ASC, issues.id ASC` ORDER BY # Example: by modeling the `issues.created_at ASC, issues.id ASC` ORDER BY
# SQL clause, this class will receive two ColumnOrderDefinition objects # SQL clause, this class will receive two ColumnOrderDefinition objects
def initialize(columns, arel_table) def initialize(columns, arel_table)
@columns = columns.map do |column| @columns = columns.map do |column|
ColumnData.new(column.attribute_name, "order_by_columns_#{column.attribute_name}", arel_table) OrderByColumnData.new(column, "order_by_columns_#{column.attribute_name}", arel_table)
end end
end end
def arel_columns def arel_columns
columns.map(&:arel_column) columns.map(&:column_for_projection)
end end
def array_aggregated_columns def array_aggregated_columns
......
...@@ -120,7 +120,7 @@ module Gitlab ...@@ -120,7 +120,7 @@ module Gitlab
.from(array_cte) .from(array_cte)
.join(Arel.sql("LEFT JOIN LATERAL (#{initial_keyset_query.to_sql}) #{table_name} ON TRUE")) .join(Arel.sql("LEFT JOIN LATERAL (#{initial_keyset_query.to_sql}) #{table_name} ON TRUE"))
order_by_columns.each { |column| q.where(column.arel_column.not_eq(nil)) } order_by_columns.each { |column| q.where(column.column_expression.not_eq(nil)) }
q.as('array_scope_lateral_query') q.as('array_scope_lateral_query')
end end
...@@ -231,7 +231,7 @@ module Gitlab ...@@ -231,7 +231,7 @@ module Gitlab
order order
.apply_cursor_conditions(keyset_scope, cursor_values, use_union_optimization: true) .apply_cursor_conditions(keyset_scope, cursor_values, use_union_optimization: true)
.reselect(*order_by_columns.arel_columns) .reselect(*order_by_columns.map(&:column_for_projection))
.limit(1) .limit(1)
end end
......
...@@ -12,11 +12,7 @@ module Gitlab ...@@ -12,11 +12,7 @@ module Gitlab
end end
def initializer_columns def initializer_columns
order_by_columns.map do |column| order_by_columns.map { |column_data| null_with_type_cast(column_data) }
column_name = column.original_column_name.to_s
type = model.columns_hash[column_name].sql_type
"NULL::#{type} AS #{column_name}"
end
end end
def columns def columns
...@@ -30,6 +26,15 @@ module Gitlab ...@@ -30,6 +26,15 @@ module Gitlab
private private
attr_reader :model, :order_by_columns attr_reader :model, :order_by_columns
def null_with_type_cast(column_data)
column_name = column_data.original_column_name.to_s
active_record_column = model.columns_hash[column_name]
type = active_record_column ? active_record_column.sql_type : column_data.column.sql_type
"NULL::#{type} AS #{column_name}"
end
end end
end end
end end
......
...@@ -9,6 +9,8 @@ module Gitlab ...@@ -9,6 +9,8 @@ module Gitlab
RECORDS_COLUMN = 'records' RECORDS_COLUMN = 'records'
def initialize(finder_query, model, order_by_columns) def initialize(finder_query, model, order_by_columns)
verify_order_by_attributes_on_model!(model, order_by_columns)
@finder_query = finder_query @finder_query = finder_query
@order_by_columns = order_by_columns @order_by_columns = order_by_columns
@table_name = model.table_name @table_name = model.table_name
...@@ -34,6 +36,20 @@ module Gitlab ...@@ -34,6 +36,20 @@ module Gitlab
private private
attr_reader :finder_query, :order_by_columns, :table_name attr_reader :finder_query, :order_by_columns, :table_name
def verify_order_by_attributes_on_model!(model, order_by_columns)
order_by_columns.map(&:column).each do |column|
unless model.columns_hash[column.attribute_name.to_s]
text = <<~TEXT
The "RecordLoaderStrategy" does not support the following ORDER BY column because
it's not available on the \"#{model.table_name}\" table: #{column.attribute_name}
Omit the "finder_query" parameter to use the "OrderValuesLoaderStrategy".
TEXT
raise text
end
end
end
end end
end end
end end
......
# frozen_string_literal: true
module Gitlab
module Pagination
module Keyset
class SqlTypeMissingError < StandardError
def self.for_column(column)
message = <<~TEXT
The "sql_type" attribute is not set for the following column definition:
#{column.attribute_name}
See the ColumnOrderDefinition class for more context.
TEXT
new(message)
end
end
end
end
end
...@@ -100,7 +100,7 @@ module Sidebars ...@@ -100,7 +100,7 @@ module Sidebars
::Sidebars::MenuItem.new( ::Sidebars::MenuItem.new(
title: _('Google Cloud'), title: _('Google Cloud'),
link: project_google_cloud_index_path(context.project), link: project_google_cloud_index_path(context.project),
active_routes: { controller: [:google_cloud, :service_accounts] }, active_routes: { controller: [:google_cloud, :service_accounts, :deployments] },
item_id: :google_cloud item_id: :google_cloud
) )
end end
......
...@@ -6906,6 +6906,9 @@ msgstr "" ...@@ -6906,6 +6906,9 @@ msgstr ""
msgid "Checkout|(x%{quantity})" msgid "Checkout|(x%{quantity})"
msgstr "" msgstr ""
msgid "Checkout|An unknown error has occurred. Please try again by refreshing this page."
msgstr ""
msgid "Checkout|Billing address" msgid "Checkout|Billing address"
msgstr "" msgstr ""
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Ci::Status::Build::WaitingForApproval do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
subject { described_class.new(Gitlab::Ci::Status::Core.new(build, user)) }
describe '#illustration' do
let(:build) { create(:ci_build, :manual, environment: 'production', project: project) }
before do
environment = create(:environment, name: 'production', project: project)
create(:deployment, :blocked, project: project, environment: environment, deployable: build)
end
it { expect(subject.illustration).to include(:image, :size) }
it { expect(subject.illustration[:title]).to eq('Waiting for approval') }
it { expect(subject.illustration[:content]).to include('This job deploys to the protected environment "production"') }
end
describe '.matches?' do
subject { described_class.matches?(build, user) }
let(:build) { create(:ci_build, :manual, environment: 'production', project: project) }
before do
create(:deployment, deployment_status, deployable: build, project: project)
end
context 'when build is waiting for approval' do
let(:deployment_status) { :blocked }
it 'is a correct match' do
expect(subject).to be_truthy
end
end
context 'when build is not waiting for approval' do
let(:deployment_status) { :created }
it 'does not match' do
expect(subject).to be_falsey
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::OrderByColumnData do
let(:arel_table) { Issue.arel_table }
let(:column) do
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: :id,
column_expression: arel_table[:id],
order_expression: arel_table[:id].desc
)
end
subject(:column_data) { described_class.new(column, 'column_alias', arel_table) }
describe '#arel_column' do
it 'delegates to column_expression' do
expect(column_data.arel_column).to eq(column.column_expression)
end
end
describe '#column_for_projection' do
it 'returns the expression with AS using the original column name' do
expect(column_data.column_for_projection.to_sql).to eq('"issues"."id" AS id')
end
end
describe '#projection' do
it 'returns the expression with AS using the specified column lias' do
expect(column_data.projection.to_sql).to eq('"issues"."id" AS column_alias')
end
end
end
...@@ -33,14 +33,14 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder ...@@ -33,14 +33,14 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder
] ]
end end
shared_examples 'correct ordering examples' do let(:iterator) do
let(:iterator) do Gitlab::Pagination::Keyset::Iterator.new(
Gitlab::Pagination::Keyset::Iterator.new( scope: scope.limit(batch_size),
scope: scope.limit(batch_size), in_operator_optimization_options: in_operator_optimization_options
in_operator_optimization_options: in_operator_optimization_options )
) end
end
shared_examples 'correct ordering examples' do |opts = {}|
let(:all_records) do let(:all_records) do
all_records = [] all_records = []
iterator.each_batch(of: batch_size) do |records| iterator.each_batch(of: batch_size) do |records|
...@@ -49,8 +49,10 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder ...@@ -49,8 +49,10 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder
all_records all_records
end end
it 'returns records in correct order' do unless opts[:skip_finder_query_test]
expect(all_records).to eq(expected_order) it 'returns records in correct order' do
expect(all_records).to eq(expected_order)
end
end end
context 'when not passing the finder query' do context 'when not passing the finder query' do
...@@ -248,4 +250,57 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder ...@@ -248,4 +250,57 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder
expect { described_class.new(**options).execute }.to raise_error(/The order on the scope does not support keyset pagination/) expect { described_class.new(**options).execute }.to raise_error(/The order on the scope does not support keyset pagination/)
end end
context 'when ordering by SQL expression' do
let(:order) do
# ORDER BY (id * 10), id
Gitlab::Pagination::Keyset::Order.build([
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'id_multiplied_by_ten',
order_expression: Arel.sql('(id * 10)').asc,
sql_type: 'integer'
),
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: :id,
order_expression: Issue.arel_table[:id].asc
)
])
end
let(:scope) { Issue.reorder(order) }
let(:expected_order) { issues.sort_by(&:id) }
let(:in_operator_optimization_options) do
{
array_scope: Project.where(namespace_id: top_level_group.self_and_descendants.select(:id)).select(:id),
array_mapping_scope: -> (id_expression) { Issue.where(Issue.arel_table[:project_id].eq(id_expression)) }
}
end
context 'when iterating records one by one' do
let(:batch_size) { 1 }
it_behaves_like 'correct ordering examples', skip_finder_query_test: true
end
context 'when iterating records with LIMIT 3' do
let(:batch_size) { 3 }
it_behaves_like 'correct ordering examples', skip_finder_query_test: true
end
context 'when passing finder query' do
let(:batch_size) { 3 }
it 'raises error, loading complete rows are not supported with SQL expressions' do
in_operator_optimization_options[:finder_query] = -> (_, _) { Issue.select(:id, '(id * 10)').where(id: -1) }
expect(in_operator_optimization_options[:finder_query]).not_to receive(:call)
expect do
iterator.each_batch(of: batch_size) { |records| records.to_a }
end.to raise_error /The "RecordLoaderStrategy" does not support/
end
end
end
end end
...@@ -31,4 +31,41 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::Strategies::O ...@@ -31,4 +31,41 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::Strategies::O
]) ])
end end
end end
context 'when an SQL expression is given' do
context 'when the sql_type attribute is missing' do
let(:order) do
Gitlab::Pagination::Keyset::Order.build([
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'id_times_ten',
order_expression: Arel.sql('id * 10').asc
)
])
end
let(:keyset_scope) { Project.order(order) }
it 'raises error' do
expect { strategy.initializer_columns }.to raise_error(Gitlab::Pagination::Keyset::SqlTypeMissingError)
end
end
context 'when the sql_type_attribute is present' do
let(:order) do
Gitlab::Pagination::Keyset::Order.build([
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'id_times_ten',
order_expression: Arel.sql('id * 10').asc,
sql_type: 'integer'
)
])
end
let(:keyset_scope) { Project.order(order) }
it 'returns the initializer columns' do
expect(strategy.initializer_columns).to eq(['NULL::integer AS id_times_ten'])
end
end
end
end end
...@@ -11,8 +11,6 @@ RSpec.describe GroupPolicy do ...@@ -11,8 +11,6 @@ RSpec.describe GroupPolicy do
it do it do
expect_allowed(:read_group) expect_allowed(:read_group)
expect_allowed(:read_crm_organization)
expect_allowed(:read_crm_contact)
expect_allowed(:read_counts) expect_allowed(:read_counts)
expect_allowed(*read_group_permissions) expect_allowed(*read_group_permissions)
expect_disallowed(:upload_file) expect_disallowed(:upload_file)
...@@ -21,11 +19,13 @@ RSpec.describe GroupPolicy do ...@@ -21,11 +19,13 @@ RSpec.describe GroupPolicy do
expect_disallowed(*maintainer_permissions) expect_disallowed(*maintainer_permissions)
expect_disallowed(*owner_permissions) expect_disallowed(*owner_permissions)
expect_disallowed(:read_namespace) expect_disallowed(:read_namespace)
expect_disallowed(:read_crm_organization)
expect_disallowed(:read_crm_contact)
end end
end end
context 'with no user and public project' do context 'with no user and public project' do
let(:project) { create(:project, :public) } let(:project) { create(:project, :public, group: create(:group, :crm_enabled)) }
let(:current_user) { nil } let(:current_user) { nil }
before do before do
...@@ -41,7 +41,7 @@ RSpec.describe GroupPolicy do ...@@ -41,7 +41,7 @@ RSpec.describe GroupPolicy do
end end
context 'with foreign user and public project' do context 'with foreign user and public project' do
let(:project) { create(:project, :public) } let(:project) { create(:project, :public, group: create(:group, :crm_enabled)) }
let(:current_user) { create(:user) } let(:current_user) { create(:user) }
before do before do
...@@ -67,7 +67,7 @@ RSpec.describe GroupPolicy do ...@@ -67,7 +67,7 @@ RSpec.describe GroupPolicy do
it { expect_allowed(*read_group_permissions) } it { expect_allowed(*read_group_permissions) }
context 'in subgroups' do context 'in subgroups' do
let(:subgroup) { create(:group, :private, parent: group) } let(:subgroup) { create(:group, :private, :crm_enabled, parent: group) }
let(:project) { create(:project, namespace: subgroup) } let(:project) { create(:project, namespace: subgroup) }
it { expect_allowed(*read_group_permissions) } it { expect_allowed(*read_group_permissions) }
...@@ -235,7 +235,7 @@ RSpec.describe GroupPolicy do ...@@ -235,7 +235,7 @@ RSpec.describe GroupPolicy do
describe 'private nested group use the highest access level from the group and inherited permissions' do describe 'private nested group use the highest access level from the group and inherited permissions' do
let_it_be(:nested_group) do let_it_be(:nested_group) do
create(:group, :private, :owner_subgroup_creation_only, parent: group) create(:group, :private, :owner_subgroup_creation_only, :crm_enabled, parent: group)
end end
before_all do before_all do
...@@ -342,7 +342,7 @@ RSpec.describe GroupPolicy do ...@@ -342,7 +342,7 @@ RSpec.describe GroupPolicy do
let(:current_user) { owner } let(:current_user) { owner }
context 'when the group share_with_group_lock is enabled' do context 'when the group share_with_group_lock is enabled' do
let(:group) { create(:group, share_with_group_lock: true, parent: parent) } let(:group) { create(:group, :crm_enabled, share_with_group_lock: true, parent: parent) }
before do before do
group.add_owner(owner) group.add_owner(owner)
...@@ -350,10 +350,10 @@ RSpec.describe GroupPolicy do ...@@ -350,10 +350,10 @@ RSpec.describe GroupPolicy do
context 'when the parent group share_with_group_lock is enabled' do context 'when the parent group share_with_group_lock is enabled' do
context 'when the group has a grandparent' do context 'when the group has a grandparent' do
let(:parent) { create(:group, share_with_group_lock: true, parent: grandparent) } let(:parent) { create(:group, :crm_enabled, share_with_group_lock: true, parent: grandparent) }
context 'when the grandparent share_with_group_lock is enabled' do context 'when the grandparent share_with_group_lock is enabled' do
let(:grandparent) { create(:group, share_with_group_lock: true) } let(:grandparent) { create(:group, :crm_enabled, share_with_group_lock: true) }
context 'when the current_user owns the parent' do context 'when the current_user owns the parent' do
before do before do
...@@ -379,7 +379,7 @@ RSpec.describe GroupPolicy do ...@@ -379,7 +379,7 @@ RSpec.describe GroupPolicy do
end end
context 'when the grandparent share_with_group_lock is disabled' do context 'when the grandparent share_with_group_lock is disabled' do
let(:grandparent) { create(:group) } let(:grandparent) { create(:group, :crm_enabled) }
context 'when the current_user owns the parent' do context 'when the current_user owns the parent' do
before do before do
...@@ -396,7 +396,7 @@ RSpec.describe GroupPolicy do ...@@ -396,7 +396,7 @@ RSpec.describe GroupPolicy do
end end
context 'when the group does not have a grandparent' do context 'when the group does not have a grandparent' do
let(:parent) { create(:group, share_with_group_lock: true) } let(:parent) { create(:group, :crm_enabled, share_with_group_lock: true) }
context 'when the current_user owns the parent' do context 'when the current_user owns the parent' do
before do before do
...@@ -413,7 +413,7 @@ RSpec.describe GroupPolicy do ...@@ -413,7 +413,7 @@ RSpec.describe GroupPolicy do
end end
context 'when the parent group share_with_group_lock is disabled' do context 'when the parent group share_with_group_lock is disabled' do
let(:parent) { create(:group) } let(:parent) { create(:group, :crm_enabled) }
it { expect_allowed(:change_share_with_group_lock) } it { expect_allowed(:change_share_with_group_lock) }
end end
...@@ -698,7 +698,7 @@ RSpec.describe GroupPolicy do ...@@ -698,7 +698,7 @@ RSpec.describe GroupPolicy do
end end
it_behaves_like 'clusterable policies' do it_behaves_like 'clusterable policies' do
let(:clusterable) { create(:group) } let(:clusterable) { create(:group, :crm_enabled) }
let(:cluster) do let(:cluster) do
create(:cluster, create(:cluster,
:provided_by_gcp, :provided_by_gcp,
...@@ -708,7 +708,7 @@ RSpec.describe GroupPolicy do ...@@ -708,7 +708,7 @@ RSpec.describe GroupPolicy do
end end
describe 'update_max_artifacts_size' do describe 'update_max_artifacts_size' do
let(:group) { create(:group, :public) } let(:group) { create(:group, :public, :crm_enabled) }
context 'when no user' do context 'when no user' do
let(:current_user) { nil } let(:current_user) { nil }
...@@ -738,7 +738,7 @@ RSpec.describe GroupPolicy do ...@@ -738,7 +738,7 @@ RSpec.describe GroupPolicy do
end end
describe 'design activity' do describe 'design activity' do
let_it_be(:group) { create(:group, :public) } let_it_be(:group) { create(:group, :public, :crm_enabled) }
let(:current_user) { nil } let(:current_user) { nil }
...@@ -935,8 +935,6 @@ RSpec.describe GroupPolicy do ...@@ -935,8 +935,6 @@ RSpec.describe GroupPolicy do
it { is_expected.to be_allowed(:read_package) } it { is_expected.to be_allowed(:read_package) }
it { is_expected.to be_allowed(:read_group) } it { is_expected.to be_allowed(:read_group) }
it { is_expected.to be_allowed(:read_crm_organization) }
it { is_expected.to be_allowed(:read_crm_contact) }
it { is_expected.to be_disallowed(:create_package) } it { is_expected.to be_disallowed(:create_package) }
end end
...@@ -946,8 +944,6 @@ RSpec.describe GroupPolicy do ...@@ -946,8 +944,6 @@ RSpec.describe GroupPolicy do
it { is_expected.to be_allowed(:create_package) } it { is_expected.to be_allowed(:create_package) }
it { is_expected.to be_allowed(:read_package) } it { is_expected.to be_allowed(:read_package) }
it { is_expected.to be_allowed(:read_group) } it { is_expected.to be_allowed(:read_group) }
it { is_expected.to be_allowed(:read_crm_organization) }
it { is_expected.to be_allowed(:read_crm_contact) }
it { is_expected.to be_disallowed(:destroy_package) } it { is_expected.to be_disallowed(:destroy_package) }
end end
...@@ -967,7 +963,7 @@ RSpec.describe GroupPolicy do ...@@ -967,7 +963,7 @@ RSpec.describe GroupPolicy do
it_behaves_like 'Self-managed Core resource access tokens' it_behaves_like 'Self-managed Core resource access tokens'
context 'support bot' do context 'support bot' do
let_it_be(:group) { create(:group, :private) } let_it_be(:group) { create(:group, :private, :crm_enabled) }
let_it_be(:current_user) { User.support_bot } let_it_be(:current_user) { User.support_bot }
before do before do
...@@ -977,7 +973,7 @@ RSpec.describe GroupPolicy do ...@@ -977,7 +973,7 @@ RSpec.describe GroupPolicy do
it { expect_disallowed(:read_label) } it { expect_disallowed(:read_label) }
context 'when group hierarchy has a project with service desk enabled' do context 'when group hierarchy has a project with service desk enabled' do
let_it_be(:subgroup) { create(:group, :private, parent: group) } let_it_be(:subgroup) { create(:group, :private, :crm_enabled, parent: group) }
let_it_be(:project) { create(:project, group: subgroup, service_desk_enabled: true) } let_it_be(:project) { create(:project, group: subgroup, service_desk_enabled: true) }
it { expect_allowed(:read_label) } it { expect_allowed(:read_label) }
...@@ -1170,7 +1166,7 @@ RSpec.describe GroupPolicy do ...@@ -1170,7 +1166,7 @@ RSpec.describe GroupPolicy do
end end
context 'when crm_enabled is false' do context 'when crm_enabled is false' do
let(:group) { create(:group) } let(:group) { create(:group, :crm_enabled) }
let(:current_user) { owner } let(:current_user) { owner }
it { is_expected.to be_disallowed(:read_crm_contact) } it { is_expected.to be_disallowed(:read_crm_contact) }
......
...@@ -108,6 +108,7 @@ RSpec.describe ::Packages::Npm::PackagePresenter do ...@@ -108,6 +108,7 @@ RSpec.describe ::Packages::Npm::PackagePresenter do
context 'with packages_installable_package_files disabled' do context 'with packages_installable_package_files disabled' do
before do before do
stub_feature_flags(packages_installable_package_files: false) stub_feature_flags(packages_installable_package_files: false)
package2.package_files.id_not_in(package_file_pending_destruction.id).delete_all
end end
it 'returns them' do it 'returns them' do
......
...@@ -73,7 +73,7 @@ RSpec.describe Groups::Crm::ContactsController do ...@@ -73,7 +73,7 @@ RSpec.describe Groups::Crm::ContactsController do
let(:group) { create(:group, :public, :crm_enabled) } let(:group) { create(:group, :public, :crm_enabled) }
context 'with anonymous user' do context 'with anonymous user' do
it_behaves_like 'ok response with index template' it_behaves_like 'response with 404 status'
end end
end end
end end
......
...@@ -73,7 +73,7 @@ RSpec.describe Groups::Crm::OrganizationsController do ...@@ -73,7 +73,7 @@ RSpec.describe Groups::Crm::OrganizationsController do
let(:group) { create(:group, :public, :crm_enabled) } let(:group) { create(:group, :public, :crm_enabled) }
context 'with anonymous user' do context 'with anonymous user' do
it_behaves_like 'ok response with index template' it_behaves_like 'response with 404 status'
end end
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::GoogleCloud::DeploymentsController do
let_it_be(:project) { create(:project, :public) }
let_it_be(:user_guest) { create(:user) }
let_it_be(:user_developer) { create(:user) }
let_it_be(:user_maintainer) { create(:user) }
let_it_be(:user_creator) { project.creator }
let_it_be(:unauthorized_members) { [user_guest, user_developer] }
let_it_be(:authorized_members) { [user_maintainer, user_creator] }
let_it_be(:urls_list) { %W[#{project_google_cloud_deployments_cloud_run_path(project)} #{project_google_cloud_deployments_cloud_storage_path(project)}] }
before do
project.add_guest(user_guest)
project.add_developer(user_developer)
project.add_maintainer(user_maintainer)
end
describe "Routes must be restricted behind Google OAuth2" do
context 'when a public request is made' do
it 'returns not found on GET request' do
urls_list.each do |url|
get url
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context 'when unauthorized members make requests' do
it 'returns not found on GET request' do
urls_list.each do |url|
unauthorized_members.each do |unauthorized_member|
sign_in(unauthorized_member)
get url
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
context 'when authorized members make requests' do
it 'redirects on GET request' do
urls_list.each do |url|
authorized_members.each do |authorized_member|
sign_in(authorized_member)
get url
expect(response).to redirect_to(assigns(:authorize_url))
end
end
end
end
end
describe 'Authorized GET project/-/google_cloud/deployments/cloud_run' do
let_it_be(:url) { "#{project_google_cloud_deployments_cloud_run_path(project)}" }
before do
allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
allow(client).to receive(:validate_token).and_return(true)
end
end
it 'renders placeholder' do
authorized_members.each do |authorized_member|
sign_in(authorized_member)
get url
expect(response).to have_gitlab_http_status(:ok)
end
end
end
describe 'Authorized GET project/-/google_cloud/deployments/cloud_storage' do
let_it_be(:url) { "#{project_google_cloud_deployments_cloud_storage_path(project)}" }
before do
allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
allow(client).to receive(:validate_token).and_return(true)
end
end
it 'renders placeholder' do
authorized_members.each do |authorized_member|
sign_in(authorized_member)
get url
expect(response).to have_gitlab_http_status(:ok)
end
end
end
end
...@@ -71,6 +71,24 @@ RSpec.describe Ci::ProcessSyncEventsService do ...@@ -71,6 +71,24 @@ RSpec.describe Ci::ProcessSyncEventsService do
expect { execute }.not_to change(Projects::SyncEvent, :count) expect { execute }.not_to change(Projects::SyncEvent, :count)
end end
end end
it 'does not delete non-executed events' do
new_project = create(:project)
sync_event_class.delete_all
project1.update!(group: parent_group_2)
new_project.update!(group: parent_group_1)
project2.update!(group: parent_group_1)
new_project_sync_event = new_project.sync_events.last
allow(sync_event_class).to receive(:preload_synced_relation).and_return(
sync_event_class.where.not(id: new_project_sync_event)
)
expect { execute }.to change(Projects::SyncEvent, :count).from(3).to(1)
expect(new_project_sync_event.reload).to be_persisted
end
end end
context 'for Namespaces::SyncEvent' do context 'for Namespaces::SyncEvent' do
......
...@@ -22,10 +22,10 @@ RSpec.describe Issues::UpdateService, :mailer do ...@@ -22,10 +22,10 @@ RSpec.describe Issues::UpdateService, :mailer do
end end
before_all do before_all do
project.add_maintainer(user) group.add_maintainer(user)
project.add_developer(user2) group.add_developer(user2)
project.add_developer(user3) group.add_developer(user3)
project.add_guest(guest) group.add_guest(guest)
end end
describe 'execute' do describe 'execute' do
......
...@@ -28,6 +28,8 @@ RSpec.shared_context 'GroupPolicy context' do ...@@ -28,6 +28,8 @@ RSpec.shared_context 'GroupPolicy context' do
read_metrics_dashboard_annotation read_metrics_dashboard_annotation
read_prometheus read_prometheus
read_package_settings read_package_settings
read_crm_contact
read_crm_organization
] ]
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment