Commit 7630770c authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 87139d33 28a2f781
...@@ -6,7 +6,6 @@ import JobPill from './job_pill.vue'; ...@@ -6,7 +6,6 @@ import JobPill from './job_pill.vue';
import StagePill from './stage_pill.vue'; import StagePill from './stage_pill.vue';
import { generateLinksData } from './drawing_utils'; import { generateLinksData } from './drawing_utils';
import { parseData } from '../parsing_utils'; import { parseData } from '../parsing_utils';
import { unwrapArrayOfJobs } from '../unwrapping_utils';
import { DRAW_FAILURE, DEFAULT, INVALID_CI_CONFIG, EMPTY_PIPELINE_DATA } from '../../constants'; import { DRAW_FAILURE, DEFAULT, INVALID_CI_CONFIG, EMPTY_PIPELINE_DATA } from '../../constants';
import { createJobsHash, generateJobNeedsDict } from '../../utils'; import { createJobsHash, generateJobNeedsDict } from '../../utils';
import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants'; import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants';
...@@ -136,7 +135,7 @@ export default { ...@@ -136,7 +135,7 @@ export default {
methods: { methods: {
prepareLinkData() { prepareLinkData() {
try { try {
const arrayOfJobs = unwrapArrayOfJobs(this.pipelineStages); const arrayOfJobs = this.pipelineStages.flatMap(({ groups }) => groups);
const parsedData = parseData(arrayOfJobs); const parsedData = parseData(arrayOfJobs);
this.links = generateLinksData(parsedData, this.$options.CONTAINER_ID); this.links = generateLinksData(parsedData, this.$options.CONTAINER_ID);
} catch { } catch {
......
/**
* This function takes the stages and add the stage name
* at the group level as `category` to have an easier
* implementation while constructions nodes with D3
* @param {Array} stages
* @returns {Array} - Array of stages with stage name at the group level as `category`
*/
export const unwrapArrayOfJobs = (stages = []) => {
return stages
.map(({ name, groups }) => {
return groups.map((group) => {
return { category: name, ...group };
});
})
.flat(2);
};
const unwrapGroups = (stages) => { const unwrapGroups = (stages) => {
return stages.map((stage) => { return stages.map((stage) => {
const { const {
......
...@@ -10,6 +10,8 @@ ...@@ -10,6 +10,8 @@
.sidebar-context-title .sidebar-context-title
= @group.name = @group.name
%ul.sidebar-top-level-items.qa-group-sidebar %ul.sidebar-top-level-items.qa-group-sidebar
= render_if_exists 'layouts/nav/sidebar/group_trial_status_widget', group: @group
- if group_sidebar_link?(:overview) - if group_sidebar_link?(:overview)
- paths = group_overview_nav_link_paths - paths = group_overview_nav_link_paths
= nav_link(path: paths, unless: -> { current_path?('groups/contribution_analytics#show') }, html_options: { class: 'home' }) do = nav_link(path: paths, unless: -> { current_path?('groups/contribution_analytics#show') }, html_options: { class: 'home' }) do
......
...@@ -842,6 +842,41 @@ Example: ...@@ -842,6 +842,41 @@ Example:
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
``` ```
#### `match_schema` and `match_response_schema`
The `match_schema` matcher allows validating that the subject matches a
[JSON schema](https://json-schema.org/). The item inside `expect` can be
a JSON string or a JSON-compatible data structure.
`match_response_schema` is a convenience matcher for using with a
response object. from a [request
spec](testing_levels.md#integration-tests).
Examples:
```ruby
# Matches against spec/fixtures/api/schemas/prometheus/additional_metrics_query_result.json
expect(data).to match_schema('prometheus/additional_metrics_query_result')
# Matches against ee/spec/fixtures/api/schemas/board.json
expect(data).to match_schema('board', dir: 'ee')
# Matches against a schema made up of Ruby data structures
expect(data).to match_schema(Atlassian::Schemata.build_info)
```
#### `be_valid_json`
`be_valid_json` allows validating that a string parses as JSON and gives
a non-empty result. To combine it with the schema matching above, use
`and`:
```ruby
expect(json_string).to be_valid_json
expect(json_string).to be_valid_json.and match_schema(schema)
```
### Testing query performance ### Testing query performance
Testing query performance allows us to: Testing query performance allows us to:
......
# frozen_string_literal: true
module TrialStatusWidgetHelper
def eligible_for_trial_status_widget_experiment?(group)
group.trial_active?
end
def show_trial_status_widget?(group)
return false unless ::Gitlab::CurrentSettings.should_check_namespace_plan?
return false unless experiment_enabled?(:show_trial_status_in_sidebar, subject: group)
eligible_for_trial_status_widget_experiment?(group)
end
def trial_days_remaining_in_words(group)
num_of_days = trial_days_remaining(group)
plan_title = group.gitlab_subscription&.plan_title
ns_(
"Trials|%{plan} Trial %{en_dash} %{num} day left",
"Trials|%{plan} Trial %{en_dash} %{num} days left",
num_of_days
) % { plan: plan_title, num: num_of_days, en_dash: '–' }
end
def trial_days_remaining(group)
(group.trial_ends_on - Date.current).to_i
end
def total_trial_duration(group)
(group.trial_ends_on - group.trial_starts_on).to_i
end
def trial_days_used(group)
total_trial_duration(group) - trial_days_remaining(group)
end
# A value between 0 & 100 rounded to 2 decimal places
def trial_percentage_complete(group)
(trial_days_used(group) / total_trial_duration(group).to_f * 100).round(2)
end
end
- return unless show_trial_status_widget?(group)
= nav_link do
= link_to group_billings_path(group), title: trial_days_remaining_in_words(group) do
.gl-display-flex.gl-flex-direction-column.gl-align-items-stretch.gl-w-full
%span.gl-display-flex.gl-align-items-center
%span.nav-icon-container.svg-container
= image_tag 'illustrations/golden_tanuki.svg', class: 'svg', size: 16
%span.nav-item-name.gl-white-space-normal
= trial_days_remaining_in_words(group)
%span.gl-display-flex.gl-align-items-stretch.gl-mt-4
.progress.gl-flex-grow-1{ value: trial_percentage_complete(group) }
.progress-bar{ role: 'progressbar', 'aria-valuemin': 0, 'aria-valuemax': 100, 'aria-valuenow': trial_percentage_complete(group), style: "width: #{trial_percentage_complete(group)}%" }
---
name: show_trial_status_in_sidebar_experiment_percentage
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50090
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/281019
milestone: '13.8'
type: experiment
group: group::conversion
default_enabled: false
...@@ -5,23 +5,25 @@ FactoryBot.define do ...@@ -5,23 +5,25 @@ FactoryBot.define do
namespace namespace
association :hosted_plan, factory: :gold_plan association :hosted_plan, factory: :gold_plan
seats { 10 } seats { 10 }
start_date { Date.today } start_date { Date.current }
end_date { Date.today.advance(years: 1) } end_date { Date.current.advance(years: 1) }
trial { false } trial { false }
trait :expired do trait :expired do
start_date { Date.today.advance(years: -1, months: -1) } start_date { Date.current.advance(years: -1, months: -1) }
end_date { Date.today.advance(months: -1) } end_date { Date.current.advance(months: -1) }
end end
trait :active_trial do trait :active_trial do
trial { true } trial { true }
trial_ends_on { Date.today.advance(months: 1) } trial_starts_on { Date.current.advance(days: -15) }
trial_ends_on { Date.current.advance(days: 15) }
end end
trait :expired_trial do trait :expired_trial do
trial { true } trial { true }
trial_ends_on { Date.today.advance(days: -1) } trial_starts_on { Date.current.advance(days: -31) }
trial_ends_on { Date.current.advance(days: -1) }
end end
trait :default do trait :default do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe TrialStatusWidgetHelper do
let_it_be(:group) { build(:group) }
let!(:subscription) { build(:gitlab_subscription, :active_trial, namespace: group) }
describe '#eligible_for_trial_status_widget_experiment?' do
subject { helper.eligible_for_trial_status_widget_experiment?(group) }
context 'when group has an active trial' do
it { is_expected.to be_truthy }
end
context 'when group does not have an active trial' do
before do
allow(group).to receive(:trial_active?).and_return(false)
end
it { is_expected.to be_falsy }
end
end
describe '#show_trial_status_widget?' do
let(:experiment_enabled) { true }
let(:eligible_for_experiment) { true }
before do
allow(helper).to receive(:experiment_enabled?).with(:show_trial_status_in_sidebar, subject: group).and_return(experiment_enabled)
allow(helper).to receive(:eligible_for_trial_status_widget_experiment?).and_return(eligible_for_experiment)
end
subject { helper.show_trial_status_widget?(group) }
context 'when the check_namespace_plan application setting is off' do
it { is_expected.to be_falsy }
end
context 'when the check_namespace_plan application setting is on' do
before do
stub_application_setting(check_namespace_plan: true)
end
context 'and the experiment is enabled and the user is eligible for it' do
it { is_expected.to be_truthy }
end
context 'but the experiment is not enabled' do
let(:experiment_enabled) { false }
it { is_expected.to be_falsy }
end
context 'but the user is not eligible for the experiment' do
let(:eligible_for_experiment) { false }
it { is_expected.to be_falsy }
end
end
end
describe '#trial_days_remaining_in_words' do
subject { helper.trial_days_remaining_in_words(group) }
context 'when there are 0 days remaining' do
before do
subscription.trial_ends_on = Date.current
end
it { is_expected.to eq('Gold Trial – 0 days left') }
end
context 'when there is 1 day remaining' do
before do
subscription.trial_ends_on = Date.current.advance(days: 1)
end
it { is_expected.to eq('Gold Trial – 1 day left') }
end
context 'when there are 2+ days remaining' do
before do
subscription.trial_ends_on = Date.current.advance(days: 13)
end
it { is_expected.to eq('Gold Trial – 13 days left') }
end
end
describe '#trial_days_remaining' do
subject { helper.trial_days_remaining(group) }
context 'at the beginning of a trial' do
before do
subscription.trial_starts_on = Date.current
subscription.trial_ends_on = Date.current.advance(days: 30)
end
it { is_expected.to eq(30) }
end
context 'in the middle of a trial' do
it { is_expected.to eq(15) }
end
context 'at the end of a trial' do
before do
subscription.trial_starts_on = Date.current.advance(days: -30)
subscription.trial_ends_on = Date.current
end
it { is_expected.to eq(0) }
end
end
describe '#total_trial_duration' do
subject { helper.total_trial_duration(group) }
context 'for a default trial duration' do
it { is_expected.to eq(30) }
end
context 'for a custom trial duration' do
before do
subscription.trial_starts_on = Date.current.advance(days: -5)
subscription.trial_ends_on = Date.current.advance(days: 5)
end
it { is_expected.to eq(10) }
end
end
describe '#trial_days_used' do
subject { helper.trial_days_used(group) }
context 'at the beginning of a trial' do
before do
subscription.trial_starts_on = Date.current
subscription.trial_ends_on = Date.current.advance(days: 30)
end
it { is_expected.to eq(0) }
end
context 'in the middle of a trial' do
it { is_expected.to eq(15) }
end
context 'at the end of a trial' do
before do
subscription.trial_starts_on = Date.current.advance(days: -30)
subscription.trial_ends_on = Date.current
end
it { is_expected.to eq(30) }
end
end
describe '#trial_percentage_complete' do
subject { helper.trial_percentage_complete(group) }
context 'at the beginning of a trial' do
before do
subscription.trial_starts_on = Date.current
subscription.trial_ends_on = Date.current.advance(days: 30)
end
it { is_expected.to eq(0.0) }
end
context 'in the middle of a trial' do
it { is_expected.to eq(50.0) }
end
context 'at the end of a trial' do
before do
subscription.trial_starts_on = Date.current.advance(days: -30)
subscription.trial_ends_on = Date.current
end
it { is_expected.to eq(100.0) }
end
end
end
...@@ -5,10 +5,34 @@ require 'spec_helper' ...@@ -5,10 +5,34 @@ require 'spec_helper'
RSpec.describe 'layouts/nav/sidebar/_group' do RSpec.describe 'layouts/nav/sidebar/_group' do
before do before do
assign(:group, group) assign(:group, group)
allow(view).to receive(:show_trial_status_widget?).with(group).and_return(show_trial_status_widget)
end end
let(:group) { create(:group) } let(:group) { create(:group) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:show_trial_status_widget) { false }
describe 'trial status widget' do
let!(:gitlab_subscription) { create(:gitlab_subscription, :active_trial, namespace: group) }
context 'when the experiment is off' do
it 'is not visible' do
render
expect(rendered).not_to have_text /Gold Trial – \d+ days left/
end
end
context 'when the experiment is on' do
let(:show_trial_status_widget) { true }
it 'is visible' do
render
expect(rendered).to have_text 'Gold Trial – 15 days left'
end
end
end
describe 'contribution analytics tab' do describe 'contribution analytics tab' do
let!(:current_user) { create(:user) } let!(:current_user) { create(:user) }
......
...@@ -100,6 +100,9 @@ module Gitlab ...@@ -100,6 +100,9 @@ module Gitlab
invite_members_new_dropdown: { invite_members_new_dropdown: {
tracking_category: 'Growth::Expansion::Experiment::InviteMembersNewDropdown' tracking_category: 'Growth::Expansion::Experiment::InviteMembersNewDropdown'
}, },
show_trial_status_in_sidebar: {
tracking_category: 'Growth::Conversion::Experiment::ShowTrialStatusInSidebar'
},
trial_onboarding_issues: { trial_onboarding_issues: {
tracking_category: 'Growth::Conversion::Experiment::TrialOnboardingIssues' tracking_category: 'Growth::Conversion::Experiment::TrialOnboardingIssues'
} }
......
...@@ -29638,6 +29638,11 @@ msgstr "" ...@@ -29638,6 +29638,11 @@ msgstr ""
msgid "Trending" msgid "Trending"
msgstr "" msgstr ""
msgid "Trials|%{plan} Trial %{en_dash} %{num} day left"
msgid_plural "Trials|%{plan} Trial %{en_dash} %{num} days left"
msgstr[0] ""
msgstr[1] ""
msgid "Trials|Create a new group to start your GitLab Gold trial." msgid "Trials|Create a new group to start your GitLab Gold trial."
msgstr "" msgstr ""
......
import { import {
unwrapArrayOfJobs,
unwrapGroups, unwrapGroups,
unwrapNodesWithName, unwrapNodesWithName,
unwrapStagesWithNeeds, unwrapStagesWithNeeds,
...@@ -95,29 +94,6 @@ const completeMock = [ ...@@ -95,29 +94,6 @@ const completeMock = [
]; ];
describe('Shared pipeline unwrapping utils', () => { describe('Shared pipeline unwrapping utils', () => {
describe('unwrapArrayOfJobs', () => {
it('returns an empty array if the input is an empty undefined', () => {
expect(unwrapArrayOfJobs(undefined)).toEqual([]);
});
it('returns an empty array if the input is an empty array', () => {
expect(unwrapArrayOfJobs([])).toEqual([]);
});
it('returns a flatten array of each job with their data and stage name', () => {
expect(
unwrapArrayOfJobs([
{ name: 'build', groups: [{ name: 'job_a_1' }, { name: 'job_a_2' }] },
{ name: 'test', groups: [{ name: 'job_b' }] },
]),
).toMatchObject([
{ category: 'build', name: 'job_a_1' },
{ category: 'build', name: 'job_a_2' },
{ category: 'test', name: 'job_b' },
]);
});
});
describe('unwrapGroups', () => { describe('unwrapGroups', () => {
it('takes stages without nodes and returns the unwrapped groups', () => { it('takes stages without nodes and returns the unwrapped groups', () => {
expect(unwrapGroups(stagesAndGroups)[0].groups).toEqual(groupsArray); expect(unwrapGroups(stagesAndGroups)[0].groups).toEqual(groupsArray);
......
...@@ -114,7 +114,7 @@ RSpec.describe Atlassian::JiraConnect::Client do ...@@ -114,7 +114,7 @@ RSpec.describe Atlassian::JiraConnect::Client do
end end
let(:body) do let(:body) do
matcher = be_valid_json.according_to_schema(schema) matcher = be_valid_json.and match_schema(schema)
->(text) { matcher.matches?(text) } ->(text) { matcher.matches?(text) }
end end
...@@ -164,7 +164,7 @@ RSpec.describe Atlassian::JiraConnect::Client do ...@@ -164,7 +164,7 @@ RSpec.describe Atlassian::JiraConnect::Client do
end end
let(:body) do let(:body) do
matcher = be_valid_json.according_to_schema(build_info_payload_schema) matcher = be_valid_json.and match_schema(build_info_payload_schema)
->(text) { matcher.matches?(text) } ->(text) { matcher.matches?(text) }
end end
......
...@@ -23,7 +23,7 @@ RSpec.describe Atlassian::JiraConnect::Serializers::BuildEntity do ...@@ -23,7 +23,7 @@ RSpec.describe Atlassian::JiraConnect::Serializers::BuildEntity do
end end
it 'is invalid, since it has no issue keys' do it 'is invalid, since it has no issue keys' do
expect(subject.to_json).not_to be_valid_json.according_to_schema(Atlassian::Schemata.build_info) expect(subject.to_json).not_to match_schema(Atlassian::Schemata.build_info)
end end
end end
end end
...@@ -43,7 +43,7 @@ RSpec.describe Atlassian::JiraConnect::Serializers::BuildEntity do ...@@ -43,7 +43,7 @@ RSpec.describe Atlassian::JiraConnect::Serializers::BuildEntity do
describe '#to_json' do describe '#to_json' do
it 'is valid according to the build info schema' do it 'is valid according to the build info schema' do
expect(subject.to_json).to be_valid_json.according_to_schema(Atlassian::Schemata.build_info) expect(subject.to_json).to be_valid_json.and match_schema(Atlassian::Schemata.build_info)
end end
end end
end end
......
...@@ -23,7 +23,7 @@ RSpec.describe Atlassian::JiraConnect::Serializers::DeploymentEntity do ...@@ -23,7 +23,7 @@ RSpec.describe Atlassian::JiraConnect::Serializers::DeploymentEntity do
end end
it 'is invalid, since it has no issue keys' do it 'is invalid, since it has no issue keys' do
expect(subject.to_json).not_to be_valid_json.according_to_schema(Atlassian::Schemata.deployment_info) expect(subject.to_json).not_to match_schema(Atlassian::Schemata.deployment_info)
end end
end end
end end
...@@ -86,7 +86,7 @@ RSpec.describe Atlassian::JiraConnect::Serializers::DeploymentEntity do ...@@ -86,7 +86,7 @@ RSpec.describe Atlassian::JiraConnect::Serializers::DeploymentEntity do
describe '#to_json' do describe '#to_json' do
it 'is valid according to the deployment info schema' do it 'is valid according to the deployment info schema' do
expect(subject.to_json).to be_valid_json.according_to_schema(Atlassian::Schemata.deployment_info) expect(subject.to_json).to be_valid_json.and match_schema(Atlassian::Schemata.deployment_info)
end end
end end
end end
......
# frozen_string_literal: true # frozen_string_literal: true
RSpec::Matchers.define :be_valid_json do RSpec::Matchers.define :be_valid_json do
def according_to_schema(schema)
@schema = schema
self
end
match do |actual| match do |actual|
data = Gitlab::Json.parse(actual) Gitlab::Json.parse(actual).present?
if @schema.present?
@validation_errors = JSON::Validator.fully_validate(@schema, data)
@validation_errors.empty?
else
data.present?
end
rescue JSON::ParserError => e rescue JSON::ParserError => e
@error = e @error = e
false false
...@@ -23,8 +11,6 @@ RSpec::Matchers.define :be_valid_json do ...@@ -23,8 +11,6 @@ RSpec::Matchers.define :be_valid_json do
def failure_message def failure_message
if @error if @error
"Parse failed with error: #{@error}" "Parse failed with error: #{@error}"
elsif @validation_errors.present?
"Validation failed because #{@validation_errors.join(', and ')}"
else else
"Parsing did not return any data" "Parsing did not return any data"
end end
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
module SchemaPath module SchemaPath
def self.expand(schema, dir = nil) def self.expand(schema, dir = nil)
return schema unless schema.is_a?(String)
if Gitlab.ee? && dir.nil? if Gitlab.ee? && dir.nil?
ee_path = expand(schema, 'ee') ee_path = expand(schema, 'ee')
......
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