From a6dbff3c3f8c283548d6f8d889170f0d1151bf59 Mon Sep 17 00:00:00 2001 From: Andrew Fontaine Date: Thu, 29 Apr 2021 12:33:11 -0400 Subject: [PATCH 1/5] Add to IDE Service to Dismiss Callouts, Fetch CI We will be using the callout mechanism to persist whether or not users need to be told about using environments with GitLab CI. This necessitates the ability to call graphql mutations. We also need to be able to query the graphql API for the fully parsed CI configuration. Fortunately, that query was already built by the pipeline editing team, so we can re-use it here. One thought is that it might be worth it to build our own query to only fetch a job's environment configuration, instead of the entirety of the pipeline, but for now re-use is better than new. --- .../dismiss_user_callout.mutation.graphql | 9 +++++ app/assets/javascripts/ide/services/gql.js | 1 + app/assets/javascripts/ide/services/index.js | 16 ++++++++- spec/frontend/ide/services/index_spec.js | 33 ++++++++++++++++++- 4 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 app/assets/javascripts/graphql_shared/mutations/dismiss_user_callout.mutation.graphql diff --git a/app/assets/javascripts/graphql_shared/mutations/dismiss_user_callout.mutation.graphql b/app/assets/javascripts/graphql_shared/mutations/dismiss_user_callout.mutation.graphql new file mode 100644 index 00000000000000..2b831bf1338871 --- /dev/null +++ b/app/assets/javascripts/graphql_shared/mutations/dismiss_user_callout.mutation.graphql @@ -0,0 +1,9 @@ +mutation dismissUserCallout($input: UserCalloutCreateInput!) { + userCalloutCreate(input: $input) { + errors + userCallout { + dismissedAt + featureName + } + } +} diff --git a/app/assets/javascripts/ide/services/gql.js b/app/assets/javascripts/ide/services/gql.js index 89dda187360702..c8c1031c0f31f3 100644 --- a/app/assets/javascripts/ide/services/gql.js +++ b/app/assets/javascripts/ide/services/gql.js @@ -18,3 +18,4 @@ const getClient = memoize(() => ); export const query = (...args) => getClient().query(...args); +export const mutate = (...args) => getClient().mutate(...args); diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js index 0aa08323d13ba9..6bd28cd4fb6187 100644 --- a/app/assets/javascripts/ide/services/index.js +++ b/app/assets/javascripts/ide/services/index.js @@ -1,8 +1,10 @@ import getIdeProject from 'ee_else_ce/ide/queries/get_ide_project.query.graphql'; import Api from '~/api'; +import dismissUserCallout from '~/graphql_shared/mutations/dismiss_user_callout.mutation.graphql'; import axios from '~/lib/utils/axios_utils'; import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility'; -import { query } from './gql'; +import ciConfig from '~/pipeline_editor/graphql/queries/ci_config.graphql'; +import { query, mutate } from './gql'; const fetchApiProjectData = (projectPath) => Api.project(projectPath).then(({ data }) => data); @@ -101,4 +103,16 @@ export default { const url = `${gon.relative_url_root}/${projectPath}/usage_ping/web_ide_pipelines_count`; return axios.post(url); }, + getCiConfig(projectPath, content) { + return query({ + query: ciConfig, + variables: { projectPath, content }, + }).then(({ data }) => data.ciConfig); + }, + dismissUserCallout(name) { + return mutate({ + mutation: dismissUserCallout, + variables: { input: { featureName: name } }, + }).then(({ data }) => data); + }, }; diff --git a/spec/frontend/ide/services/index_spec.js b/spec/frontend/ide/services/index_spec.js index 3503834e24b764..4a726cff3b6b3f 100644 --- a/spec/frontend/ide/services/index_spec.js +++ b/spec/frontend/ide/services/index_spec.js @@ -2,9 +2,11 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import getIdeProject from 'ee_else_ce/ide/queries/get_ide_project.query.graphql'; import Api from '~/api'; +import dismissUserCallout from '~/graphql_shared/mutations/dismiss_user_callout.mutation.graphql'; import services from '~/ide/services'; -import { query } from '~/ide/services/gql'; +import { query, mutate } from '~/ide/services/gql'; import { escapeFileUrl } from '~/lib/utils/url_utility'; +import ciConfig from '~/pipeline_editor/graphql/queries/ci_config.graphql'; import { projectData } from '../mock_data'; jest.mock('~/api'); @@ -299,4 +301,33 @@ describe('IDE services', () => { }); }); }); + describe('getCiConfig', () => { + const TEST_PROJECT_PATH = 'foo/bar'; + const TEST_CI_CONFIG = 'test config'; + + it('queries with the given CI config and project', () => { + const result = { data: { ciConfig: { test: 'data' } } }; + query.mockResolvedValue(result); + return services.getCiConfig(TEST_PROJECT_PATH, TEST_CI_CONFIG).then((data) => { + expect(data).toEqual(result.data.ciConfig); + expect(query).toHaveBeenCalledWith({ + query: ciConfig, + variables: { projectPath: TEST_PROJECT_PATH, content: TEST_CI_CONFIG }, + }); + }); + }); + }); + describe('dismissUserCallout', () => { + it('mutates the callout to dismiss', () => { + const result = { data: { callouts: { test: 'data' } } }; + mutate.mockResolvedValue(result); + return services.dismissUserCallout('test').then((data) => { + expect(data).toEqual(result.data); + expect(mutate).toHaveBeenCalledWith({ + mutation: dismissUserCallout, + variables: { input: { featureName: 'test' } }, + }); + }); + }); + }); }); -- GitLab From 8b33b3adeff50e4f199714316e22d0b1f6dba700 Mon Sep 17 00:00:00 2001 From: Andrew Fontaine Date: Thu, 29 Apr 2021 12:56:53 -0400 Subject: [PATCH 2/5] Add Alerts Module to the WebIDE An alert is a message to be displayed in a GlAlert in some conditional context of the WebIDE. The first alert added, for example, conditionally shows a message suggesting users investigate the use of environments in their CI configuration. An alert has a very specific interface to publicly conform to: interface Alert { key: Symbol; show: (state: WebIdeState, file: File) => boolean; props: GlAlertProps; dismiss: (store: WebIdeStore) => void; message: VueComponent; } - The key must be a unique value, so Symobl is used to ease uniqueness (two symbols made with the same key are still unique). - The show function takes the state of the WebIDE Vue store and the currently active file to compute a boolean indicating whether or not this alert should be currently shown. In this first alert, we only wish to show it if: - we are currently on the commit tab - we are viewing the root GitLab CI file - displaying this alert is enabled for the current user - the GitLab CI file has been successfully parsed, - and the CI configuration contains no environment configuration - The props must match the props used to configure a GlAlert - The dismiss function may optionally take the WebIDE store to execute further actions or mutations. - The message must be a Vue component definition. This is so that rich HTML content may be easily used within the message. Currently, it must not take any props or attributes, although it should have access to the WebIDE store. As only one alert may be shown at a time, any future alerts with overlapping conditions must keep in mind that these should be listed by priority. This is purposely simple to not expose the user to too many alerts at once. If this is ever converted to a notification system, it may make sense to alter this. --- .../ide/lib/alerts/environments.vue | 26 +++++++++++++++++++ .../javascripts/ide/lib/alerts/index.js | 20 ++++++++++++++ locale/gitlab.pot | 3 +++ .../ide/lib/alerts/environment_spec.js | 21 +++++++++++++++ 4 files changed, 70 insertions(+) create mode 100644 app/assets/javascripts/ide/lib/alerts/environments.vue create mode 100644 app/assets/javascripts/ide/lib/alerts/index.js create mode 100644 spec/frontend/ide/lib/alerts/environment_spec.js diff --git a/app/assets/javascripts/ide/lib/alerts/environments.vue b/app/assets/javascripts/ide/lib/alerts/environments.vue new file mode 100644 index 00000000000000..7a0893fe343bf6 --- /dev/null +++ b/app/assets/javascripts/ide/lib/alerts/environments.vue @@ -0,0 +1,26 @@ + + diff --git a/app/assets/javascripts/ide/lib/alerts/index.js b/app/assets/javascripts/ide/lib/alerts/index.js new file mode 100644 index 00000000000000..c9db9779b1fa95 --- /dev/null +++ b/app/assets/javascripts/ide/lib/alerts/index.js @@ -0,0 +1,20 @@ +import { leftSidebarViews } from '../../constants'; +import EnvironmentsMessage from './environments.vue'; + +const alerts = [ + { + key: Symbol('ALERT_ENVIRONMENT'), + show: (state, file) => + state.currentActivityView === leftSidebarViews.commit.name && + file.path === '.gitlab-ci.yml' && + state.environmentsGuidanceAlertDetected && + !state.environmentsGuidanceAlertDismissed, + props: { variant: 'tip' }, + dismiss: ({ dispatch }) => dispatch('dismissEnvironmentsGuidance'), + message: EnvironmentsMessage, + }, +]; + +export const findAlertKeyToShow = (...args) => alerts.find((x) => x.show(...args))?.key; + +export const getAlert = (key) => alerts.find((x) => x.key === key); diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ac56337b7cf0b8..78271197ff22d8 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -21975,6 +21975,9 @@ msgstr "" msgid "No data to display" msgstr "" +msgid "No deployments detected. Use environments to control your software's continuous deployment. %{linkStart}Learn more about deployment jobs.%{linkEnd}" +msgstr "" + msgid "No deployments found" msgstr "" diff --git a/spec/frontend/ide/lib/alerts/environment_spec.js b/spec/frontend/ide/lib/alerts/environment_spec.js new file mode 100644 index 00000000000000..d645209345ce01 --- /dev/null +++ b/spec/frontend/ide/lib/alerts/environment_spec.js @@ -0,0 +1,21 @@ +import { GlLink } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import Environments from '~/ide/lib/alerts/environments.vue'; + +describe('~/ide/lib/alerts/environment.vue', () => { + let wrapper; + + beforeEach(() => { + wrapper = mount(Environments); + }); + + it('shows a message regarding environments', () => { + expect(wrapper.text()).toBe( + "No deployments detected. Use environments to control your software's continuous deployment. Learn more about deployment jobs.", + ); + }); + + it('links to the help page on environments', () => { + expect(wrapper.findComponent(GlLink).attributes('href')).toBe('/help/ci/environments/index.md'); + }); +}); -- GitLab From 78375ddcac3577917e45341047c97aedd72789bb Mon Sep 17 00:00:00 2001 From: Andrew Fontaine Date: Thu, 29 Apr 2021 13:31:37 -0400 Subject: [PATCH 3/5] 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. --- app/assets/javascripts/ide/index.js | 5 +- app/assets/javascripts/ide/stores/actions.js | 3 +- .../javascripts/ide/stores/actions/alert.js | 18 ++++++++ app/assets/javascripts/ide/stores/getters.js | 2 + .../javascripts/ide/stores/getters/alert.js | 3 ++ .../javascripts/ide/stores/mutation_types.js | 5 ++ .../javascripts/ide/stores/mutations.js | 2 + .../javascripts/ide/stores/mutations/alert.js | 21 +++++++++ app/assets/javascripts/ide/stores/state.js | 2 + app/helpers/ide_helper.rb | 7 ++- app/models/user_callout.rb | 3 +- doc/api/graphql/reference/index.md | 1 + .../frontend/ide/stores/actions/alert_spec.js | 46 +++++++++++++++++++ spec/frontend/ide/stores/actions_spec.js | 19 ++++---- .../frontend/ide/stores/getters/alert_spec.js | 46 +++++++++++++++++++ .../ide/stores/mutations/alert_spec.js | 26 +++++++++++ 16 files changed, 195 insertions(+), 14 deletions(-) create mode 100644 app/assets/javascripts/ide/stores/actions/alert.js create mode 100644 app/assets/javascripts/ide/stores/getters/alert.js create mode 100644 app/assets/javascripts/ide/stores/mutations/alert.js create mode 100644 spec/frontend/ide/stores/actions/alert_spec.js create mode 100644 spec/frontend/ide/stores/getters/alert_spec.js create mode 100644 spec/frontend/ide/stores/mutations/alert_spec.js diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js index 2ce5bf7e271812..7109c45a3fecea 100644 --- a/app/assets/javascripts/ide/index.js +++ b/app/assets/javascripts/ide/index.js @@ -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); diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index bf94f9d31c88c5..062dc150805a4c 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -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'; diff --git a/app/assets/javascripts/ide/stores/actions/alert.js b/app/assets/javascripts/ide/stores/actions/alert.js new file mode 100644 index 00000000000000..4c33dc19520980 --- /dev/null +++ b/app/assets/javascripts/ide/stores/actions/alert.js @@ -0,0 +1,18 @@ +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); + }); diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js index e8b1a0ea494375..3c02b1d1da7b56 100644 --- a/app/assets/javascripts/ide/stores/getters.js +++ b/app/assets/javascripts/ide/stores/getters.js @@ -262,3 +262,5 @@ export const getJsonSchemaForPath = (state, getters) => (path) => { fileMatch: [`*${path}`], }; }; + +export * from './getters/alert'; diff --git a/app/assets/javascripts/ide/stores/getters/alert.js b/app/assets/javascripts/ide/stores/getters/alert.js new file mode 100644 index 00000000000000..714e2d89b4f359 --- /dev/null +++ b/app/assets/javascripts/ide/stores/getters/alert.js @@ -0,0 +1,3 @@ +import { findAlertKeyToShow } from '../../lib/alerts'; + +export const getAlert = (state) => (file) => findAlertKeyToShow(state, file); diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js index 76ba8339703e09..77755b179ef3de 100644 --- a/app/assets/javascripts/ide/stores/mutation_types.js +++ b/app/assets/javascripts/ide/stores/mutation_types.js @@ -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'; diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js index 576f861a090024..48648796e660b5 100644 --- a/app/assets/javascripts/ide/stores/mutations.js +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -1,5 +1,6 @@ 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, }; diff --git a/app/assets/javascripts/ide/stores/mutations/alert.js b/app/assets/javascripts/ide/stores/mutations/alert.js new file mode 100644 index 00000000000000..bb2d33a836bf9b --- /dev/null +++ b/app/assets/javascripts/ide/stores/mutations/alert.js @@ -0,0 +1,21 @@ +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; + }, +}; diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js index c1a83bf0726add..83551e87f09932 100644 --- a/app/assets/javascripts/ide/stores/state.js +++ b/app/assets/javascripts/ide/stores/state.js @@ -30,4 +30,6 @@ export default () => ({ renderWhitespaceInCode: false, editorTheme: DEFAULT_THEME, codesandboxBundlerUrl: null, + environmentsGuidanceAlertDismissed: false, + environmentsGuidanceAlertDetected: false, }); diff --git a/app/helpers/ide_helper.rb b/app/helpers/ide_helper.rb index 61d8d0f779deba..a33616a8051e14 100644 --- a/app/helpers/ide_helper.rb +++ b/app/helpers/ide_helper.rb @@ -17,7 +17,8 @@ def ide_data '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 @@ def convert_to_project_entity_json(project) 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') diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb index 852ea05b77fdd8..8fc9efddac9bad 100644 --- a/app/models/user_callout.rb +++ b/app/models/user_callout.rb @@ -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 diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 7f9bd21722406b..161fdb670bf2fb 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -14360,6 +14360,7 @@ Name of the feature that the callout is for. | `UNFINISHED_TAG_CLEANUP_CALLOUT` | Callout feature name for unfinished_tag_cleanup_callout. | | `WEBHOOKS_MOVED` | Callout feature name for webhooks_moved. | | `WEB_IDE_ALERT_DISMISSED` | Callout feature name for web_ide_alert_dismissed. | +| `WEB_IDE_CI_ENVIRONMENTS_GUIDANCE` | Callout feature name for web_ide_ci_environments_guidance. | ### `UserState` diff --git a/spec/frontend/ide/stores/actions/alert_spec.js b/spec/frontend/ide/stores/actions/alert_spec.js new file mode 100644 index 00000000000000..1321c402ebbc26 --- /dev/null +++ b/spec/frontend/ide/stores/actions/alert_spec.js @@ -0,0 +1,46 @@ +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', + ), + ); + }); + }); +}); diff --git a/spec/frontend/ide/stores/actions_spec.js b/spec/frontend/ide/stores/actions_spec.js index d47dd88dd4796e..ad55313da9358b 100644 --- a/spec/frontend/ide/stores/actions_spec.js +++ b/spec/frontend/ide/stores/actions_spec.js @@ -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 } }], + [], + ); }); }); diff --git a/spec/frontend/ide/stores/getters/alert_spec.js b/spec/frontend/ide/stores/getters/alert_spec.js new file mode 100644 index 00000000000000..7068b8e637f6cc --- /dev/null +++ b/spec/frontend/ide/stores/getters/alert_spec.js @@ -0,0 +1,46 @@ +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' }); + }); + }); + }); +}); diff --git a/spec/frontend/ide/stores/mutations/alert_spec.js b/spec/frontend/ide/stores/mutations/alert_spec.js new file mode 100644 index 00000000000000..2840ec4ebb70e1 --- /dev/null +++ b/spec/frontend/ide/stores/mutations/alert_spec.js @@ -0,0 +1,26 @@ +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); + }); + }); +}); -- GitLab From 4cf8a89891087246f78b5098a124b22e2e8e16f2 Mon Sep 17 00:00:00 2001 From: Andrew Fontaine Date: Thu, 29 Apr 2021 13:55:06 -0400 Subject: [PATCH 4/5] Display File-Based Alerts on the WebIDE Provides a way to display alerts attached to files within the WebIDE. The first alert shows guidance to users who are editing their CI configuration about environments, if we are reasonably certain they don't know about environments. We need to request the parsed CI configuration while the user edits it, and accomplish this by watching the content. This watcher is conditionally created and destroyed when the config file is active. --- .../javascripts/ide/components/file_alert.vue | 26 +++++++++++++ .../ide/components/repo_editor.vue | 37 +++++++++++++++++-- .../ide/components/repo_editor_spec.js | 1 + 3 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 app/assets/javascripts/ide/components/file_alert.vue diff --git a/app/assets/javascripts/ide/components/file_alert.vue b/app/assets/javascripts/ide/components/file_alert.vue new file mode 100644 index 00000000000000..2a894596bf4765 --- /dev/null +++ b/app/assets/javascripts/ide/components/file_alert.vue @@ -0,0 +1,26 @@ + + diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index b57dcd4276c078..bf2af9ffd49ae3 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -1,4 +1,5 @@