Commit c2f0d50a authored by Paul Slaughter's avatar Paul Slaughter Committed by Phil Hughes

Setup IDE terminal store for environment checks

parent 5f138cdd
......@@ -14,6 +14,7 @@ const Api = {
projectMergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid',
projectMergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes',
projectMergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions',
projectRunnersPath: '/api/:version/projects/:id/runners',
mergeRequestsPath: '/api/:version/merge_requests',
groupLabelsPath: '/groups/:namespace_path/-/labels',
ldapGroupsPath: '/api/:version/ldap/:provider/groups.json',
......@@ -126,6 +127,15 @@ const Api = {
return axios.get(url);
},
projectRunners(projectPath, config = {}) {
const url = Api.buildUrl(Api.projectRunnersPath).replace(
':id',
encodeURIComponent(projectPath),
);
return axios.get(url, config);
},
mergeRequests(params = {}) {
const url = Api.buildUrl(Api.mergeRequestsPath);
......
import Vue from 'vue';
import { mapActions } from 'vuex';
import _ from 'underscore';
import Translate from '~/vue_shared/translate';
import ide from './components/ide.vue';
import store from './stores';
......@@ -13,19 +14,19 @@ Vue.use(Translate);
*
* @param {Element} el - The element that will contain the IDE.
* @param {Object} options - Extra options for the IDE (Used by EE).
* @param {(e:Element) => Object} options.extraInitialData -
* Function that returns extra properties to seed initial data.
* @param {Component} options.rootComponent -
* Component that overrides the root component.
* @param {(store:Vuex.Store, el:Element) => Vuex.Store} options.extendStore -
* Function that receives the default store and returns an extended one.
*/
export function initIde(el, options = {}) {
if (!el) return null;
const { extraInitialData = () => ({}), rootComponent = ide } = options;
const { rootComponent = ide, extendStore = _.identity } = options;
return new Vue({
el,
store,
store: extendStore(store, el),
router,
created() {
this.setEmptyStateSvgs({
......@@ -41,7 +42,6 @@ export function initIde(el, options = {}) {
});
this.setInitialData({
clientsidePreviewEnabled: parseBoolean(el.dataset.clientsidePreviewEnabled),
...extraInitialData(el),
});
},
methods: {
......
......@@ -16,7 +16,9 @@ const httpStatusCodes = {
IM_USED: 226,
MULTIPLE_CHOICES: 300,
BAD_REQUEST: 400,
FORBIDDEN: 403,
NOT_FOUND: 404,
UNPROCESSABLE_ENTITY: 422,
};
export const successCodes = [
......
# frozen_string_literal: true
module IdeHelper
def ide_data
{
"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg'),
"no-changes-state-svg-path" => image_path('illustrations/multi-editor_no_changes_empty.svg'),
"committed-state-svg-path" => image_path('illustrations/multi-editor_all_changes_committed_empty.svg'),
"pipelines-empty-state-svg-path": image_path('illustrations/pipelines_empty.svg'),
"promotion-svg-path": image_path('illustrations/web-ide_promotion.svg'),
"ci-help-page-path" => help_page_path('ci/quick_start/README'),
"web-ide-help-page-path" => help_page_path('user/project/web_ide/index.html'),
"clientside-preview-enabled": Gitlab::CurrentSettings.current_application_settings.web_ide_clientside_preview_enabled.to_s
}
end
end
- @body_class = 'ide-layout'
- page_title 'IDE'
- content_for :page_specific_javascripts do
= stylesheet_link_tag 'page_bundles/ide'
#ide.ide-loading{ data: ide_data() }
.text-center
= icon('spinner spin 2x')
%h2.clgray= _('Loading the GitLab IDE...')
- @body_class = 'ide-layout'
- page_title 'IDE'
- content_for :page_specific_javascripts do
= stylesheet_link_tag 'page_bundles/ide'
#ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg'),
"no-changes-state-svg-path" => image_path('illustrations/multi-editor_no_changes_empty.svg'),
"committed-state-svg-path" => image_path('illustrations/multi-editor_all_changes_committed_empty.svg'),
"pipelines-empty-state-svg-path": image_path('illustrations/pipelines_empty.svg'),
"promotion-svg-path": image_path('illustrations/web-ide_promotion.svg'),
"ci-help-page-path" => help_page_path('ci/quick_start/README'),
"web-ide-help-page-path" => help_page_path('user/project/web_ide/index.html'),
"clientside-preview-enabled": Gitlab::CurrentSettings.current_application_settings.web_ide_clientside_preview_enabled.to_s } }
.text-center
= icon('spinner spin 2x')
%h2.clgray= _('Loading the GitLab IDE...')
= render 'ide/show'
<script>
import { mapState } from 'vuex';
import { __ } from '~/locale';
import RightPane from '~/ide/components/panes/right.vue';
import TerminalView from '../terminal/view.vue';
......@@ -8,17 +9,14 @@ export default {
components: {
RightPane,
},
data() {
return {
// this will come from Vuex store in https://gitlab.com/gitlab-org/gitlab-ee/issues/5426
isTerminalEnabled: false,
};
},
computed: {
...mapState('terminal', {
isTerminalVisible: 'isVisible',
}),
extensionTabs() {
return [
{
show: this.isTerminalEnabled,
show: this.isTerminalVisible,
title: __('Terminal'),
views: [{ name: 'terminal', keepAlive: true, component: TerminalView }],
icon: 'terminal',
......
<script>
import { GlLoadingIcon } from '@gitlab/ui';
export default {
components: {
GlLoadingIcon,
},
props: {
isLoading: {
type: Boolean,
required: false,
default: true,
},
isValid: {
type: Boolean,
required: false,
default: false,
},
message: {
type: String,
required: false,
default: '',
},
helpPath: {
type: String,
required: false,
default: '',
},
illustrationPath: {
type: String,
required: false,
default: '',
},
},
methods: {
onStart() {
this.$emit('start');
},
},
};
</script>
<template>
<div class="text-center">
<div v-if="illustrationPath" class="svg-content svg-130"><img :src="illustrationPath" /></div>
<h4>{{ __('Web Terminal') }}</h4>
<gl-loading-icon v-if="isLoading" :size="2" class="prepend-top-default" />
<template v-else>
<p>{{ __('Run tests against your code live using the Web Terminal') }}</p>
<div class="bs-callout text-left">The Web Terminal is coming soon. Stay tuned!</div>
<p>
<button :disabled="!isValid" class="btn btn-info" type="button" @click="onStart">
{{ __('Start Web Terminal') }}
</button>
</p>
<div v-if="!isValid && message" class="bs-callout text-left" v-html="message"></div>
<p v-else>
<a
v-if="helpPath"
:href="helpPath"
target="_blank"
v-text="__('Learn more about Web Terminal')"
></a>
</p>
</template>
</div>
</template>
<script>
import { mapState } from 'vuex';
import { mapActions, mapGetters, mapState } from 'vuex';
import EmptyState from './empty_state.vue';
export default {
......@@ -7,15 +7,32 @@ export default {
EmptyState,
},
computed: {
...mapState(['emptyStateSvgPath']),
...mapState('terminal', ['isShowSplash', 'paths']),
...mapGetters('terminal', ['allCheck']),
},
methods: {
...mapActions('terminal', ['hideSplash']),
start() {
this.hideSplash();
},
},
};
</script>
<template>
<div class="h-100">
<div class="h-100 d-flex flex-column justify-content-center">
<empty-state :illustration-path="emptyStateSvgPath" />
<div v-if="isShowSplash" class="h-100 d-flex flex-column justify-content-center">
<empty-state
:is-loading="allCheck.isLoading"
:is-valid="allCheck.isValid"
:message="allCheck.message"
:help-path="paths.webTerminalHelpPath"
:illustration-path="paths.webTerminalSvgPath"
@start="start();"
/>
</div>
<template v-else>
<h5>{{ __('Web Terminal') }}</h5>
</template>
</div>
</template>
export const CHECK_CONFIG = 'config';
export const CHECK_RUNNERS = 'runners';
export const RETRY_RUNNERS_INTERVAL = 10000;
import * as mutationTypes from '~/ide/stores/mutation_types';
import terminalModule from './modules/terminal';
function getPathsFromData(el) {
return {
ciYamlHelpPath: el.dataset.eeCiYamlHelpPath,
ciRunnersHelpPath: el.dataset.eeCiRunnersHelpPath,
webTerminalHelpPath: el.dataset.eeWebTerminalHelpPath,
webTerminalSvgPath: el.dataset.eeWebTerminalSvgPath,
};
}
export default (store, el) => {
store.registerModule('terminal', terminalModule());
store.dispatch('terminal/setPaths', getPathsFromData(el));
store.subscribe(({ type }) => {
if (type === mutationTypes.SET_BRANCH_WORKING_REFERENCE) {
store.dispatch('terminal/init');
}
});
return store;
};
import Api from '~/api';
import httpStatus from '~/lib/utils/http_status';
import * as types from '../mutation_types';
import * as messages from '../messages';
import { CHECK_CONFIG, CHECK_RUNNERS, RETRY_RUNNERS_INTERVAL } from '../../../../constants';
export const requestConfigCheck = ({ commit }) => {
commit(types.REQUEST_CHECK, CHECK_CONFIG);
};
export const receiveConfigCheckSuccess = ({ commit }) => {
commit(types.SET_VISIBLE, true);
commit(types.RECEIVE_CHECK_SUCCESS, CHECK_CONFIG);
};
export const receiveConfigCheckError = ({ commit, state }, e) => {
const { status } = e.response;
const { paths } = state;
const isVisible = status !== httpStatus.FORBIDDEN && status !== httpStatus.NOT_FOUND;
commit(types.SET_VISIBLE, isVisible);
const message = messages.configCheckError(status, paths.ciYamlHelpPath);
commit(types.RECEIVE_CHECK_ERROR, { type: CHECK_CONFIG, message });
};
export const fetchConfigCheck = ({ dispatch }) => {
dispatch('requestConfigCheck');
// This will use a real endpoint in https://gitlab.com/gitlab-org/gitlab-ee/issues/5426
Promise.resolve({})
.then(() => {
dispatch('receiveConfigCheckSuccess');
})
.catch(e => {
dispatch('receiveConfigCheckError', e);
});
};
export const requestRunnersCheck = ({ commit }) => {
commit(types.REQUEST_CHECK, CHECK_RUNNERS);
};
export const receiveRunnersCheckSuccess = ({ commit, dispatch, state }, data) => {
if (data.length) {
commit(types.RECEIVE_CHECK_SUCCESS, CHECK_RUNNERS);
} else {
const { paths } = state;
commit(types.RECEIVE_CHECK_ERROR, {
type: CHECK_RUNNERS,
message: messages.runnersCheckEmpty(paths.ciRunnersHelpPath),
});
dispatch('retryRunnersCheck');
}
};
export const receiveRunnersCheckError = ({ commit }) => {
commit(types.RECEIVE_CHECK_ERROR, {
type: CHECK_RUNNERS,
message: messages.UNEXPECTED_ERROR_RUNNERS,
});
};
export const retryRunnersCheck = ({ dispatch, state }) => {
// if the overall check has failed, don't worry about retrying
const check = state.checks[CHECK_CONFIG];
if (!check.isLoading && !check.isValid) {
return;
}
setTimeout(() => {
dispatch('fetchRunnersCheck', { background: true });
}, RETRY_RUNNERS_INTERVAL);
};
export const fetchRunnersCheck = ({ dispatch, rootGetters }, options = {}) => {
const { background = false } = options;
if (!background) {
dispatch('requestRunnersCheck');
}
const { currentProject } = rootGetters;
Api.projectRunners(currentProject.id, { params: { scope: 'active' } })
.then(({ data }) => {
dispatch('receiveRunnersCheckSuccess', data);
})
.catch(e => {
dispatch('receiveRunnersCheckError', e);
});
};
export * from './setup';
export * from './checks';
export default () => {};
import * as types from '../mutation_types';
// This will be used in https://gitlab.com/gitlab-org/gitlab-ee/issues/5426
// export const init = ({ dispatch }) => {
// dispatch('fetchConfigCheck');
// dispatch('fetchRunnersCheck');
// };
export const init = () => {};
export const hideSplash = ({ commit }) => {
commit(types.HIDE_SPLASH);
};
export const setPaths = ({ commit }, paths) => {
commit(types.SET_PATHS, paths);
};
export const allCheck = state => {
const checks = Object.values(state.checks);
if (checks.some(check => check.isLoading)) {
return { isLoading: true };
}
const invalidCheck = checks.find(check => !check.isValid);
const isValid = !invalidCheck;
const message = !invalidCheck ? '' : invalidCheck.message;
return {
isLoading: false,
isValid,
message,
};
};
export default () => {};
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import state from './state';
export default () => ({
namespaced: true,
actions,
getters,
mutations,
state: state(),
});
import _ from 'underscore';
import { __, sprintf } from '~/locale';
import httpStatus from '~/lib/utils/http_status';
export const UNEXPECTED_ERROR_CONFIG = __(
'An unexpected error occurred while checking the project environment.',
);
export const UNEXPECTED_ERROR_RUNNERS = __(
'An unexpected error occurred while checking the project runners.',
);
export const EMPTY_RUNNERS = __(
'Configure GitLab runners to start using Web Terminal. %{helpStart}Learn more.%{helpEnd}',
);
export const ERROR_CONFIG = __(
'Configure a <code>.gitlab-webide.yml</code> file in the <code>.gitlab</code> directory to start using the Web Terminal. %{helpStart}Learn more.%{helpEnd}',
);
export const ERROR_PERMISSION = __(
'You do not have permission to run the Web Terminal. Please contact a project administrator.',
);
export const configCheckError = (status, helpUrl) => {
if (status === httpStatus.UNPROCESSABLE_ENTITY) {
return sprintf(
ERROR_CONFIG,
{
helpStart: `<a href="${_.escape(helpUrl)}" target="_blank">`,
helpEnd: '</a>',
},
false,
);
} else if (status === httpStatus.FORBIDDEN) {
return ERROR_PERMISSION;
}
return UNEXPECTED_ERROR_CONFIG;
};
export const runnersCheckEmpty = helpUrl =>
sprintf(
EMPTY_RUNNERS,
{
helpStart: `<a href="${_.escape(helpUrl)}" target="_blank">`,
helpEnd: '</a>',
},
false,
);
export const SET_VISIBLE = 'SET_VISIBLE';
export const HIDE_SPLASH = 'HIDE_SPLASH';
export const SET_PATHS = 'SET_PATHS';
export const REQUEST_CHECK = 'REQUEST_CHECK';
export const RECEIVE_CHECK_SUCCESS = 'RECEIVE_CHECK_SUCCESS';
export const RECEIVE_CHECK_ERROR = 'RECEIVE_CHECK_ERROR';
import * as types from './mutation_types';
export default {
[types.SET_VISIBLE](state, isVisible) {
Object.assign(state, {
isVisible,
});
},
[types.HIDE_SPLASH](state) {
Object.assign(state, {
isShowSplash: false,
});
},
[types.SET_PATHS](state, paths) {
Object.assign(state, {
paths,
});
},
[types.REQUEST_CHECK](state, type) {
Object.assign(state.checks, {
[type]: {
isLoading: true,
},
});
},
[types.RECEIVE_CHECK_ERROR](state, { type, message }) {
Object.assign(state.checks, {
[type]: {
isLoading: false,
isValid: false,
message,
},
});
},
[types.RECEIVE_CHECK_SUCCESS](state, type) {
Object.assign(state.checks, {
[type]: {
isLoading: false,
isValid: true,
message: null,
},
});
},
};
import { CHECK_CONFIG, CHECK_RUNNERS } from '../../../constants';
export default () => ({
checks: {
[CHECK_CONFIG]: { isLoading: true },
[CHECK_RUNNERS]: { isLoading: true },
},
isVisible: false,
isShowSplash: true,
paths: {},
});
import { startIde } from '~/ide/index';
import EEIde from 'ee/ide/components/ide.vue';
function extraInitialData() {
// This is empty now, but it will be used in: https://gitlab.com/gitlab-org/gitlab-ee/issues/5426
return {};
}
import extendStore from 'ee/ide/stores/extend';
startIde({
extraInitialData,
extendStore,
rootComponent: EEIde,
});
# frozen_string_literal: true
module EE
module IdeHelper
extend ::Gitlab::Utils::Override
override :ide_data
def ide_data
super.merge({
"ee-web-terminal-svg-path" => image_path('illustrations/web-ide_promotion.svg'),
"ee-ci-yaml-help-path" => help_page_path('ci/yaml/README.md'),
"ee-ci-runners-help-path" => help_page_path('ci/runners/README.md'),
"ee-web-terminal-help-path" => help_page_path('user/project/web_ide/index.md', anchor: 'client-side-evaluation')
})
end
end
end
::IdeHelper.prepend(::EE::IdeHelper)
= render partial: "ide/show"
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import RightPane from '~/ide/components/panes/right.vue';
import EERightPane from 'ee/ide/components/panes/right.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('IDE EERightPane', () => {
let wrapper;
let terminalState;
const factory = () => {
const store = new Vuex.Store({
modules: {
terminal: {
namespaced: true,
state: terminalState,
},
},
});
wrapper = shallowMount(localVue.extend(EERightPane), { localVue, store });
};
beforeEach(() => {
terminalState = {};
});
afterEach(() => {
wrapper.destroy();
});
it('adds terminal tab', () => {
terminalState.isVisible = true;
factory();
expect(wrapper.find(RightPane).props('extensionTabs')).toEqual([
jasmine.objectContaining({
show: true,
title: 'Terminal',
}),
]);
});
it('hides terminal tab when not visible', () => {
terminalState.isVisible = false;
factory();
expect(wrapper.find(RightPane).props('extensionTabs')).toEqual([
jasmine.objectContaining({
show: false,
title: 'Terminal',
}),
]);
});
});
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
import { TEST_HOST } from 'spec/test_constants';
import TerminalEmptyState from 'ee/ide/components/terminal/empty_state.vue';
const TEST_HELP_PATH = `${TEST_HOST}/help/test`;
const TEST_PATH = `${TEST_HOST}/home.png`;
const TEST_HTML_MESSAGE = 'lorem <strong>ipsum</strong>';
describe('EE IDE TerminalEmptyState', () => {
let wrapper;
describe('TerminalEmptyState', () => {
const factory = (options = {}) => {
const localVue = createLocalVue();
return shallowMount(TerminalEmptyState, {
wrapper = shallowMount(localVue.extend(TerminalEmptyState), {
sync: false,
localVue,
...options,
});
};
afterEach(() => {
wrapper.destroy();
});
it('does not show illustration, if no path specified', () => {
const wrapper = factory();
factory();
expect(wrapper.find('.svg-content').exists()).toBe(false);
});
it('shows illustration with path', () => {
const wrapper = factory({
factory({
propsData: {
illustrationPath: TEST_PATH,
},
......@@ -32,4 +42,70 @@ describe('TerminalEmptyState', () => {
expect(img.exists()).toBe(true);
expect(img.attributes('src')).toEqual(TEST_PATH);
});
it('when loading, shows loading icon', () => {
factory({
propsData: {
isLoading: true,
},
});
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
it('when not loading, does not show loading icon', () => {
factory({
propsData: {
isLoading: false,
},
});
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
});
describe('when valid', () => {
let button;
beforeEach(() => {
factory({
propsData: {
isLoading: false,
isValid: true,
helpPath: TEST_HELP_PATH,
},
});
button = wrapper.find('button');
});
it('shows button', () => {
expect(button.text()).toEqual('Start Web Terminal');
expect(button.attributes('disabled')).toBeFalsy();
});
it('emits start when button is clicked', () => {
expect(wrapper.emitted().start).toBeFalsy();
button.trigger('click');
expect(wrapper.emitted().start.length).toBe(1);
});
it('shows help path link', () => {
expect(wrapper.find('a').attributes('href')).toEqual(TEST_HELP_PATH);
});
});
it('when not valid, shows disabled button and message', () => {
factory({
propsData: {
isLoading: false,
isValid: false,
message: TEST_HTML_MESSAGE,
},
});
expect(wrapper.find('button').attributes('disabled')).not.toBe(null);
expect(wrapper.find('.bs-callout').element.innerHTML).toEqual(TEST_HTML_MESSAGE);
});
});
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { TEST_HOST } from 'spec/test_constants';
import TerminalView from 'ee/ide/components/terminal/view.vue';
import TerminalEmptyState from 'ee/ide/components/terminal/empty_state.vue';
import TerminalView from 'ee/ide/components/terminal/view.vue';
const TEST_HELP_PATH = `${TEST_HOST}/help`;
const TEST_SVG_PATH = `${TEST_HOST}/illustration.svg`;
const localVue = createLocalVue();
localVue.use(Vuex);
describe('TerminalView', () => {
describe('EE IDE TerminalView', () => {
let state;
let actions;
let getters;
let wrapper;
const factory = () => {
const store = new Vuex.Store({
state: {
emptyStateSvgPath: TEST_SVG_PATH,
modules: {
terminal: {
namespaced: true,
state,
actions,
getters,
},
},
});
return shallowMount(TerminalView, { localVue, store });
wrapper = shallowMount(localVue.extend(TerminalView), { localVue, store });
};
beforeEach(() => {
state = {
isShowSplash: true,
paths: {
webTerminalHelpPath: TEST_HELP_PATH,
webTerminalSvgPath: TEST_SVG_PATH,
},
};
actions = {
hideSplash: jasmine.createSpy('hideSplash'),
};
getters = {
allCheck: () => ({
isLoading: false,
isValid: false,
message: 'bad',
}),
};
});
afterEach(() => {
wrapper.destroy();
});
it('renders empty state', () => {
const wrapper = factory();
factory();
expect(wrapper.find(TerminalEmptyState).props()).toEqual({
helpPath: TEST_HELP_PATH,
illustrationPath: TEST_SVG_PATH,
...getters.allCheck(),
});
});
it('hides splash when started', () => {
factory();
expect(actions.hideSplash).not.toHaveBeenCalled();
wrapper.find(TerminalEmptyState).vm.$emit('start');
expect(actions.hideSplash).toHaveBeenCalled();
});
it('shows Web Terminal when started', () => {
state.isShowSplash = false;
factory();
expect(wrapper.find(TerminalEmptyState).exists()).toBe(false);
expect(wrapper.text()).toContain('Web Terminal');
});
});
import { createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { SET_BRANCH_WORKING_REFERENCE } from '~/ide/stores/mutation_types';
import { TEST_HOST } from 'spec/test_constants';
import terminalModule from 'ee/ide/stores/modules/terminal';
import extendStore from 'ee/ide/stores/extend';
const TEST_DATASET = {
eeCiYamlHelpPath: `${TEST_HOST}/ci/yaml/help`,
eeCiRunnersHelpPath: `${TEST_HOST}/ci/runners/help`,
eeWebTerminalHelpPath: `${TEST_HOST}/web/terminal/help`,
eeWebTerminalSvgPath: `${TEST_HOST}/web/terminal/svg`,
};
const localVue = createLocalVue();
localVue.use(Vuex);
describe('ee/ide/stores/extend', () => {
let store;
beforeEach(() => {
const el = document.createElement('div');
Object.assign(el.dataset, TEST_DATASET);
store = new Vuex.Store({
mutations: {
[SET_BRANCH_WORKING_REFERENCE]: () => {},
},
});
spyOn(store, 'registerModule');
spyOn(store, 'dispatch');
store = extendStore(store, el);
});
it('registers terminal module', () => {
expect(store.registerModule).toHaveBeenCalledWith('terminal', terminalModule());
});
it('dispatches terminal/setPaths', () => {
expect(store.dispatch).toHaveBeenCalledWith('terminal/setPaths', {
ciYamlHelpPath: TEST_DATASET.eeCiYamlHelpPath,
ciRunnersHelpPath: TEST_DATASET.eeCiRunnersHelpPath,
webTerminalHelpPath: TEST_DATASET.eeWebTerminalHelpPath,
webTerminalSvgPath: TEST_DATASET.eeWebTerminalSvgPath,
});
});
it(`dispatches terminal/init on ${SET_BRANCH_WORKING_REFERENCE}`, () => {
store.dispatch.calls.reset();
store.commit(SET_BRANCH_WORKING_REFERENCE);
expect(store.dispatch).toHaveBeenCalledWith('terminal/init');
});
});
import MockAdapter from 'axios-mock-adapter';
import httpStatus from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils';
import testAction from 'spec/helpers/vuex_action_helper';
import { TEST_HOST } from 'spec/test_constants';
import { CHECK_CONFIG, CHECK_RUNNERS, RETRY_RUNNERS_INTERVAL } from 'ee/ide/constants';
import * as mutationTypes from 'ee/ide/stores/modules/terminal/mutation_types';
import * as messages from 'ee/ide/stores/modules/terminal/messages';
import * as actions from 'ee/ide/stores/modules/terminal/actions/checks';
const TEST_YAML_HELP_PATH = `${TEST_HOST}/test/yaml/help`;
const TEST_RUNNERS_HELP_PATH = `${TEST_HOST}/test/runners/help`;
describe('EE IDE store terminal check actions', () => {
let mock;
let state;
beforeEach(() => {
mock = new MockAdapter(axios);
state = {
paths: {
ciYamlHelpPath: TEST_YAML_HELP_PATH,
ciRunnersHelpPath: TEST_RUNNERS_HELP_PATH,
},
checks: {
config: { isLoading: true },
},
};
jasmine.clock().install();
});
afterEach(() => {
mock.restore();
jasmine.clock().uninstall();
});
describe('requestConfigCheck', () => {
it('handles request loading', done => {
testAction(
actions.requestConfigCheck,
null,
{},
[{ type: mutationTypes.REQUEST_CHECK, payload: CHECK_CONFIG }],
[],
done,
);
});
});
describe('receiveConfigCheckSuccess', () => {
it('handles successful response', done => {
testAction(
actions.receiveConfigCheckSuccess,
null,
{},
[
{ type: mutationTypes.SET_VISIBLE, payload: true },
{ type: mutationTypes.RECEIVE_CHECK_SUCCESS, payload: CHECK_CONFIG },
],
[],
done,
);
});
});
describe('receiveConfigCheckError', () => {
it('handles error response', done => {
const status = httpStatus.UNPROCESSABLE_ENTITY;
const payload = { response: { status } };
testAction(
actions.receiveConfigCheckError,
payload,
state,
[
{
type: mutationTypes.SET_VISIBLE,
payload: true,
},
{
type: mutationTypes.RECEIVE_CHECK_ERROR,
payload: {
type: CHECK_CONFIG,
message: messages.configCheckError(status, TEST_YAML_HELP_PATH),
},
},
],
[],
done,
);
});
[httpStatus.FORBIDDEN, httpStatus.NOT_FOUND].forEach(status => {
it(`hides tab, when status is ${status}`, done => {
const payload = { response: { status } };
testAction(
actions.receiveConfigCheckError,
payload,
state,
[
{
type: mutationTypes.SET_VISIBLE,
payload: false,
},
jasmine.objectContaining({ type: mutationTypes.RECEIVE_CHECK_ERROR }),
],
[],
done,
);
});
});
});
describe('fetchConfigCheck', () => {
it('dispatches request and receive', done => {
testAction(
actions.fetchConfigCheck,
null,
{},
[],
[{ type: 'requestConfigCheck' }, { type: 'receiveConfigCheckSuccess' }],
done,
);
});
});
describe('requestRunnersCheck', () => {
it('handles request loading', done => {
testAction(
actions.requestRunnersCheck,
null,
{},
[{ type: mutationTypes.REQUEST_CHECK, payload: CHECK_RUNNERS }],
[],
done,
);
});
});
describe('receiveRunnersCheckSuccess', () => {
it('handles successful response, with data', done => {
const payload = [{}];
testAction(
actions.receiveRunnersCheckSuccess,
payload,
state,
[{ type: mutationTypes.RECEIVE_CHECK_SUCCESS, payload: CHECK_RUNNERS }],
[],
done,
);
});
it('handles successful response, with empty data', done => {
const commitPayload = {
type: CHECK_RUNNERS,
message: messages.runnersCheckEmpty(TEST_RUNNERS_HELP_PATH),
};
testAction(
actions.receiveRunnersCheckSuccess,
[],
state,
[{ type: mutationTypes.RECEIVE_CHECK_ERROR, payload: commitPayload }],
[{ type: 'retryRunnersCheck' }],
done,
);
});
});
describe('receiveRunnersCheckError', () => {
it('dispatches handle with message', done => {
const commitPayload = {
type: CHECK_RUNNERS,
message: messages.UNEXPECTED_ERROR_RUNNERS,
};
testAction(
actions.receiveRunnersCheckError,
null,
{},
[{ type: mutationTypes.RECEIVE_CHECK_ERROR, payload: commitPayload }],
[],
done,
);
});
});
describe('retryRunnersCheck', () => {
it('dispatches fetch again after timeout', () => {
const dispatch = jasmine.createSpy('dispatch');
actions.retryRunnersCheck({ dispatch, state });
expect(dispatch).not.toHaveBeenCalled();
jasmine.clock().tick(RETRY_RUNNERS_INTERVAL + 1);
expect(dispatch).toHaveBeenCalledWith('fetchRunnersCheck', { background: true });
});
it('does not dispatch fetch if config check is error', () => {
const dispatch = jasmine.createSpy('dispatch');
state.checks.config = {
isLoading: false,
isValid: false,
};
actions.retryRunnersCheck({ dispatch, state });
expect(dispatch).not.toHaveBeenCalled();
jasmine.clock().tick(RETRY_RUNNERS_INTERVAL + 1);
expect(dispatch).not.toHaveBeenCalled();
});
});
describe('fetchRunnersCheck', () => {
let rootGetters;
beforeEach(() => {
rootGetters = {
currentProject: { id: 7 },
};
});
it('dispatches request and receive', done => {
mock.onGet(/api\/.*\/projects\/.*\/runners/, { params: { scope: 'active' } }).reply(200, []);
testAction(
actions.fetchRunnersCheck,
{},
rootGetters,
[],
[{ type: 'requestRunnersCheck' }, { type: 'receiveRunnersCheckSuccess', payload: [] }],
done,
);
});
it('does not dispatch request when background is true', done => {
mock.onGet(/api\/.*\/projects\/.*\/runners/, { params: { scope: 'active' } }).reply(200, []);
testAction(
actions.fetchRunnersCheck,
{ background: true },
rootGetters,
[],
[{ type: 'receiveRunnersCheckSuccess', payload: [] }],
done,
);
});
it('dispatches request and receive, when error', done => {
mock.onGet(/api\/.*\/projects\/.*\/runners/, { params: { scope: 'active' } }).reply(500, []);
testAction(
actions.fetchRunnersCheck,
{},
rootGetters,
[],
[
{ type: 'requestRunnersCheck' },
{ type: 'receiveRunnersCheckError', payload: jasmine.any(Error) },
],
done,
);
});
});
});
import testAction from 'spec/helpers/vuex_action_helper';
import * as mutationTypes from 'ee/ide/stores/modules/terminal/mutation_types';
import * as actions from 'ee/ide/stores/modules/terminal/actions/setup';
describe('EE IDE store terminal setup actions', () => {
describe('hideSplash', () => {
it('commits HIDE_SPLASH', done => {
testAction(actions.hideSplash, null, {}, [{ type: mutationTypes.HIDE_SPLASH }], [], done);
});
});
describe('setPaths', () => {
it('commits SET_PATHS', done => {
const paths = {
foo: 'bar',
lorem: 'ipsum',
};
testAction(
actions.setPaths,
paths,
{},
[{ type: mutationTypes.SET_PATHS, payload: paths }],
[],
done,
);
});
});
});
import { CHECK_CONFIG, CHECK_RUNNERS } from 'ee/ide/constants';
import * as getters from 'ee/ide/stores/modules/terminal/getters';
describe('EE IDE store terminal getters', () => {
describe('allCheck', () => {
it('is loading if one check is loading', () => {
const checks = {
[CHECK_CONFIG]: { isLoading: false, isValid: true },
[CHECK_RUNNERS]: { isLoading: true },
};
const result = getters.allCheck({ checks });
expect(result).toEqual({
isLoading: true,
});
});
it('is invalid if one check is invalid', () => {
const message = 'lorem ipsum';
const checks = {
[CHECK_CONFIG]: { isLoading: false, isValid: false, message },
[CHECK_RUNNERS]: { isLoading: false, isValid: true },
};
const result = getters.allCheck({ checks });
expect(result).toEqual({
isLoading: false,
isValid: false,
message,
});
});
it('is valid if all checks are valid', () => {
const checks = {
[CHECK_CONFIG]: { isLoading: false, isValid: true },
[CHECK_RUNNERS]: { isLoading: false, isValid: true },
};
const result = getters.allCheck({ checks });
expect(result).toEqual({
isLoading: false,
isValid: true,
message: '',
});
});
});
});
import _ from 'underscore';
import { sprintf } from '~/locale';
import httpStatus from '~/lib/utils/http_status';
import { TEST_HOST } from 'spec/test_constants';
import * as messages from 'ee/ide/stores/modules/terminal/messages';
const TEST_HELP_URL = `${TEST_HOST}/help`;
describe('EE IDE store terminal messages', () => {
describe('configCheckError', () => {
it('returns job error, with status UNPROCESSABLE_ENTITY', () => {
const result = messages.configCheckError(httpStatus.UNPROCESSABLE_ENTITY, TEST_HELP_URL);
expect(result).toBe(
sprintf(
messages.ERROR_CONFIG,
{
helpStart: `<a href="${_.escape(TEST_HELP_URL)}" target="_blank">`,
helpEnd: '</a>',
},
false,
),
);
});
it('returns permission error, with status FORBIDDEN', () => {
const result = messages.configCheckError(httpStatus.FORBIDDEN, TEST_HELP_URL);
expect(result).toBe(messages.ERROR_PERMISSION);
});
it('returns unexpected error, with unexpected status', () => {
const result = messages.configCheckError(httpStatus.NOT_FOUND, TEST_HELP_URL);
expect(result).toBe(messages.UNEXPECTED_ERROR_CONFIG);
});
});
});
import { CHECK_CONFIG, CHECK_RUNNERS } from 'ee/ide/constants';
import createState from 'ee/ide/stores/modules/terminal/state';
import * as types from 'ee/ide/stores/modules/terminal/mutation_types';
import mutations from 'ee/ide/stores/modules/terminal/mutations';
describe('EE IDE store terminal mutations', () => {
let state;
beforeEach(() => {
state = createState();
});
describe(types.SET_VISIBLE, () => {
it('sets isVisible', () => {
state.isVisible = false;
mutations[types.SET_VISIBLE](state, true);
expect(state.isVisible).toBe(true);
});
});
describe(types.HIDE_SPLASH, () => {
it('sets isShowSplash', () => {
state.isShowSplash = true;
mutations[types.HIDE_SPLASH](state);
expect(state.isShowSplash).toBe(false);
});
});
describe(types.SET_PATHS, () => {
it('sets paths', () => {
const paths = {
test: 'foo',
};
mutations[types.SET_PATHS](state, paths);
expect(state.paths).toBe(paths);
});
});
describe(types.REQUEST_CHECK, () => {
it('sets isLoading for check', () => {
const type = CHECK_CONFIG;
state.checks[type] = {};
mutations[types.REQUEST_CHECK](state, type);
expect(state.checks[type]).toEqual({
isLoading: true,
});
});
});
describe(types.RECEIVE_CHECK_ERROR, () => {
it('sets error for check', () => {
const type = CHECK_RUNNERS;
const message = 'lorem ipsum';
state.checks[type] = {};
mutations[types.RECEIVE_CHECK_ERROR](state, { type, message });
expect(state.checks[type]).toEqual({
isLoading: false,
isValid: false,
message,
});
});
});
describe(types.RECEIVE_CHECK_SUCCESS, () => {
it('sets success for check', () => {
const type = CHECK_CONFIG;
state.checks[type] = {};
mutations[types.RECEIVE_CHECK_SUCCESS](state, type);
expect(state.checks[type]).toEqual({
isLoading: false,
isValid: true,
message: null,
});
});
});
});
import Vue from 'vue';
import store from 'ee/operations/store/index';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { GlLoadingIcon } from '@gitlab/ui';
import ProjectAvatar from '~/vue_shared/components/project_avatar/default.vue';
import ProjectSearch from 'ee/operations/components/dashboard/project_search.vue';
import TokenizedInput from 'ee/operations/components/tokenized_input/input.vue';
......@@ -10,7 +9,6 @@ import { getChildInstances, mouseEvent, clearState } from '../../helpers';
describe('project search component', () => {
const ProjectSearchComponent = Vue.extend(ProjectSearch);
const GlLoadingIconComponent = Vue.extend(GlLoadingIcon);
const TokenizedInputComponent = Vue.extend(TokenizedInput);
const ProjectAvatarComponent = Vue.extend(ProjectAvatar);
......@@ -84,7 +82,7 @@ describe('project search component', () => {
store.state.searchCount = 1;
vm = mount();
expect(getChildInstances(vm, GlLoadingIconComponent).length).toBe(1);
expect(vm.$el).toContainElement('.loading-container');
});
it('renders search results', () => {
......
......@@ -764,6 +764,12 @@ msgstr ""
msgid "An error occurred. Please try again."
msgstr ""
msgid "An unexpected error occurred while checking the project environment."
msgstr ""
msgid "An unexpected error occurred while checking the project runners."
msgstr ""
msgid "Analytics"
msgstr ""
......@@ -2276,12 +2282,18 @@ msgstr ""
msgid "Confidentiality"
msgstr ""
msgid "Configure GitLab runners to start using Web Terminal. %{helpStart}Learn more.%{helpEnd}"
msgstr ""
msgid "Configure Gitaly timeouts."
msgstr ""
msgid "Configure Tracing"
msgstr ""
msgid "Configure a <code>.gitlab-webide.yml</code> file in the <code>.gitlab</code> directory to start using the Web Terminal. %{helpStart}Learn more.%{helpEnd}"
msgstr ""
msgid "Configure automatic git checks and housekeeping on repositories."
msgstr ""
......@@ -4936,6 +4948,9 @@ msgstr ""
msgid "Learn more about Kubernetes"
msgstr ""
msgid "Learn more about Web Terminal"
msgstr ""
msgid "Learn more about custom project templates"
msgstr ""
......@@ -8041,6 +8056,9 @@ msgstr ""
msgid "Starred projects"
msgstr ""
msgid "Start Web Terminal"
msgstr ""
msgid "Start a %{new_merge_request} with these changes"
msgstr ""
......@@ -9613,6 +9631,9 @@ msgstr ""
msgid "You do not have any subscriptions yet"
msgstr ""
msgid "You do not have permission to run the Web Terminal. Please contact a project administrator."
msgstr ""
msgid "You do not have the correct permissions to override the settings from the LDAP group sync."
msgstr ""
......
......@@ -180,6 +180,23 @@ describe('Api', () => {
});
});
describe('projectRunners', () => {
it('fetches the runners of a project', done => {
const projectPath = 7;
const params = { scope: 'active' };
const mockData = [{ id: 4 }];
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/runners`;
mock.onGet(expectedUrl, { params }).reply(200, mockData);
Api.projectRunners(projectPath, { params })
.then(({ data }) => {
expect(data).toEqual(mockData);
})
.then(done)
.catch(done.fail);
});
});
describe('newLabel', () => {
it('creates a new label', done => {
const namespace = 'some namespace';
......
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