diff options
Diffstat (limited to 'app/assets')
30 files changed, 643 insertions, 243 deletions
diff --git a/app/assets/javascripts/behaviors/shortcuts/keybindings.js b/app/assets/javascripts/behaviors/shortcuts/keybindings.js index cc8a9baf69e..f4facc39f4a 100644 --- a/app/assets/javascripts/behaviors/shortcuts/keybindings.js +++ b/app/assets/javascripts/behaviors/shortcuts/keybindings.js @@ -2,8 +2,11 @@ import { memoize } from 'lodash'; import AccessorUtilities from '~/lib/utils/accessor'; import { __ } from '~/locale'; -const isCustomizable = (command) => - 'customizable' in command ? Boolean(command.customizable) : true; +/** + * @param {object} command + * @param {boolean} [command.customizable] + */ +const isCustomizable = ({ customizable }) => Boolean(customizable ?? true); export const LOCAL_STORAGE_KEY = 'gl-keyboard-shortcuts-customizations'; @@ -183,7 +186,10 @@ export const TOGGLE_MARKDOWN_PREVIEW = { defaultKeys: ['ctrl+shift+p', 'command+shift+p'], }; -export const EDIT_RECENT_COMMENT = { +/** + * @keydown.up event is handled here: https://gitlab.com/gitlab-org/gitlab/-/blob/f3e807cdff5cf25765894163b4e92f8b2bcf8a68/app/assets/javascripts/notes/components/comment_form.vue#L379 + */ +const EDIT_RECENT_COMMENT = { id: 'editing.editRecentComment', description: __('Edit your most recent comment in a thread (from an empty textarea)'), defaultKeys: ['up'], @@ -472,13 +478,22 @@ export const ISSUE_CLOSE_DESIGN = { defaultKeys: ['esc'], }; -export const WEB_IDE_GO_TO_FILE = { +/** + * Legacy Web IDE uses the same shortcuts as MR_GO_TO_FILE, from this shared component: + * https://gitlab.com/gitlab-org/gitlab/-/blob/f3e807cdff5cf25765894163b4e92f8b2bcf8a68/app/assets/javascripts/vue_shared/components/file_finder/index.vue#L6 + */ +const WEB_IDE_GO_TO_FILE = { id: 'webIDE.goToFile', description: __('Go to file'), - defaultKeys: ['mod+p'], + defaultKeys: ['mod+p', 't'], + customizable: false /* customize MR_GO_TO_FILE instead */, }; -export const WEB_IDE_COMMIT = { +/** + * Legacy Web IDE uses @keydown.ctrl.enter and @keydown.meta.enter events here: + * https://gitlab.com/gitlab-org/gitlab/-/blob/f3e807cdff5cf25765894163b4e92f8b2bcf8a68/app/assets/javascripts/ide/components/shared/commit_message_field.vue#L131-132 + */ +const WEB_IDE_COMMIT = { id: 'webIDE.commit', description: __('Commit (when editing commit message)'), defaultKeys: ['mod+enter'], @@ -489,39 +504,28 @@ export const METRICS_EXPAND_PANEL = { id: 'metrics.expandPanel', description: __('Expand panel'), defaultKeys: ['e'], - customizable: false, -}; - -export const METRICS_VIEW_LOGS = { - id: 'metrics.viewLogs', - description: __('View logs'), - defaultKeys: ['l'], - customizable: false, }; export const METRICS_DOWNLOAD_CSV = { id: 'metrics.downloadCSV', description: __('Download CSV'), defaultKeys: ['d'], - customizable: false, }; export const METRICS_COPY_LINK_TO_CHART = { id: 'metrics.copyLinkToChart', description: __('Copy link to chart'), defaultKeys: ['c'], - customizable: false, }; export const METRICS_SHOW_ALERTS = { id: 'metrics.showAlerts', description: __('Alerts'), defaultKeys: ['a'], - customizable: false, }; // All keybinding groups -export const GLOBAL_SHORTCUTS_GROUP = { +const GLOBAL_SHORTCUTS_GROUP = { id: 'globalShortcuts', name: __('Global Shortcuts'), keybindings: [ @@ -559,13 +563,13 @@ export const EDITING_SHORTCUTS_GROUP = { ], }; -export const WIKI_SHORTCUTS_GROUP = { +const WIKI_SHORTCUTS_GROUP = { id: 'wiki', name: __('Wiki'), keybindings: [EDIT_WIKI_PAGE], }; -export const REPOSITORY_GRAPH_SHORTCUTS_GROUP = { +const REPOSITORY_GRAPH_SHORTCUTS_GROUP = { id: 'repositoryGraph', name: __('Repository Graph'), keybindings: [ @@ -578,7 +582,7 @@ export const REPOSITORY_GRAPH_SHORTCUTS_GROUP = { ], }; -export const PROJECT_SHORTCUTS_GROUP = { +const PROJECT_SHORTCUTS_GROUP = { id: 'project', name: __('Project'), keybindings: [ @@ -604,7 +608,7 @@ export const PROJECT_SHORTCUTS_GROUP = { ], }; -export const PROJECT_FILES_SHORTCUTS_GROUP = { +const PROJECT_FILES_SHORTCUTS_GROUP = { id: 'projectFiles', name: __('Project Files'), keybindings: [ @@ -616,19 +620,19 @@ export const PROJECT_FILES_SHORTCUTS_GROUP = { ], }; -export const ISSUABLE_SHORTCUTS_GROUP = { +const ISSUABLE_SHORTCUTS_GROUP = { id: 'issuables', name: __('Epics, issues, and merge requests'), keybindings: [ISSUABLE_COMMENT_OR_REPLY, ISSUABLE_EDIT_DESCRIPTION, ISSUABLE_CHANGE_LABEL], }; -export const ISSUE_MR_SHORTCUTS_GROUP = { +const ISSUE_MR_SHORTCUTS_GROUP = { id: 'issuesMRs', name: __('Issues and merge requests'), keybindings: [ISSUE_MR_CHANGE_ASSIGNEE, ISSUE_MR_CHANGE_MILESTONE], }; -export const MR_SHORTCUTS_GROUP = { +const MR_SHORTCUTS_GROUP = { id: 'mergeRequests', name: __('Merge requests'), keybindings: [ @@ -641,30 +645,29 @@ export const MR_SHORTCUTS_GROUP = { ], }; -export const MR_COMMITS_SHORTCUTS_GROUP = { +const MR_COMMITS_SHORTCUTS_GROUP = { id: 'mergeRequestCommits', name: __('Merge request commits'), keybindings: [MR_COMMITS_NEXT_COMMIT, MR_COMMITS_PREVIOUS_COMMIT], }; -export const ISSUES_SHORTCUTS_GROUP = { +const ISSUES_SHORTCUTS_GROUP = { id: 'issues', name: __('Issues'), keybindings: [ISSUE_NEXT_DESIGN, ISSUE_PREVIOUS_DESIGN, ISSUE_CLOSE_DESIGN], }; -export const WEB_IDE_SHORTCUTS_GROUP = { +const WEB_IDE_SHORTCUTS_GROUP = { id: 'webIDE', - name: __('Web IDE'), + name: __('Legacy Web IDE'), keybindings: [WEB_IDE_GO_TO_FILE, WEB_IDE_COMMIT], }; -export const METRICS_SHORTCUTS_GROUP = { +const METRICS_SHORTCUTS_GROUP = { id: 'metrics', name: __('Metrics'), keybindings: [ METRICS_EXPAND_PANEL, - METRICS_VIEW_LOGS, METRICS_DOWNLOAD_CSV, METRICS_COPY_LINK_TO_CHART, METRICS_SHOW_ALERTS, @@ -700,6 +703,9 @@ export const keybindingGroups = [ * * @param {Object} command The command object. All command objects are * available as imports from this file. + * @param {string} command.id + * @param {string[]} command.defaultKeys + * @param {boolean} [command.customizable] * * @returns {string[]} An array of keyboard shortcut strings bound to the command * diff --git a/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue b/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue index abc2eca38f4..821e002581c 100644 --- a/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue +++ b/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue @@ -1,9 +1,11 @@ <script> import { createAlert, VARIANT_SUCCESS } from '~/alert'; +import { redirectTo, setUrlParams } from '~/lib/utils/url_utility'; import { s__ } from '~/locale'; import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue'; import RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue'; -import { DEFAULT_PLATFORM, PROJECT_TYPE } from '../constants'; +import { DEFAULT_PLATFORM, PARAM_KEY_PLATFORM, PROJECT_TYPE } from '../constants'; +import { saveAlertToLocalStorage } from '../local_storage_alert/save_alert_to_local_storage'; export default { name: 'ProjectNewRunnerApp', @@ -23,11 +25,15 @@ export default { }; }, methods: { - onSaved() { - createAlert({ + onSaved(runner) { + const params = { [PARAM_KEY_PLATFORM]: this.platform }; + const ephemeralRegisterUrl = setUrlParams(params, runner.ephemeralRegisterUrl); + + saveAlertToLocalStorage({ message: s__('Runners|Runner created.'), variant: VARIANT_SUCCESS, }); + redirectTo(ephemeralRegisterUrl); }, onError(error) { createAlert({ message: error.message }); diff --git a/app/assets/javascripts/ci/runner/project_register_runner/index.js b/app/assets/javascripts/ci/runner/project_register_runner/index.js new file mode 100644 index 00000000000..63e88359ee7 --- /dev/null +++ b/app/assets/javascripts/ci/runner/project_register_runner/index.js @@ -0,0 +1,36 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import { showAlertFromLocalStorage } from '../local_storage_alert/show_alert_from_local_storage'; +import ProjectRegisterRunnerApp from './project_register_runner_app.vue'; + +Vue.use(VueApollo); + +export const initProjectRegisterRunner = (selector = '#js-project-register-runner') => { + showAlertFromLocalStorage(); + + const el = document.querySelector(selector); + + if (!el) { + return null; + } + + const { runnerId, runnersPath } = el.dataset; + + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), + }); + + return new Vue({ + el, + apolloProvider, + render(h) { + return h(ProjectRegisterRunnerApp, { + props: { + runnerId, + runnersPath, + }, + }); + }, + }); +}; diff --git a/app/assets/javascripts/ci/runner/project_register_runner/project_register_runner_app.vue b/app/assets/javascripts/ci/runner/project_register_runner/project_register_runner_app.vue new file mode 100644 index 00000000000..b3fad595c7e --- /dev/null +++ b/app/assets/javascripts/ci/runner/project_register_runner/project_register_runner_app.vue @@ -0,0 +1,69 @@ +<script> +import { GlButton } from '@gitlab/ui'; +import { getParameterByName, updateHistory, mergeUrlParams } from '~/lib/utils/url_utility'; +import { PARAM_KEY_PLATFORM, DEFAULT_PLATFORM } from '../constants'; +import RegistrationInstructions from '../components/registration/registration_instructions.vue'; +import PlatformsDrawer from '../components/registration/platforms_drawer.vue'; + +export default { + name: 'ProjectRegisterRunnerApp', + components: { + GlButton, + RegistrationInstructions, + PlatformsDrawer, + }, + props: { + runnerId: { + type: String, + required: true, + }, + runnersPath: { + type: String, + required: true, + }, + }, + data() { + return { + platform: getParameterByName(PARAM_KEY_PLATFORM) || DEFAULT_PLATFORM, + isDrawerOpen: false, + }; + }, + watch: { + platform(platform) { + updateHistory({ + url: mergeUrlParams({ [PARAM_KEY_PLATFORM]: platform }, window.location.href), + }); + }, + }, + methods: { + onSelectPlatform(platform) { + this.platform = platform; + }, + onToggleDrawer(val = !this.isDrawerOpen) { + this.isDrawerOpen = val; + }, + }, +}; +</script> +<template> + <div> + <registration-instructions + :runner-id="runnerId" + :platform="platform" + @toggleDrawer="onToggleDrawer" + > + <template #runner-list-name>{{ s__('Runners|Project › CI/CD Settings › Runners') }}</template> + </registration-instructions> + + <platforms-drawer + :platform="platform" + :open="isDrawerOpen" + @selectPlatform="onSelectPlatform" + @close="onToggleDrawer(false)" + /> + + <gl-button :href="runnersPath" variant="confirm">{{ + s__('Runners|Go to runners page') + }}</gl-button> + </div> +</template> diff --git a/app/assets/javascripts/environments/components/kubernetes_overview.vue b/app/assets/javascripts/environments/components/kubernetes_overview.vue index 736eaa7062d..41abfcf6dc8 100644 --- a/app/assets/javascripts/environments/components/kubernetes_overview.vue +++ b/app/assets/javascripts/environments/components/kubernetes_overview.vue @@ -5,6 +5,7 @@ import csrf from '~/lib/utils/csrf'; import { getIdFromGraphQLId, isGid } from '~/graphql_shared/utils'; import KubernetesAgentInfo from './kubernetes_agent_info.vue'; import KubernetesPods from './kubernetes_pods.vue'; +import KubernetesTabs from './kubernetes_tabs.vue'; export default { components: { @@ -13,6 +14,7 @@ export default { GlAlert, KubernetesAgentInfo, KubernetesPods, + KubernetesTabs, }, inject: ['kasTunnelUrl'], props: { @@ -103,6 +105,10 @@ export default { :configuration="k8sAccessConfiguration" :namespace="namespace" class="gl-mb-5" + @cluster-error="onClusterError" /> + <kubernetes-tabs + :configuration="k8sAccessConfiguration" + class="gl-mb-5" @cluster-error="onClusterError" /></template> </gl-collapse> diff --git a/app/assets/javascripts/environments/components/kubernetes_tabs.vue b/app/assets/javascripts/environments/components/kubernetes_tabs.vue new file mode 100644 index 00000000000..b1eb92a4049 --- /dev/null +++ b/app/assets/javascripts/environments/components/kubernetes_tabs.vue @@ -0,0 +1,158 @@ +<script> +import { GlTabs, GlTab, GlLoadingIcon, GlBadge, GlTable, GlPagination } from '@gitlab/ui'; +import { __, s__ } from '~/locale'; +import k8sServicesQuery from '../graphql/queries/k8s_services.query.graphql'; +import { generateServicePortsString, getServiceAge } from '../helpers/k8s_integration_helper'; +import { SERVICES_LIMIT_PER_PAGE } from '../constants'; + +const tableHeadingClasses = 'gl-bg-gray-50! gl-font-weight-bold gl-white-space-nowrap'; + +export default { + components: { + GlTabs, + GlTab, + GlBadge, + GlTable, + GlPagination, + GlLoadingIcon, + }, + apollo: { + k8sServices: { + query: k8sServicesQuery, + variables() { + return { + configuration: this.configuration, + }; + }, + update(data) { + return data?.k8sServices || []; + }, + error(error) { + this.$emit('cluster-error', error); + }, + }, + }, + props: { + configuration: { + required: true, + type: Object, + }, + }, + data() { + return { + currentPage: 1, + }; + }, + computed: { + servicesItems() { + if (!this.k8sServices?.length) return []; + + return this.k8sServices.map((service) => { + return { + name: service?.metadata?.name, + namespace: service?.metadata?.namespace, + type: service?.spec?.type, + clusterIP: service?.spec?.clusterIP, + externalIP: service?.spec?.externalIP, + ports: generateServicePortsString(service?.spec?.ports), + age: getServiceAge(service?.metadata?.creationTimestamp), + }; + }); + }, + servicesLoading() { + return this.$apollo.queries.k8sServices.loading; + }, + showPagination() { + return this.servicesItems.length > SERVICES_LIMIT_PER_PAGE; + }, + prevPage() { + return Math.max(this.currentPage - 1, 0); + }, + nextPage() { + const nextPage = this.currentPage + 1; + return nextPage > Math.ceil(this.servicesItems.length / SERVICES_LIMIT_PER_PAGE) + ? null + : nextPage; + }, + }, + i18n: { + servicesTitle: s__('Environment|Services'), + name: __('Name'), + namespace: __('Namespace'), + status: __('Status'), + type: __('Type'), + clusterIP: s__('Environment|Cluster IP'), + externalIP: s__('Environment|External IP'), + ports: s__('Environment|Ports'), + age: s__('Environment|Age'), + }, + servicesFields: [ + { + key: 'name', + label: __('Name'), + thClass: tableHeadingClasses, + }, + { + key: 'namespace', + label: __('Namespace'), + thClass: tableHeadingClasses, + }, + { + key: 'type', + label: __('Type'), + thClass: tableHeadingClasses, + }, + { + key: 'clusterIP', + label: s__('Environment|Cluster IP'), + thClass: tableHeadingClasses, + }, + { + key: 'externalIP', + label: s__('Environment|External IP'), + thClass: tableHeadingClasses, + }, + { + key: 'ports', + label: s__('Environment|Ports'), + thClass: tableHeadingClasses, + }, + { + key: 'age', + label: s__('Environment|Age'), + thClass: tableHeadingClasses, + }, + ], + SERVICES_LIMIT_PER_PAGE, +}; +</script> +<template> + <gl-tabs> + <gl-tab> + <template #title> + {{ $options.i18n.servicesTitle }} + <gl-badge size="sm" class="gl-tab-counter-badge">{{ servicesItems.length }}</gl-badge> + </template> + + <gl-loading-icon v-if="servicesLoading" /> + + <gl-table + v-else + :fields="$options.servicesFields" + :items="servicesItems" + :per-page="$options.SERVICES_LIMIT_PER_PAGE" + :current-page="currentPage" + stacked="lg" + class="gl-bg-white! gl-mt-3" + /> + <gl-pagination + v-if="showPagination" + v-model="currentPage" + :prev-page="prevPage" + :next-page="nextPage" + align="center" + class="gl-mt-6" + /> + </gl-tab> + </gl-tabs> +</template> diff --git a/app/assets/javascripts/environments/constants.js b/app/assets/javascripts/environments/constants.js index 28424322dd2..e675a73ba7d 100644 --- a/app/assets/javascripts/environments/constants.js +++ b/app/assets/javascripts/environments/constants.js @@ -87,3 +87,5 @@ export const ENVIRONMENT_NEW_HELP_TEXT = __( ); export const ENVIRONMENT_EDIT_HELP_TEXT = ENVIRONMENT_NEW_HELP_TEXT; + +export const SERVICES_LIMIT_PER_PAGE = 10; diff --git a/app/assets/javascripts/environments/graphql/client.js b/app/assets/javascripts/environments/graphql/client.js index 0482741979b..bb6f57e7e80 100644 --- a/app/assets/javascripts/environments/graphql/client.js +++ b/app/assets/javascripts/environments/graphql/client.js @@ -6,6 +6,7 @@ import environmentToDeleteQuery from './queries/environment_to_delete.query.grap import environmentToRollbackQuery from './queries/environment_to_rollback.query.graphql'; import environmentToStopQuery from './queries/environment_to_stop.query.graphql'; import k8sPodsQuery from './queries/k8s_pods.query.graphql'; +import k8sServicesQuery from './queries/k8s_services.query.graphql'; import { resolvers } from './resolvers'; import typeDefs from './typedefs.graphql'; @@ -87,7 +88,23 @@ export const apolloProvider = (endpoint) => { query: k8sPodsQuery, data: { status: { - phase: '', + phase: null, + }, + }, + }); + cache.writeQuery({ + query: k8sServicesQuery, + data: { + metadata: { + name: null, + namespace: null, + creationTimestamp: null, + }, + spec: { + type: null, + clusterIP: null, + externalIP: null, + ports: [], }, }, }); diff --git a/app/assets/javascripts/environments/graphql/queries/k8s_pods.query.graphql b/app/assets/javascripts/environments/graphql/queries/k8s_pods.query.graphql index 818bca24d51..2d57ede8c15 100644 --- a/app/assets/javascripts/environments/graphql/queries/k8s_pods.query.graphql +++ b/app/assets/javascripts/environments/graphql/queries/k8s_pods.query.graphql @@ -1,4 +1,4 @@ -query getK8sPods($configuration: Object, $namespace: String) { +query getK8sPods($configuration: LocalConfiguration, $namespace: String) { k8sPods(configuration: $configuration, namespace: $namespace) @client { status { phase diff --git a/app/assets/javascripts/environments/graphql/queries/k8s_services.query.graphql b/app/assets/javascripts/environments/graphql/queries/k8s_services.query.graphql new file mode 100644 index 00000000000..d97849eecc1 --- /dev/null +++ b/app/assets/javascripts/environments/graphql/queries/k8s_services.query.graphql @@ -0,0 +1,15 @@ +query getK8sServices($configuration: LocalConfiguration) { + k8sServices(configuration: $configuration) @client { + metadata { + name + namespace + creationTimestamp + } + spec { + type + clusterIP + externalIP + ports + } + } +} diff --git a/app/assets/javascripts/environments/graphql/resolvers.js b/app/assets/javascripts/environments/graphql/resolvers.js index 013467e34be..8ebeeb92a53 100644 --- a/app/assets/javascripts/environments/graphql/resolvers.js +++ b/app/assets/javascripts/environments/graphql/resolvers.js @@ -85,6 +85,30 @@ export const resolvers = (endpoint) => ({ throw error; }); }, + k8sServices(_, { configuration }) { + const coreV1Api = new CoreV1Api(new Configuration(configuration)); + return coreV1Api + .listCoreV1ServiceForAllNamespaces() + .then((res) => { + const items = res?.data?.items || []; + return items.map((item) => { + const { type, clusterIP, externalIP, ports } = item.spec; + return { + metadata: item.metadata, + spec: { + type, + clusterIP: clusterIP || '-', + externalIP: externalIP || '-', + ports, + }, + }; + }); + }) + .catch((err) => { + const error = err?.response?.data?.message ? new Error(err.response.data.message) : err; + throw error; + }); + }, }, Mutation: { stopEnvironmentREST(_, { environment }, { client }) { diff --git a/app/assets/javascripts/environments/graphql/typedefs.graphql b/app/assets/javascripts/environments/graphql/typedefs.graphql index 4de271935d6..b85bf1e10d3 100644 --- a/app/assets/javascripts/environments/graphql/typedefs.graphql +++ b/app/assets/javascripts/environments/graphql/typedefs.graphql @@ -75,6 +75,24 @@ input LocalConfiguration { baseOptions: JSON } +type k8sServiceMetadata { + name: String + namespace: String + creationTimestamp: String +} + +type k8sServiceSpec { + type: String + clusterIP: String + externalIP: String + ports: JSON +} + +type LocalK8sServices { + metadata: k8sServiceMetadata + spec: k8sServiceSpec +} + extend type Query { environmentApp(page: Int, scope: String): LocalEnvironmentApp folder(environment: NestedLocalEnvironmentInput): LocalEnvironmentFolder @@ -85,6 +103,7 @@ extend type Query { isEnvironmentStopping(environment: LocalEnvironmentInput): Boolean isLastDeployment(environment: LocalEnvironmentInput): Boolean k8sPods(configuration: LocalConfiguration, namespace: String): [LocalK8sPods] + k8sServices(configuration: LocalConfiguration): [LocalK8sServices] } extend type Mutation { diff --git a/app/assets/javascripts/environments/helpers/k8s_integration_helper.js b/app/assets/javascripts/environments/helpers/k8s_integration_helper.js new file mode 100644 index 00000000000..6a94ac31fa1 --- /dev/null +++ b/app/assets/javascripts/environments/helpers/k8s_integration_helper.js @@ -0,0 +1,36 @@ +import { differenceInSeconds } from '~/lib/utils/datetime_utility'; + +export function generateServicePortsString(ports) { + if (!ports?.length) return ''; + + return ports + .map((port) => { + const nodePort = port.nodePort ? `:${port.nodePort}` : ''; + return `${port.port}${nodePort}/${port.protocol}`; + }) + .join(', '); +} + +export function getServiceAge(creationTimestamp) { + if (!creationTimestamp) return ''; + + const timeDifference = differenceInSeconds(new Date(creationTimestamp), new Date()); + + const seconds = Math.floor(timeDifference); + const minutes = Math.floor(seconds / 60) % 60; + const hours = Math.floor(seconds / 60 / 60) % 24; + const days = Math.floor(seconds / 60 / 60 / 24); + + let ageString; + if (days > 0) { + ageString = `${days}d`; + } else if (hours > 0) { + ageString = `${hours}h`; + } else if (minutes > 0) { + ageString = `${minutes}m`; + } else { + ageString = `${seconds}s`; + } + + return ageString; +} diff --git a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue index 6d1a3ceba16..fadce6457bc 100644 --- a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue +++ b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue @@ -1,5 +1,5 @@ <script> -import { GlButton, GlLink, GlDropdownItem } from '@gitlab/ui'; +import { GlButton, GlLink, GlDropdownItem, GlDisclosureDropdownItem } from '@gitlab/ui'; import { s__ } from '~/locale'; import eventHub from '../event_hub'; import { @@ -7,10 +7,11 @@ import { TRIGGER_DEFAULT_QA_SELECTOR, TRIGGER_ELEMENT_WITH_EMOJI, TRIGGER_ELEMENT_DROPDOWN_WITH_EMOJI, + TRIGGER_ELEMENT_DISCLOSURE_DROPDOWN, } from '../constants'; export default { - components: { GlButton, GlLink, GlDropdownItem }, + components: { GlButton, GlLink, GlDropdownItem, GlDisclosureDropdownItem }, props: { displayText: { type: String, @@ -55,6 +56,9 @@ export default { 'data-test-id': 'invite-members-button', }; }, + item() { + return { text: this.displayText }; + }, }, methods: { checkTrigger(targetTriggerElement) { @@ -63,10 +67,15 @@ export default { openModal() { eventHub.$emit('openModal', { source: this.triggerSource }); }, + handleDisclosureDropdownAction() { + this.openModal(); + this.$emit('modal-opened'); + }, }, TRIGGER_ELEMENT_BUTTON, TRIGGER_ELEMENT_WITH_EMOJI, TRIGGER_ELEMENT_DROPDOWN_WITH_EMOJI, + TRIGGER_ELEMENT_DISCLOSURE_DROPDOWN, }; </script> @@ -97,6 +106,12 @@ export default { {{ displayText }} <gl-emoji class="gl-vertical-align-baseline gl-reset-font-size gl-mr-1" :data-name="icon" /> </gl-dropdown-item> + <gl-disclosure-dropdown-item + v-else-if="checkTrigger($options.TRIGGER_ELEMENT_DISCLOSURE_DROPDOWN)" + v-bind="componentAttributes" + :item="item" + @action="handleDisclosureDropdownAction" + /> <gl-link v-else v-bind="componentAttributes" data-is-link="true" @click="openModal"> {{ displayText }} </gl-link> diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js index d5e9e498c6b..58457c80524 100644 --- a/app/assets/javascripts/invite_members/constants.js +++ b/app/assets/javascripts/invite_members/constants.js @@ -21,6 +21,7 @@ export const TRIGGER_ELEMENT_BUTTON = 'button'; export const TOP_NAV_INVITE_MEMBERS_COMPONENT = 'invite_members'; export const TRIGGER_ELEMENT_WITH_EMOJI = 'text-emoji'; export const TRIGGER_ELEMENT_DROPDOWN_WITH_EMOJI = 'dropdown-text-emoji'; +export const TRIGGER_ELEMENT_DISCLOSURE_DROPDOWN = 'dropdown-text'; export const INVITE_MEMBER_MODAL_TRACKING_CATEGORY = 'invite_members_modal'; export const TRIGGER_DEFAULT_QA_SELECTOR = 'invite_members_button'; export const MEMBERS_MODAL_DEFAULT_TITLE = s__('InviteMembersModal|Invite members'); diff --git a/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue index 970ab12cc85..b4a9b37d487 100644 --- a/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue +++ b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue @@ -53,7 +53,7 @@ import { TOKEN_TYPE_TYPE, } from '~/vue_shared/components/filtered_search_bar/constants'; import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue'; -import { DEFAULT_PAGE_SIZE, IssuableListTabs } from '~/vue_shared/issuable/list/constants'; +import { DEFAULT_PAGE_SIZE, issuableListTabs } from '~/vue_shared/issuable/list/constants'; import getIssuesCountsQuery from '../queries/get_issues_counts.query.graphql'; import { AutocompleteCache } from '../utils'; @@ -67,7 +67,7 @@ const MilestoneToken = () => export default { i18n, - IssuableListTabs, + issuableListTabs, components: { GlDisclosureDropdown, GlEmptyState, @@ -457,7 +457,7 @@ export default { show-work-item-type-icon :sort-options="sortOptions" :tab-counts="tabCounts" - :tabs="$options.IssuableListTabs" + :tabs="$options.issuableListTabs" truncate-counts :url-params="urlParams" use-keyset-pagination diff --git a/app/assets/javascripts/issues/list/components/issues_list_app.vue b/app/assets/javascripts/issues/list/components/issues_list_app.vue index 5c4bf8f19e4..525daab41d5 100644 --- a/app/assets/javascripts/issues/list/components/issues_list_app.vue +++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue @@ -52,7 +52,7 @@ import { TOKEN_TYPE_TYPE, } from '~/vue_shared/components/filtered_search_bar/constants'; import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue'; -import { DEFAULT_PAGE_SIZE, IssuableListTabs } from '~/vue_shared/issuable/list/constants'; +import { DEFAULT_PAGE_SIZE, issuableListTabs } from '~/vue_shared/issuable/list/constants'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import NewResourceDropdown from '~/vue_shared/components/new_resource_dropdown/new_resource_dropdown.vue'; import { @@ -108,7 +108,7 @@ const CrmOrganizationToken = () => export default { i18n, - IssuableListTabs, + issuableListTabs, components: { CsvImportExportButtons, EmptyStateWithAnyIssues, @@ -774,7 +774,7 @@ export default { :issuables="issues" :error="issuesError" label-filter-param="label_name" - :tabs="$options.IssuableListTabs" + :tabs="$options.issuableListTabs" :current-tab="state" :tab-counts="tabCounts" :truncate-counts="!isProject" diff --git a/app/assets/javascripts/ml/experiment_tracking/components/delete_button.vue b/app/assets/javascripts/ml/experiment_tracking/components/delete_button.vue index 4c0f99cf62c..c10b0cb52b9 100644 --- a/app/assets/javascripts/ml/experiment_tracking/components/delete_button.vue +++ b/app/assets/javascripts/ml/experiment_tracking/components/delete_button.vue @@ -1,9 +1,9 @@ <script> import { GlModal, - GlDropdown, + GlDisclosureDropdown, GlTooltipDirective, - GlDropdownItem, + GlDisclosureDropdownItem, GlModalDirective, } from '@gitlab/ui'; import { __ } from '~/locale'; @@ -12,8 +12,8 @@ import csrf from '~/lib/utils/csrf'; export default { components: { GlModal, - GlDropdown, - GlDropdownItem, + GlDisclosureDropdown, + GlDisclosureDropdownItem, }, directives: { GlTooltip: GlTooltipDirective, @@ -63,36 +63,42 @@ export default { </script> <template> - <gl-dropdown - right - category="tertiary" - :aria-label="__('More actions')" - icon="ellipsis_v" - no-caret - > - <gl-dropdown-item - v-gl-modal-directive="modal.id" - :aria-label="actionPrimaryText" - variant="danger" + <div> + <gl-disclosure-dropdown + placement="right" + category="tertiary" + :aria-label="__('More actions')" + icon="ellipsis_v" + no-caret > - {{ actionPrimaryText }} + <gl-disclosure-dropdown-item + v-gl-modal-directive="modal.id" + :aria-label="actionPrimaryText" + variant="danger" + > + <template #list-item> + <span class="gl-text-red-500"> + {{ actionPrimaryText }} + </span> + </template> + </gl-disclosure-dropdown-item> + </gl-disclosure-dropdown> - <form ref="deleteForm" method="post" :action="deletePath"> - <input type="hidden" name="_method" value="delete" /> - <input type="hidden" name="authenticity_token" :value="$options.csrf.token" /> - </form> + <form ref="deleteForm" method="post" :action="deletePath"> + <input type="hidden" name="_method" value="delete" /> + <input type="hidden" name="authenticity_token" :value="$options.csrf.token" /> + </form> - <gl-modal - :modal-id="modal.id" - :title="modalTitle" - :action-primary="modal.actionPrimary" - :action-cancel="modal.actionCancel" - @primary="confirmDelete" - > - <p> - {{ deleteConfirmationText }} - </p> - </gl-modal> - </gl-dropdown-item> - </gl-dropdown> + <gl-modal + :modal-id="modal.id" + :title="modalTitle" + :action-primary="modal.actionPrimary" + :action-cancel="modal.actionCancel" + @primary="confirmDelete" + > + <p> + {{ deleteConfirmationText }} + </p> + </gl-modal> + </div> </template> diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 100ef11409f..752ba4241d8 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -11,6 +11,13 @@ import { import VueDraggable from 'vuedraggable'; import { mapActions, mapState, mapGetters } from 'vuex'; import { createAlert } from '~/alert'; +import { + keysFor, + METRICS_COPY_LINK_TO_CHART, + METRICS_DOWNLOAD_CSV, + METRICS_EXPAND_PANEL, + METRICS_SHOW_ALERTS, +} from '~/behaviors/shortcuts/keybindings'; import invalidUrl from '~/lib/utils/invalid_url'; import { ESC_KEY } from '~/lib/utils/keys'; import { Mousetrap } from '~/lib/mousetrap'; @@ -18,7 +25,7 @@ import { mergeUrlParams, updateHistory } from '~/lib/utils/url_utility'; import { s__ } from '~/locale'; import { defaultTimeRange } from '~/vue_shared/constants'; import TrackEventDirective from '~/vue_shared/directives/track_event'; -import { metricStates, keyboardShortcutKeys } from '../constants'; +import { metricStates } from '../constants'; import { timeRangeFromUrl, panelToUrl, @@ -214,12 +221,28 @@ export default { created() { window.addEventListener('keyup', this.onKeyup); - Mousetrap.bind(Object.values(keyboardShortcutKeys), this.runShortcut); + Mousetrap.bind(keysFor(METRICS_EXPAND_PANEL), () => + this.runShortcut('onExpandFromKeyboardShortcut'), + ); + Mousetrap.bind(keysFor(METRICS_SHOW_ALERTS), () => + this.runShortcut('showAlertModalFromKeyboardShortcut'), + ); + Mousetrap.bind(keysFor(METRICS_DOWNLOAD_CSV), () => + this.runShortcut('downloadCsvFromKeyboardShortcut'), + ); + Mousetrap.bind(keysFor(METRICS_COPY_LINK_TO_CHART), () => + this.runShortcut('copyChartLinkFromKeyboardShotcut'), + ); }, destroyed() { window.removeEventListener('keyup', this.onKeyup); - Mousetrap.unbind(Object.values(keyboardShortcutKeys)); + [ + METRICS_COPY_LINK_TO_CHART, + METRICS_DOWNLOAD_CSV, + METRICS_EXPAND_PANEL, + METRICS_SHOW_ALERTS, + ].forEach((command) => Mousetrap.unbind(keysFor(command))); }, mounted() { if (!this.hasMetrics) { @@ -339,42 +362,18 @@ export default { }, /** * TODO: Investigate this to utilize the eventBus from Vue - * The intentation behind this cleanup is to allow for better tests + * The intention behind this cleanup is to allow for better tests * as well as use the correct eventBus facilities that are compatible * with Vue 3 * https://gitlab.com/gitlab-org/gitlab/-/issues/225583 */ // - runShortcut(e) { + runShortcut(actionToRun) { const panel = this.$refs[this.hoveredPanel]; if (!panel) return; const [panelInstance] = panel; - let actionToRun = ''; - - switch (e.key) { - case keyboardShortcutKeys.EXPAND: - actionToRun = 'onExpandFromKeyboardShortcut'; - break; - - case keyboardShortcutKeys.SHOW_ALERT: - actionToRun = 'showAlertModalFromKeyboardShortcut'; - break; - - case keyboardShortcutKeys.DOWNLOAD_CSV: - actionToRun = 'downloadCsvFromKeyboardShortcut'; - break; - - case keyboardShortcutKeys.CHART_COPY: - actionToRun = 'copyChartLinkFromKeyboardShotcut'; - break; - - default: - actionToRun = 'onExpandFromKeyboardShortcut'; - break; - } - panelInstance[actionToRun](); }, setHoveredPanel(groupKey, graphIndex) { diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js index faef4b01c27..e35dcc350f2 100644 --- a/app/assets/javascripts/monitoring/constants.js +++ b/app/assets/javascripts/monitoring/constants.js @@ -256,19 +256,6 @@ export const VARIABLE_TYPES = { */ export const VARIABLE_PREFIX = 'var-'; -/** - * All of the actions inside each panel dropdown can be accessed - * via keyboard shortcuts than can be activated via mouse hovers - * and or focus via tabs. - */ - -export const keyboardShortcutKeys = { - EXPAND: 'e', - SHOW_ALERT: 'a', - DOWNLOAD_CSV: 'd', - CHART_COPY: 'c', -}; - export const thresholdModeTypes = { ABSOLUTE: 'absolute', PERCENTAGE: 'percentage', diff --git a/app/assets/javascripts/pages/projects/runners/register/index.js b/app/assets/javascripts/pages/projects/runners/register/index.js new file mode 100644 index 00000000000..a55ff95f84d --- /dev/null +++ b/app/assets/javascripts/pages/projects/runners/register/index.js @@ -0,0 +1,3 @@ +import { initProjectRegisterRunner } from '~/ci/runner/project_register_runner'; + +initProjectRegisterRunner(); diff --git a/app/assets/javascripts/super_sidebar/components/create_menu.vue b/app/assets/javascripts/super_sidebar/components/create_menu.vue index 4cff4642cf7..fa6056aff5e 100644 --- a/app/assets/javascripts/super_sidebar/components/create_menu.vue +++ b/app/assets/javascripts/super_sidebar/components/create_menu.vue @@ -1,6 +1,16 @@ <script> -import { GlDisclosureDropdown, GlTooltip } from '@gitlab/ui'; +import { + GlDisclosureDropdown, + GlTooltip, + GlDisclosureDropdownGroup, + GlDisclosureDropdownItem, +} from '@gitlab/ui'; +import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue'; import { __ } from '~/locale'; +import { + TOP_NAV_INVITE_MEMBERS_COMPONENT, + TRIGGER_ELEMENT_DISCLOSURE_DROPDOWN, +} from '~/invite_members/constants'; import { DROPDOWN_Y_OFFSET } from '../constants'; // Left offset required for the dropdown to be aligned with the super sidebar @@ -9,7 +19,10 @@ const DROPDOWN_X_OFFSET = -147; export default { components: { GlDisclosureDropdown, + GlDisclosureDropdownGroup, + GlDisclosureDropdownItem, GlTooltip, + InviteMembersTrigger, }, i18n: { createNew: __('Create new...'), @@ -25,6 +38,14 @@ export default { dropdownOpen: false, }; }, + methods: { + isInvitedMembers(groupItem) { + return groupItem.component === TOP_NAV_INVITE_MEMBERS_COMPONENT; + }, + closeAndFocus() { + this.$refs.dropdown.closeAndFocus(); + }, + }, toggleId: 'create-menu-toggle', popperOptions: { modifiers: [ @@ -36,24 +57,44 @@ export default { }, ], }, + TRIGGER_ELEMENT_DISCLOSURE_DROPDOWN, }; </script> <template> <div> <gl-disclosure-dropdown + ref="dropdown" category="tertiary" icon="plus" - :items="groups" no-caret text-sr-only :toggle-text="$options.i18n.createNew" :toggle-id="$options.toggleId" :popper-options="$options.popperOptions" data-qa-selector="new_menu_toggle" + data-testid="new-menu-toggle" @shown="dropdownOpen = true" @hidden="dropdownOpen = false" - /> + > + <gl-disclosure-dropdown-group + v-for="(group, index) in groups" + :key="group.name" + :bordered="index !== 0" + :group="group" + > + <template v-for="groupItem in group.items"> + <invite-members-trigger + v-if="isInvitedMembers(groupItem)" + :key="`${groupItem.text}-trigger`" + trigger-source="top-nav" + :trigger-element="$options.TRIGGER_ELEMENT_DISCLOSURE_DROPDOWN" + @modal-opened="closeAndFocus" + /> + <gl-disclosure-dropdown-item v-else :key="groupItem.text" :item="groupItem" /> + </template> + </gl-disclosure-dropdown-group> + </gl-disclosure-dropdown> <gl-tooltip v-if="!dropdownOpen" :target="`#${$options.toggleId}`" diff --git a/app/assets/javascripts/super_sidebar/components/help_center.vue b/app/assets/javascripts/super_sidebar/components/help_center.vue index a75d1fd6bff..70e1780aae1 100644 --- a/app/assets/javascripts/super_sidebar/components/help_center.vue +++ b/app/assets/javascripts/super_sidebar/components/help_center.vue @@ -12,7 +12,7 @@ import { PROMO_URL } from 'jh_else_ce/lib/utils/url_utility'; import { __ } from '~/locale'; import { STORAGE_KEY } from '~/whats_new/utils/notification'; import Tracking from '~/tracking'; -import { DROPDOWN_Y_OFFSET, HELP_MENU_TRACKING_DEFAULTS } from '../constants'; +import { DROPDOWN_Y_OFFSET, HELP_MENU_TRACKING_DEFAULTS, helpCenterState } from '../constants'; // Left offset required for the dropdown to be aligned with the super sidebar const DROPDOWN_X_OFFSET = -4; @@ -49,6 +49,7 @@ export default { data() { return { showWhatsNewNotification: this.shouldShowWhatsNewNotification(), + helpCenterState, }; }, computed: { @@ -175,9 +176,10 @@ export default { this.$refs.dropdown.close(); }, - async showTanukiBotChat() { - // This will be implemented in the following MR: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/117930 - return true; + showTanukiBotChat() { + this.$refs.dropdown.close(); + + this.helpCenterState.showTanukiBotChatDrawer = true; }, async showWhatsNew() { diff --git a/app/assets/javascripts/super_sidebar/constants.js b/app/assets/javascripts/super_sidebar/constants.js index 4f5b027c138..f3f71feaa8a 100644 --- a/app/assets/javascripts/super_sidebar/constants.js +++ b/app/assets/javascripts/super_sidebar/constants.js @@ -20,6 +20,10 @@ export const sidebarState = Vue.observable({ closePeekTimer: null, }); +export const helpCenterState = Vue.observable({ + showTanukiBotChatDrawer: false, +}); + export const MAX_FREQUENT_PROJECTS_COUNT = 5; export const MAX_FREQUENT_GROUPS_COUNT = 3; diff --git a/app/assets/javascripts/vue_shared/issuable/list/constants.js b/app/assets/javascripts/vue_shared/issuable/list/constants.js index 1b71819bdc2..7ece3b60bd5 100644 --- a/app/assets/javascripts/vue_shared/issuable/list/constants.js +++ b/app/assets/javascripts/vue_shared/issuable/list/constants.js @@ -1,7 +1,7 @@ import { STATUS_ALL, STATUS_CLOSED, STATUS_OPEN } from '~/issues/constants'; import { __ } from '~/locale'; -export const IssuableListTabs = [ +export const issuableListTabs = [ { id: 'state-opened', name: STATUS_OPEN, @@ -22,7 +22,7 @@ export const IssuableListTabs = [ }, ]; -export const AvailableSortOptions = [ +export const availableSortOptions = [ { id: 1, title: __('Created date'), diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue index 3c999d166dc..c7ac0b71510 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue @@ -5,7 +5,6 @@ import { s__ } from '~/locale'; import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import { TYPENAME_ISSUE, TYPENAME_WORK_ITEM } from '~/graphql_shared/constants'; -import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import getIssueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql'; import { isMetaKey } from '~/lib/utils/common_utils'; import { getParameterByName, setUrlParams, updateHistory } from '~/lib/utils/url_utility'; @@ -21,7 +20,6 @@ import getWorkItemLinksQuery from '../../graphql/work_item_links.query.graphql'; import addHierarchyChildMutation from '../../graphql/add_hierarchy_child.mutation.graphql'; import removeHierarchyChildMutation from '../../graphql/remove_hierarchy_child.mutation.graphql'; import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql'; -import workItemQuery from '../../graphql/work_item.query.graphql'; import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql'; import WidgetWrapper from '../widget_wrapper.vue'; import WorkItemDetailModal from '../work_item_detail_modal.vue'; @@ -43,14 +41,8 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, - mixins: [glFeatureFlagMixin()], inject: ['projectPath', 'reportAbusePath'], props: { - workItemId: { - type: String, - required: false, - default: null, - }, issuableId: { type: Number, required: false, @@ -75,10 +67,8 @@ export default { this.error = e.message || this.$options.i18n.fetchError; }, async result() { - const { id, iid } = this.childUrlParams(); - this.activeChild = this.fetchByIid - ? this.children.find((child) => child.iid === iid) ?? {} - : this.children.find((child) => child.id === id) ?? {}; + const iid = getParameterByName('work_item_iid'); + this.activeChild = this.children.find((child) => child.iid === iid) ?? {}; await this.$nextTick(); if (!isEmpty(this.activeChild)) { this.$refs.modal.show(); @@ -147,31 +137,11 @@ export default { childrenCountLabel() { return this.isLoading && this.children.length === 0 ? '...' : this.children.length; }, - fetchByIid() { - return true; - }, }, mounted() { - if (!isEmpty(this.childUrlParams())) { - this.addWorkItemQuery(this.childUrlParams()); - } + this.addWorkItemQuery(getParameterByName('work_item_iid')); }, methods: { - childUrlParams() { - const params = {}; - if (this.fetchByIid) { - const iid = getParameterByName('work_item_iid'); - if (iid) { - params.iid = iid; - } - } else { - const workItemId = getParameterByName('work_item_id'); - if (workItemId) { - params.id = convertToGraphQLId(TYPENAME_WORK_ITEM, workItemId); - } - } - return params; - }, showAddForm(formType) { this.$refs.wrapper.show(); this.isShownAddForm = true; @@ -200,11 +170,8 @@ export default { this.removeHierarchyChild(child); this.activeToast = this.$toast.show(s__('WorkItem|Task deleted')); }, - updateWorkItemIdUrlQuery({ id, iid } = {}) { - const params = this.fetchByIid - ? { work_item_iid: iid } - : { work_item_id: getIdFromGraphQLId(id) }; - updateHistory({ url: setUrlParams(params), replace: true }); + updateWorkItemIdUrlQuery({ iid } = {}) { + updateHistory({ url: setUrlParams({ work_item_iid: iid }), replace: true }); }, async addHierarchyChild(workItem) { return this.$apollo.mutate({ @@ -251,31 +218,28 @@ export default { }); } }, - addWorkItemQuery({ id, iid }) { - const variables = this.fetchByIid - ? { - fullPath: this.projectPath, - iid, - } - : { - id, - }; + addWorkItemQuery(iid) { + if (!iid) { + return; + } + this.$apollo.addSmartQuery('prefetchedWorkItem', { - query() { - return this.fetchByIid ? workItemByIidQuery : workItemQuery; + query: workItemByIidQuery, + variables: { + fullPath: this.projectPath, + iid, }, - variables, update(data) { - return this.fetchByIid ? data.workspace.workItems.nodes[0] : data.workItem; + return data.workspace.workItems.nodes[0]; }, context: { isSingleRequest: true, }, }); }, - prefetchWorkItem({ id, iid }) { + prefetchWorkItem({ iid }) { this.prefetch = setTimeout( - () => this.addWorkItemQuery({ id, iid }), + () => this.addWorkItemQuery(iid), DEFAULT_DEBOUNCE_AND_THROTTLE_MS, ); }, diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue index dd0d50861e4..06d6d62e603 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue @@ -1,7 +1,6 @@ <script> import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; -import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; - +import { getParameterByName } from '~/lib/utils/url_utility'; import { FORM_TYPES, WIDGET_TYPE_HIERARCHY, @@ -10,7 +9,6 @@ import { WORK_ITEM_TYPE_ENUM_KEY_RESULT, WORK_ITEM_TYPE_VALUE_OBJECTIVE, } from '../../constants'; -import workItemQuery from '../../graphql/work_item.query.graphql'; import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql'; import WidgetWrapper from '../widget_wrapper.vue'; import OkrActionsSplitButton from './okr_actions_split_button.vue'; @@ -28,7 +26,6 @@ export default { WorkItemLinksForm, WorkItemLinkChild, }, - mixins: [glFeatureFlagMixin()], props: { workItemType: { type: String, @@ -72,9 +69,6 @@ export default { }; }, computed: { - fetchByIid() { - return true; - }, childrenIds() { return this.children.map((c) => c.id); }, @@ -86,6 +80,9 @@ export default { .some((hierarchy) => hierarchy.hasChildren); }, }, + mounted() { + this.addWorkItemQuery(getParameterByName('work_item_iid')); + }, methods: { showAddForm(formType, childType) { this.$refs.wrapper.show(); @@ -99,10 +96,10 @@ export default { hideAddForm() { this.isShownAddForm = false; }, - prefetchWorkItem({ id, iid }) { + prefetchWorkItem({ iid }) { if (this.workItemType !== WORK_ITEM_TYPE_VALUE_OBJECTIVE) { this.prefetch = setTimeout( - () => this.addWorkItemQuery({ id, iid }), + () => this.addWorkItemQuery(iid), DEFAULT_DEBOUNCE_AND_THROTTLE_MS, ); } @@ -112,22 +109,19 @@ export default { clearTimeout(this.prefetch); } }, - addWorkItemQuery({ id, iid }) { - const variables = this.fetchByIid - ? { - fullPath: this.projectPath, - iid, - } - : { - id, - }; + addWorkItemQuery(iid) { + if (!iid) { + return; + } + this.$apollo.addSmartQuery('prefetchedWorkItem', { - query() { - return this.fetchByIid ? workItemByIidQuery : workItemQuery; + query: workItemByIidQuery, + variables: { + fullPath: this.projectPath, + iid, }, - variables, update(data) { - return this.fetchByIid ? data.workspace.workItems.nodes[0] : data.workItem; + return data.workspace.workItems.nodes[0]; }, context: { isSingleRequest: true, diff --git a/app/assets/javascripts/work_items/pages/create_work_item.vue b/app/assets/javascripts/work_items/pages/create_work_item.vue index 9f653fc8f71..49ec12db4e1 100644 --- a/app/assets/javascripts/work_items/pages/create_work_item.vue +++ b/app/assets/javascripts/work_items/pages/create_work_item.vue @@ -1,13 +1,12 @@ <script> import { GlButton, GlAlert, GlLoadingIcon, GlFormSelect } from '@gitlab/ui'; +import { TYPENAME_PROJECT } from '~/graphql_shared/constants'; import { getPreferredLocales, s__ } from '~/locale'; -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; -import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { sprintfWorkItem, I18N_WORK_ITEM_ERROR_CREATING } from '../constants'; import createWorkItemMutation from '../graphql/create_work_item.mutation.graphql'; import projectWorkItemTypesQuery from '../graphql/project_work_item_types.query.graphql'; -import { getWorkItemQuery } from '../utils'; +import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql'; import ItemTitle from '../components/item_title.vue'; @@ -22,7 +21,6 @@ export default { ItemTitle, GlFormSelect, }, - mixins: [glFeatureFlagMixin()], inject: ['fullPath'], props: { initialTitle: { @@ -73,9 +71,6 @@ export default { return sprintfWorkItem(I18N_WORK_ITEM_ERROR_CREATING, workItemType); }, - fetchByIid() { - return true; - }, }, methods: { async createWorkItem() { @@ -96,45 +91,31 @@ export default { }, update: (store, { data: { workItemCreate } }) => { const { workItem } = workItemCreate; - const data = this.fetchByIid - ? { - workspace: { - // eslint-disable-next-line @gitlab/require-i18n-strings - __typename: 'Project', - id: workItem.project.id, - workItems: { - __typename: 'WorkItemConnection', - nodes: [workItem], - }, - }, - } - : { workItem }; store.writeQuery({ - query: getWorkItemQuery(this.fetchByIid), - variables: this.fetchByIid - ? { - fullPath: this.fullPath, - iid: workItem.iid, - } - : { - id: workItem.id, + query: workItemByIidQuery, + variables: { + fullPath: this.fullPath, + iid: workItem.iid, + }, + data: { + workspace: { + __typename: TYPENAME_PROJECT, + id: workItem.project.id, + workItems: { + __typename: 'WorkItemConnection', + nodes: [workItem], }, - data, + }, + }, }); }, }); - const { - data: { - workItemCreate: { - workItem: { id, iid }, - }, - }, - } = response; - const routerParams = this.fetchByIid - ? { name: 'workItem', params: { id: iid } } - : { name: 'workItem', params: { id: `${getIdFromGraphQLId(id)}` } }; - this.$router.push(routerParams); + + this.$router.push({ + name: 'workItem', + params: { id: response.data.workItemCreate.workItem.iid }, + }); } catch { this.error = this.createErrorText; } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index dba9cafbd71..82da6e959d0 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -886,6 +886,11 @@ Multi file editor $border-color-settings: #e1e1e1; /* +Drawers +*/ +$wide-drawer: 500px; + +/* Modals */ $modal-body-height: 80px; diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss index e3c901d787c..267bd3f9506 100644 --- a/app/assets/stylesheets/utilities.scss +++ b/app/assets/stylesheets/utilities.scss @@ -145,3 +145,7 @@ width: $gl-spacing-scale-30; } } + +.gl-fill-orange-500 { + fill: $orange-500; +} |