Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
bb19d18713
commit
be81c1578d
25 changed files with 356 additions and 12 deletions
61
app/assets/javascripts/clusters_list/components/clusters.vue
Normal file
61
app/assets/javascripts/clusters_list/components/clusters.vue
Normal file
|
@ -0,0 +1,61 @@
|
|||
<script>
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import { GlTable, GlLoadingIcon, GlBadge } from '@gitlab/ui';
|
||||
import { CLUSTER_TYPES } from '../constants';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlTable,
|
||||
GlLoadingIcon,
|
||||
GlBadge,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
key: 'name',
|
||||
label: __('Kubernetes cluster'),
|
||||
},
|
||||
{
|
||||
key: 'environmentScope',
|
||||
label: __('Environment scope'),
|
||||
},
|
||||
{
|
||||
key: 'size',
|
||||
label: __('Size'),
|
||||
},
|
||||
{
|
||||
key: 'clusterType',
|
||||
label: __('Cluster level'),
|
||||
formatter: value => CLUSTER_TYPES[value],
|
||||
},
|
||||
],
|
||||
computed: {
|
||||
...mapState(['clusters', 'loading']),
|
||||
},
|
||||
mounted() {
|
||||
// TODO - uncomment this once integrated with BE
|
||||
// this.fetchClusters();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['fetchClusters']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-loading-icon v-if="loading" size="md" class="mt-3" />
|
||||
<gl-table
|
||||
v-else
|
||||
:items="clusters"
|
||||
:fields="$options.fields"
|
||||
stacked="md"
|
||||
variant="light"
|
||||
class="qa-clusters-table"
|
||||
>
|
||||
<template #cell(clusterType)="{value}">
|
||||
<gl-badge variant="light">
|
||||
{{ value }}
|
||||
</gl-badge>
|
||||
</template>
|
||||
</gl-table>
|
||||
</template>
|
11
app/assets/javascripts/clusters_list/constants.js
Normal file
11
app/assets/javascripts/clusters_list/constants.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { __ } from '~/locale';
|
||||
|
||||
export const CLUSTER_TYPES = {
|
||||
project_type: __('Project'),
|
||||
group_type: __('Group'),
|
||||
instance_type: __('Instance'),
|
||||
};
|
||||
|
||||
export default {
|
||||
CLUSTER_TYPES,
|
||||
};
|
22
app/assets/javascripts/clusters_list/index.js
Normal file
22
app/assets/javascripts/clusters_list/index.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import Vue from 'vue';
|
||||
import Clusters from './components/clusters.vue';
|
||||
import { createStore } from './store';
|
||||
|
||||
export default () => {
|
||||
const entryPoint = document.querySelector('#js-clusters-list-app');
|
||||
|
||||
if (!entryPoint) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { endpoint } = entryPoint.dataset;
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el: '#js-clusters-list-app',
|
||||
store: createStore({ endpoint }),
|
||||
render(createElement) {
|
||||
return createElement(Clusters);
|
||||
},
|
||||
});
|
||||
};
|
18
app/assets/javascripts/clusters_list/store/actions.js
Normal file
18
app/assets/javascripts/clusters_list/store/actions.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import flash from '~/flash';
|
||||
import { __ } from '~/locale';
|
||||
import * as types from './mutation_types';
|
||||
|
||||
export const fetchClusters = ({ state, commit }) => {
|
||||
return axios
|
||||
.get(state.endpoint)
|
||||
.then(({ data }) => {
|
||||
commit(types.SET_CLUSTERS_DATA, convertObjectPropsToCamelCase(data, { deep: true }));
|
||||
commit(types.SET_LOADING_STATE, false);
|
||||
})
|
||||
.catch(() => flash(__('An error occurred while loading clusters')));
|
||||
};
|
||||
|
||||
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
||||
export default () => {};
|
16
app/assets/javascripts/clusters_list/store/index.js
Normal file
16
app/assets/javascripts/clusters_list/store/index.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import state from './state';
|
||||
import mutations from './mutations';
|
||||
import * as actions from './actions';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
export const createStore = initialState =>
|
||||
new Vuex.Store({
|
||||
actions,
|
||||
mutations,
|
||||
state: state(initialState),
|
||||
});
|
||||
|
||||
export default createStore;
|
|
@ -0,0 +1,2 @@
|
|||
export const SET_CLUSTERS_DATA = 'SET_CLUSTERS_DATA';
|
||||
export const SET_LOADING_STATE = 'SET_LOADING_STATE';
|
12
app/assets/javascripts/clusters_list/store/mutations.js
Normal file
12
app/assets/javascripts/clusters_list/store/mutations.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import * as types from './mutation_types';
|
||||
|
||||
export default {
|
||||
[types.SET_LOADING_STATE](state, value) {
|
||||
state.loading = value;
|
||||
},
|
||||
[types.SET_CLUSTERS_DATA](state, clusters) {
|
||||
Object.assign(state, {
|
||||
clusters,
|
||||
});
|
||||
},
|
||||
};
|
19
app/assets/javascripts/clusters_list/store/state.js
Normal file
19
app/assets/javascripts/clusters_list/store/state.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
export default (initialState = {}) => ({
|
||||
endpoint: initialState.endpoint,
|
||||
loading: false, // TODO - set this to true once integrated with BE
|
||||
clusters: [
|
||||
// TODO - remove mock data once integrated with BE
|
||||
// {
|
||||
// name: 'My Cluster',
|
||||
// environmentScope: '*',
|
||||
// size: '3',
|
||||
// clusterType: 'group_type',
|
||||
// },
|
||||
// {
|
||||
// name: 'My other cluster',
|
||||
// environmentScope: 'production',
|
||||
// size: '12',
|
||||
// clusterType: 'project_type',
|
||||
// },
|
||||
],
|
||||
});
|
|
@ -1,6 +1,8 @@
|
|||
import PersistentUserCallout from '~/persistent_user_callout';
|
||||
import initClustersListApp from '~/clusters_list';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const callout = document.querySelector('.gcp-signup-offer');
|
||||
PersistentUserCallout.factory(callout);
|
||||
initClustersListApp();
|
||||
});
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import PersistentUserCallout from '~/persistent_user_callout';
|
||||
import initClustersListApp from '~/clusters_list';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const callout = document.querySelector('.gcp-signup-offer');
|
||||
PersistentUserCallout.factory(callout);
|
||||
initClustersListApp();
|
||||
});
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import PersistentUserCallout from '~/persistent_user_callout';
|
||||
import initClustersListApp from '~/clusters_list';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const callout = document.querySelector('.gcp-signup-offer');
|
||||
PersistentUserCallout.factory(callout);
|
||||
initClustersListApp();
|
||||
});
|
||||
|
|
|
@ -180,6 +180,7 @@ export default class MergeRequestStore {
|
|||
this.mergeRequestAddCiConfigPath = data.merge_request_add_ci_config_path;
|
||||
this.pipelinesEmptySvgPath = data.pipelines_empty_svg_path;
|
||||
this.humanAccess = data.human_access;
|
||||
this.newPipelinePath = data.new_project_pipeline_path;
|
||||
}
|
||||
|
||||
get isNothingToMergeState() {
|
||||
|
|
|
@ -64,6 +64,10 @@ class MergeRequestWidgetEntity < Grape::Entity
|
|||
merge_request.project.team.human_max_access(current_user&.id)
|
||||
end
|
||||
|
||||
expose :new_project_pipeline_path do |merge_request|
|
||||
new_project_pipeline_path(merge_request.project)
|
||||
end
|
||||
|
||||
# Rendering and redacting Markdown can be expensive. These links are
|
||||
# just nice to have in the merge request widget, so only
|
||||
# include them if they are explicitly requested on first load.
|
||||
|
|
|
@ -18,13 +18,16 @@
|
|||
%strong
|
||||
= link_to _('More information'), help_page_path('user/group/clusters/index', anchor: 'cluster-precedence')
|
||||
|
||||
.clusters-table.js-clusters-list
|
||||
.gl-responsive-table-row.table-row-header{ role: "row" }
|
||||
.table-section.section-60{ role: "rowheader" }
|
||||
= s_("ClusterIntegration|Kubernetes cluster")
|
||||
.table-section.section-30{ role: "rowheader" }
|
||||
= s_("ClusterIntegration|Environment scope")
|
||||
.table-section.section-10{ role: "rowheader" }
|
||||
- @clusters.each do |cluster|
|
||||
= render "cluster", cluster: cluster.present(current_user: current_user)
|
||||
= paginate @clusters, theme: "gitlab"
|
||||
- if Feature.enabled?(:clusters_list_redesign)
|
||||
#js-clusters-list-app{ data: { endpoint: 'todo/add/endpoint' } }
|
||||
- else
|
||||
.clusters-table.js-clusters-list
|
||||
.gl-responsive-table-row.table-row-header{ role: "row" }
|
||||
.table-section.section-60{ role: "rowheader" }
|
||||
= s_("ClusterIntegration|Kubernetes cluster")
|
||||
.table-section.section-30{ role: "rowheader" }
|
||||
= s_("ClusterIntegration|Environment scope")
|
||||
.table-section.section-10{ role: "rowheader" }
|
||||
- @clusters.each do |cluster|
|
||||
= render "cluster", cluster: cluster.present(current_user: current_user)
|
||||
= paginate @clusters, theme: "gitlab"
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Create table & setup operations endpoint for Status Page Settings
|
||||
merge_request: 25863
|
||||
author:
|
||||
type: added
|
18
db/migrate/20200224020219_add_status_page_settings.rb
Normal file
18
db/migrate/20200224020219_add_status_page_settings.rb
Normal file
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddStatusPageSettings < ActiveRecord::Migration[6.0]
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
create_table :status_page_settings, id: false do |t|
|
||||
t.references :project, index: true, primary_key: true, foreign_key: { on_delete: :cascade }, unique: true, null: false
|
||||
t.timestamps_with_timezone null: false
|
||||
t.boolean :enabled, default: false, null: false
|
||||
t.string :aws_s3_bucket_name, limit: 63, null: false
|
||||
t.string :aws_region, limit: 255, null: false
|
||||
t.string :aws_access_key, limit: 255, null: false
|
||||
t.string :encrypted_aws_secret_key, limit: 255, null: false
|
||||
t.string :encrypted_aws_secret_key_iv, limit: 255, null: false
|
||||
end
|
||||
end
|
||||
end
|
13
db/schema.rb
13
db/schema.rb
|
@ -4002,6 +4002,18 @@ ActiveRecord::Schema.define(version: 2020_02_27_165129) do
|
|||
t.boolean "recaptcha_verified", default: false, null: false
|
||||
end
|
||||
|
||||
create_table "status_page_settings", primary_key: "project_id", force: :cascade do |t|
|
||||
t.datetime_with_timezone "created_at", null: false
|
||||
t.datetime_with_timezone "updated_at", null: false
|
||||
t.boolean "enabled", default: false, null: false
|
||||
t.string "aws_s3_bucket_name", limit: 63, null: false
|
||||
t.string "aws_region", limit: 255, null: false
|
||||
t.string "aws_access_key", limit: 255, null: false
|
||||
t.string "encrypted_aws_secret_key", limit: 255, null: false
|
||||
t.string "encrypted_aws_secret_key_iv", limit: 255, null: false
|
||||
t.index ["project_id"], name: "index_status_page_settings_on_project_id"
|
||||
end
|
||||
|
||||
create_table "subscriptions", id: :serial, force: :cascade do |t|
|
||||
t.integer "user_id"
|
||||
t.integer "subscribable_id"
|
||||
|
@ -5018,6 +5030,7 @@ ActiveRecord::Schema.define(version: 2020_02_27_165129) do
|
|||
add_foreign_key "snippets", "projects", name: "fk_be41fd4bb7", on_delete: :cascade
|
||||
add_foreign_key "software_license_policies", "projects", on_delete: :cascade
|
||||
add_foreign_key "software_license_policies", "software_licenses", on_delete: :cascade
|
||||
add_foreign_key "status_page_settings", "projects", on_delete: :cascade
|
||||
add_foreign_key "subscriptions", "projects", on_delete: :cascade
|
||||
add_foreign_key "suggestions", "notes", on_delete: :cascade
|
||||
add_foreign_key "system_note_metadata", "description_versions", name: "fk_fbd87415c9", on_delete: :nullify
|
||||
|
|
|
@ -1862,6 +1862,9 @@ msgstr ""
|
|||
msgid "An error occurred while loading chart data"
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred while loading clusters"
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred while loading commit signatures"
|
||||
msgstr ""
|
||||
|
||||
|
@ -3962,6 +3965,9 @@ msgstr ""
|
|||
msgid "Cluster does not exist"
|
||||
msgstr ""
|
||||
|
||||
msgid "Cluster level"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration| %{custom_domain_start}More information%{custom_domain_end}."
|
||||
msgstr ""
|
||||
|
||||
|
@ -7454,6 +7460,9 @@ msgstr ""
|
|||
msgid "Environment does not have deployments"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environment scope"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environment variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. Additionally, they can be masked so they are hidden in job logs, though they must match certain regexp requirements to do so. You can use environment variables for passwords, secret keys, or whatever you want."
|
||||
msgstr ""
|
||||
|
||||
|
@ -11181,6 +11190,9 @@ msgstr ""
|
|||
msgid "Kubernetes Clusters"
|
||||
msgstr ""
|
||||
|
||||
msgid "Kubernetes cluster"
|
||||
msgstr ""
|
||||
|
||||
msgid "Kubernetes cluster creation time exceeds timeout; %{timeout}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -17192,10 +17204,10 @@ msgstr ""
|
|||
msgid "Security dashboard"
|
||||
msgstr ""
|
||||
|
||||
msgid "Security report is out of date. Please incorporate latest changes from %{targetBranchName}"
|
||||
msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
|
||||
msgstr ""
|
||||
|
||||
msgid "Security report is out of date. Retry the pipeline for the target branch."
|
||||
msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityConfiguration|Configured"
|
||||
|
|
|
@ -11,6 +11,7 @@ describe 'Clusters', :js do
|
|||
before do
|
||||
project.add_maintainer(user)
|
||||
gitlab_sign_in(user)
|
||||
stub_feature_flags(clusters_list_redesign: false)
|
||||
end
|
||||
|
||||
context 'when user does not have a cluster and visits cluster index page' do
|
||||
|
|
55
spec/frontend/clusters_list/components/clusters_spec.js
Normal file
55
spec/frontend/clusters_list/components/clusters_spec.js
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { createLocalVue, mount } from '@vue/test-utils';
|
||||
import { GlTable, GlLoadingIcon } from '@gitlab/ui';
|
||||
import Clusters from '~/clusters_list/components/clusters.vue';
|
||||
import Vuex from 'vuex';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
describe('Clusters', () => {
|
||||
let wrapper;
|
||||
|
||||
const findTable = () => wrapper.find(GlTable);
|
||||
const findLoader = () => wrapper.find(GlLoadingIcon);
|
||||
|
||||
const mountComponent = _state => {
|
||||
const state = { clusters: [], endpoint: 'some/endpoint', ..._state };
|
||||
const store = new Vuex.Store({
|
||||
state,
|
||||
});
|
||||
|
||||
wrapper = mount(Clusters, { localVue, store });
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mountComponent({ loading: false });
|
||||
});
|
||||
|
||||
describe('clusters table', () => {
|
||||
it('displays a loader instead of the table while loading', () => {
|
||||
mountComponent({ loading: true });
|
||||
expect(findLoader().exists()).toBe(true);
|
||||
expect(findTable().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('displays a table component', () => {
|
||||
expect(findTable().exists()).toBe(true);
|
||||
expect(findTable().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders the correct table headers', () => {
|
||||
const tableHeaders = wrapper.vm.$options.fields;
|
||||
const headers = findTable().findAll('th');
|
||||
|
||||
expect(headers.length).toBe(tableHeaders.length);
|
||||
|
||||
tableHeaders.forEach((headerText, i) =>
|
||||
expect(headers.at(i).text()).toEqual(headerText.label),
|
||||
);
|
||||
});
|
||||
|
||||
it('should stack on smaller devices', () => {
|
||||
expect(findTable().classes()).toContain('b-table-stacked-md');
|
||||
});
|
||||
});
|
||||
});
|
50
spec/frontend/clusters_list/store/actions_spec.js
Normal file
50
spec/frontend/clusters_list/store/actions_spec.js
Normal file
|
@ -0,0 +1,50 @@
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import flashError from '~/flash';
|
||||
import testAction from 'helpers/vuex_action_helper';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import * as types from '~/clusters_list/store/mutation_types';
|
||||
import * as actions from '~/clusters_list/store/actions';
|
||||
|
||||
jest.mock('~/flash.js');
|
||||
|
||||
describe('Clusters store actions', () => {
|
||||
describe('fetchClusters', () => {
|
||||
let mock;
|
||||
const endpoint = '/clusters';
|
||||
const clusters = [{ name: 'test' }];
|
||||
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
});
|
||||
|
||||
afterEach(() => mock.restore());
|
||||
|
||||
it('should commit SET_CLUSTERS_DATA with received response', done => {
|
||||
mock.onGet().reply(200, clusters);
|
||||
|
||||
testAction(
|
||||
actions.fetchClusters,
|
||||
{ endpoint },
|
||||
{},
|
||||
[
|
||||
{ type: types.SET_CLUSTERS_DATA, payload: clusters },
|
||||
{ type: types.SET_LOADING_STATE, payload: false },
|
||||
],
|
||||
[],
|
||||
() => done(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should show flash on API error', done => {
|
||||
mock.onGet().reply(400, 'Not Found');
|
||||
|
||||
testAction(actions.fetchClusters, { endpoint }, {}, [], [], () => {
|
||||
expect(flashError).toHaveBeenCalledWith(expect.stringMatching('error'));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
||||
export default () => {};
|
|
@ -37,6 +37,7 @@ export default {
|
|||
target_project_id: 19,
|
||||
target_project_full_path: '/group2/project2',
|
||||
merge_request_add_ci_config_path: '/group2/project2/new/pipeline',
|
||||
new_project_pipeline_path: '/group2/project2/pipelines/new',
|
||||
metrics: {
|
||||
merged_by: {
|
||||
name: 'Administrator',
|
||||
|
|
|
@ -102,5 +102,11 @@ describe('MergeRequestStore', () => {
|
|||
|
||||
expect(store.pipelinesEmptySvgPath).toBe('/path/to/svg');
|
||||
});
|
||||
|
||||
it('should set newPipelinePath', () => {
|
||||
store.setData({ ...mockData });
|
||||
|
||||
expect(store.newPipelinePath).toBe('/group2/project2/pipelines/new');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -466,6 +466,7 @@ project:
|
|||
- container_expiration_policy
|
||||
- resource_groups
|
||||
- autoclose_referenced_issues
|
||||
- status_page_setting
|
||||
award_emoji:
|
||||
- awardable
|
||||
- user
|
||||
|
|
|
@ -162,6 +162,13 @@ describe MergeRequestWidgetEntity do
|
|||
.to eq('Maintainer')
|
||||
end
|
||||
|
||||
it 'has new pipeline path for project' do
|
||||
project.add_maintainer(user)
|
||||
|
||||
expect(subject[:new_project_pipeline_path])
|
||||
.to eq("/#{resource.project.full_path}/pipelines/new")
|
||||
end
|
||||
|
||||
describe 'when source project is deleted' do
|
||||
let(:project) { create(:project, :repository) }
|
||||
let(:forked_project) { fork_project(project) }
|
||||
|
|
Loading…
Reference in a new issue