diff options
Diffstat (limited to 'app/assets/javascripts/ide')
19 files changed, 193 insertions, 12 deletions
diff --git a/app/assets/javascripts/ide/components/branches/item.vue b/app/assets/javascripts/ide/components/branches/item.vue index 35e2f99cb6a..bdfcff3136b 100644 --- a/app/assets/javascripts/ide/components/branches/item.vue +++ b/app/assets/javascripts/ide/components/branches/item.vue @@ -34,7 +34,7 @@ export default { <template> <a :href="branchHref" class="btn-link d-flex align-items-center"> <span class="d-flex gl-mr-3 ide-search-list-current-icon"> - <gl-icon v-if="isActive" :size="18" name="mobile-issue-close" use-deprecated-sizes /> + <gl-icon v-if="isActive" :size="16" name="mobile-issue-close" /> </span> <span> <strong> {{ item.name }} </strong> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue index 273d8d972f7..fcc900bbc96 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue @@ -76,8 +76,9 @@ export default { :value="$options.commitToCurrentBranch" :disabled="!canPushToBranch" :title="$options.currentBranchPermissionsTooltip" + data-qa-selector="commit_to_current_branch_radio_container" > - <span class="ide-option-label" data-qa-selector="commit_to_current_branch_radio"> + <span class="ide-option-label"> <gl-sprintf :message="s__('IDE|Commit to %{branchName} branch')"> <template #branchName> <strong class="monospace">{{ currentBranchText }}</strong> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue index 039b4a54b26..870355e884e 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue @@ -64,6 +64,7 @@ export default { :disabled="disabled" type="radio" name="commit-action" + data-qa-selector="commit_type_radio" @change="updateCommitAction($event.target.value)" /> <span class="gl-ml-3"> 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 00000000000..2a894596bf4 --- /dev/null +++ b/app/assets/javascripts/ide/components/file_alert.vue @@ -0,0 +1,26 @@ +<script> +import { GlAlert } from '@gitlab/ui'; +import { getAlert } from '../lib/alerts'; + +export default { + components: { + GlAlert, + }, + props: { + alertKey: { + type: Symbol, + required: true, + }, + }, + computed: { + alert() { + return getAlert(this.alertKey); + }, + }, +}; +</script> +<template> + <gl-alert v-bind="alert.props" @dismiss="alert.dismiss($store)"> + <component :is="alert.message" /> + </gl-alert> +</template> diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index b57dcd4276c..bf2af9ffd49 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -1,4 +1,5 @@ <script> +import { debounce } from 'lodash'; import { mapState, mapGetters, mapActions } from 'vuex'; import { EDITOR_TYPE_DIFF, @@ -34,11 +35,13 @@ import mapRulesToMonaco from '../lib/editorconfig/rules_mapper'; import { getFileEditorOrDefault } from '../stores/modules/editor/utils'; import { extractMarkdownImagesFromEntries } from '../stores/utils'; import { getPathParent, readFileAsDataURL, registerSchema, isTextFile } from '../utils'; +import FileAlert from './file_alert.vue'; import FileTemplatesBar from './file_templates/bar.vue'; export default { name: 'RepoEditor', components: { + FileAlert, ContentViewer, DiffViewer, FileTemplatesBar, @@ -57,6 +60,7 @@ export default { globalEditor: null, modelManager: new ModelManager(), isEditorLoading: true, + unwatchCiYaml: null, }; }, computed: { @@ -74,6 +78,7 @@ export default { 'currentProjectId', ]), ...mapGetters([ + 'getAlert', 'currentMergeRequest', 'getStagedFile', 'isEditModeActive', @@ -82,6 +87,9 @@ export default { 'getJsonSchemaForPath', ]), ...mapGetters('fileTemplates', ['showFileTemplatesBar']), + alertKey() { + return this.getAlert(this.file); + }, fileEditor() { return getFileEditorOrDefault(this.fileEditors, this.file.path); }, @@ -136,6 +144,16 @@ export default { }, }, watch: { + 'file.name': { + handler() { + this.stopWatchingCiYaml(); + + if (this.file.name === '.gitlab-ci.yml') { + this.startWatchingCiYaml(); + } + }, + immediate: true, + }, file(newVal, oldVal) { if (oldVal.pending) { this.removePendingTab(oldVal); @@ -216,6 +234,7 @@ export default { 'removePendingTab', 'triggerFilesChange', 'addTempImage', + 'detectGitlabCiFileAlerts', ]), ...mapActions('editor', ['updateFileEditor']), initEditor() { @@ -422,6 +441,18 @@ export default { this.updateFileEditor({ path: this.file.path, data }); }, + startWatchingCiYaml() { + this.unwatchCiYaml = this.$watch( + 'file.content', + debounce(this.detectGitlabCiFileAlerts, 500), + ); + }, + stopWatchingCiYaml() { + if (this.unwatchCiYaml) { + this.unwatchCiYaml(); + this.unwatchCiYaml = null; + } + }, }, viewerTypes, FILE_VIEW_MODE_EDITOR, @@ -439,9 +470,8 @@ export default { role="button" data-testid="edit-tab" @click.prevent="updateEditor({ viewMode: $options.FILE_VIEW_MODE_EDITOR })" + >{{ __('Edit') }}</a > - {{ __('Edit') }} - </a> </li> <li v-if="previewMode" :class="previewTabCSS"> <a @@ -454,7 +484,8 @@ export default { </li> </ul> </div> - <file-templates-bar v-if="showFileTemplatesBar(file.name)" /> + <file-alert v-if="alertKey" :alert-key="alertKey" /> + <file-templates-bar v-else-if="showFileTemplatesBar(file.name)" /> <div v-show="showEditor" ref="editor" diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js index 2ce5bf7e271..7109c45a3fe 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/lib/alerts/environments.vue b/app/assets/javascripts/ide/lib/alerts/environments.vue new file mode 100644 index 00000000000..ac9a3c3f82c --- /dev/null +++ b/app/assets/javascripts/ide/lib/alerts/environments.vue @@ -0,0 +1,32 @@ +<script> +import { GlSprintf, GlLink } from '@gitlab/ui'; +import { helpPagePath } from '~/helpers/help_page_helper'; +import { __ } from '~/locale'; + +export default { + components: { GlSprintf, GlLink }, + message: __( + "No deployments detected. Use environments to control your software's continuous deployment. %{linkStart}Learn more about deployment jobs.%{linkEnd}", + ), + computed: { + helpLink() { + return helpPagePath('ci/environments/index.md'); + }, + }, +}; +</script> +<template> + <span> + <gl-sprintf :message="$options.message"> + <template #link="{ content }"> + <gl-link + :href="helpLink" + target="_blank" + data-track-action="click_link" + data-track-experiment="in_product_guidance_environments_webide" + >{{ content }}</gl-link + > + </template> + </gl-sprintf> + </span> +</template> 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 00000000000..c9db9779b1f --- /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/app/assets/javascripts/ide/messages.js b/app/assets/javascripts/ide/messages.js index 189226ef835..fe8eba823a8 100644 --- a/app/assets/javascripts/ide/messages.js +++ b/app/assets/javascripts/ide/messages.js @@ -1,11 +1,11 @@ import { s__ } from '~/locale'; export const MSG_CANNOT_PUSH_CODE_SHOULD_FORK = s__( - 'WebIDE|You need permission to edit files directly in this project. Fork this project to make your changes and submit a merge request.', + 'WebIDE|You can’t edit files directly in this project. Fork this project and submit a merge request with your changes.', ); export const MSG_CANNOT_PUSH_CODE_GO_TO_FORK = s__( - 'WebIDE|You need permission to edit files directly in this project. Go to your fork to make changes and submit a merge request.', + 'WebIDE|You can’t edit files directly in this project. Go to your fork and submit a merge request with your changes.', ); export const MSG_CANNOT_PUSH_CODE = s__( @@ -13,7 +13,7 @@ export const MSG_CANNOT_PUSH_CODE = s__( ); export const MSG_CANNOT_PUSH_UNSIGNED = s__( - 'WebIDE|This project does not accept unsigned commits. You will not be able to commit your changes through the Web IDE.', + 'WebIDE|This project does not accept unsigned commits. You can’t commit changes through the Web IDE.', ); export const MSG_CANNOT_PUSH_UNSIGNED_SHORT = s__( diff --git a/app/assets/javascripts/ide/services/gql.js b/app/assets/javascripts/ide/services/gql.js index 89dda187360..c8c1031c0f3 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 0aa08323d13..6bd28cd4fb6 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/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index bf94f9d31c8..062dc150805 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 00000000000..4c33dc19520 --- /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 e8b1a0ea494..3c02b1d1da7 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 00000000000..714e2d89b4f --- /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 76ba8339703..77755b179ef 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 576f861a090..48648796e66 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 00000000000..bb2d33a836b --- /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 c1a83bf0726..83551e87f09 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, }); |