diff options
Diffstat (limited to 'app/assets/javascripts/vue_shared')
36 files changed, 813 insertions, 425 deletions
diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue index c93057c491c..271cfd210a6 100644 --- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue +++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue @@ -66,6 +66,7 @@ export default { <template> <gl-link v-gl-tooltip + class="gl-display-inline-flex gl-align-items-center gl-line-height-0 gl-px-3 gl-py-2 gl-rounded-base" :class="cssClass" :title="title" data-qa-selector="status_badge_link" @@ -75,7 +76,7 @@ export default { <ci-icon :status="status" :css-classes="iconClasses" /> <template v-if="showText"> - {{ status.text }} + <span class="gl-ml-2">{{ status.text }}</span> </template> </gl-link> </template> diff --git a/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue b/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue index 6a03e38a31d..47b96934420 100644 --- a/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue +++ b/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue @@ -2,6 +2,7 @@ import { s__, sprintf } from '~/locale'; import SegmentedControlButtonGroup from '~/vue_shared/components/segmented_control_button_group.vue'; import CiCdAnalyticsAreaChart from './ci_cd_analytics_area_chart.vue'; +import { DEFAULT_SELECTED_CHART } from './constants'; export default { components: { @@ -20,7 +21,7 @@ export default { }, data() { return { - selectedChart: 0, + selectedChart: DEFAULT_SELECTED_CHART, }; }, computed: { diff --git a/app/assets/javascripts/vue_shared/components/ci_cd_analytics/constants.js b/app/assets/javascripts/vue_shared/components/ci_cd_analytics/constants.js index 1561674c0ad..3ac632b4690 100644 --- a/app/assets/javascripts/vue_shared/components/ci_cd_analytics/constants.js +++ b/app/assets/javascripts/vue_shared/components/ci_cd_analytics/constants.js @@ -1 +1,2 @@ export const CHART_CONTAINER_HEIGHT = 300; +export const DEFAULT_SELECTED_CHART = 2; diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue index 8bffc2479a1..0d7547d88a1 100644 --- a/app/assets/javascripts/vue_shared/components/ci_icon.vue +++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue @@ -69,7 +69,7 @@ export default { computed: { wrapperStyleClasses() { const status = this.status.group; - return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status} gl-rounded-full gl-justify-content-center`; + return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status} gl-rounded-full gl-justify-content-center gl-line-height-0`; }, icon() { return this.isBorderless ? `${this.status.icon}_borderless` : this.status.icon; diff --git a/app/assets/javascripts/vue_shared/components/constants.js b/app/assets/javascripts/vue_shared/components/constants.js new file mode 100644 index 00000000000..b7ff715922d --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/constants.js @@ -0,0 +1,4 @@ +export const KEY_EDIT = 'edit'; +export const KEY_WEB_IDE = 'webide'; +export const KEY_GITPOD = 'gitpod'; +export const KEY_PIPELINE_EDITOR = 'pipeline_editor'; diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue index 74905dc2ae0..9c30ec67d5a 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue @@ -67,6 +67,7 @@ export default { :suggestions="emojis" :suggestions-loading="loading" :get-active-token-value="getActiveEmoji" + value-identifier="name" v-bind="$attrs" @fetch-suggestions="fetchEmojis" v-on="$listeners" diff --git a/app/assets/javascripts/vue_shared/components/group_select/constants.js b/app/assets/javascripts/vue_shared/components/group_select/constants.js index bc70936eb36..06537d682fe 100644 --- a/app/assets/javascripts/vue_shared/components/group_select/constants.js +++ b/app/assets/javascripts/vue_shared/components/group_select/constants.js @@ -1,6 +1,7 @@ import { __ } from '~/locale'; export const TOGGLE_TEXT = __('Search for a group'); +export const RESET_LABEL = __('Reset'); export const FETCH_GROUPS_ERROR = __('Unable to fetch groups. Reload the page to try again.'); export const FETCH_GROUP_ERROR = __('Unable to fetch group. Reload the page to try again.'); export const QUERY_TOO_SHORT_MESSAGE = __('Enter at least three characters to search.'); diff --git a/app/assets/javascripts/vue_shared/components/group_select/group_select.vue b/app/assets/javascripts/vue_shared/components/group_select/group_select.vue index 5db723e1e5a..d295052e2ce 100644 --- a/app/assets/javascripts/vue_shared/components/group_select/group_select.vue +++ b/app/assets/javascripts/vue_shared/components/group_select/group_select.vue @@ -1,26 +1,35 @@ <script> import { debounce } from 'lodash'; -import { GlCollapsibleListbox } from '@gitlab/ui'; +import { GlFormGroup, GlAlert, GlCollapsibleListbox } from '@gitlab/ui'; +import * as Sentry from '@sentry/browser'; import axios from '~/lib/utils/axios_utils'; +import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils'; import Api from '~/api'; import { __ } from '~/locale'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; -import { createAlert } from '~/flash'; import { groupsPath } from './utils'; import { TOGGLE_TEXT, + RESET_LABEL, FETCH_GROUPS_ERROR, FETCH_GROUP_ERROR, QUERY_TOO_SHORT_MESSAGE, } from './constants'; const MINIMUM_QUERY_LENGTH = 3; +const GROUPS_PER_PAGE = 20; export default { components: { + GlFormGroup, + GlAlert, GlCollapsibleListbox, }, props: { + label: { + type: String, + required: true, + }, inputName: { type: String, required: true, @@ -54,10 +63,14 @@ export default { return { pristine: true, searching: false, + hasMoreGroups: true, + infiniteScrollLoading: false, searchString: '', groups: [], + page: 1, selectedValue: null, selectedText: null, + errorMessage: '', }; }, computed: { @@ -74,6 +87,9 @@ export default { toggleText() { return this.selectedText ?? this.$options.i18n.toggleText; }, + resetButtonLabel() { + return this.clearable ? RESET_LABEL : ''; + }, inputValue() { return this.selectedValue ? this.selectedValue : ''; }, @@ -95,35 +111,48 @@ export default { if (this.isSearchQueryTooShort) { this.groups = []; } else { - this.fetchGroups(searchString); + this.fetchGroups(); } }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS), - async fetchGroups(searchString = '') { - this.searching = true; + async fetchGroups(page = 1) { + if (page === 1) { + this.searching = true; + this.groups = []; + this.hasMoreGroups = true; + } else { + this.infiniteScrollLoading = true; + } try { - const { data } = await axios.get( + const { data, headers } = await axios.get( Api.buildUrl(groupsPath(this.groupsFilter, this.parentGroupID)), { params: { - search: searchString, + search: this.searchString, + per_page: GROUPS_PER_PAGE, + page, }, }, ); const groups = data.length ? data : data.results || []; - this.groups = groups.map((group) => ({ - ...group, - value: String(group.id), - })); + this.groups.push( + ...groups.map((group) => ({ + ...group, + value: String(group.id), + })), + ); + + const { totalPages } = parseIntPagination(normalizeHeaders(headers)); + if (page === totalPages) { + this.hasMoreGroups = false; + } + this.page = page; this.searching = false; + this.infiniteScrollLoading = false; } catch (error) { - createAlert({ - message: FETCH_GROUPS_ERROR, - error, - parent: this.$el, - }); + this.handleError({ message: FETCH_GROUPS_ERROR, error }); } }, async fetchInitialSelection() { @@ -139,11 +168,7 @@ export default { this.pristine = false; this.searching = false; } catch (error) { - createAlert({ - message: FETCH_GROUP_ERROR, - error, - parent: this.$el, - }); + this.handleError({ message: FETCH_GROUP_ERROR, error }); } }, onShown() { @@ -154,11 +179,20 @@ export default { onReset() { this.selected = null; }, + onBottomReached() { + this.fetchGroups(this.page + 1); + }, + handleError({ message, error }) { + Sentry.captureException(error); + this.errorMessage = message; + }, + dismissError() { + this.errorMessage = ''; + }, }, i18n: { toggleText: TOGGLE_TEXT, selectGroup: __('Select a group'), - reset: __('Reset'), noResultsText: __('No results found.'), searchQueryTooShort: QUERY_TOO_SHORT_MESSAGE, }, @@ -166,21 +200,27 @@ export default { </script> <template> - <div> + <gl-form-group :label="label"> + <gl-alert v-if="errorMessage" class="gl-mb-3" variant="danger" @dismiss="dismissError">{{ + errorMessage + }}</gl-alert> <gl-collapsible-listbox ref="listbox" v-model="selected" :header-text="$options.i18n.selectGroup" - :reset-button-label="$options.i18n.reset" + :reset-button-label="resetButtonLabel" :toggle-text="toggleText" :loading="searching && pristine" :searching="searching" :items="groups" :no-results-text="noResultsText" + :infinite-scroll="hasMoreGroups" + :infinite-scroll-loading="infiniteScrollLoading" searchable @shown="onShown" @search="search" @reset="onReset" + @bottom-reached="onBottomReached" > <template #list-item="{ item }"> <div class="gl-font-weight-bold"> @@ -189,7 +229,6 @@ export default { <div class="gl-text-gray-300">{{ item.full_path }}</div> </template> </gl-collapsible-listbox> - <div class="flash-container"></div> <input :id="inputId" data-testid="input" type="hidden" :name="inputName" :value="inputValue" /> - </div> + </gl-form-group> </template> diff --git a/app/assets/javascripts/vue_shared/components/group_select/init_group_selects.js b/app/assets/javascripts/vue_shared/components/group_select/init_group_selects.js new file mode 100644 index 00000000000..dbfac8a0339 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/group_select/init_group_selects.js @@ -0,0 +1,48 @@ +import Vue from 'vue'; +import { parseBoolean } from '~/lib/utils/common_utils'; +import GroupSelect from './group_select.vue'; + +const SELECTOR = '.js-vue-group-select'; + +export const initGroupSelects = () => { + if (process.env.NODE_ENV !== 'production' && document.querySelector(SELECTOR) === null) { + // eslint-disable-next-line no-console + console.warn(`Attempted to initialize GroupSelect but '${SELECTOR}' not found in the page`); + } + + [...document.querySelectorAll(SELECTOR)].forEach((el) => { + const { + parentId: parentGroupID, + groupsFilter, + label, + inputName, + inputId, + selected: initialSelection, + testid, + } = el.dataset; + const clearable = parseBoolean(el.dataset.clearable); + + return new Vue({ + el, + components: { + GroupSelect, + }, + render(createElement) { + return createElement(GroupSelect, { + props: { + label, + inputName, + initialSelection, + parentGroupID, + groupsFilter, + inputId, + clearable, + }, + attrs: { + 'data-testid': testid, + }, + }); + }, + }); + }); +}; diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue index 3c4ae08d2f7..8e459cc21ac 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -75,6 +75,10 @@ export default { // GraphQL returns `avatarUrl` and Rest `avatar_url` return this.user?.avatarUrl || this.user?.avatar_url; }, + webUrl() { + // GraphQL returns `webUrl` and Rest `web_url` + return this.user?.webUrl || this.user?.web_url; + }, statusTooltipHTML() { // Rest `status_tooltip_html` which is a ready to work // html for the emoji and the status text inside a tooltip. @@ -132,7 +136,7 @@ export default { :data-user-id="userId" :data-username="user.username" :data-name="user.name" - :href="user.webUrl" + :href="webUrl" target="_blank" class="js-user-link gl-vertical-align-middle gl-mx-2 gl-align-items-center" > diff --git a/app/assets/javascripts/vue_shared/components/listbox_input/init_listbox_inputs.js b/app/assets/javascripts/vue_shared/components/listbox_input/init_listbox_inputs.js new file mode 100644 index 00000000000..ad89b78b521 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/listbox_input/init_listbox_inputs.js @@ -0,0 +1,42 @@ +import Vue from 'vue'; +import ListboxInput from '~/vue_shared/components/listbox_input/listbox_input.vue'; + +export const initListboxInputs = () => { + const els = [...document.querySelectorAll('.js-listbox-input')]; + + els.forEach((el, index) => { + const { label, description, name, defaultToggleText, value = null } = el.dataset; + const { id } = el; + const items = JSON.parse(el.dataset.items); + + return new Vue({ + el, + name: `ListboxInputRoot${index + 1}`, + data() { + return { + selected: value, + }; + }, + render(createElement) { + return createElement(ListboxInput, { + on: { + select: (newValue) => { + this.selected = newValue; + }, + }, + props: { + label, + description, + name, + defaultToggleText, + selected: this.selected, + items, + }, + attrs: { + id, + }, + }); + }, + }); + }); +}; diff --git a/app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.vue b/app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.vue index b1809e6a9f3..bc6b5d3176f 100644 --- a/app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.vue +++ b/app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.vue @@ -1,25 +1,37 @@ <script> -import { GlListbox } from '@gitlab/ui'; +import { GlFormGroup, GlListbox } from '@gitlab/ui'; import { __ } from '~/locale'; -const MIN_ITEMS_COUNT_FOR_SEARCHING = 20; +const MIN_ITEMS_COUNT_FOR_SEARCHING = 10; export default { i18n: { noResultsText: __('No results found'), }, components: { + GlFormGroup, GlListbox, }, model: GlListbox.model, props: { + label: { + type: String, + required: false, + default: '', + }, + description: { + type: String, + required: false, + default: '', + }, name: { type: String, required: true, }, defaultToggleText: { type: String, - required: true, + required: false, + default: '', }, selected: { type: String, @@ -30,6 +42,11 @@ export default { type: GlListbox.props.items.type, required: true, }, + disabled: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -37,6 +54,9 @@ export default { }; }, computed: { + wrapperComponent() { + return this.label || this.description ? 'gl-form-group' : 'div'; + }, allOptions() { const allOptions = []; @@ -95,16 +115,17 @@ export default { </script> <template> - <div> + <component :is="wrapperComponent" :label="label" :description="description" v-bind="$attrs"> <gl-listbox :selected="selected" :toggle-text="toggleText" :items="filteredItems" :searchable="isSearchable" :no-results-text="$options.i18n.noResultsText" + :disabled="disabled" @search="search" @select="$emit($options.model.event, $event)" /> <input ref="input" type="hidden" :name="name" :value="selected" /> - </div> + </component> </template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/editor_mode_dropdown.vue b/app/assets/javascripts/vue_shared/components/markdown/editor_mode_dropdown.vue new file mode 100644 index 00000000000..6702a81e747 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/markdown/editor_mode_dropdown.vue @@ -0,0 +1,58 @@ +<script> +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { __ } from '~/locale'; + +export default { + components: { + GlDropdown, + GlDropdownItem, + }, + props: { + size: { + type: String, + required: false, + default: 'medium', + }, + value: { + type: String, + required: true, + }, + }, + computed: { + markdownEditorSelected() { + return this.value === 'markdown'; + }, + text() { + return this.markdownEditorSelected ? __('View rich text') : __('View markdown'); + }, + }, +}; +</script> +<template> + <gl-dropdown + category="tertiary" + data-qa-selector="editing_mode_switcher" + :size="size" + :text="text" + right + > + <gl-dropdown-item + is-check-item + :is-checked="!markdownEditorSelected" + @click="$emit('input', 'richText')" + ><div class="gl-font-weight-bold">{{ __('Rich text') }}</div> + <div class="gl-text-secondary"> + {{ __('View the formatted output in real-time as you edit.') }} + </div> + </gl-dropdown-item> + <gl-dropdown-item + is-check-item + :is-checked="markdownEditorSelected" + @click="$emit('input', 'markdown')" + ><div class="gl-font-weight-bold">{{ __('Markdown') }}</div> + <div class="gl-text-secondary"> + {{ __('View and edit markdown, with the option to preview the formatted output.') }} + </div></gl-dropdown-item + > + </gl-dropdown> +</template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index b5f2602af5e..7b76fc3fc6d 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -122,6 +122,11 @@ export default { required: false, default: () => [], }, + showContentEditorSwitcher: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -364,6 +369,8 @@ export default { :quick-actions-docs-path="quickActionsDocsPath" :can-attach-file="canAttachFile" :show-comment-tool-bar="showCommentToolBar" + :show-content-editor-switcher="showContentEditorSwitcher" + @enableContentEditor="$emit('enableContentEditor')" /> </div> </div> diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index 89fffdedbfd..e83441e59a2 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -10,6 +10,7 @@ import { INDENT_LINE, OUTDENT_LINE, } from '~/behaviors/shortcuts/keybindings'; +import { getModifierKey } from '~/constants'; import { getSelectedFragment } from '~/lib/utils/common_utils'; import { s__, __ } from '~/locale'; import { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm'; @@ -66,6 +67,7 @@ export default { return { tag: '> ', suggestPopoverVisible: false, + modifierKey: getModifierKey(), }; }, computed: { @@ -90,15 +92,6 @@ export default { const expandText = s__('MarkdownEditor|Click to expand'); return [`<details><summary>${expandText}</summary>`, `{text}`, '</details>'].join('\n'); }, - isMac() { - // Accessing properties using ?. to allow tests to use - // this component without setting up window.gl.client. - // In production, window.gl.client should always be present. - return Boolean(window.gl?.client?.isMac); - }, - modifierKey() { - return this.isMac ? '⌘' : s__('KeyboardKey|Ctrl+'); - }, }, watch: { showSuggestPopover() { diff --git a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue index d01eae0308f..c53118b9f62 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue @@ -1,16 +1,13 @@ <script> -import { GlSegmentedControl } from '@gitlab/ui'; -import { __ } from '~/locale'; -import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import axios from '~/lib/utils/axios_utils'; +import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import { EDITING_MODE_MARKDOWN_FIELD, EDITING_MODE_CONTENT_EDITOR } from '../../constants'; import MarkdownField from './field.vue'; export default { components: { - MarkdownField, LocalStorageSync, - GlSegmentedControl, + MarkdownField, ContentEditor: () => import( /* webpackChunkName: 'content_editor' */ '~/content_editor/components/content_editor.vue' @@ -91,7 +88,6 @@ export default { data() { return { editingMode: EDITING_MODE_MARKDOWN_FIELD, - switchEditingControlEnabled: true, autofocused: false, }; }, @@ -114,19 +110,16 @@ export default { updateMarkdownFromMarkdownField({ target }) { this.$emit('input', target.value); }, - enableSwitchEditingControl() { - this.switchEditingControlEnabled = true; - }, - disableSwitchEditingControl() { - this.switchEditingControlEnabled = false; - }, renderMarkdown(markdown) { return axios.post(this.renderMarkdownPath, { text: markdown }).then(({ data }) => data.body); }, onEditingModeChange(editingMode) { + this.editingMode = editingMode; this.notifyEditingModeChange(editingMode); }, onEditingModeRestored(editingMode) { + this.editingMode = editingMode; + this.$emit(editingMode); this.notifyEditingModeChange(editingMode); }, notifyEditingModeChange(editingMode) { @@ -142,25 +135,10 @@ export default { this.autofocused = true; }, }, - switchEditingControlOptions: [ - { text: __('Source'), value: EDITING_MODE_MARKDOWN_FIELD }, - { text: __('Rich text'), value: EDITING_MODE_CONTENT_EDITOR }, - ], }; </script> <template> <div> - <div class="gl-display-flex gl-justify-content-start gl-mb-3"> - <gl-segmented-control - v-model="editingMode" - data-testid="toggle-editing-mode-button" - data-qa-selector="editing_mode_button" - class="gl-display-flex" - :options="$options.switchEditingControlOptions" - :disabled="!enableContentEditor || !switchEditingControlEnabled" - @change="onEditingModeChange" - /> - </div> <local-storage-sync v-model="editingMode" storage-key="gl-wiki-content-editor-enabled" @@ -176,7 +154,9 @@ export default { :quick-actions-docs-path="quickActionsDocsPath" :uploads-path="uploadsPath" :enable-preview="enablePreview" + show-content-editor-switcher class="bordered-box" + @enableContentEditor="onEditingModeChange('contentEditor')" > <template #textarea> <textarea @@ -205,10 +185,8 @@ export default { :use-bottom-toolbar="useBottomToolbar" @initialized="setEditorAsAutofocused" @change="updateMarkdownFromContentEditor" - @loading="disableSwitchEditingControl" - @loadingSuccess="enableSwitchEditingControl" - @loadingError="enableSwitchEditingControl" @keydown="$emit('keydown', $event)" + @enableMarkdownEditor="onEditingModeChange('markdownField')" /> <input :id="formFieldId" diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue index b5640e12541..e8be242f660 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue @@ -1,5 +1,6 @@ <script> import { GlButton, GlLink, GlLoadingIcon, GlSprintf, GlIcon } from '@gitlab/ui'; +import EditorModeDropdown from './editor_mode_dropdown.vue'; export default { components: { @@ -8,6 +9,7 @@ export default { GlLoadingIcon, GlSprintf, GlIcon, + EditorModeDropdown, }, props: { markdownDocsPath: { @@ -29,12 +31,24 @@ export default { required: false, default: true, }, + showContentEditorSwitcher: { + type: Boolean, + required: false, + default: false, + }, }, computed: { hasQuickActionsDocsPath() { return this.quickActionsDocsPath !== ''; }, }, + methods: { + handleEditorModeChanged(mode) { + if (mode === 'richText') { + this.$emit('enableContentEditor'); + } + }, + }, }; </script> @@ -121,5 +135,12 @@ export default { {{ __('Cancel') }} </gl-button> </span> + <editor-mode-dropdown + v-if="showContentEditorSwitcher" + size="small" + class="gl-float-right gl-line-height-28 gl-display-block" + value="markdown" + @input="handleEditorModeChanged" + /> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue index 21212e82de4..c83643ca4de 100644 --- a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue +++ b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue @@ -1,6 +1,6 @@ <script> import { GlBadge, GlTabs, GlTab } from '@gitlab/ui'; -import $ from 'jquery'; +import { initScrollingTabs } from '~/layout_nav'; /** * Given an array of tabs, renders non linked bootstrap tabs. @@ -41,7 +41,7 @@ export default { }, }, mounted() { - $(document).trigger('init.scrolling-tabs'); + initScrollingTabs(); }, methods: { shouldRenderBadge(count) { diff --git a/app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue b/app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue index 5f2a66ee0b7..e1f042f78ab 100644 --- a/app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue +++ b/app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue @@ -64,8 +64,9 @@ export default { <template> <gl-pagination v-if="showPagination" - class="justify-content-center gl-mt-3" + class="gl-mt-3" v-bind="$attrs" + align="center" :value="pageInfo.page" :per-page="pageInfo.perPage" :total-items="pageInfo.total" diff --git a/app/assets/javascripts/vue_shared/components/runner_aws_deployments/constants.js b/app/assets/javascripts/vue_shared/components/runner_aws_deployments/constants.js deleted file mode 100644 index 88c975b97b9..00000000000 --- a/app/assets/javascripts/vue_shared/components/runner_aws_deployments/constants.js +++ /dev/null @@ -1,63 +0,0 @@ -import { s__, sprintf } from '~/locale'; - -export const README_URL = - 'https://gitlab.com/guided-explorations/aws/gitlab-runner-autoscaling-aws-asg/-/blob/main/easybuttons.md'; - -export const CF_BASE_URL = - 'https://us-west-2.console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/create/review?'; - -export const TEMPLATES_BASE_URL = 'https://gl-public-templates.s3.amazonaws.com/cfn/experimental/'; - -export const EASY_BUTTONS = [ - { - stackName: 'linux-docker-nonspot', - templateName: - 'easybutton-amazon-linux-2-docker-manual-scaling-with-schedule-ondemandonly.cf.yml', - description: s__( - 'Runners|Amazon Linux 2 Docker HA with manual scaling and optional scheduling. Non-spot.', - ), - moreDetails1: s__('Runners|No spot. This is the default choice for Linux Docker executor.'), - moreDetails2: s__( - 'Runners|A capacity of 1 enables warm HA through Auto Scaling group re-spawn. A capacity of 2 enables hot HA because the service is available even when a node is lost. A capacity of 3 or more enables hot HA and manual scaling of runner fleet.', - ), - }, - { - stackName: 'linux-docker-spotonly', - templateName: 'easybutton-amazon-linux-2-docker-manual-scaling-with-schedule-spotonly.cf.yml', - description: sprintf( - s__( - 'Runners|Amazon Linux 2 Docker HA with manual scaling and optional scheduling. %{percentage} spot.', - ), - { percentage: '100%' }, - ), - moreDetails1: sprintf(s__('Runners|%{percentage} spot.'), { percentage: '100%' }), - moreDetails2: s__( - 'Runners|Capacity of 1 enables warm HA through Auto Scaling group re-spawn. Capacity of 2 enables hot HA because the service is available even when a node is lost. Capacity of 3 or more enables hot HA and manual scaling of runner fleet.', - ), - }, - { - stackName: 'win2019-shell-non-spot', - templateName: 'easybutton-windows2019-shell-manual-scaling-with-scheduling-ondemandonly.cf.yml', - description: s__( - 'Runners|Windows 2019 Shell with manual scaling and optional scheduling. Non-spot.', - ), - moreDetails1: s__('Runners|No spot. Default choice for Windows Shell executor.'), - moreDetails2: s__( - 'Runners|Capacity of 1 enables warm HA through Auto Scaling group re-spawn. Capacity of 2 enables hot HA because the service is available even when a node is lost. Capacity of 3 or more enables hot HA and manual scaling of runner fleet.', - ), - }, - { - stackName: 'win2019-shell-spot', - templateName: 'easybutton-windows2019-shell-manual-scaling-with-scheduling-spotonly.cf.yml', - description: sprintf( - s__( - 'Runners|Windows 2019 Shell with manual scaling and optional scheduling. %{percentage} spot.', - ), - { percentage: '100%' }, - ), - moreDetails1: sprintf(s__('Runners|%{percentage} spot.'), { percentage: '100%' }), - moreDetails2: s__( - 'Runners|Capacity of 1 enables warm HA through Auto Scaling group re-spawn. Capacity of 2 enables hot HA because the service is available even when a node is lost. Capacity of 3 or more enables hot HA and manual scaling of runner fleet.', - ), - }, -]; diff --git a/app/assets/javascripts/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal.vue b/app/assets/javascripts/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal.vue index eee65d90285..08acde1aefc 100644 --- a/app/assets/javascripts/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal.vue +++ b/app/assets/javascripts/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal.vue @@ -1,125 +1,29 @@ <script> -import { - GlModal, - GlSprintf, - GlLink, - GlFormRadioGroup, - GlFormRadio, - GlAccordion, - GlAccordionItem, -} from '@gitlab/ui'; -import Tracking from '~/tracking'; -import { getBaseURL, objectToQuery, visitUrl } from '~/lib/utils/url_utility'; -import { __, s__ } from '~/locale'; -import { README_URL, CF_BASE_URL, TEMPLATES_BASE_URL, EASY_BUTTONS } from './constants'; +import { GlModal } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import RunnerAwsInstructions from '~/vue_shared/components/runner_instructions/instructions/runner_aws_instructions.vue'; export default { components: { GlModal, - GlSprintf, - GlLink, - GlFormRadioGroup, - GlFormRadio, - GlAccordion, - GlAccordionItem, + RunnerAwsInstructions, }, - mixins: [Tracking.mixin()], props: { modalId: { type: String, required: true, }, }, - data() { - return { - selected: this.$options.easyButtons[0], - }; - }, methods: { - borderBottom(idx) { - return idx < this.$options.easyButtons.length - 1; - }, - easyButtonUrl(easyButton) { - const params = { - templateURL: TEMPLATES_BASE_URL + easyButton.templateName, - stackName: easyButton.stackName, - param_3GITLABRunnerInstanceURL: getBaseURL(), - }; - return CF_BASE_URL + objectToQuery(params); - }, - trackCiRunnerTemplatesClick(stackName) { - this.track('template_clicked', { - label: stackName, - }); - }, - handleModalPrimary() { - this.trackCiRunnerTemplatesClick(this.selected.stackName); - visitUrl(this.easyButtonUrl(this.selected), true); + onClose() { + this.$refs.modal.close(); }, }, - i18n: { - title: s__('Runners|Deploy GitLab Runner in AWS'), - instructions: s__( - 'Runners|Select your preferred option here. In the next step, you can choose the capacity for your runner in the AWS CloudFormation console.', - ), - chooseRunner: s__('Runners|Choose your preferred GitLab Runner'), - dontSeeWhatYouAreLookingFor: s__( - "Runners|Don't see what you are looking for? See the full list of options, including a fully customizable option %{linkStart}here%{linkEnd}.", - ), - moreDetails: __('More Details'), - lessDetails: __('Less Details'), - }, - deployButton: { - text: s__('Runners|Deploy GitLab Runner in AWS'), - attributes: [{ variant: 'confirm' }], - }, - closeButton: { - text: __('Cancel'), - attributes: [{ variant: 'default' }], - }, - readmeUrl: README_URL, - easyButtons: EASY_BUTTONS, + i18n_title: s__('Runners|Deploy GitLab Runner in AWS'), }; </script> <template> - <gl-modal - :modal-id="modalId" - :title="$options.i18n.title" - :action-primary="$options.deployButton" - :action-secondary="$options.closeButton" - size="sm" - @primary="handleModalPrimary" - > - <p>{{ $options.i18n.instructions }}</p> - <gl-form-radio-group v-model="selected" :label="$options.i18n.chooseRunner" label-sr-only> - <gl-form-radio - v-for="(easyButton, idx) in $options.easyButtons" - :key="easyButton.templateName" - :value="easyButton" - class="gl-py-5 gl-pl-8" - :class="{ 'gl-border-b': borderBottom(idx) }" - > - <div class="gl-mt-n1 gl-pl-4 gl-pb-2 gl-font-weight-bold"> - {{ easyButton.description }} - <gl-accordion :header-level="3" class="gl-pt-3"> - <gl-accordion-item - :title="$options.i18n.moreDetails" - :title-visible="$options.i18n.lessDetails" - class="gl-font-weight-normal" - > - <p class="gl-pt-2">{{ easyButton.moreDetails1 }}</p> - <p class="gl-m-0">{{ easyButton.moreDetails2 }}</p> - </gl-accordion-item> - </gl-accordion> - </div> - </gl-form-radio> - </gl-form-radio-group> - <p> - <gl-sprintf :message="$options.i18n.dontSeeWhatYouAreLookingFor"> - <template #link="{ content }"> - <gl-link :href="$options.readmeUrl" target="_blank">{{ content }}</gl-link> - </template> - </gl-sprintf> - </p> + <gl-modal ref="modal" :modal-id="modalId" :title="$options.i18n_title" hide-footer size="sm"> + <runner-aws-instructions @close="onClose" /> </gl-modal> </template> diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js b/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js index c97e191b630..3dbc5246c3d 100644 --- a/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js +++ b/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js @@ -1,18 +1,69 @@ -import { s__ } from '~/locale'; +import { s__, sprintf } from '~/locale'; export const REGISTRATION_TOKEN_PLACEHOLDER = '$REGISTRATION_TOKEN'; -export const INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES = { - docker: { - instructions: s__( - 'Runners|To install Runner in a container follow the instructions described in the GitLab documentation', +export const PLATFORM_DOCKER = 'docker'; +export const PLATFORM_KUBERNETES = 'kubernetes'; + +export const AWS_README_URL = + 'https://gitlab.com/guided-explorations/aws/gitlab-runner-autoscaling-aws-asg/-/blob/main/easybuttons.md'; + +export const AWS_CF_BASE_URL = + 'https://us-west-2.console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/create/review?'; + +export const AWS_TEMPLATES_BASE_URL = + 'https://gl-public-templates.s3.amazonaws.com/cfn/experimental/'; + +export const AWS_EASY_BUTTONS = [ + { + stackName: 'linux-docker-nonspot', + templateName: + 'easybutton-amazon-linux-2-docker-manual-scaling-with-schedule-ondemandonly.cf.yml', + description: s__( + 'Runners|Amazon Linux 2 Docker HA with manual scaling and optional scheduling. Non-spot.', + ), + moreDetails1: s__('Runners|No spot. This is the default choice for Linux Docker executor.'), + moreDetails2: s__( + 'Runners|A capacity of 1 enables warm HA through Auto Scaling group re-spawn. A capacity of 2 enables hot HA because the service is available even when a node is lost. A capacity of 3 or more enables hot HA and manual scaling of runner fleet.', + ), + }, + { + stackName: 'linux-docker-spotonly', + templateName: 'easybutton-amazon-linux-2-docker-manual-scaling-with-schedule-spotonly.cf.yml', + description: sprintf( + s__( + 'Runners|Amazon Linux 2 Docker HA with manual scaling and optional scheduling. %{percentage} spot.', + ), + { percentage: '100%' }, + ), + moreDetails1: sprintf(s__('Runners|%{percentage} spot.'), { percentage: '100%' }), + moreDetails2: s__( + 'Runners|Capacity of 1 enables warm HA through Auto Scaling group re-spawn. Capacity of 2 enables hot HA because the service is available even when a node is lost. Capacity of 3 or more enables hot HA and manual scaling of runner fleet.', + ), + }, + { + stackName: 'win2019-shell-non-spot', + templateName: 'easybutton-windows2019-shell-manual-scaling-with-scheduling-ondemandonly.cf.yml', + description: s__( + 'Runners|Windows 2019 Shell with manual scaling and optional scheduling. Non-spot.', + ), + moreDetails1: s__('Runners|No spot. Default choice for Windows Shell executor.'), + moreDetails2: s__( + 'Runners|Capacity of 1 enables warm HA through Auto Scaling group re-spawn. Capacity of 2 enables hot HA because the service is available even when a node is lost. Capacity of 3 or more enables hot HA and manual scaling of runner fleet.', ), - link: 'https://docs.gitlab.com/runner/install/docker.html', }, - kubernetes: { - instructions: s__( - 'Runners|To install Runner in Kubernetes follow the instructions described in the GitLab documentation.', + { + stackName: 'win2019-shell-spot', + templateName: 'easybutton-windows2019-shell-manual-scaling-with-scheduling-spotonly.cf.yml', + description: sprintf( + s__( + 'Runners|Windows 2019 Shell with manual scaling and optional scheduling. %{percentage} spot.', + ), + { percentage: '100%' }, + ), + moreDetails1: sprintf(s__('Runners|%{percentage} spot.'), { percentage: '100%' }), + moreDetails2: s__( + 'Runners|Capacity of 1 enables warm HA through Auto Scaling group re-spawn. Capacity of 2 enables hot HA because the service is available even when a node is lost. Capacity of 3 or more enables hot HA and manual scaling of runner fleet.', ), - link: 'https://docs.gitlab.com/runner/install/kubernetes.html', }, -}; +]; diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql b/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/get_runner_platforms.query.graphql index 76f152e5453..76f152e5453 100644 --- a/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql +++ b/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/get_runner_platforms.query.graphql diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql b/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/get_runner_setup.query.graphql index c0248a35e3f..c0248a35e3f 100644 --- a/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql +++ b/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/get_runner_setup.query.graphql diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_aws_instructions.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_aws_instructions.vue new file mode 100644 index 00000000000..cafebdfe5f4 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_aws_instructions.vue @@ -0,0 +1,123 @@ +<script> +import { + GlButton, + GlSprintf, + GlLink, + GlFormRadioGroup, + GlFormRadio, + GlAccordion, + GlAccordionItem, +} from '@gitlab/ui'; +import Tracking from '~/tracking'; +import { getBaseURL, objectToQuery, visitUrl } from '~/lib/utils/url_utility'; +import { __, s__ } from '~/locale'; +import { + AWS_README_URL, + AWS_CF_BASE_URL, + AWS_TEMPLATES_BASE_URL, + AWS_EASY_BUTTONS, +} from '../constants'; + +export default { + components: { + GlButton, + GlSprintf, + GlLink, + GlFormRadioGroup, + GlFormRadio, + GlAccordion, + GlAccordionItem, + }, + mixins: [Tracking.mixin()], + data() { + return { + selectedIndex: 0, + }; + }, + computed: { + selected() { + return this.$options.easyButtons[this.selectedIndex]; + }, + }, + methods: { + borderBottom(idx) { + return idx < this.$options.easyButtons.length - 1; + }, + easyButtonUrl(easyButton) { + const params = { + templateURL: AWS_TEMPLATES_BASE_URL + easyButton.templateName, + stackName: easyButton.stackName, + param_3GITLABRunnerInstanceURL: getBaseURL(), + }; + return AWS_CF_BASE_URL + objectToQuery(params); + }, + trackCiRunnerTemplatesClick(stackName) { + this.track('template_clicked', { + label: stackName, + }); + }, + onOk() { + this.trackCiRunnerTemplatesClick(this.selected.stackName); + visitUrl(this.easyButtonUrl(this.selected), true); + }, + onClose() { + this.$emit('close'); + }, + }, + i18n: { + title: s__('Runners|Deploy GitLab Runner in AWS'), + instructions: s__( + 'Runners|Select your preferred option here. In the next step, you can choose the capacity for your runner in the AWS CloudFormation console.', + ), + chooseRunner: s__('Runners|Choose your preferred GitLab Runner'), + dontSeeWhatYouAreLookingFor: s__( + "Runners|Don't see what you are looking for? See the full list of options, including a fully customizable option %{linkStart}here%{linkEnd}.", + ), + moreDetails: __('More Details'), + lessDetails: __('Less Details'), + }, + readmeUrl: AWS_README_URL, + easyButtons: AWS_EASY_BUTTONS, +}; +</script> +<template> + <div> + <p>{{ $options.i18n.instructions }}</p> + <gl-form-radio-group v-model="selectedIndex" :label="$options.i18n.chooseRunner" label-sr-only> + <gl-form-radio + v-for="(easyButton, idx) in $options.easyButtons" + :key="easyButton.templateName" + :value="idx" + class="gl-py-5 gl-pl-8" + :class="{ 'gl-border-b': borderBottom(idx) }" + > + <div class="gl-mt-n1 gl-pl-4 gl-pb-2 gl-font-weight-bold"> + {{ easyButton.description }} + <gl-accordion :header-level="3" class="gl-pt-3"> + <gl-accordion-item + :title="$options.i18n.moreDetails" + :title-visible="$options.i18n.lessDetails" + class="gl-font-weight-normal" + > + <p class="gl-pt-2">{{ easyButton.moreDetails1 }}</p> + <p class="gl-m-0">{{ easyButton.moreDetails2 }}</p> + </gl-accordion-item> + </gl-accordion> + </div> + </gl-form-radio> + </gl-form-radio-group> + <p> + <gl-sprintf :message="$options.i18n.dontSeeWhatYouAreLookingFor"> + <template #link="{ content }"> + <gl-link :href="$options.readmeUrl" target="_blank">{{ content }}</gl-link> + </template> + </gl-sprintf> + </p> + <footer class="gl-display-flex gl-justify-content-end gl-pt-3 gl-gap-3"> + <gl-button @click="onClose()">{{ __('Close') }}</gl-button> + <gl-button variant="confirm" @click="onOk()"> + {{ s__('Runners|Deploy GitLab Runner in AWS') }} + </gl-button> + </footer> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_cli_instructions.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_cli_instructions.vue new file mode 100644 index 00000000000..36e608a068b --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_cli_instructions.vue @@ -0,0 +1,169 @@ +<script> +import { GlButton, GlDropdown, GlDropdownItem, GlLoadingIcon } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue'; +import { REGISTRATION_TOKEN_PLACEHOLDER } from '../constants'; +import getRunnerSetupInstructionsQuery from '../graphql/get_runner_setup.query.graphql'; + +export default { + components: { + GlButton, + GlDropdown, + GlDropdownItem, + GlLoadingIcon, + ModalCopyButton, + }, + props: { + platform: { + type: Object, + required: false, + default: null, + }, + registrationToken: { + type: String, + required: false, + default: null, + }, + }, + data() { + return { + selectedArchitecture: this.platform?.architectures[0] || null, + instructions: null, + }; + }, + apollo: { + instructions: { + query: getRunnerSetupInstructionsQuery, + skip() { + return !this.platform || !this.selectedArchitecture; + }, + variables() { + return { + platform: this.platform.name, + architecture: this.selectedArchitecture.name, + }; + }, + update(data) { + return data?.runnerSetup; + }, + error() { + this.$emit('error'); + }, + }, + }, + computed: { + architectures() { + return this.platform?.architectures || []; + }, + binaryUrl() { + return this.selectedArchitecture?.downloadLocation; + }, + registerInstructionsWithToken() { + const { registerInstructions } = this.instructions || {}; + + if (this.registrationToken) { + return registerInstructions?.replace( + REGISTRATION_TOKEN_PLACEHOLDER, + this.registrationToken, + ); + } + return registerInstructions; + }, + }, + watch: { + platform() { + // reset selection if architecture is not in this list + const arch = this.architectures.find(({ name }) => name === this.selectedArchitecture.name); + if (!arch) { + this.selectArchitecture(this.architectures[0]); + } + }, + }, + methods: { + selectArchitecture(architecture) { + this.selectedArchitecture = architecture; + }, + onClose() { + this.$emit('close'); + }, + }, + i18n: { + architecture: s__('Runners|Architecture'), + downloadInstallBinary: s__('Runners|Download and install binary'), + downloadLatestBinary: s__('Runners|Download latest binary'), + registerRunnerCommand: s__('Runners|Command to register runner'), + copyInstructions: s__('Runners|Copy instructions'), + }, +}; +</script> + +<template> + <div> + <h5> + {{ $options.i18n.architecture }} + <gl-loading-icon v-if="$apollo.loading" size="sm" inline /> + </h5> + + <gl-dropdown class="gl-mb-3" :text="selectedArchitecture.name"> + <gl-dropdown-item + v-for="architecture in architectures" + :key="architecture.name" + is-check-item + :is-checked="selectedArchitecture.name === architecture.name" + data-testid="architecture-dropdown-item" + @click="selectArchitecture(architecture)" + > + {{ architecture.name }} + </gl-dropdown-item> + </gl-dropdown> + <div class="gl-sm-display-flex gl-align-items-center gl-mb-3"> + <h5>{{ $options.i18n.downloadInstallBinary }}</h5> + <gl-button + v-if="binaryUrl" + class="gl-ml-auto" + :href="binaryUrl" + download + icon="download" + data-testid="binary-download-button" + > + {{ $options.i18n.downloadLatestBinary }} + </gl-button> + </div> + + <template v-if="instructions"> + <div class="gl-display-flex"> + <pre + class="gl-bg-gray gl-flex-grow-1 gl-white-space-pre-line" + data-testid="binary-instructions" + >{{ instructions.installInstructions }}</pre + > + <modal-copy-button + :title="$options.i18n.copyInstructions" + :text="instructions.installInstructions" + :modal-id="$options.modalId" + css-classes="gl-align-self-start gl-ml-2 gl-mt-2" + category="tertiary" + /> + </div> + <h5 class="gl-mb-3">{{ $options.i18n.registerRunnerCommand }}</h5> + <div class="gl-display-flex"> + <pre + class="gl-bg-gray gl-flex-grow-1 gl-white-space-pre-line" + data-testid="register-command" + >{{ registerInstructionsWithToken }}</pre + > + <modal-copy-button + :title="$options.i18n.copyInstructions" + :text="registerInstructionsWithToken" + :modal-id="$options.modalId" + css-classes="gl-align-self-start gl-ml-2 gl-mt-2" + category="tertiary" + /> + </div> + </template> + + <footer class="gl-display-flex gl-justify-content-end gl-pt-3"> + <gl-button @click="onClose()">{{ __('Close') }}</gl-button> + </footer> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_docker_instructions.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_docker_instructions.vue new file mode 100644 index 00000000000..ff7e803af2a --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_docker_instructions.vue @@ -0,0 +1,35 @@ +<script> +import { GlButton, GlIcon } from '@gitlab/ui'; +import { s__ } from '~/locale'; + +export default { + components: { + GlButton, + GlIcon, + }, + methods: { + onClose() { + this.$emit('close'); + }, + }, + I18N_INSTRUCTIONS_TEXT: s__( + 'Runners|To install Runner in a container follow the instructions described in the GitLab documentation', + ), + I18N_VIEW_INSTRUCTIONS: s__('Runners|View installation instructions'), + HELP_URL: 'https://docs.gitlab.com/runner/install/docker.html', +}; +</script> +<template> + <div> + <p> + {{ $options.I18N_INSTRUCTIONS_TEXT }} + </p> + <gl-button :href="$options.HELP_URL"> + <gl-icon name="external-link" /> + {{ $options.I18N_VIEW_INSTRUCTIONS }} + </gl-button> + <footer class="gl-display-flex gl-justify-content-end gl-pt-3"> + <gl-button @click="onClose()">{{ __('Close') }}</gl-button> + </footer> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_kubernetes_instructions.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_kubernetes_instructions.vue new file mode 100644 index 00000000000..ee41dab0cec --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_kubernetes_instructions.vue @@ -0,0 +1,35 @@ +<script> +import { GlButton, GlIcon } from '@gitlab/ui'; +import { s__ } from '~/locale'; + +export default { + components: { + GlButton, + GlIcon, + }, + methods: { + onClose() { + this.$emit('close'); + }, + }, + I18N_INSTRUCTIONS_TEXT: s__( + 'Runners|To install Runner in Kubernetes follow the instructions described in the GitLab documentation.', + ), + I18N_VIEW_INSTRUCTIONS: s__('Runners|View installation instructions'), + HELP_URL: 'https://docs.gitlab.com/runner/install/kubernetes.html', +}; +</script> +<template> + <div> + <p> + {{ $options.I18N_INSTRUCTIONS_TEXT }} + </p> + <gl-button :href="$options.HELP_URL"> + <gl-icon name="external-link" /> + {{ $options.I18N_VIEW_INSTRUCTIONS }} + </gl-button> + <footer class="gl-display-flex gl-justify-content-end gl-pt-3"> + <gl-button @click="onClose()">{{ __('Close') }}</gl-button> + </footer> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue index c5d3704ead9..729fe9c462c 100644 --- a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue +++ b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue @@ -12,15 +12,13 @@ import { GlResizeObserverDirective, } from '@gitlab/ui'; import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; -import { isEmpty } from 'lodash'; import { __, s__ } from '~/locale'; -import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue'; -import { - INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES, - REGISTRATION_TOKEN_PLACEHOLDER, -} from './constants'; -import getRunnerPlatformsQuery from './graphql/queries/get_runner_platforms.query.graphql'; -import getRunnerSetupInstructionsQuery from './graphql/queries/get_runner_setup.query.graphql'; +import getRunnerPlatformsQuery from './graphql/get_runner_platforms.query.graphql'; +import { PLATFORM_DOCKER, PLATFORM_KUBERNETES } from './constants'; + +import RunnerCliInstructions from './instructions/runner_cli_instructions.vue'; +import RunnerDockerInstructions from './instructions/runner_docker_instructions.vue'; +import RunnerKubernetesInstructions from './instructions/runner_kubernetes_instructions.vue'; export default { components: { @@ -33,7 +31,7 @@ export default { GlIcon, GlLoadingIcon, GlSkeletonLoader, - ModalCopyButton, + RunnerDockerInstructions, }, directives: { GlResizeObserver: GlResizeObserverDirective, @@ -74,27 +72,13 @@ export default { ); }, result() { - // If it is set and available, select the defaultSelectedPlatform. + // If found, select the defaultSelectedPlatform. // Otherwise, select the first available platform - this.selectPlatform(this.defaultPlatformName || this.platforms?.[0].name); - }, - error() { - this.toggleAlert(true); - }, - }, - instructions: { - query: getRunnerSetupInstructionsQuery, - skip() { - return !this.shown || !this.selectedPlatform; - }, - variables() { - return { - platform: this.selectedPlatform, - architecture: this.selectedArchitecture || '', - }; - }, - update(data) { - return data?.runnerSetup; + const platform = + this.platforms?.find(({ name }) => this.defaultPlatformName === name) || + this.platforms?.[0]; + + this.selectPlatform(platform); }, error() { this.toggleAlert(true); @@ -106,39 +90,23 @@ export default { shown: false, platforms: [], selectedPlatform: null, - selectedArchitecture: null, showAlert: false, - instructions: {}, platformsButtonGroupVertical: false, }; }, computed: { - instructionsEmpty() { - return isEmpty(this.instructions); - }, - architectures() { - return this.platforms.find(({ name }) => name === this.selectedPlatform)?.architectures || []; - }, - binaryUrl() { - return this.architectures.find(({ name }) => name === this.selectedArchitecture) - ?.downloadLocation; - }, - instructionsWithoutArchitecture() { - return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatform]?.instructions; - }, - runnerInstallationLink() { - return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatform]?.link; - }, - registerInstructionsWithToken() { - const { registerInstructions } = this.instructions || {}; - - if (this.registrationToken) { - return registerInstructions?.replace( - REGISTRATION_TOKEN_PLACEHOLDER, - this.registrationToken, - ); + instructionsComponent() { + if (this.selectedPlatform?.architectures?.length) { + return RunnerCliInstructions; + } + switch (this.selectedPlatform?.name) { + case PLATFORM_DOCKER: + return RunnerDockerInstructions; + case PLATFORM_KUBERNETES: + return RunnerKubernetesInstructions; + default: + return null; } - return registerInstructions; }, }, updated() { @@ -149,6 +117,12 @@ export default { show() { this.$refs.modal.show(); }, + close() { + this.$refs.modal.close(); + }, + onClose() { + this.close(); + }, onShown() { this.shown = true; this.refocusSelectedPlatformButton(); @@ -159,21 +133,13 @@ export default { // get focused when setting a `defaultPlatformName`. // This method refocuses the expected button. // See more about this auto-focus: https://bootstrap-vue.org/docs/components/modal#auto-focus-on-open - this.$refs[this.selectedPlatform]?.[0].$el.focus(); + this.$refs[this.selectedPlatform?.name]?.[0].$el.focus(); }, - selectPlatform(platformName) { - this.selectedPlatform = platformName; - - // Update architecture when platform changes - const arch = this.architectures.find(({ name }) => name === this.selectedArchitecture); - if (arch) { - this.selectArchitecture(arch.name); - } else { - this.selectArchitecture(this.architectures[0]?.name); - } + selectPlatform(platform) { + this.selectedPlatform = platform; }, - selectArchitecture(architecture) { - this.selectedArchitecture = architecture; + isPlatformSelected(platform) { + return this.selectedPlatform.name === platform.name; }, toggleAlert(state) { this.showAlert = state; @@ -189,17 +155,9 @@ export default { i18n: { environment: __('Environment'), installARunner: s__('Runners|Install a runner'), - architecture: s__('Runners|Architecture'), downloadInstallBinary: s__('Runners|Download and install binary'), downloadLatestBinary: s__('Runners|Download latest binary'), - registerRunnerCommand: s__('Runners|Command to register runner'), fetchError: s__('Runners|An error has occurred fetching instructions'), - copyInstructions: s__('Runners|Copy instructions'), - viewInstallationInstructions: s__('Runners|View installation instructions'), - }, - closeButton: { - text: __('Close'), - attributes: [{ variant: 'default' }], }, }; </script> @@ -208,8 +166,8 @@ export default { ref="modal" :modal-id="modalId" :title="$options.i18n.installARunner" - :action-secondary="$options.closeButton" v-bind="$attrs" + hide-footer v-on="$listeners" @shown="onShown" > @@ -234,88 +192,23 @@ export default { v-for="platform in platforms" :key="platform.name" :ref="platform.name" - :selected="selectedPlatform === platform.name" - @click="selectPlatform(platform.name)" + :selected="isPlatformSelected(platform)" + @click="selectPlatform(platform)" > {{ platform.humanReadableName }} </gl-button> </gl-button-group> </div> </template> - <template v-if="architectures.length"> - <template v-if="selectedPlatform"> - <h5> - {{ $options.i18n.architecture }} - <gl-loading-icon v-if="$apollo.loading" size="sm" inline /> - </h5> - - <gl-dropdown class="gl-mb-3" :text="selectedArchitecture"> - <gl-dropdown-item - v-for="architecture in architectures" - :key="architecture.name" - is-check-item - :is-checked="selectedArchitecture === architecture.name" - data-testid="architecture-dropdown-item" - @click="selectArchitecture(architecture.name)" - > - {{ architecture.name }} - </gl-dropdown-item> - </gl-dropdown> - <div class="gl-sm-display-flex gl-align-items-center gl-mb-3"> - <h5>{{ $options.i18n.downloadInstallBinary }}</h5> - <gl-button - v-if="binaryUrl" - class="gl-ml-auto" - :href="binaryUrl" - download - icon="download" - data-testid="binary-download-button" - > - {{ $options.i18n.downloadLatestBinary }} - </gl-button> - </div> - </template> - <template v-if="!instructionsEmpty"> - <div class="gl-display-flex"> - <pre - class="gl-bg-gray gl-flex-grow-1 gl-white-space-pre-line" - data-testid="binary-instructions" - >{{ instructions.installInstructions }}</pre - > - <modal-copy-button - :title="$options.i18n.copyInstructions" - :text="instructions.installInstructions" - :modal-id="$options.modalId" - css-classes="gl-align-self-start gl-ml-2 gl-mt-2" - category="tertiary" - /> - </div> - <h5 class="gl-mb-3">{{ $options.i18n.registerRunnerCommand }}</h5> - <div class="gl-display-flex"> - <pre - class="gl-bg-gray gl-flex-grow-1 gl-white-space-pre-line" - data-testid="register-command" - >{{ registerInstructionsWithToken }}</pre - > - <modal-copy-button - :title="$options.i18n.copyInstructions" - :text="registerInstructionsWithToken" - :modal-id="$options.modalId" - css-classes="gl-align-self-start gl-ml-2 gl-mt-2" - category="tertiary" - /> - </div> - </template> - </template> - <template v-else> - <div> - <p>{{ instructionsWithoutArchitecture }}</p> - <gl-button :href="runnerInstallationLink"> - <gl-icon name="external-link" /> - {{ $options.i18n.viewInstallationInstructions }} - </gl-button> - </div> - </template> + <keep-alive> + <component + :is="instructionsComponent" + :registration-token="registrationToken" + :platform="selectedPlatform" + @close="onClose" + @error="toggleAlert(true)" + /> + </keep-alive> </gl-modal> </template> diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js index a28460dd58e..f382ded90d7 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js +++ b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js @@ -140,3 +140,7 @@ export const BIDI_CHARS_CLASS_LIST = 'unicode-bidi has-tooltip'; export const BIDI_CHAR_TOOLTIP = 'Potentially unwanted character detected: Unicode BiDi Control'; export const HLJS_ON_AFTER_HIGHLIGHT = 'after:highlight'; + +// We fallback to highlighting these languages with Rouge, see the following issue for more detail: +// https://gitlab.com/gitlab-org/gitlab/-/issues/384375#note_1212752013 +export const LEGACY_FALLBACKS = ['python']; diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue index 0cfee93ce5d..efafa67a733 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue @@ -11,6 +11,7 @@ import { EVENT_LABEL_FALLBACK, ROUGE_TO_HLJS_LANGUAGE_MAP, LINES_PER_CHUNK, + LEGACY_FALLBACKS, } from './constants'; import Chunk from './components/chunk.vue'; import { registerPlugins } from './plugins/index'; @@ -57,10 +58,11 @@ export default { }, unsupportedLanguage() { const supportedLanguages = Object.keys(languageLoader); - return ( + const unsupportedLanguage = !supportedLanguages.includes(this.language) && - !supportedLanguages.includes(this.blob.language?.toLowerCase()) - ); + !supportedLanguages.includes(this.blob.language?.toLowerCase()); + + return LEGACY_FALLBACKS.includes(this.language) || unsupportedLanguage; }, totalChunks() { return Object.keys(this.chunks).length; diff --git a/app/assets/javascripts/vue_shared/components/timezone_dropdown/timezone_dropdown.vue b/app/assets/javascripts/vue_shared/components/timezone_dropdown/timezone_dropdown.vue index 423501265d7..247f49c1345 100644 --- a/app/assets/javascripts/vue_shared/components/timezone_dropdown/timezone_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/timezone_dropdown/timezone_dropdown.vue @@ -30,6 +30,11 @@ export default { required: true, default: () => [], }, + additionalClass: { + type: Array, + required: false, + default: () => [], + }, }, data() { return { @@ -96,7 +101,14 @@ export default { :value="timezoneIdentifier || value" type="hidden" /> - <gl-dropdown :text="selectedTimezoneLabel" block lazy menu-class="gl-w-full!" v-bind="$attrs"> + <gl-dropdown + :text="selectedTimezoneLabel" + :class="additionalClass" + block + lazy + menu-class="gl-w-full!" + v-bind="$attrs" + > <gl-search-box-by-type v-model.trim="searchTerm" v-autofocusonshow autofocus /> <gl-dropdown-item v-for="timezone in filteredResults" diff --git a/app/assets/javascripts/vue_shared/components/web_ide_link.vue b/app/assets/javascripts/vue_shared/components/web_ide_link.vue index 383dc27ea5e..98630512308 100644 --- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue +++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue @@ -1,16 +1,13 @@ <script> import { GlModal, GlSprintf, GlLink, GlPopover } from '@gitlab/ui'; import { s__, __ } from '~/locale'; +import { visitUrl } from '~/lib/utils/url_utility'; import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue'; import ActionsButton from '~/vue_shared/components/actions_button.vue'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import ConfirmForkModal from '~/vue_shared/components/confirm_fork_modal.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; - -export const KEY_EDIT = 'edit'; -export const KEY_WEB_IDE = 'webide'; -export const KEY_GITPOD = 'gitpod'; -export const KEY_PIPELINE_EDITOR = 'pipeline_editor'; +import { KEY_EDIT, KEY_WEB_IDE, KEY_GITPOD, KEY_PIPELINE_EDITOR } from './constants'; export const i18n = { modal: { @@ -221,7 +218,13 @@ export default { this.showModal('showForkModal'); }, } - : { href: this.webIdeUrl }; + : { + href: this.webIdeUrl, + handle: (evt) => { + evt.preventDefault(); + visitUrl(this.webIdeUrl, true); + }, + }; return { key: KEY_WEB_IDE, diff --git a/app/assets/javascripts/vue_shared/constants.js b/app/assets/javascripts/vue_shared/constants.js index 2f85a29fb84..c93dd95a886 100644 --- a/app/assets/javascripts/vue_shared/constants.js +++ b/app/assets/javascripts/vue_shared/constants.js @@ -9,7 +9,7 @@ const INTERVALS = { export const FILE_SYMLINK_MODE = '120000'; -export const SHORT_DATE_FORMAT = 'd mmm, yyyy'; +export const SHORT_DATE_FORMAT = 'mmm dd, yyyy'; export const ISO_SHORT_FORMAT = 'yyyy-mm-dd'; diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_edit_form.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_edit_form.vue index 2fc1f935501..387fc5e0d1c 100644 --- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_edit_form.vue +++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_edit_form.vue @@ -1,6 +1,5 @@ <script> import { GlForm, GlFormGroup, GlFormInput } from '@gitlab/ui'; -import $ from 'jquery'; import Autosave from '~/autosave'; import MarkdownField from '~/vue_shared/components/markdown/field.vue'; @@ -81,13 +80,13 @@ export default { if (!titleInput || !descriptionInput) return; - this.autosaveTitle = new Autosave($(titleInput.$el), [ + this.autosaveTitle = new Autosave(titleInput.$el, [ document.location.pathname, document.location.search, 'title', ]); - this.autosaveDescription = new Autosave($(descriptionInput.$el), [ + this.autosaveDescription = new Autosave(descriptionInput, [ document.location.pathname, document.location.search, 'description', diff --git a/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue b/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue index b6a459f21e0..26309a25f07 100644 --- a/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue +++ b/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue @@ -34,7 +34,7 @@ export default { :href="`#${panel.name}`" data-qa-selector="panel_link" :data-qa-panel-name="panel.name" - class="new-namespace-panel gl-display-flex gl-flex-shrink-0 gl-flex-direction-column gl-lg-flex-direction-row gl-align-items-center gl-rounded-base gl-border-gray-100 gl-border-solid gl-border-1 gl-w-full gl-py-6 gl-px-8 gl-hover-text-decoration-none!" + class="new-namespace-panel gl-display-flex gl-flex-shrink-0 gl-flex-direction-column gl-lg-flex-direction-row gl-align-items-center gl-rounded-base gl-border-gray-100 gl-border-solid gl-border-1 gl-w-full gl-py-6 gl-px-3 gl-hover-text-decoration-none!" @click="track('click_tab', { label: panel.name })" > <div |