Commit 78375ddc authored by Andrew Fontaine's avatar Andrew Fontaine

Add Store Configuration for Environment Guidance

The getAlert function searches through all available alerts (added
previously), returning the first alert where its show function returns
true, or null if none is found.

Actions to fetch the CI configuration for its given current
content (when it is being edited), as well as to dismiss the callout for
environment guidance are added here.

We parse the CI configuration during the mutation to only retain whether
or not we have:

- sucessfully parsed the CI config, and
- found an environment within the config.
parent 8b33b3ad
......@@ -56,11 +56,12 @@ export function initIde(el, options = {}) {
webIDEHelpPagePath: el.dataset.webIdeHelpPagePath,
forkInfo: el.dataset.forkInfo ? JSON.parse(el.dataset.forkInfo) : null,
});
this.setInitialData({
this.init({
clientsidePreviewEnabled: parseBoolean(el.dataset.clientsidePreviewEnabled),
renderWhitespaceInCode: parseBoolean(el.dataset.renderWhitespaceInCode),
editorTheme: window.gon?.user_color_scheme || DEFAULT_THEME,
codesandboxBundlerUrl: el.dataset.codesandboxBundlerUrl,
environmentsGuidanceAlertDismissed: !parseBoolean(el.dataset.enableEnvironmentsGuidance),
});
},
beforeDestroy() {
......@@ -68,7 +69,7 @@ export function initIde(el, options = {}) {
this.$emit('destroy');
},
methods: {
...mapActions(['setEmptyStateSvgs', 'setLinks', 'setInitialData']),
...mapActions(['setEmptyStateSvgs', 'setLinks', 'init']),
},
render(createElement) {
return createElement(rootComponent);
......
......@@ -17,7 +17,7 @@ import * as types from './mutation_types';
export const redirectToUrl = (self, url) => visitUrl(url);
export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
export const init = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
export const discardAllChanges = ({ state, commit, dispatch }) => {
state.changedFiles.forEach((file) => dispatch('restoreOriginalFile', file.path));
......@@ -316,3 +316,4 @@ export * from './actions/tree';
export * from './actions/file';
export * from './actions/project';
export * from './actions/merge_request';
export * from './actions/alert';
import service from '../../services';
import {
DETECT_ENVIRONMENTS_GUIDANCE_ALERT,
DISMISS_ENVIRONMENTS_GUIDANCE_ALERT,
} from '../mutation_types';
export const detectGitlabCiFileAlerts = ({ dispatch }, content) =>
dispatch('detectEnvironmentsGuidance', content);
export const detectEnvironmentsGuidance = ({ commit, state }, content) =>
service.getCiConfig(state.currentProjectId, content).then((data) => {
commit(DETECT_ENVIRONMENTS_GUIDANCE_ALERT, data?.stages);
});
export const dismissEnvironmentsGuidance = ({ commit }) =>
service.dismissUserCallout('web_ide_ci_environments_guidance').then(() => {
commit(DISMISS_ENVIRONMENTS_GUIDANCE_ALERT);
});
......@@ -262,3 +262,5 @@ export const getJsonSchemaForPath = (state, getters) => (path) => {
fileMatch: [`*${path}`],
};
};
export * from './getters/alert';
import { findAlertKeyToShow } from '../../lib/alerts';
export const getAlert = (state) => (file) => findAlertKeyToShow(state, file);
......@@ -70,3 +70,8 @@ export const RENAME_ENTRY = 'RENAME_ENTRY';
export const REVERT_RENAME_ENTRY = 'REVERT_RENAME_ENTRY';
export const RESTORE_TREE = 'RESTORE_TREE';
// Alert mutation types
export const DETECT_ENVIRONMENTS_GUIDANCE_ALERT = 'DETECT_ENVIRONMENTS_GUIDANCE_ALERT';
export const DISMISS_ENVIRONMENTS_GUIDANCE_ALERT = 'DISMISS_ENVIRONMENTS_GUIDANCE_ALERT';
import Vue from 'vue';
import * as types from './mutation_types';
import alertMutations from './mutations/alert';
import branchMutations from './mutations/branch';
import fileMutations from './mutations/file';
import mergeRequestMutation from './mutations/merge_request';
......@@ -244,4 +245,5 @@ export default {
...fileMutations,
...treeMutations,
...branchMutations,
...alertMutations,
};
import {
DETECT_ENVIRONMENTS_GUIDANCE_ALERT,
DISMISS_ENVIRONMENTS_GUIDANCE_ALERT,
} from '../mutation_types';
export default {
[DETECT_ENVIRONMENTS_GUIDANCE_ALERT](state, stages) {
if (!stages) {
return;
}
const hasEnvironments = stages?.nodes?.some((stage) =>
stage.groups.nodes.some((group) => group.jobs.nodes.some((job) => job.environment)),
);
const hasParsedCi = Array.isArray(stages.nodes);
state.environmentsGuidanceAlertDetected = !hasEnvironments && hasParsedCi;
},
[DISMISS_ENVIRONMENTS_GUIDANCE_ALERT](state) {
state.environmentsGuidanceAlertDismissed = true;
},
};
......@@ -30,4 +30,6 @@ export default () => ({
renderWhitespaceInCode: false,
editorTheme: DEFAULT_THEME,
codesandboxBundlerUrl: null,
environmentsGuidanceAlertDismissed: false,
environmentsGuidanceAlertDetected: false,
});
......@@ -17,7 +17,8 @@ module IdeHelper
'file-path' => @path,
'merge-request' => @merge_request,
'fork-info' => @fork_info&.to_json,
'project' => convert_to_project_entity_json(@project)
'project' => convert_to_project_entity_json(@project),
'enable-environments-guidance' => enable_environment_guidance?.to_s
}
end
......@@ -28,6 +29,10 @@ module IdeHelper
API::Entities::Project.represent(project).to_json
end
def enable_environment_guidance?
current_user.find_or_initialize_callout(:web_ide_ci_environments_guidance).dismissed_at.nil?
end
end
::IdeHelper.prepend_if_ee('::EE::IdeHelper')
......@@ -31,7 +31,8 @@ class UserCallout < ApplicationRecord
unfinished_tag_cleanup_callout: 27,
eoa_bronze_plan_banner: 28, # EE-only
pipeline_needs_banner: 29,
pipeline_needs_hover_tip: 30
pipeline_needs_hover_tip: 30,
web_ide_ci_environments_guidance: 31
}
validates :user, presence: true
......
......@@ -14360,6 +14360,7 @@ Name of the feature that the callout is for.
| <a id="usercalloutfeaturenameenumunfinished_tag_cleanup_callout"></a>`UNFINISHED_TAG_CLEANUP_CALLOUT` | Callout feature name for unfinished_tag_cleanup_callout. |
| <a id="usercalloutfeaturenameenumwebhooks_moved"></a>`WEBHOOKS_MOVED` | Callout feature name for webhooks_moved. |
| <a id="usercalloutfeaturenameenumweb_ide_alert_dismissed"></a>`WEB_IDE_ALERT_DISMISSED` | Callout feature name for web_ide_alert_dismissed. |
| <a id="usercalloutfeaturenameenumweb_ide_ci_environments_guidance"></a>`WEB_IDE_CI_ENVIRONMENTS_GUIDANCE` | Callout feature name for web_ide_ci_environments_guidance. |
### `UserState`
......
import testAction from 'helpers/vuex_action_helper';
import service from '~/ide/services';
import {
detectEnvironmentsGuidance,
dismissEnvironmentsGuidance,
} from '~/ide/stores/actions/alert';
import * as types from '~/ide/stores/mutation_types';
jest.mock('~/ide/services');
describe('~/ide/stores/actions/alert', () => {
describe('detectEnvironmentsGuidance', () => {
it('should try to fetch CI info', () => {
const stages = ['a', 'b', 'c'];
service.getCiConfig.mockResolvedValue({ stages });
return testAction(
detectEnvironmentsGuidance,
'the content',
{ currentProjectId: 'gitlab/test' },
[{ type: types.DETECT_ENVIRONMENTS_GUIDANCE_ALERT, payload: stages }],
[],
() => expect(service.getCiConfig).toHaveBeenCalledWith('gitlab/test', 'the content'),
);
});
});
describe('dismissCallout', () => {
it('should try to dismiss the given callout', () => {
const callout = { featureName: 'test', dismissedAt: 'now' };
service.dismissUserCallout.mockResolvedValue({ userCalloutCreate: { userCallout: callout } });
return testAction(
dismissEnvironmentsGuidance,
undefined,
{},
[{ type: types.DISMISS_ENVIRONMENTS_GUIDANCE_ALERT }],
[],
() =>
expect(service.dismissUserCallout).toHaveBeenCalledWith(
'web_ide_ci_environments_guidance',
),
);
});
});
});
......@@ -4,6 +4,7 @@ import eventHub from '~/ide/eventhub';
import { createRouter } from '~/ide/ide_router';
import { createStore } from '~/ide/stores';
import {
init,
stageAllChanges,
unstageAllChanges,
toggleFileFinder,
......@@ -54,15 +55,15 @@ describe('Multi-file store actions', () => {
});
});
describe('setInitialData', () => {
it('commits initial data', (done) => {
store
.dispatch('setInitialData', { canCommit: true })
.then(() => {
expect(store.state.canCommit).toBeTruthy();
done();
})
.catch(done.fail);
describe('init', () => {
it('commits initial data and requests user callouts', () => {
return testAction(
init,
{ canCommit: true },
store.state,
[{ type: 'SET_INITIAL_DATA', payload: { canCommit: true } }],
[],
);
});
});
......
import { getAlert } from '~/ide/lib/alerts';
import EnvironmentsMessage from '~/ide/lib/alerts/environments.vue';
import { createStore } from '~/ide/stores';
import * as getters from '~/ide/stores/getters/alert';
import { file } from '../../helpers';
describe('IDE store alert getters', () => {
let localState;
let localStore;
beforeEach(() => {
localStore = createStore();
localState = localStore.state;
});
describe('alerts', () => {
describe('shows an alert about environments', () => {
let alert;
beforeEach(() => {
const f = file('.gitlab-ci.yml');
localState.openFiles.push(f);
localState.currentActivityView = 'repo-commit-section';
localState.environmentsGuidanceAlertDetected = true;
localState.environmentsGuidanceAlertDismissed = false;
const alertKey = getters.getAlert(localState)(f);
alert = getAlert(alertKey);
});
it('has a message suggesting to use environments', () => {
expect(alert.message).toEqual(EnvironmentsMessage);
});
it('dispatches to dismiss the callout on dismiss', () => {
jest.spyOn(localStore, 'dispatch').mockImplementation();
alert.dismiss(localStore);
expect(localStore.dispatch).toHaveBeenCalledWith('dismissEnvironmentsGuidance');
});
it('should be a tip alert', () => {
expect(alert.props).toEqual({ variant: 'tip' });
});
});
});
});
import * as types from '~/ide/stores/mutation_types';
import mutations from '~/ide/stores/mutations/alert';
describe('~/ide/stores/mutations/alert', () => {
const state = {};
describe(types.DETECT_ENVIRONMENTS_GUIDANCE_ALERT, () => {
it('checks the stages for any that configure environments', () => {
mutations[types.DETECT_ENVIRONMENTS_GUIDANCE_ALERT](state, {
nodes: [{ groups: { nodes: [{ jobs: { nodes: [{}] } }] } }],
});
expect(state.environmentsGuidanceAlertDetected).toBe(true);
mutations[types.DETECT_ENVIRONMENTS_GUIDANCE_ALERT](state, {
nodes: [{ groups: { nodes: [{ jobs: { nodes: [{ environment: {} }] } }] } }],
});
expect(state.environmentsGuidanceAlertDetected).toBe(false);
});
});
describe(types.DISMISS_ENVIRONMENTS_GUIDANCE_ALERT, () => {
it('stops environments guidance', () => {
mutations[types.DISMISS_ENVIRONMENTS_GUIDANCE_ALERT](state);
expect(state.environmentsGuidanceAlertDismissed).toBe(true);
});
});
});
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