diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-08-11 12:09:55 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-08-11 12:09:55 +0000 |
commit | bd27a42f5497d66db227aaa5978e11c0fe072105 (patch) | |
tree | 2dae237465c4f240371b866e0918575a3d7a7c1c /app | |
parent | e184bc1abfe4fe4fef8c25c0d2ccb4c0063e7d5e (diff) | |
download | gitlab-ce-bd27a42f5497d66db227aaa5978e11c0fe072105.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
13 files changed, 493 insertions, 193 deletions
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js index 74c00d21535..262e7c4e412 100644 --- a/app/assets/javascripts/flash.js +++ b/app/assets/javascripts/flash.js @@ -1,3 +1,4 @@ +import * as Sentry from '@sentry/browser'; import { escape } from 'lodash'; import { spriteIcon } from './lib/utils/common_utils'; @@ -109,8 +110,65 @@ const createFlash = function createFlash( return flashContainer; }; +/* + * Flash banner supports different types of Flash configurations + * along with ability to provide actionConfig which can be used to show + * additional action or link on banner next to message + * + * @param {Object} options Options to control the flash message + * @param {String} options.message Flash message text + * @param {String} options.type Type of Flash, it can be `notice`, `success`, `warning` or `alert` (default) + * @param {Object} options.parent Reference to parent element under which Flash needs to appear + * @param {Object} options.actonConfig Map of config to show action on banner + * @param {String} href URL to which action config should point to (default: '#') + * @param {String} title Title of action + * @param {Function} clickHandler Method to call when action is clicked on + * @param {Boolean} options.fadeTransition Boolean to determine whether to fade the alert out + * @param {Boolean} options.captureError Boolean to determine whether to send error to sentry + * @param {Object} options.error Error to be captured in sentry + */ +const newCreateFlash = function newCreateFlash({ + message, + type = FLASH_TYPES.ALERT, + parent = document, + actionConfig = null, + fadeTransition = true, + addBodyClass = false, + captureError = false, + error = null, +}) { + const flashContainer = parent.querySelector('.flash-container'); + + if (!flashContainer) return null; + + flashContainer.innerHTML = createFlashEl(message, type); + + const flashEl = flashContainer.querySelector(`.flash-${type}`); + + if (actionConfig) { + flashEl.insertAdjacentHTML('beforeend', createAction(actionConfig)); + + if (actionConfig.clickHandler) { + flashEl + .querySelector('.flash-action') + .addEventListener('click', e => actionConfig.clickHandler(e)); + } + } + + removeFlashClickListener(flashEl, fadeTransition); + + flashContainer.classList.add('gl-display-block'); + + if (addBodyClass) document.body.classList.add('flash-shown'); + + if (captureError && error) Sentry.captureException(error); + + return flashContainer; +}; + export { createFlash as default, + newCreateFlash, createFlashEl, createAction, hideFlash, diff --git a/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue b/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue new file mode 100644 index 00000000000..54586c67fef --- /dev/null +++ b/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue @@ -0,0 +1,249 @@ +<script> +import { mapState, mapGetters, mapActions } from 'vuex'; +import { + GlDeprecatedButton, + GlNewDropdown, + GlNewDropdownDivider, + GlNewDropdownItem, + GlModal, + GlIcon, + GlModalDirective, + GlTooltipDirective, +} from '@gitlab/ui'; +import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue'; +import DuplicateDashboardModal from './duplicate_dashboard_modal.vue'; +import CreateDashboardModal from './create_dashboard_modal.vue'; +import { s__ } from '~/locale'; +import invalidUrl from '~/lib/utils/invalid_url'; +import { redirectTo } from '~/lib/utils/url_utility'; +import TrackEventDirective from '~/vue_shared/directives/track_event'; +import { getAddMetricTrackingOptions } from '../utils'; + +export default { + components: { + GlDeprecatedButton, + GlNewDropdown, + GlNewDropdownDivider, + GlNewDropdownItem, + GlModal, + GlIcon, + DuplicateDashboardModal, + CreateDashboardModal, + CustomMetricsFormFields, + }, + directives: { + GlModal: GlModalDirective, + GlTooltip: GlTooltipDirective, + TrackEvent: TrackEventDirective, + }, + props: { + addingMetricsAvailable: { + type: Boolean, + required: false, + default: false, + }, + customMetricsPath: { + type: String, + required: false, + default: invalidUrl, + }, + validateQueryPath: { + type: String, + required: false, + default: invalidUrl, + }, + defaultBranch: { + type: String, + required: true, + }, + }, + data() { + return { customMetricsFormIsValid: null }; + }, + computed: { + ...mapState('monitoringDashboard', [ + 'projectPath', + 'isUpdatingStarredValue', + 'addDashboardDocumentationPath', + ]), + ...mapGetters('monitoringDashboard', ['selectedDashboard']), + isOutOfTheBoxDashboard() { + return this.selectedDashboard?.out_of_the_box_dashboard; + }, + isMenuItemEnabled() { + return { + createDashboard: Boolean(this.projectPath), + editDashboard: this.selectedDashboard?.can_edit, + }; + }, + isMenuItemShown() { + return { + duplicateDashboard: this.isOutOfTheBoxDashboard, + }; + }, + }, + methods: { + ...mapActions('monitoringDashboard', ['toggleStarredValue']), + setFormValidity(isValid) { + this.customMetricsFormIsValid = isValid; + }, + hideAddMetricModal() { + this.$refs.addMetricModal.hide(); + }, + getAddMetricTrackingOptions, + submitCustomMetricsForm() { + this.$refs.customMetricsForm.submit(); + }, + selectDashboard(dashboard) { + // Once the sidebar See metrics link is updated to the new URL, + // this sort of hardcoding will not be necessary. + // https://gitlab.com/gitlab-org/gitlab/-/issues/229277 + const baseURL = `${this.projectPath}/-/metrics`; + const dashboardPath = encodeURIComponent( + dashboard.out_of_the_box_dashboard ? dashboard.path : dashboard.display_name, + ); + redirectTo(`${baseURL}/${dashboardPath}`); + }, + }, + + modalIds: { + addMetric: 'addMetric', + createDashboard: 'createDashboard', + duplicateDashboard: 'duplicateDashboard', + }, + i18n: { + actionsMenu: s__('Metrics|More actions'), + duplicateDashboard: s__('Metrics|Duplicate current dashboard'), + starDashboard: s__('Metrics|Star dashboard'), + unstarDashboard: s__('Metrics|Unstar dashboard'), + addMetric: s__('Metrics|Add metric'), + editDashboardInfo: s__('Metrics|Duplicate this dashboard to edit dashboard YAML'), + editDashboard: s__('Metrics|Edit dashboard YAML'), + createDashboard: s__('Metrics|Create new dashboard'), + }, +}; +</script> + +<template> + <gl-new-dropdown + v-gl-tooltip + data-testid="actions-menu" + data-qa-selector="actions_menu_dropdown" + right + no-caret + toggle-class="gl-px-3!" + :title="$options.i18n.actionsMenu" + > + <template #button-content> + <gl-icon class="gl-mr-0!" name="ellipsis_v" /> + </template> + + <template v-if="addingMetricsAvailable"> + <gl-new-dropdown-item + v-gl-modal="$options.modalIds.addMetric" + data-qa-selector="add_metric_button" + data-testid="add-metric-item" + > + {{ $options.i18n.addMetric }} + </gl-new-dropdown-item> + <gl-modal + ref="addMetricModal" + :modal-id="$options.modalIds.addMetric" + :title="$options.i18n.addMetric" + data-testid="add-metric-modal" + > + <form ref="customMetricsForm" :action="customMetricsPath" method="post"> + <custom-metrics-form-fields + :validate-query-path="validateQueryPath" + form-operation="post" + @formValidation="setFormValidity" + /> + </form> + <div slot="modal-footer"> + <gl-deprecated-button @click="hideAddMetricModal"> + {{ __('Cancel') }} + </gl-deprecated-button> + <gl-deprecated-button + v-track-event="getAddMetricTrackingOptions()" + data-testid="add-metric-modal-submit-button" + :disabled="!customMetricsFormIsValid" + variant="success" + @click="submitCustomMetricsForm" + > + {{ __('Save changes') }} + </gl-deprecated-button> + </div> + </gl-modal> + </template> + + <gl-new-dropdown-item + v-if="isMenuItemEnabled.editDashboard" + :href="selectedDashboard ? selectedDashboard.project_blob_path : null" + data-qa-selector="edit_dashboard_button_enabled" + data-testid="edit-dashboard-item-enabled" + > + {{ $options.i18n.editDashboard }} + </gl-new-dropdown-item> + + <!-- + wrapper for tooltip as button can be `disabled` + https://bootstrap-vue.org/docs/components/tooltip#disabled-elements + --> + <div v-else v-gl-tooltip :title="$options.i18n.editDashboardInfo"> + <gl-new-dropdown-item + :alt="$options.i18n.editDashboardInfo" + :href="selectedDashboard ? selectedDashboard.project_blob_path : null" + data-testid="edit-dashboard-item-disabled" + disabled + class="gl-cursor-not-allowed" + > + <span class="gl-text-gray-400">{{ $options.i18n.editDashboard }}</span> + </gl-new-dropdown-item> + </div> + + <template v-if="isMenuItemShown.duplicateDashboard"> + <gl-new-dropdown-item + v-gl-modal="$options.modalIds.duplicateDashboard" + data-testid="duplicate-dashboard-item" + > + {{ $options.i18n.duplicateDashboard }} + </gl-new-dropdown-item> + + <duplicate-dashboard-modal + :default-branch="defaultBranch" + :modal-id="$options.modalIds.duplicateDashboard" + data-testid="duplicate-dashboard-modal" + @dashboardDuplicated="selectDashboard" + /> + </template> + + <gl-new-dropdown-item + v-if="selectedDashboard" + data-testid="star-dashboard-item" + :disabled="isUpdatingStarredValue" + @click="toggleStarredValue()" + > + {{ selectedDashboard.starred ? $options.i18n.unstarDashboard : $options.i18n.starDashboard }} + </gl-new-dropdown-item> + + <gl-new-dropdown-divider /> + + <gl-new-dropdown-item + v-gl-modal="$options.modalIds.createDashboard" + data-testid="create-dashboard-item" + :disabled="!isMenuItemEnabled.createDashboard" + :class="{ 'monitoring-actions-item-disabled': !isMenuItemEnabled.createDashboard }" + > + {{ $options.i18n.createDashboard }} + </gl-new-dropdown-item> + + <template v-if="isMenuItemEnabled.createDashboard"> + <create-dashboard-modal + data-testid="create-dashboard-modal" + :add-dashboard-documentation-path="addDashboardDocumentationPath" + :modal-id="$options.modalIds.createDashboard" + :project-path="projectPath" + /> + </template> + </gl-new-dropdown> +</template> diff --git a/app/assets/javascripts/monitoring/components/dashboard_header.vue b/app/assets/javascripts/monitoring/components/dashboard_header.vue index a7e23be98b3..1c921548ce7 100644 --- a/app/assets/javascripts/monitoring/components/dashboard_header.vue +++ b/app/assets/javascripts/monitoring/components/dashboard_header.vue @@ -7,17 +7,11 @@ import { GlDeprecatedDropdownItem, GlDeprecatedDropdownHeader, GlDeprecatedDropdownDivider, - GlNewDropdown, - GlNewDropdownDivider, - GlNewDropdownItem, - GlModal, GlLoadingIcon, GlSearchBoxByType, GlModalDirective, GlTooltipDirective, } from '@gitlab/ui'; -import { s__ } from '~/locale'; -import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue'; import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility'; import invalidUrl from '~/lib/utils/invalid_url'; import Icon from '~/vue_shared/components/icon.vue'; @@ -25,11 +19,9 @@ import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_p import DashboardsDropdown from './dashboards_dropdown.vue'; import RefreshButton from './refresh_button.vue'; -import CreateDashboardModal from './create_dashboard_modal.vue'; -import DuplicateDashboardModal from './duplicate_dashboard_modal.vue'; +import ActionsMenu from './dashboard_actions_menu.vue'; -import TrackEventDirective from '~/vue_shared/directives/track_event'; -import { getAddMetricTrackingOptions, timeRangeToUrl } from '../utils'; +import { timeRangeToUrl } from '../utils'; import { timeRanges } from '~/vue_shared/constants'; import { timezones } from '../format_date'; @@ -42,23 +34,17 @@ export default { GlDeprecatedDropdownItem, GlDeprecatedDropdownHeader, GlDeprecatedDropdownDivider, - GlNewDropdown, - GlNewDropdownDivider, - GlNewDropdownItem, GlSearchBoxByType, - GlModal, - CustomMetricsFormFields, DateTimePicker, DashboardsDropdown, RefreshButton, - DuplicateDashboardModal, - CreateDashboardModal, + + ActionsMenu, }, directives: { GlModal: GlModalDirective, GlTooltip: GlTooltipDirective, - TrackEvent: TrackEventDirective, }, props: { defaultBranch: { @@ -94,29 +80,19 @@ export default { required: true, }, }, - data() { - return { - formIsValid: null, - }; - }, computed: { ...mapState('monitoringDashboard', [ 'emptyState', 'environmentsLoading', 'currentEnvironmentName', - 'isUpdatingStarredValue', 'dashboardTimezone', 'projectPath', 'canAccessOperationsSettings', 'operationsSettingsPath', 'currentDashboard', - 'addDashboardDocumentationPath', 'externalDashboardUrl', ]), ...mapGetters('monitoringDashboard', ['selectedDashboard', 'filteredEnvironments']), - isOutOfTheBoxDashboard() { - return this.selectedDashboard?.out_of_the_box_dashboard; - }, shouldShowEmptyState() { return Boolean(this.emptyState); }, @@ -130,7 +106,7 @@ export default { // Custom metrics only avaialble on system dashboards because // they are stored in the database. This can be improved. See: // https://gitlab.com/gitlab-org/gitlab/-/issues/28241 - this.selectedDashboard?.system_dashboard + this.selectedDashboard?.out_of_the_box_dashboard ); }, showRearrangePanelsBtn() { @@ -139,15 +115,12 @@ export default { displayUtc() { return this.dashboardTimezone === timezones.UTC; }, - shouldShowActionsMenu() { - return Boolean(this.projectPath); - }, shouldShowSettingsButton() { return this.canAccessOperationsSettings && this.operationsSettingsPath; }, }, methods: { - ...mapActions('monitoringDashboard', ['filterEnvironments', 'toggleStarredValue']), + ...mapActions('monitoringDashboard', ['filterEnvironments']), selectDashboard(dashboard) { // Once the sidebar See metrics link is updated to the new URL, // this sort of hardcoding will not be necessary. @@ -171,16 +144,6 @@ export default { toggleRearrangingPanels() { this.$emit('setRearrangingPanels', !this.isRearrangingPanels); }, - setFormValidity(isValid) { - this.formIsValid = isValid; - }, - hideAddMetricModal() { - this.$refs.addMetricModal.hide(); - }, - getAddMetricTrackingOptions, - submitCustomMetricsForm() { - this.$refs.customMetricsForm.submit(); - }, getEnvironmentPath(environment) { // Once the sidebar See metrics link is updated to the new URL, // this sort of hardcoding will not be necessary. @@ -193,16 +156,6 @@ export default { return mergeUrlParams({ environment }, url); }, }, - modalIds: { - addMetric: 'addMetric', - createDashboard: 'createDashboard', - duplicateDashboard: 'duplicateDashboard', - }, - i18n: { - starDashboard: s__('Metrics|Star dashboard'), - unstarDashboard: s__('Metrics|Unstar dashboard'), - addMetric: s__('Metrics|Add metric'), - }, timeRanges, }; </script> @@ -280,29 +233,6 @@ export default { <div class="flex-grow-1"></div> <div class="d-sm-flex"> - <div v-if="selectedDashboard" class="mb-2 mr-2 d-flex"> - <!-- - wrapper for tooltip as button can be `disabled` - https://bootstrap-vue.org/docs/components/tooltip#disabled-elements - --> - <div - v-gl-tooltip - class="flex-grow-1" - :title=" - selectedDashboard.starred ? $options.i18n.unstarDashboard : $options.i18n.starDashboard - " - > - <gl-button - ref="toggleStarBtn" - class="w-100" - :disabled="isUpdatingStarredValue" - variant="default" - :icon="selectedDashboard.starred ? 'star' : 'star-o'" - @click="toggleStarredValue()" - /> - </div> - </div> - <div v-if="showRearrangePanelsBtn" class="mb-2 mr-2 d-flex"> <gl-button :pressed="isRearrangingPanels" @@ -313,58 +243,6 @@ export default { {{ __('Arrange charts') }} </gl-button> </div> - <div v-if="addingMetricsAvailable" class="mb-2 mr-2 d-flex d-sm-block"> - <gl-button - ref="addMetricBtn" - v-gl-modal="$options.modalIds.addMetric" - variant="default" - data-qa-selector="add_metric_button" - class="flex-grow-1" - > - {{ $options.i18n.addMetric }} - </gl-button> - <gl-modal - ref="addMetricModal" - :modal-id="$options.modalIds.addMetric" - :title="$options.i18n.addMetric" - > - <form ref="customMetricsForm" :action="customMetricsPath" method="post"> - <custom-metrics-form-fields - :validate-query-path="validateQueryPath" - form-operation="post" - @formValidation="setFormValidity" - /> - </form> - <div slot="modal-footer"> - <gl-button @click="hideAddMetricModal"> - {{ __('Cancel') }} - </gl-button> - <gl-button - ref="submitCustomMetricsFormBtn" - v-track-event="getAddMetricTrackingOptions()" - :disabled="!formIsValid" - variant="success" - category="primary" - @click="submitCustomMetricsForm" - > - {{ __('Save changes') }} - </gl-button> - </div> - </gl-modal> - </div> - - <div - v-if="selectedDashboard && selectedDashboard.can_edit" - class="mb-2 mr-2 d-flex d-sm-block" - > - <gl-button - class="flex-grow-1 js-edit-link" - :href="selectedDashboard.project_blob_path" - data-qa-selector="edit_dashboard_button" - > - {{ __('Edit dashboard') }} - </gl-button> - </div> <div v-if="externalDashboardUrl && externalDashboardUrl.length" @@ -382,65 +260,28 @@ export default { </gl-button> </div> - <!-- This separator should be displayed only if at least one of the action menu or settings button are displayed --> - <span - v-if="shouldShowActionsMenu || shouldShowSettingsButton" - aria-hidden="true" - class="gl-pl-3 border-left gl-mb-3 d-none d-sm-block" - ></span> + <div class="gl-mb-3 gl-mr-3 d-flex d-sm-block"> + <actions-menu + :adding-metrics-available="addingMetricsAvailable" + :custom-metrics-path="customMetricsPath" + :validate-query-path="validateQueryPath" + :default-branch="defaultBranch" + /> + </div> - <div v-if="shouldShowActionsMenu" class="gl-mb-3 gl-mr-3 d-flex d-sm-block"> - <gl-new-dropdown - v-gl-tooltip - right - class="gl-flex-grow-1" - data-testid="actions-menu" - data-qa-selector="actions_menu_dropdown" - :title="s__('Metrics|Create dashboard')" - :icon="'plus-square'" - > - <gl-new-dropdown-item - v-gl-modal="$options.modalIds.createDashboard" - data-testid="action-create-dashboard" - >{{ s__('Metrics|Create new dashboard') }}</gl-new-dropdown-item - > + <template v-if="shouldShowSettingsButton"> + <span aria-hidden="true" class="gl-pl-3 border-left gl-mb-3 d-none d-sm-block"></span> - <create-dashboard-modal - data-testid="create-dashboard-modal" - :add-dashboard-documentation-path="addDashboardDocumentationPath" - :modal-id="$options.modalIds.createDashboard" - :project-path="projectPath" + <div class="mb-2 mr-2 d-flex d-sm-block"> + <gl-button + v-gl-tooltip + data-testid="metrics-settings-button" + icon="settings" + :href="operationsSettingsPath" + :title="s__('Metrics|Metrics Settings')" /> - - <template v-if="isOutOfTheBoxDashboard"> - <gl-new-dropdown-divider /> - - <gl-new-dropdown-item - ref="duplicateDashboardItem" - v-gl-modal="$options.modalIds.duplicateDashboard" - data-testid="action-duplicate-dashboard" - > - {{ s__('Metrics|Duplicate current dashboard') }} - </gl-new-dropdown-item> - - <duplicate-dashboard-modal - :default-branch="defaultBranch" - :modal-id="$options.modalIds.duplicateDashboard" - @dashboardDuplicated="selectDashboard" - /> - </template> - </gl-new-dropdown> - </div> - - <div v-if="shouldShowSettingsButton" class="mb-2 mr-2 d-flex d-sm-block"> - <gl-button - v-gl-tooltip - data-testid="metrics-settings-button" - icon="settings" - :href="operationsSettingsPath" - :title="s__('Metrics|Metrics Settings')" - /> - </div> + </div> + </template> </div> </div> </template> diff --git a/app/assets/javascripts/packages/details/components/composer_installation.vue b/app/assets/javascripts/packages/details/components/composer_installation.vue new file mode 100644 index 00000000000..c295995935f --- /dev/null +++ b/app/assets/javascripts/packages/details/components/composer_installation.vue @@ -0,0 +1,59 @@ +<script> +import { GlLink, GlSprintf } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import CodeInstruction from './code_instruction.vue'; +import { TrackingActions } from '../constants'; +import { mapGetters, mapState } from 'vuex'; + +export default { + name: 'ComposerInstallation', + components: { + CodeInstruction, + GlLink, + GlSprintf, + }, + computed: { + ...mapState(['composerHelpPath']), + ...mapGetters(['composerRegistryInclude', 'composerPackageInclude']), + }, + i18n: { + registryInclude: s__('PackageRegistry|composer.json registry include'), + copyRegistryInclude: s__('PackageRegistry|Copy registry include'), + packageInclude: s__('PackageRegistry|composer.json require package include'), + copyPackageInclude: s__('PackageRegistry|Copy require package include'), + infoLine: s__( + 'PackageRegistry|For more information on Composer packages in GitLab, %{linkStart}see the documentation.%{linkEnd}', + ), + }, + trackingActions: { ...TrackingActions }, +}; +</script> + +<template> + <div> + <p class="gl-mt-3 gl-font-weight-bold" data-testid="registry-include-title"> + {{ $options.i18n.registryInclude }} + </p> + <code-instruction + :instruction="composerRegistryInclude" + :copy-text="$options.i18n.copyRegistryInclude" + :tracking-action="$options.trackingActions.COPY_COMPOSER_REGISTRY_INCLUDE_COMMAND" + /> + + <p class="gl-mt-3 gl-font-weight-bold" data-testid="package-include-title"> + {{ $options.i18n.packageInclude }} + </p> + <code-instruction + :instruction="composerPackageInclude" + :copy-text="$options.i18n.copyPackageInclude" + :tracking-action="$options.trackingActions.COPY_COMPOSER_PACKAGE_INCLUDE_COMMAND" + /> + <span data-testid="help-text"> + <gl-sprintf :message="$options.i18n.infoLine"> + <template #link="{ content }"> + <gl-link :href="composerHelpPath" target="_blank">{{ content }}</gl-link> + </template> + </gl-sprintf> + </span> + </div> +</template> diff --git a/app/assets/javascripts/packages/details/components/installation_commands.vue b/app/assets/javascripts/packages/details/components/installation_commands.vue index 8ed1c0f267f..219e72df9dc 100644 --- a/app/assets/javascripts/packages/details/components/installation_commands.vue +++ b/app/assets/javascripts/packages/details/components/installation_commands.vue @@ -4,6 +4,7 @@ import MavenInstallation from './maven_installation.vue'; import NpmInstallation from './npm_installation.vue'; import NugetInstallation from './nuget_installation.vue'; import PypiInstallation from './pypi_installation.vue'; +import ComposerInstallation from './composer_installation.vue'; import { PackageType } from '../../shared/constants'; export default { @@ -14,6 +15,7 @@ export default { [PackageType.NPM]: NpmInstallation, [PackageType.NUGET]: NugetInstallation, [PackageType.PYPI]: PypiInstallation, + [PackageType.COMPOSER]: ComposerInstallation, }, props: { packageEntity: { diff --git a/app/assets/javascripts/packages/details/constants.js b/app/assets/javascripts/packages/details/constants.js index 88469656eb2..c6e1b388132 100644 --- a/app/assets/javascripts/packages/details/constants.js +++ b/app/assets/javascripts/packages/details/constants.js @@ -7,6 +7,7 @@ export const TrackingLabels = { NPM_INSTALLATION: 'npm_installation', NUGET_INSTALLATION: 'nuget_installation', PYPI_INSTALLATION: 'pypi_installation', + COMPOSER_INSTALLATION: 'composer_installation', }; export const TrackingActions = { @@ -31,6 +32,9 @@ export const TrackingActions = { COPY_PIP_INSTALL_COMMAND: 'copy_pip_install_command', COPY_PYPI_SETUP_COMMAND: 'copy_pypi_setup_command', + + COPY_COMPOSER_REGISTRY_INCLUDE_COMMAND: 'copy_composer_registry_include_command', + COPY_COMPOSER_PACKAGE_INCLUDE_COMMAND: 'copy_composer_package_include_command', }; export const NpmManager = { diff --git a/app/assets/javascripts/packages/details/store/getters.js b/app/assets/javascripts/packages/details/store/getters.js index bcf74713f03..77dc24ff169 100644 --- a/app/assets/javascripts/packages/details/store/getters.js +++ b/app/assets/javascripts/packages/details/store/getters.js @@ -104,3 +104,12 @@ export const pypiSetupCommand = ({ pypiSetupPath }) => `[gitlab] repository = ${pypiSetupPath} username = __token__ password = <your personal access token>`; + +export const composerRegistryInclude = ({ composerPath }) => { + const base = { type: 'composer', url: composerPath }; + return JSON.stringify(base); +}; +export const composerPackageInclude = ({ packageEntity }) => { + const base = { package_name: packageEntity.name }; + return JSON.stringify(base); +}; diff --git a/app/controllers/import/gitea_controller.rb b/app/controllers/import/gitea_controller.rb index efeff8439e4..4785a71b8a1 100644 --- a/app/controllers/import/gitea_controller.rb +++ b/app/controllers/import/gitea_controller.rb @@ -54,6 +54,16 @@ class Import::GiteaController < Import::GithubController end end + override :client_repos + def client_repos + @client_repos ||= filtered(client.repos) + end + + override :client + def client + @client ||= Gitlab::LegacyGithubImport::Client.new(session[access_token_key], client_options) + end + override :client_options def client_options { host: provider_url, api_version: 'v1' } diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index ac6b8c06d66..29fe34f0734 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -10,6 +10,9 @@ class Import::GithubController < Import::BaseController before_action :provider_auth, only: [:status, :realtime_changes, :create] before_action :expire_etag_cache, only: [:status, :create] + OAuthConfigMissingError = Class.new(StandardError) + + rescue_from OAuthConfigMissingError, with: :missing_oauth_config rescue_from Octokit::Unauthorized, with: :provider_unauthorized rescue_from Octokit::TooManyRequests, with: :provider_rate_limit @@ -22,7 +25,7 @@ class Import::GithubController < Import::BaseController end def callback - session[access_token_key] = client.get_token(params[:code]) + session[access_token_key] = get_token(params[:code]) redirect_to status_import_url end @@ -77,9 +80,7 @@ class Import::GithubController < Import::BaseController override :provider_url def provider_url strong_memoize(:provider_url) do - provider = Gitlab::Auth::OAuth::Provider.config_for('github') - - provider&.dig('url').presence || 'https://github.com' + oauth_config&.dig('url').presence || 'https://github.com' end end @@ -104,11 +105,66 @@ class Import::GithubController < Import::BaseController end def client - @client ||= Gitlab::LegacyGithubImport::Client.new(session[access_token_key], client_options) + @client ||= if Feature.enabled?(:remove_legacy_github_client) + Gitlab::GithubImport::Client.new(session[access_token_key]) + else + Gitlab::LegacyGithubImport::Client.new(session[access_token_key], client_options) + end end def client_repos - @client_repos ||= filtered(client.repos) + @client_repos ||= if Feature.enabled?(:remove_legacy_github_client) + filtered(concatenated_repos) + else + filtered(client.repos) + end + end + + def concatenated_repos + return [] unless client.respond_to?(:each_page) + + client.each_page(:repos).flat_map(&:objects) + end + + def oauth_client + raise OAuthConfigMissingError unless oauth_config + + @oauth_client ||= ::OAuth2::Client.new( + oauth_config.app_id, + oauth_config.app_secret, + oauth_options.merge(ssl: { verify: oauth_config['verify_ssl'] }) + ) + end + + def oauth_config + @oauth_config ||= Gitlab::Auth::OAuth::Provider.config_for('github') + end + + def oauth_options + if oauth_config + oauth_config.dig('args', 'client_options').deep_symbolize_keys + else + OmniAuth::Strategies::GitHub.default_options[:client_options].symbolize_keys + end + end + + def authorize_url + if Feature.enabled?(:remove_legacy_github_client) + oauth_client.auth_code.authorize_url( + redirect_uri: callback_import_url, + scope: 'repo, user, user:email' + ) + else + client.authorize_url(callback_import_url) + end + end + + def get_token(code) + if Feature.enabled?(:remove_legacy_github_client) + oauth_client.auth_code.get_token(code).token + else + client.get_token(code) + end end def verify_import_enabled @@ -116,7 +172,7 @@ class Import::GithubController < Import::BaseController end def go_to_provider_for_permissions - redirect_to client.authorize_url(callback_import_url) + redirect_to authorize_url end def import_enabled? @@ -152,6 +208,12 @@ class Import::GithubController < Import::BaseController alert: _("GitHub API rate limit exceeded. Try again after %{reset_time}") % { reset_time: reset_time } end + def missing_oauth_config + session[access_token_key] = nil + redirect_to new_import_url, + alert: _('Missing OAuth configuration for GitHub.') + end + def access_token_key :"#{provider_name}_access_token" end diff --git a/app/helpers/packages_helper.rb b/app/helpers/packages_helper.rb index a0434284ce6..e6ecc403a88 100644 --- a/app/helpers/packages_helper.rb +++ b/app/helpers/packages_helper.rb @@ -30,6 +30,10 @@ module PackagesHelper full_url.sub!('://', '://__token__:<your_personal_token>@') end + def composer_registry_url(group_id) + expose_url(api_v4_group___packages_composer_packages_path(id: group_id, format: '.json')) + end + def packages_coming_soon_enabled?(resource) ::Feature.enabled?(:packages_coming_soon, resource) && ::Gitlab.dev_env_or_com? end diff --git a/app/policies/personal_access_token_policy.rb b/app/policies/personal_access_token_policy.rb index aa87550fd6b..1e5404b7822 100644 --- a/app/policies/personal_access_token_policy.rb +++ b/app/policies/personal_access_token_policy.rb @@ -3,7 +3,7 @@ class PersonalAccessTokenPolicy < BasePolicy condition(:is_owner) { user && subject.user_id == user.id } - rule { is_owner | admin & ~blocked }.policy do + rule { (is_owner | admin) & ~blocked }.policy do enable :read_token enable :revoke_token end diff --git a/app/services/import/github_service.rb b/app/services/import/github_service.rb index 0cf17568c78..a2923b1e4f9 100644 --- a/app/services/import/github_service.rb +++ b/app/services/import/github_service.rb @@ -33,7 +33,7 @@ module Import end def repo - @repo ||= client.repo(params[:repo_id].to_i) + @repo ||= client.repository(params[:repo_id].to_i) end def project_name diff --git a/app/views/projects/packages/packages/show.html.haml b/app/views/projects/packages/packages/show.html.haml index 2f547a4811f..a66ae466d9d 100644 --- a/app/views/projects/packages/packages/show.html.haml +++ b/app/views/projects/packages/packages/show.html.haml @@ -20,4 +20,6 @@ pypi_path: pypi_registry_url(@project.id), pypi_setup_path: package_registry_project_url(@project.id, :pypi), pypi_help_path: help_page_path('user/packages/pypi_repository/index'), + composer_path: composer_registry_url(@project&.group&.id), + composer_help_path: help_page_path('user/packages/composer_repository/index'), project_name: @project.name} } |