Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
bd5ff5cb65
commit
aa072fd68c
28 changed files with 316 additions and 181 deletions
|
@ -175,8 +175,8 @@ That's all of the required database changes.
|
|||
#### Step 1. Implement replication and verification
|
||||
|
||||
- [ ] Add the following lines to the `cool_widget` model to accomplish some important tasks:
|
||||
- Include `Gitlab::Geo::ReplicableModel` in the `CoolWidget` class, and specify the Replicator class `with_replicator Geo::CoolWidgetReplicator`.
|
||||
- Include the `::Gitlab::Geo::VerificationState` concern.
|
||||
- Include `::Geo::ReplicableModel` in the `CoolWidget` class, and specify the Replicator class `with_replicator Geo::CoolWidgetReplicator`.
|
||||
- Include the `::Geo::VerifiableModel` concern.
|
||||
- Delegate verification related methods to the `cool_widget_state` model.
|
||||
- For verification, override some scopes to use the `cool_widget_states` table instead of the model table.
|
||||
- Implement the `verification_state_object` method to return the object that holds
|
||||
|
@ -192,8 +192,8 @@ That's all of the required database changes.
|
|||
|
||||
class CoolWidget < ApplicationRecord
|
||||
...
|
||||
include ::Gitlab::Geo::ReplicableModel
|
||||
include ::Gitlab::Geo::VerificationState
|
||||
include ::Geo::ReplicableModel
|
||||
include ::Geo::VerifiableModel
|
||||
|
||||
with_replicator Geo::CoolWidgetReplicator
|
||||
|
||||
|
|
|
@ -179,8 +179,8 @@ That's all of the required database changes.
|
|||
#### Step 1. Implement replication and verification
|
||||
|
||||
- [ ] Add the following lines to the `cool_widget` model to accomplish some important tasks:
|
||||
- Include `Gitlab::Geo::ReplicableModel` in the `CoolWidget` class, and specify the Replicator class `with_replicator Geo::CoolWidgetReplicator`.
|
||||
- Include the `::Gitlab::Geo::VerificationState` concern.
|
||||
- Include `::Geo::ReplicableModel` in the `CoolWidget` class, and specify the Replicator class `with_replicator Geo::CoolWidgetReplicator`.
|
||||
- Include the `::Geo::VerifiableModel` concern.
|
||||
- Delegate verification related methods to the `cool_widget_state` model.
|
||||
- For verification, override some scopes to use the `cool_widget_states` table instead of the model table.
|
||||
- Implement the `verification_state_object` method to return the object that holds
|
||||
|
@ -194,8 +194,8 @@ That's all of the required database changes.
|
|||
|
||||
class CoolWidget < ApplicationRecord
|
||||
...
|
||||
include ::Gitlab::Geo::ReplicableModel
|
||||
include ::Gitlab::Geo::VerificationState
|
||||
include ::Geo::ReplicableModel
|
||||
include ::Geo::VerifiableModel
|
||||
|
||||
with_replicator Geo::CoolWidgetReplicator
|
||||
|
||||
|
|
5
Gemfile
5
Gemfile
|
@ -98,10 +98,7 @@ gem 'rack-cors', '~> 1.0.6', require: 'rack/cors'
|
|||
|
||||
# GraphQL API
|
||||
gem 'graphql', '~> 1.11.10'
|
||||
# NOTE: graphiql-rails v1.5+ doesn't work: https://gitlab.com/gitlab-org/gitlab/issues/31771
|
||||
# TODO: remove app/views/graphiql/rails/editors/show.html.erb when https://github.com/rmosolgo/graphiql-rails/pull/71 is released:
|
||||
# https://gitlab.com/gitlab-org/gitlab/issues/31747
|
||||
gem 'graphiql-rails', '~> 1.4.10'
|
||||
gem 'graphiql-rails', '~> 1.8'
|
||||
gem 'apollo_upload_server', '~> 2.1.0'
|
||||
gem 'graphql-docs', '~> 1.6.0', group: [:development, :test]
|
||||
gem 'graphlient', '~> 0.4.0' # Used by BulkImport feature (group::import)
|
||||
|
|
|
@ -561,7 +561,7 @@ GEM
|
|||
grape_logging (1.8.3)
|
||||
grape
|
||||
rack
|
||||
graphiql-rails (1.4.10)
|
||||
graphiql-rails (1.8.0)
|
||||
railties
|
||||
sprockets-rails
|
||||
graphlient (0.4.0)
|
||||
|
@ -1498,7 +1498,7 @@ DEPENDENCIES
|
|||
grape-entity (~> 0.10.0)
|
||||
grape-path-helpers (~> 1.7.0)
|
||||
grape_logging (~> 1.7)
|
||||
graphiql-rails (~> 1.4.10)
|
||||
graphiql-rails (~> 1.8)
|
||||
graphlient (~> 0.4.0)
|
||||
graphql (~> 1.11.10)
|
||||
graphql-docs (~> 1.6.0)
|
||||
|
|
|
@ -60,10 +60,7 @@ export const trackSaasTrialSubmit = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
const form = document.getElementById('new_trial');
|
||||
form.addEventListener('submit', () => {
|
||||
pushEvent('saasTrialSubmit');
|
||||
});
|
||||
pushEvent('saasTrialSubmit');
|
||||
};
|
||||
|
||||
export const trackSaasTrialSkip = () => {
|
||||
|
|
|
@ -3,6 +3,7 @@ import { GlBadge, GlLink } from '@gitlab/ui';
|
|||
import { createAlert } from '~/flash';
|
||||
import { fetchPolicies } from '~/lib/graphql';
|
||||
import { updateHistory } from '~/lib/utils/url_utility';
|
||||
import { formatNumber } from '~/locale';
|
||||
|
||||
import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
|
||||
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
|
||||
|
@ -172,18 +173,27 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
tabCount({ runnerType }) {
|
||||
let count;
|
||||
switch (runnerType) {
|
||||
case null:
|
||||
return this.allRunnersCount;
|
||||
count = this.allRunnersCount;
|
||||
break;
|
||||
case INSTANCE_TYPE:
|
||||
return this.instanceRunnersCount;
|
||||
count = this.instanceRunnersCount;
|
||||
break;
|
||||
case GROUP_TYPE:
|
||||
return this.groupRunnersCount;
|
||||
count = this.groupRunnersCount;
|
||||
break;
|
||||
case PROJECT_TYPE:
|
||||
return this.projectRunnersCount;
|
||||
count = this.projectRunnersCount;
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
if (typeof count === 'number') {
|
||||
return formatNumber(count);
|
||||
}
|
||||
return '';
|
||||
},
|
||||
reportToSentry(error) {
|
||||
captureException({ error, component: this.$options.name });
|
||||
|
@ -208,7 +218,7 @@ export default {
|
|||
>
|
||||
<template #title="{ tab }">
|
||||
{{ tab.title }}
|
||||
<gl-badge v-if="typeof tabCount(tab) == 'number'" class="gl-ml-1" size="sm">
|
||||
<gl-badge v-if="tabCount(tab)" class="gl-ml-1" size="sm">
|
||||
{{ tabCount(tab) }}
|
||||
</gl-badge>
|
||||
</template>
|
||||
|
|
|
@ -2,7 +2,10 @@
|
|||
import { escape } from 'lodash';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
import { WI_TITLE_TRACK_LABEL } from '../constants';
|
||||
|
||||
export default {
|
||||
WI_TITLE_TRACK_LABEL,
|
||||
props: {
|
||||
initialTitle: {
|
||||
type: String,
|
||||
|
@ -56,6 +59,7 @@ export default {
|
|||
role="textbox"
|
||||
:aria-label="__('Title')"
|
||||
:data-placeholder="placeholder"
|
||||
:data-track-label="$options.WI_TITLE_TRACK_LABEL"
|
||||
:contenteditable="!disabled"
|
||||
class="gl-pseudo-placeholder"
|
||||
@blur="handleBlur"
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
export const widgetTypes = {
|
||||
title: 'TITLE',
|
||||
};
|
||||
|
||||
export const WI_TITLE_TRACK_LABEL = 'item_title';
|
||||
|
|
|
@ -1,16 +1,21 @@
|
|||
<script>
|
||||
import { GlAlert } from '@gitlab/ui';
|
||||
import Tracking from '~/tracking';
|
||||
import workItemQuery from '../graphql/work_item.query.graphql';
|
||||
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
|
||||
import { widgetTypes } from '../constants';
|
||||
import { widgetTypes, WI_TITLE_TRACK_LABEL } from '../constants';
|
||||
|
||||
import ItemTitle from '../components/item_title.vue';
|
||||
|
||||
const trackingMixin = Tracking.mixin();
|
||||
|
||||
export default {
|
||||
titleUpdatedEvent: 'updated_title',
|
||||
components: {
|
||||
ItemTitle,
|
||||
GlAlert,
|
||||
},
|
||||
mixins: [trackingMixin],
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
|
@ -34,6 +39,14 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
tracking() {
|
||||
return {
|
||||
category: 'workItems:show',
|
||||
action: 'updated_title',
|
||||
label: WI_TITLE_TRACK_LABEL,
|
||||
property: '[type_work_item]',
|
||||
};
|
||||
},
|
||||
titleWidgetData() {
|
||||
return this.workItem?.widgets?.nodes?.find((widget) => widget.type === widgetTypes.title);
|
||||
},
|
||||
|
@ -50,6 +63,7 @@ export default {
|
|||
},
|
||||
},
|
||||
});
|
||||
this.track();
|
||||
} catch {
|
||||
this.error = true;
|
||||
}
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
|
||||
.flash-container {
|
||||
margin-bottom: $gl-padding;
|
||||
position: relative;
|
||||
top: 8px;
|
||||
}
|
||||
|
||||
.brand-holder {
|
||||
|
|
|
@ -548,6 +548,8 @@ svg {
|
|||
}
|
||||
.login-page .flash-container {
|
||||
margin-bottom: 16px;
|
||||
position: relative;
|
||||
top: 8px;
|
||||
}
|
||||
.login-page .brand-holder {
|
||||
font-size: 18px;
|
||||
|
|
|
@ -81,8 +81,7 @@ module Projects
|
|||
@protected_branch = @project.protected_branches.new
|
||||
@protected_tag = @project.protected_tags.new
|
||||
|
||||
@protected_branches_count = @protected_branches.reduce(0) { |sum, branch| sum + branch.matching(@project.repository.branches).size }
|
||||
@protected_tags_count = @protected_tags.reduce(0) { |sum, tag| sum + tag.matching(@project.repository.tags).size }
|
||||
@protected_tags_count = @protected_tags.reduce(0) { |sum, tag| sum + tag.matching(@project.repository.tag_names).size }
|
||||
|
||||
load_gon_index
|
||||
end
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
class ProtectableDropdown
|
||||
REF_TYPES = %i[branches tags].freeze
|
||||
REF_NAME_METHODS = {
|
||||
branches: :branch_names,
|
||||
tags: :tag_names
|
||||
}.freeze
|
||||
|
||||
def initialize(project, ref_type)
|
||||
raise ArgumentError, "invalid ref type `#{ref_type}`" unless ref_type.in?(REF_TYPES)
|
||||
|
@ -23,12 +27,12 @@ class ProtectableDropdown
|
|||
|
||||
private
|
||||
|
||||
def refs
|
||||
@project.repository.public_send(@ref_type) # rubocop:disable GitlabSecurity/PublicSend
|
||||
def ref_names
|
||||
@project.repository.public_send(ref_name_method) # rubocop:disable GitlabSecurity/PublicSend
|
||||
end
|
||||
|
||||
def ref_names
|
||||
refs.map(&:name)
|
||||
def ref_name_method
|
||||
REF_NAME_METHODS[@ref_type]
|
||||
end
|
||||
|
||||
def protections
|
||||
|
|
|
@ -5,10 +5,10 @@ class RefMatcher
|
|||
@ref_name_or_pattern = ref_name_or_pattern
|
||||
end
|
||||
|
||||
# Returns all branches/tags (among the given list of refs [`Gitlab::Git::Branch`])
|
||||
# Returns all branches/tags (among the given list of refs [`Gitlab::Git::Branch`] or their names [`String`])
|
||||
# that match the current protected ref.
|
||||
def matching(refs)
|
||||
refs.select { |ref| matches?(ref.name) }
|
||||
refs.select { |ref| ref.is_a?(String) ? matches?(ref) : matches?(ref.name) }
|
||||
end
|
||||
|
||||
# Checks if the protected ref matches the given ref name.
|
||||
|
|
|
@ -1,99 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>GraphiQL</title>
|
||||
<%= stylesheet_link_tag("graphiql/rails/application") %>
|
||||
<%# TODO: This file was included to fix a CSP failure. Please remove when https://github.com/rmosolgo/graphiql-rails/pull/71 will be released %>
|
||||
<%= javascript_include_tag("graphiql/rails/application", nonce: true) %>
|
||||
</head>
|
||||
<body>
|
||||
<div id="graphiql-container">
|
||||
Loading...
|
||||
</div>
|
||||
<%= javascript_tag nonce: true do -%>
|
||||
var parameters = {};
|
||||
|
||||
<% if GraphiQL::Rails.config.query_params %>
|
||||
// Parse the search string to get url parameters.
|
||||
var search = window.location.search;
|
||||
search.substr(1).split('&').forEach(function (entry) {
|
||||
var eq = entry.indexOf('=');
|
||||
if (eq >= 0) {
|
||||
parameters[decodeURIComponent(entry.slice(0, eq))] =
|
||||
decodeURIComponent(entry.slice(eq + 1));
|
||||
}
|
||||
});
|
||||
// if variables was provided, try to format it.
|
||||
if (parameters.variables) {
|
||||
try {
|
||||
parameters.variables =
|
||||
JSON.stringify(JSON.parse(parameters.variables), null, 2);
|
||||
} catch (e) {
|
||||
// Do nothing, we want to display the invalid JSON as a string, rather
|
||||
// than present an error.
|
||||
}
|
||||
}
|
||||
// When the query and variables string is edited, update the URL bar so
|
||||
// that it can be easily shared
|
||||
function onEditQuery(newQuery) {
|
||||
parameters.query = newQuery;
|
||||
updateURL();
|
||||
}
|
||||
function onEditVariables(newVariables) {
|
||||
parameters.variables = newVariables;
|
||||
updateURL();
|
||||
}
|
||||
function updateURL() {
|
||||
var newSearch = '?' + Object.keys(parameters).map(function (key) {
|
||||
return encodeURIComponent(key) + '=' +
|
||||
encodeURIComponent(parameters[key]);
|
||||
}).join('&');
|
||||
history.replaceState(null, null, newSearch);
|
||||
}
|
||||
<% end %>
|
||||
|
||||
// Defines a GraphQL fetcher using the fetch API.
|
||||
var graphQLEndpoint = "<%= graphql_endpoint_path %>";
|
||||
function graphQLFetcher(graphQLParams) {
|
||||
return fetch(graphQLEndpoint, {
|
||||
method: 'post',
|
||||
headers: <%= raw JSON.pretty_generate(GraphiQL::Rails.config.resolve_headers(self)) %>,
|
||||
body: JSON.stringify(graphQLParams),
|
||||
credentials: 'include',
|
||||
}).then(function(response) {
|
||||
return response.text();
|
||||
}).then(function(text) {
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch(error) {
|
||||
return {
|
||||
"message": "The server responded with invalid JSON, this is probably a server-side error",
|
||||
"response": text,
|
||||
};
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
<% if GraphiQL::Rails.config.initial_query %>
|
||||
var defaultQuery = "<%= GraphiQL::Rails.config.initial_query.gsub("\n", '\n').gsub('"', '\"').html_safe %>";
|
||||
<% else %>
|
||||
var defaultQuery = undefined
|
||||
<% end %>
|
||||
|
||||
// Render <GraphiQL /> into the body.
|
||||
ReactDOM.render(
|
||||
React.createElement(GraphiQL, {
|
||||
fetcher: graphQLFetcher,
|
||||
defaultQuery: defaultQuery,
|
||||
<% if GraphiQL::Rails.config.query_params %>
|
||||
query: parameters.query,
|
||||
variables: parameters.variables,
|
||||
onEditQuery: onEditQuery,
|
||||
onEditVariables: onEditVariables
|
||||
<% end %>
|
||||
}),
|
||||
document.getElementById("graphiql-container")
|
||||
);
|
||||
<% end -%>
|
||||
</body>
|
||||
</html>
|
|
@ -1,7 +1,7 @@
|
|||
.protected-branches-list.js-protected-branches-list.qa-protected-branches-list
|
||||
- if @protected_branches.empty?
|
||||
.card-header.bg-white
|
||||
= s_("ProtectedBranch|Protected branch (%{protected_branches_count})") % { protected_branches_count: @protected_branches_count }
|
||||
= s_("ProtectedBranch|Protected branch (%{protected_branches_count})") % { protected_branches_count: 0 }
|
||||
%p.settings-message.text-center
|
||||
= s_("ProtectedBranch|There are currently no protected branches, protect a branch with the form above.")
|
||||
- else
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
%div
|
||||
- if protected_branch.wildcard?
|
||||
- matching_branches = protected_branch.matching(repository.branches)
|
||||
- matching_branches = protected_branch.matching(repository.branch_names)
|
||||
= link_to pluralize(matching_branches.count, "matching branch"), namespace_project_protected_branch_path(@project.namespace, @project, protected_branch)
|
||||
- elsif !protected_branch.commit
|
||||
%span.text-muted Branch was deleted.
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
= gl_badge_tag s_('ProtectedTags|default'), variant: :info, class: 'gl-ml-2'
|
||||
%td
|
||||
- if protected_tag.wildcard?
|
||||
- matching_tags = protected_tag.matching(repository.tags)
|
||||
- matching_tags = protected_tag.matching(repository.tag_names)
|
||||
= link_to pluralize(matching_tags.count, "matching tag"), project_protected_tag_path(@project, protected_tag)
|
||||
- else
|
||||
- if commit = protected_tag.commit
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
.protected-tags-list.js-protected-tags-list
|
||||
- if @protected_tags.empty?
|
||||
.card-header
|
||||
Protected tags (#{@protected_tags_count})
|
||||
Protected tags (0)
|
||||
%p.settings-message.text-center
|
||||
No tags are protected.
|
||||
- else
|
||||
|
|
9
doc/development/experiment_guide/experimentation.md
Normal file
9
doc/development/experiment_guide/experimentation.md
Normal file
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
redirect_to: 'gitlab_experiment.md'
|
||||
remove_date: '2022-04-13'
|
||||
---
|
||||
|
||||
This document was moved to [another location](gitlab_experiment.md).
|
||||
|
||||
<!-- This redirect file can be deleted after <2022-04-13>. -->
|
||||
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->
|
|
@ -117,7 +117,7 @@ the model code:
|
|||
|
||||
```ruby
|
||||
class Packages::PackageFile < ApplicationRecord
|
||||
include ::Gitlab::Geo::ReplicableModel
|
||||
include ::Geo::ReplicableModel
|
||||
|
||||
with_replicator Geo::PackageFileReplicator
|
||||
end
|
||||
|
|
|
@ -37550,6 +37550,9 @@ msgstr ""
|
|||
msgid "Trials|Your trial ends on %{boldStart}%{trialEndDate}%{boldEnd}. We hope you’re enjoying the features of GitLab %{planName}. To keep those features after your trial ends, you’ll need to buy a subscription. (You can also choose GitLab Premium if it meets your needs.)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Trial|Allowed characters: +, 0-9, -, and spaces."
|
||||
msgstr ""
|
||||
|
||||
msgid "Trial|Company name"
|
||||
msgstr ""
|
||||
|
||||
|
@ -37565,15 +37568,9 @@ msgstr ""
|
|||
msgid "Trial|Dismiss"
|
||||
msgstr ""
|
||||
|
||||
msgid "Trial|First name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Trial|GitLab Ultimate trial (optional)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Trial|Last name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Trial|Number of employees"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ RSpec.describe 'GraphiQL' do
|
|||
end
|
||||
|
||||
it 'has the correct graphQLEndpoint' do
|
||||
expect(page.body).to include('var graphQLEndpoint = "/api/graphql";')
|
||||
expect(page.body).to include('<div id="graphiql-container" data-graphql-endpoint-path="/api/graphql"')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -26,7 +26,7 @@ RSpec.describe 'GraphiQL' do
|
|||
end
|
||||
|
||||
it 'has the correct graphQLEndpoint' do
|
||||
expect(page.body).to include('var graphQLEndpoint = "/gitlab/root/api/graphql";')
|
||||
expect(page.body).to include('<div id="graphiql-container" data-graphql-endpoint-path="/gitlab/root/api/graphql"')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -135,9 +135,6 @@ describe('~/google_tag_manager/index', () => {
|
|||
describe.each([
|
||||
createOmniAuthTestCase(trackFreeTrialAccountSubmissions, 'freeThirtyDayTrial'),
|
||||
createOmniAuthTestCase(trackNewRegistrations, 'standardSignUp'),
|
||||
createTestCase(trackSaasTrialSubmit, {
|
||||
forms: [{ id: 'new_trial', expectation: { event: 'saasTrialSubmit' } }],
|
||||
}),
|
||||
createTestCase(trackSaasTrialSkip, {
|
||||
links: [{ cls: 'js-skip-trial', expectation: { event: 'saasTrialSkip' } }],
|
||||
}),
|
||||
|
@ -202,6 +199,18 @@ describe('~/google_tag_manager/index', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('No listener events', () => {
|
||||
it('when trackSaasTrialSubmit is invoked', () => {
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
|
||||
trackSaasTrialSubmit();
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
expect(spy).toHaveBeenCalledWith({ event: 'saasTrialSubmit' });
|
||||
expect(logError).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe.each([
|
||||
{ dataLayer: null },
|
||||
{ gon: { features: null } },
|
||||
|
|
|
@ -85,19 +85,7 @@ describe('AdminRunnersApp', () => {
|
|||
setWindowLocation('/admin/runners');
|
||||
|
||||
mockRunnersQuery = jest.fn().mockResolvedValue(runnersData);
|
||||
mockRunnersCountQuery = jest.fn().mockImplementation(({ type }) => {
|
||||
const mockResponse = {
|
||||
[INSTANCE_TYPE]: 3,
|
||||
[GROUP_TYPE]: 2,
|
||||
[PROJECT_TYPE]: 1,
|
||||
};
|
||||
if (mockResponse[type]) {
|
||||
return Promise.resolve({
|
||||
data: { runners: { count: mockResponse[type] } },
|
||||
});
|
||||
}
|
||||
return Promise.resolve(runnersCountData);
|
||||
});
|
||||
mockRunnersCountQuery = jest.fn().mockResolvedValue(runnersCountData);
|
||||
createComponent();
|
||||
await waitForPromises();
|
||||
});
|
||||
|
@ -107,13 +95,59 @@ describe('AdminRunnersApp', () => {
|
|||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('shows the runner tabs with a runner count', async () => {
|
||||
createComponent({ mountFn: mount });
|
||||
it('shows the runner tabs with a runner count for each type', async () => {
|
||||
mockRunnersCountQuery.mockImplementation(({ type }) => {
|
||||
let count;
|
||||
switch (type) {
|
||||
case INSTANCE_TYPE:
|
||||
count = 3;
|
||||
break;
|
||||
case GROUP_TYPE:
|
||||
count = 2;
|
||||
break;
|
||||
case PROJECT_TYPE:
|
||||
count = 1;
|
||||
break;
|
||||
default:
|
||||
count = 6;
|
||||
break;
|
||||
}
|
||||
return Promise.resolve({ data: { runners: { count } } });
|
||||
});
|
||||
|
||||
createComponent({ mountFn: mount });
|
||||
await waitForPromises();
|
||||
|
||||
expect(findRunnerTypeTabs().text()).toMatchInterpolatedText(
|
||||
`All ${runnersCountData.data.runners.count} Instance 3 Group 2 Project 1`,
|
||||
`All 6 Instance 3 Group 2 Project 1`,
|
||||
);
|
||||
});
|
||||
|
||||
it('shows the runner tabs with a formatted runner count', async () => {
|
||||
mockRunnersCountQuery.mockImplementation(({ type }) => {
|
||||
let count;
|
||||
switch (type) {
|
||||
case INSTANCE_TYPE:
|
||||
count = 3000;
|
||||
break;
|
||||
case GROUP_TYPE:
|
||||
count = 2000;
|
||||
break;
|
||||
case PROJECT_TYPE:
|
||||
count = 1000;
|
||||
break;
|
||||
default:
|
||||
count = 6000;
|
||||
break;
|
||||
}
|
||||
return Promise.resolve({ data: { runners: { count } } });
|
||||
});
|
||||
|
||||
createComponent({ mountFn: mount });
|
||||
await waitForPromises();
|
||||
|
||||
expect(findRunnerTypeTabs().text()).toMatchInterpolatedText(
|
||||
`All 6,000 Instance 3,000 Group 2,000 Project 1,000`,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import { shallowMount } from '@vue/test-utils';
|
|||
import VueApollo from 'vue-apollo';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
|
||||
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
|
||||
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
|
||||
import WorkItemsRoot from '~/work_items/pages/work_item_root.vue';
|
||||
|
@ -15,6 +16,7 @@ Vue.use(VueApollo);
|
|||
const WORK_ITEM_ID = '1';
|
||||
|
||||
describe('Work items root component', () => {
|
||||
const mockUpdatedTitle = 'Updated title';
|
||||
let wrapper;
|
||||
let fakeApollo;
|
||||
|
||||
|
@ -53,7 +55,6 @@ describe('Work items root component', () => {
|
|||
it('updates the title when it is edited', async () => {
|
||||
createComponent();
|
||||
jest.spyOn(wrapper.vm.$apollo, 'mutate');
|
||||
const mockUpdatedTitle = 'Updated title';
|
||||
|
||||
await findTitle().vm.$emit('title-changed', mockUpdatedTitle);
|
||||
|
||||
|
@ -91,4 +92,32 @@ describe('Work items root component', () => {
|
|||
|
||||
expect(findTitle().exists()).toBe(false);
|
||||
});
|
||||
|
||||
describe('tracking', () => {
|
||||
let trackingSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
trackingSpy = mockTracking('_category_', undefined, jest.spyOn);
|
||||
|
||||
createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
unmockTracking();
|
||||
});
|
||||
|
||||
it('tracks item title updates', async () => {
|
||||
await findTitle().vm.$emit('title-changed', mockUpdatedTitle);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(trackingSpy).toHaveBeenCalledTimes(1);
|
||||
expect(trackingSpy).toHaveBeenCalledWith('workItems:show', undefined, {
|
||||
action: 'updated_title',
|
||||
category: 'workItems:show',
|
||||
label: 'item_title',
|
||||
property: '[type_work_item]',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,8 +3,9 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe ProtectableDropdown do
|
||||
subject(:dropdown) { described_class.new(project, ref_type) }
|
||||
|
||||
let(:project) { create(:project, :repository) }
|
||||
let(:subject) { described_class.new(project, :branches) }
|
||||
|
||||
describe 'initialize' do
|
||||
it 'raises ArgumentError for invalid ref type' do
|
||||
|
@ -13,34 +14,75 @@ RSpec.describe ProtectableDropdown do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#protectable_ref_names' do
|
||||
shared_examples 'protectable_ref_names' do
|
||||
context 'when project repository is not empty' do
|
||||
before do
|
||||
create(:protected_branch, project: project, name: 'master')
|
||||
end
|
||||
it 'includes elements matching a protected ref wildcard' do
|
||||
is_expected.to include(matching_ref)
|
||||
|
||||
it { expect(subject.protectable_ref_names).to include('feature') }
|
||||
it { expect(subject.protectable_ref_names).not_to include('master') }
|
||||
factory = ref_type == :branches ? :protected_branch : :protected_tag
|
||||
|
||||
it "includes branches matching a protected branch wildcard" do
|
||||
expect(subject.protectable_ref_names).to include('feature')
|
||||
create(factory, name: "#{matching_ref[0]}*", project: project)
|
||||
|
||||
create(:protected_branch, name: 'feat*', project: project)
|
||||
subject = described_class.new(project.reload, ref_type)
|
||||
|
||||
subject = described_class.new(project.reload, :branches)
|
||||
|
||||
expect(subject.protectable_ref_names).to include('feature')
|
||||
expect(subject.protectable_ref_names).to include(matching_ref)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when project repository is empty' do
|
||||
let(:project) { create(:project) }
|
||||
|
||||
it "returns empty list" do
|
||||
subject = described_class.new(project, :branches)
|
||||
|
||||
expect(subject.protectable_ref_names).to be_empty
|
||||
it 'returns empty list' do
|
||||
is_expected.to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#protectable_ref_names' do
|
||||
subject { dropdown.protectable_ref_names }
|
||||
|
||||
context 'for branches' do
|
||||
let(:ref_type) { :branches }
|
||||
let(:matching_ref) { 'feature' }
|
||||
|
||||
before do
|
||||
create(:protected_branch, project: project, name: 'master')
|
||||
end
|
||||
|
||||
it { is_expected.to include(matching_ref) }
|
||||
it { is_expected.not_to include('master') }
|
||||
|
||||
it_behaves_like 'protectable_ref_names'
|
||||
end
|
||||
|
||||
context 'for tags' do
|
||||
let(:ref_type) { :tags }
|
||||
let(:matching_ref) { 'v1.0.0' }
|
||||
|
||||
before do
|
||||
create(:protected_tag, project: project, name: 'v1.1.0')
|
||||
end
|
||||
|
||||
it { is_expected.to include(matching_ref) }
|
||||
it { is_expected.not_to include('v1.1.0') }
|
||||
|
||||
it_behaves_like 'protectable_ref_names'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#hash' do
|
||||
subject { dropdown.hash }
|
||||
|
||||
context 'for branches' do
|
||||
let(:ref_type) { :branches }
|
||||
|
||||
it { is_expected.to include(id: 'feature', text: 'feature', title: 'feature') }
|
||||
end
|
||||
|
||||
context 'for tags' do
|
||||
let(:ref_type) { :tags }
|
||||
|
||||
it { is_expected.to include(id: 'v1.0.0', text: 'v1.0.0', title: 'v1.0.0') }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
83
spec/models/ref_matcher_spec.rb
Normal file
83
spec/models/ref_matcher_spec.rb
Normal file
|
@ -0,0 +1,83 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe RefMatcher do
|
||||
subject(:ref_matcher) { described_class.new(ref_pattern) }
|
||||
|
||||
let(:ref_pattern) { 'v1.0' }
|
||||
|
||||
shared_examples 'matching_refs' do
|
||||
context 'when there is no match' do
|
||||
let(:ref_pattern) { 'unknown' }
|
||||
|
||||
it { is_expected.to match_array([]) }
|
||||
end
|
||||
|
||||
context 'when ref pattern is a wildcard' do
|
||||
let(:ref_pattern) { 'v*' }
|
||||
|
||||
it { is_expected.to match_array(refs) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#matching' do
|
||||
subject { ref_matcher.matching(refs) }
|
||||
|
||||
context 'when refs are strings' do
|
||||
let(:refs) { ['v1.0', 'v1.1'] }
|
||||
|
||||
it { is_expected.to match_array([ref_pattern]) }
|
||||
|
||||
it_behaves_like 'matching_refs'
|
||||
end
|
||||
|
||||
context 'when refs are ref objects' do
|
||||
let(:matching_ref) { double('tag', name: 'v1.0') }
|
||||
let(:not_matching_ref) { double('tag', name: 'v1.1') }
|
||||
let(:refs) { [matching_ref, not_matching_ref] }
|
||||
|
||||
it { is_expected.to match_array([matching_ref]) }
|
||||
|
||||
it_behaves_like 'matching_refs'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#matches?' do
|
||||
subject { ref_matcher.matches?(ref_name) }
|
||||
|
||||
let(:ref_name) { 'v1.0' }
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
|
||||
context 'when ref_name is empty' do
|
||||
let(:ref_name) { '' }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
context 'when ref pattern matches wildcard' do
|
||||
let(:ref_pattern) { 'v*' }
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
|
||||
context 'when ref pattern does not match wildcard' do
|
||||
let(:ref_pattern) { 'v2.*' }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#wildcard?' do
|
||||
subject { ref_matcher.wildcard? }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
|
||||
context 'when pattern is a wildcard' do
|
||||
let(:ref_pattern) { 'v*' }
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue