From 9deebb82648de387463f3ec10b33b8dc38e3f456 Mon Sep 17 00:00:00 2001
From: Pirate Praveen Expand for Details
+
+- [ ] Title:
+ - Length limit: 7 words (not including articles or prepositions).
+ - Capitalization: ensure the title is [sentence cased](https://design.gitlab.com/content/punctuation#case).
+ - No Markdown `` `code` `` formatting in the title, as it doesn't render correctly in the release post.
+- [ ] Consistency:
+ - Ensure that all resources (docs, deprecation, etc.) refer to the feature with the same term / feature name.
+- [ ] Content:
+ - Make sure the deprecation is accurate based on your understanding. Look for typos or grammar mistakes. Work with PM and PMM to ensure a consistent GitLab style and tone for messaging, based on other features and deprecations.
+ - Review use of whitespace and bullet lists. Will the deprecation item be easily scannable when published? Consider adding line breaks or breaking content into bullets if you have more than a few sentences.
+ - Make sure there aren't acronyms readers may not understand per
{{ username }}
+ {{ username }}
-
+
-
+
-
+
diff --git a/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue b/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue
index 6b3a4424a5..51251c0cac 100644
--- a/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue
+++ b/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue
@@ -18,6 +18,7 @@ import cancelJobMutation from '../graphql/mutations/job_cancel.mutation.graphql'
import playJobMutation from '../graphql/mutations/job_play.mutation.graphql';
import retryJobMutation from '../graphql/mutations/job_retry.mutation.graphql';
import unscheduleJobMutation from '../graphql/mutations/job_unschedule.mutation.graphql';
+import { reportMessageToSentry } from '../../../utils';
export default {
ACTIONS_DOWNLOAD_ARTIFACTS,
@@ -34,6 +35,7 @@ export default {
jobPlay: 'jobPlay',
jobUnschedule: 'jobUnschedule',
playJobModalId: 'play-job-modal',
+ name: 'JobActionsCell',
components: {
GlButton,
GlButtonGroup,
@@ -99,15 +101,17 @@ export default {
variables: { id: this.job.id },
});
if (errors.length > 0) {
- this.reportFailure();
+ reportMessageToSentry(this.$options.name, errors.join(', '), {});
+ this.showToastMessage();
} else {
eventHub.$emit('jobActionPerformed');
}
- } catch {
- this.reportFailure();
+ } catch (failure) {
+ reportMessageToSentry(this.$options.name, failure, {});
+ this.showToastMessage();
}
},
- reportFailure() {
+ showToastMessage() {
const toastProps = {
text: this.$options.GENERIC_ERROR,
variant: 'danger',
@@ -136,7 +140,13 @@ export default {
-
+
diff --git a/app/assets/javascripts/jobs/store/actions.js b/app/assets/javascripts/jobs/store/actions.js
index 53e3dbbad0..927ba7c7e1 100644
--- a/app/assets/javascripts/jobs/store/actions.js
+++ b/app/assets/javascripts/jobs/store/actions.js
@@ -18,16 +18,16 @@ import * as types from './mutation_types';
export const init = ({ dispatch }, { endpoint, logState, pagePath }) => {
dispatch('setJobEndpoint', endpoint);
- dispatch('setTraceOptions', {
+ dispatch('setJobLogOptions', {
logState,
pagePath,
});
- return Promise.all([dispatch('fetchJob'), dispatch('fetchTrace')]);
+ return Promise.all([dispatch('fetchJob'), dispatch('fetchJobLog')]);
};
export const setJobEndpoint = ({ commit }, endpoint) => commit(types.SET_JOB_ENDPOINT, endpoint);
-export const setTraceOptions = ({ commit }, options) => commit(types.SET_TRACE_OPTIONS, options);
+export const setJobLogOptions = ({ commit }, options) => commit(types.SET_JOB_LOG_OPTIONS, options);
export const hideSidebar = ({ commit }) => commit(types.HIDE_SIDEBAR);
export const showSidebar = ({ commit }) => commit(types.SHOW_SIDEBAR);
@@ -107,7 +107,7 @@ export const receiveJobError = ({ commit }) => {
};
/**
- * Job's Trace
+ * Job Log
*/
export const scrollTop = ({ dispatch }) => {
scrollUp();
@@ -156,59 +156,62 @@ export const toggleScrollAnimation = ({ commit }, toggle) =>
* Responsible to handle automatic scroll
*/
export const toggleScrollisInBottom = ({ commit }, toggle) => {
- commit(types.TOGGLE_IS_SCROLL_IN_BOTTOM_BEFORE_UPDATING_TRACE, toggle);
+ commit(types.TOGGLE_IS_SCROLL_IN_BOTTOM_BEFORE_UPDATING_JOB_LOG, toggle);
};
-export const requestTrace = ({ commit }) => commit(types.REQUEST_TRACE);
+export const requestJobLog = ({ commit }) => commit(types.REQUEST_JOB_LOG);
-export const fetchTrace = ({ dispatch, state }) =>
+export const fetchJobLog = ({ dispatch, state }) =>
+ // update trace endpoint once BE compeletes trace re-naming in #340626
axios
- .get(`${state.traceEndpoint}/trace.json`, {
- params: { state: state.traceState },
+ .get(`${state.jobLogEndpoint}/trace.json`, {
+ params: { state: state.jobLogState },
})
.then(({ data }) => {
dispatch('toggleScrollisInBottom', isScrolledToBottom());
- dispatch('receiveTraceSuccess', data);
+ dispatch('receiveJobLogSuccess', data);
if (data.complete) {
- dispatch('stopPollingTrace');
- } else if (!state.traceTimeout) {
- dispatch('startPollingTrace');
+ dispatch('stopPollingJobLog');
+ } else if (!state.jobLogTimeout) {
+ dispatch('startPollingJobLog');
}
})
.catch((e) => {
if (e.response.status === httpStatusCodes.FORBIDDEN) {
- dispatch('receiveTraceUnauthorizedError');
+ dispatch('receiveJobLogUnauthorizedError');
} else {
reportToSentry('job_actions', e);
- dispatch('receiveTraceError');
+ dispatch('receiveJobLogError');
}
});
-export const startPollingTrace = ({ dispatch, commit }) => {
- const traceTimeout = setTimeout(() => {
- commit(types.SET_TRACE_TIMEOUT, 0);
- dispatch('fetchTrace');
+export const startPollingJobLog = ({ dispatch, commit }) => {
+ const jobLogTimeout = setTimeout(() => {
+ commit(types.SET_JOB_LOG_TIMEOUT, 0);
+ dispatch('fetchJobLog');
}, 4000);
- commit(types.SET_TRACE_TIMEOUT, traceTimeout);
+ commit(types.SET_JOB_LOG_TIMEOUT, jobLogTimeout);
};
-export const stopPollingTrace = ({ state, commit }) => {
- clearTimeout(state.traceTimeout);
- commit(types.SET_TRACE_TIMEOUT, 0);
- commit(types.STOP_POLLING_TRACE);
+export const stopPollingJobLog = ({ state, commit }) => {
+ clearTimeout(state.jobLogTimeout);
+ commit(types.SET_JOB_LOG_TIMEOUT, 0);
+ commit(types.STOP_POLLING_JOB_LOG);
};
-export const receiveTraceSuccess = ({ commit }, log) => commit(types.RECEIVE_TRACE_SUCCESS, log);
-export const receiveTraceError = ({ dispatch }) => {
- dispatch('stopPollingTrace');
+export const receiveJobLogSuccess = ({ commit }, log) => commit(types.RECEIVE_JOB_LOG_SUCCESS, log);
+
+export const receiveJobLogError = ({ dispatch }) => {
+ dispatch('stopPollingJobLog');
createFlash({
message: __('An error occurred while fetching the job log.'),
});
};
-export const receiveTraceUnauthorizedError = ({ dispatch }) => {
- dispatch('stopPollingTrace');
+
+export const receiveJobLogUnauthorizedError = ({ dispatch }) => {
+ dispatch('stopPollingJobLog');
createFlash({
message: __('The current user is not authorized to access the job log.'),
});
@@ -248,6 +251,7 @@ export const fetchJobsForStage = ({ dispatch }, stage = {}) => {
};
export const receiveJobsForStageSuccess = ({ commit }, data) =>
commit(types.RECEIVE_JOBS_FOR_STAGE_SUCCESS, data);
+
export const receiveJobsForStageError = ({ commit }) => {
commit(types.RECEIVE_JOBS_FOR_STAGE_ERROR);
createFlash({
diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/jobs/store/getters.js
index 6cb96bee07..9d25582225 100644
--- a/app/assets/javascripts/jobs/store/getters.js
+++ b/app/assets/javascripts/jobs/store/getters.js
@@ -21,11 +21,12 @@ export const shouldRenderTriggeredLabel = (state) => isString(state.job.started)
export const hasEnvironment = (state) => !isEmpty(state.job.deployment_status);
/**
- * Checks if it the job has trace.
+ * Checks if it the job has a log.
* Used to check if it should render the job log or the empty state
* @returns {Boolean}
*/
-export const hasTrace = (state) =>
+export const hasJobLog = (state) =>
+ // update has_trace once BE compeletes trace re-naming in #340626
state.job.has_trace || (!isEmpty(state.job.status) && state.job.status.group === 'running');
export const emptyStateIllustration = (state) => state?.job?.status?.illustration || {};
@@ -43,7 +44,7 @@ export const shouldRenderSharedRunnerLimitWarning = (state) =>
!isEmpty(state.job.runners.quota) &&
state.job.runners.quota.used >= state.job.runners.quota.limit;
-export const isScrollingDown = (state) => isScrolledToBottom() && !state.isTraceComplete;
+export const isScrollingDown = (state) => isScrolledToBottom() && !state.isJobLogComplete;
export const hasRunnersForProject = (state) =>
state?.job?.runners?.available && !state?.job?.runners?.online;
diff --git a/app/assets/javascripts/jobs/store/mutation_types.js b/app/assets/javascripts/jobs/store/mutation_types.js
index 6c4f1b5a19..4915a826b8 100644
--- a/app/assets/javascripts/jobs/store/mutation_types.js
+++ b/app/assets/javascripts/jobs/store/mutation_types.js
@@ -1,5 +1,5 @@
export const SET_JOB_ENDPOINT = 'SET_JOB_ENDPOINT';
-export const SET_TRACE_OPTIONS = 'SET_TRACE_OPTIONS';
+export const SET_JOB_LOG_OPTIONS = 'SET_JOB_LOG_OPTIONS';
export const HIDE_SIDEBAR = 'HIDE_SIDEBAR';
export const SHOW_SIDEBAR = 'SHOW_SIDEBAR';
@@ -12,17 +12,17 @@ export const ENABLE_SCROLL_BOTTOM = 'ENABLE_SCROLL_BOTTOM';
export const ENABLE_SCROLL_TOP = 'ENABLE_SCROLL_TOP';
export const TOGGLE_SCROLL_ANIMATION = 'TOGGLE_SCROLL_ANIMATION';
-export const TOGGLE_IS_SCROLL_IN_BOTTOM_BEFORE_UPDATING_TRACE = 'TOGGLE_IS_SCROLL_IN_BOTTOM';
+export const TOGGLE_IS_SCROLL_IN_BOTTOM_BEFORE_UPDATING_JOB_LOG = 'TOGGLE_IS_SCROLL_IN_BOTTOM';
export const REQUEST_JOB = 'REQUEST_JOB';
export const RECEIVE_JOB_SUCCESS = 'RECEIVE_JOB_SUCCESS';
export const RECEIVE_JOB_ERROR = 'RECEIVE_JOB_ERROR';
-export const REQUEST_TRACE = 'REQUEST_TRACE';
-export const SET_TRACE_TIMEOUT = 'SET_TRACE_TIMEOUT';
-export const STOP_POLLING_TRACE = 'STOP_POLLING_TRACE';
-export const RECEIVE_TRACE_SUCCESS = 'RECEIVE_TRACE_SUCCESS';
-export const RECEIVE_TRACE_ERROR = 'RECEIVE_TRACE_ERROR';
+export const REQUEST_JOB_LOG = 'REQUEST_JOB_LOG';
+export const SET_JOB_LOG_TIMEOUT = 'SET_JOB_LOG_TIMEOUT';
+export const STOP_POLLING_JOB_LOG = 'STOP_POLLING_JOB_LOG';
+export const RECEIVE_JOB_LOG_SUCCESS = 'RECEIVE_JOB_LOG_SUCCESS';
+export const RECEIVE_JOB_LOG_ERROR = 'RECEIVE_JOB_LOG_ERROR';
export const TOGGLE_COLLAPSIBLE_LINE = 'TOGGLE_COLLAPSIBLE_LINE';
export const SET_SELECTED_STAGE = 'SET_SELECTED_STAGE';
diff --git a/app/assets/javascripts/jobs/store/mutations.js b/app/assets/javascripts/jobs/store/mutations.js
index 4045d8a0c1..eda2ee0349 100644
--- a/app/assets/javascripts/jobs/store/mutations.js
+++ b/app/assets/javascripts/jobs/store/mutations.js
@@ -1,16 +1,16 @@
import Vue from 'vue';
import { INFINITELY_NESTED_COLLAPSIBLE_SECTIONS_FF } from '../constants';
import * as types from './mutation_types';
-import { logLinesParser, logLinesParserLegacy, updateIncrementalTrace } from './utils';
+import { logLinesParser, logLinesParserLegacy, updateIncrementalJobLog } from './utils';
export default {
[types.SET_JOB_ENDPOINT](state, endpoint) {
state.jobEndpoint = endpoint;
},
- [types.SET_TRACE_OPTIONS](state, options = {}) {
- state.traceEndpoint = options.pagePath;
- state.traceState = options.logState;
+ [types.SET_JOB_LOG_OPTIONS](state, options = {}) {
+ state.jobLogEndpoint = options.pagePath;
+ state.jobLogState = options.logState;
},
[types.HIDE_SIDEBAR](state) {
@@ -20,11 +20,11 @@ export default {
state.isSidebarOpen = true;
},
- [types.RECEIVE_TRACE_SUCCESS](state, log = {}) {
+ [types.RECEIVE_JOB_LOG_SUCCESS](state, log = {}) {
const infinitelyCollapsibleSectionsFlag =
gon.features?.[INFINITELY_NESTED_COLLAPSIBLE_SECTIONS_FF];
if (log.state) {
- state.traceState = log.state;
+ state.jobLogState = log.state;
}
if (log.append) {
@@ -32,52 +32,52 @@ export default {
if (log.lines) {
const parsedResult = logLinesParser(
log.lines,
- state.auxiliaryPartialTraceHelpers,
- state.trace,
+ state.auxiliaryPartialJobLogHelpers,
+ state.jobLog,
);
- state.trace = parsedResult.parsedLines;
- state.auxiliaryPartialTraceHelpers = parsedResult.auxiliaryPartialTraceHelpers;
+ state.jobLog = parsedResult.parsedLines;
+ state.auxiliaryPartialJobLogHelpers = parsedResult.auxiliaryPartialJobLogHelpers;
}
} else {
- state.trace = log.lines ? updateIncrementalTrace(log.lines, state.trace) : state.trace;
+ state.jobLog = log.lines ? updateIncrementalJobLog(log.lines, state.jobLog) : state.jobLog;
}
- state.traceSize += log.size;
+ state.jobLogSize += log.size;
} else {
- // When the job still does not have a trace
- // the trace response will not have a defined
+ // When the job still does not have a log
+ // the job log response will not have a defined
// html or size. We keep the old value otherwise these
// will be set to `null`
if (infinitelyCollapsibleSectionsFlag) {
const parsedResult = logLinesParser(log.lines);
- state.trace = parsedResult.parsedLines;
- state.auxiliaryPartialTraceHelpers = parsedResult.auxiliaryPartialTraceHelpers;
+ state.jobLog = parsedResult.parsedLines;
+ state.auxiliaryPartialJobLogHelpers = parsedResult.auxiliaryPartialJobLogHelpers;
} else {
- state.trace = log.lines ? logLinesParserLegacy(log.lines) : state.trace;
+ state.jobLog = log.lines ? logLinesParserLegacy(log.lines) : state.jobLog;
}
- state.traceSize = log.size || state.traceSize;
+ state.jobLogSize = log.size || state.jobLogSize;
}
- if (state.traceSize < log.total) {
- state.isTraceSizeVisible = true;
+ if (state.jobLogSize < log.total) {
+ state.isJobLogSizeVisible = true;
} else {
- state.isTraceSizeVisible = false;
+ state.isJobLogSizeVisible = false;
}
- state.isTraceComplete = log.complete || state.isTraceComplete;
+ state.isJobLogComplete = log.complete || state.isJobLogComplete;
},
- [types.SET_TRACE_TIMEOUT](state, id) {
- state.traceTimeout = id;
+ [types.SET_JOB_LOG_TIMEOUT](state, id) {
+ state.jobLogTimeout = id;
},
/**
* Will remove loading animation
*/
- [types.STOP_POLLING_TRACE](state) {
- state.isTraceComplete = true;
+ [types.STOP_POLLING_JOB_LOG](state) {
+ state.isJobLogComplete = true;
},
/**
@@ -137,8 +137,8 @@ export default {
state.isScrollingDown = toggle;
},
- [types.TOGGLE_IS_SCROLL_IN_BOTTOM_BEFORE_UPDATING_TRACE](state, toggle) {
- state.isScrolledToBottomBeforeReceivingTrace = toggle;
+ [types.TOGGLE_IS_SCROLL_IN_BOTTOM_BEFORE_UPDATING_JOB_LOG](state, toggle) {
+ state.isScrolledToBottomBeforeReceivingJobLog = toggle;
},
[types.REQUEST_JOBS_FOR_STAGE](state, stage = {}) {
diff --git a/app/assets/javascripts/jobs/store/state.js b/app/assets/javascripts/jobs/store/state.js
index 718324c8ba..a1ba64aa71 100644
--- a/app/assets/javascripts/jobs/store/state.js
+++ b/app/assets/javascripts/jobs/store/state.js
@@ -1,6 +1,6 @@
export default () => ({
jobEndpoint: null,
- traceEndpoint: null,
+ jobLogEndpoint: null,
// sidebar
isSidebarOpen: true,
@@ -14,16 +14,16 @@ export default () => ({
isScrollTopDisabled: true,
// Used to check if we should keep the automatic scroll
- isScrolledToBottomBeforeReceivingTrace: true,
+ isScrolledToBottomBeforeReceivingJobLog: true,
- trace: [],
- isTraceComplete: false,
- traceSize: 0,
- isTraceSizeVisible: false,
- traceTimeout: 0,
+ jobLog: [],
+ isJobLogComplete: false,
+ jobLogSize: 0,
+ isJobLogSizeVisible: false,
+ jobLogTimeout: 0,
- // used as a query parameter to fetch the trace
- traceState: null,
+ // used as a query parameter to fetch the job log
+ jobLogState: null,
// sidebar dropdown & list of jobs
isLoadingJobs: false,
@@ -32,5 +32,5 @@ export default () => ({
jobs: [],
// to parse partial logs
- auxiliaryPartialTraceHelpers: {},
+ auxiliaryPartialJobLogHelpers: {},
});
diff --git a/app/assets/javascripts/jobs/store/utils.js b/app/assets/javascripts/jobs/store/utils.js
index b64734e29f..8bca448ee1 100644
--- a/app/assets/javascripts/jobs/store/utils.js
+++ b/app/assets/javascripts/jobs/store/utils.js
@@ -131,17 +131,17 @@ export const logLinesParserLegacy = (lines = [], accumulator = []) =>
[...accumulator],
);
-export const logLinesParser = (lines = [], previousTraceState = {}, prevParsedLines = []) => {
- let currentLineCount = previousTraceState?.prevLineCount ?? 0;
- let currentHeader = previousTraceState?.currentHeader;
- let isPreviousLineHeader = previousTraceState?.isPreviousLineHeader ?? false;
+export const logLinesParser = (lines = [], previousJobLogState = {}, prevParsedLines = []) => {
+ let currentLineCount = previousJobLogState?.prevLineCount ?? 0;
+ let currentHeader = previousJobLogState?.currentHeader;
+ let isPreviousLineHeader = previousJobLogState?.isPreviousLineHeader ?? false;
const parsedLines = prevParsedLines.length > 0 ? prevParsedLines : [];
- const sectionsQueue = previousTraceState?.sectionsQueue ?? [];
+ const sectionsQueue = previousJobLogState?.sectionsQueue ?? [];
for (let i = 0; i < lines.length; i += 1) {
const line = lines[i];
// First run we can use the current index, later runs we have to retrieve the last number of lines
- currentLineCount = previousTraceState?.prevLineCount ? currentLineCount + 1 : i + 1;
+ currentLineCount = previousJobLogState?.prevLineCount ? currentLineCount + 1 : i + 1;
if (line.section_header && !isPreviousLineHeader) {
// If there's no previous line header that means we're at the root of the log
@@ -198,7 +198,7 @@ export const logLinesParser = (lines = [], previousTraceState = {}, prevParsedLi
return {
parsedLines,
- auxiliaryPartialTraceHelpers: {
+ auxiliaryPartialJobLogHelpers: {
isPreviousLineHeader,
currentHeader,
sectionsQueue,
@@ -241,7 +241,7 @@ export const findOffsetAndRemove = (newLog = [], oldParsed = []) => {
};
/**
- * When the trace is not complete, backend may send the last received line
+ * When the job log is not complete, backend may send the last received line
* in the new response.
*
* We need to check if that is the case by looking for the offset property
@@ -250,7 +250,7 @@ export const findOffsetAndRemove = (newLog = [], oldParsed = []) => {
* @param array oldLog
* @param array newLog
*/
-export const updateIncrementalTrace = (newLog = [], oldParsed = []) => {
+export const updateIncrementalJobLog = (newLog = [], oldParsed = []) => {
const parsedLog = findOffsetAndRemove(newLog, oldParsed);
return logLinesParserLegacy(newLog, parsedLog);
diff --git a/app/assets/javascripts/jobs/utils.js b/app/assets/javascripts/jobs/utils.js
index bb27658369..a4e695518f 100644
--- a/app/assets/javascripts/jobs/utils.js
+++ b/app/assets/javascripts/jobs/utils.js
@@ -19,3 +19,12 @@ export const reportToSentry = (component, failureType) => {
Sentry.captureException(failureType);
});
};
+
+export const reportMessageToSentry = (component, message, context) => {
+ Sentry.withScope((scope) => {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ scope.setContext('Vue data', context);
+ scope.setTag('component', component);
+ Sentry.captureMessage(message);
+ });
+};
diff --git a/app/assets/javascripts/lib/apollo/suppress_network_errors_during_navigation_link.js b/app/assets/javascripts/lib/apollo/suppress_network_errors_during_navigation_link.js
new file mode 100644
index 0000000000..ad92bd4de4
--- /dev/null
+++ b/app/assets/javascripts/lib/apollo/suppress_network_errors_during_navigation_link.js
@@ -0,0 +1,36 @@
+import { Observable } from 'apollo-link';
+import { onError } from 'apollo-link-error';
+import { isNavigatingAway } from '~/lib/utils/is_navigating_away';
+
+/**
+ * Returns an ApolloLink (or null if not enabled) which supresses network
+ * errors when the browser is navigating away.
+ *
+ * @returns {ApolloLink|null}
+ */
+export const getSuppressNetworkErrorsDuringNavigationLink = () => {
+ if (!gon.features?.suppressApolloErrorsDuringNavigation) {
+ return null;
+ }
+
+ return onError(({ networkError }) => {
+ if (networkError && isNavigatingAway()) {
+ // Return an observable that will never notify any subscribers with any
+ // values, errors, or completions. This ensures that requests aborted due
+ // to navigating away do not trigger any failure behaviour.
+ //
+ // See '../utils/suppress_ajax_errors_during_navigation.js' for an axios
+ // interceptor that performs a similar role.
+ return new Observable(() => {});
+ }
+
+ // We aren't suppressing anything here, so simply do nothing.
+ // The onError helper will forward all values/errors/completions from the
+ // underlying request observable to the next link if you return a falsey
+ // value.
+ //
+ // Note that this return statement is technically redundant, but is kept
+ // for explicitness.
+ return undefined;
+ });
+};
diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js
index b96a55fe11..39bf804b54 100644
--- a/app/assets/javascripts/lib/graphql.js
+++ b/app/assets/javascripts/lib/graphql.js
@@ -11,6 +11,7 @@ import csrf from '~/lib/utils/csrf';
import { objectToQuery, queryToObject } from '~/lib/utils/url_utility';
import PerformanceBarService from '~/performance_bar/services/performance_bar_service';
import { getInstrumentationLink } from './apollo/instrumentation_link';
+import { getSuppressNetworkErrorsDuringNavigationLink } from './apollo/suppress_network_errors_during_navigation_link';
export const fetchPolicies = {
CACHE_FIRST: 'cache-first',
@@ -143,6 +144,7 @@ export default (resolvers = {}, config = {}) => {
new ActionCableLink(),
ApolloLink.from(
[
+ getSuppressNetworkErrorsDuringNavigationLink(),
getInstrumentationLink(),
requestCounterLink,
performanceBarLink,
diff --git a/app/assets/javascripts/lib/logger/hello.js b/app/assets/javascripts/lib/logger/hello.js
index 18fa35ab55..ccfdfe91e6 100644
--- a/app/assets/javascripts/lib/logger/hello.js
+++ b/app/assets/javascripts/lib/logger/hello.js
@@ -1,15 +1,36 @@
+import { s__, sprintf } from '~/locale';
+
const HANDSHAKE = String.fromCodePoint(0x1f91d);
const MAG = String.fromCodePoint(0x1f50e);
+const ROCKET = String.fromCodePoint(0x1f680);
export const logHello = () => {
// eslint-disable-next-line no-console
console.log(
- `%cWelcome to GitLab!%c
+ `%c${s__('HelloMessage|Welcome to GitLab!')}%c
-Does this page need fixes or improvements? Open an issue or contribute a merge request to help make GitLab more lovable. At GitLab, everyone can contribute!
+${s__(
+ 'HelloMessage|Does this page need fixes or improvements? Open an issue or contribute a merge request to help make GitLab more lovable. At GitLab, everyone can contribute!',
+)}
-${HANDSHAKE} Contribute to GitLab: https://about.gitlab.com/community/contribute/
-${MAG} Create a new GitLab issue: https://gitlab.com/gitlab-org/gitlab/-/issues/new`,
+${sprintf(s__('HelloMessage|%{handshake_emoji} Contribute to GitLab: %{contribute_link}'), {
+ handshake_emoji: `${HANDSHAKE}`,
+ contribute_link: 'https://about.gitlab.com/community/contribute/',
+})}
+${sprintf(s__('HelloMessage|%{magnifier_emoji} Create a new GitLab issue: %{new_issue_link}'), {
+ magnifier_emoji: `${MAG}`,
+ new_issue_link: 'https://gitlab.com/gitlab-org/gitlab/-/issues/new',
+})}
+${
+ window.gon?.dot_com
+ ? `${sprintf(
+ s__(
+ 'HelloMessage|%{rocket_emoji} We like your curiosity! Help us improve GitLab by joining the team: %{jobs_page_link}',
+ ),
+ { rocket_emoji: `${ROCKET}`, jobs_page_link: 'https://about.gitlab.com/jobs/' },
+ )}`
+ : ''
+}`,
`padding-top: 0.5em; font-size: 2em;`,
'padding-bottom: 0.5em;',
);
diff --git a/app/assets/javascripts/lib/utils/axios_utils.js b/app/assets/javascripts/lib/utils/axios_utils.js
index 0a26f78e25..de6d85b8a1 100644
--- a/app/assets/javascripts/lib/utils/axios_utils.js
+++ b/app/assets/javascripts/lib/utils/axios_utils.js
@@ -2,6 +2,7 @@ import axios from 'axios';
import { registerCaptchaModalInterceptor } from '~/captcha/captcha_modal_axios_interceptor';
import setupAxiosStartupCalls from './axios_startup_calls';
import csrf from './csrf';
+import { isNavigatingAway } from './is_navigating_away';
import suppressAjaxErrorsDuringNavigation from './suppress_ajax_errors_during_navigation';
axios.defaults.headers.common[csrf.headerKey] = csrf.token;
@@ -30,16 +31,11 @@ axios.interceptors.response.use(
},
);
-let isUserNavigating = false;
-window.addEventListener('beforeunload', () => {
- isUserNavigating = true;
-});
-
// Ignore AJAX errors caused by requests
// being cancelled due to browser navigation
axios.interceptors.response.use(
(response) => response,
- (err) => suppressAjaxErrorsDuringNavigation(err, isUserNavigating),
+ (err) => suppressAjaxErrorsDuringNavigation(err, isNavigatingAway()),
);
registerCaptchaModalInterceptor(axios);
diff --git a/app/assets/javascripts/lib/utils/color_utils.js b/app/assets/javascripts/lib/utils/color_utils.js
index da2c10076b..66d5205190 100644
--- a/app/assets/javascripts/lib/utils/color_utils.js
+++ b/app/assets/javascripts/lib/utils/color_utils.js
@@ -1,3 +1,22 @@
+const colorValidatorEl = document.createElement('div');
+
+/**
+ * Validates whether the specified color expression
+ * is supported by the browser’s DOM API and has a valid form.
+ *
+ * This utility assigns the color expression to a detached DOM
+ * element’s color property. If the color expression is valid,
+ * the DOM API will accept the value.
+ *
+ * @param {String} color color expression rgba, hex, hsla, etc.
+ */
+export const isValidColorExpression = (colorExpression) => {
+ colorValidatorEl.style.color = '';
+ colorValidatorEl.style.color = colorExpression;
+
+ return colorValidatorEl.style.color.length > 0;
+};
+
/**
* Convert hex color to rgb array
*
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index fd9629499b..813fd3dbb1 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -6,6 +6,7 @@ import { GlBreakpointInstance as breakpointInstance } from '@gitlab/ui/dist/util
import $ from 'jquery';
import Cookies from 'js-cookie';
import { isFunction, defer } from 'lodash';
+import { SCOPED_LABEL_DELIMITER } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import { convertToCamelCase, convertToSnakeCase } from './text_utility';
import { isObject } from './type_utility';
import { getLocationHash } from './url_utility';
@@ -685,7 +686,7 @@ export const searchBy = (query = '', searchSpace = {}) => {
* @param {Object} label
* @returns Boolean
*/
-export const isScopedLabel = ({ title = '' } = {}) => title.indexOf('::') !== -1;
+export const isScopedLabel = ({ title = '' } = {}) => title.includes(SCOPED_LABEL_DELIMITER);
/**
* Returns the base value of the scoped label
@@ -696,7 +697,8 @@ export const isScopedLabel = ({ title = '' } = {}) => title.indexOf('::') !== -1
* @param {Object} label
* @returns String
*/
-export const scopedLabelKey = ({ title = '' }) => isScopedLabel({ title }) && title.split('::')[0];
+export const scopedLabelKey = ({ title = '' }) =>
+ isScopedLabel({ title }) && title.split(SCOPED_LABEL_DELIMITER)[0];
// Methods to set and get Cookie
export const setCookie = (name, value) => Cookies.set(name, value, { expires: 365 });
diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js
index e41de72ded..0e5a23a5cb 100644
--- a/app/assets/javascripts/lib/utils/constants.js
+++ b/app/assets/javascripts/lib/utils/constants.js
@@ -20,3 +20,7 @@ export const BV_DROPDOWN_HIDE = 'bv::dropdown::hide';
export const DEFAULT_TH_CLASSES =
'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1!';
+
+// We set the drawer's z-index to 252 to clear flash messages that might
+// be displayed in the page and that have a z-index of 251.
+export const DRAWER_Z_INDEX = 252;
diff --git a/app/assets/javascripts/lib/utils/datetime/date_format_utility.js b/app/assets/javascripts/lib/utils/datetime/date_format_utility.js
index 0a35efb0ac..3c446c2186 100644
--- a/app/assets/javascripts/lib/utils/datetime/date_format_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime/date_format_utility.js
@@ -1,6 +1,8 @@
import dateFormat from 'dateformat';
-import { isString, mapValues, reduce, isDate } from 'lodash';
-import { s__, n__, __ } from '../../../locale';
+import { isString, mapValues, reduce, isDate, unescape } from 'lodash';
+import { roundToNearestHalf } from '~/lib/utils/common_utils';
+import { sanitize } from '~/lib/dompurify';
+import { s__, n__, __, sprintf } from '../../../locale';
/**
* Returns i18n month names array.
@@ -361,3 +363,26 @@ export const dateToTimeInputValue = (date) => {
hour12: false,
});
};
+
+export const formatTimeAsSummary = ({ seconds, hours, days, minutes, weeks, months }) => {
+ if (months) {
+ return sprintf(s__('ValueStreamAnalytics|%{value}M'), {
+ value: roundToNearestHalf(months),
+ });
+ } else if (weeks) {
+ return sprintf(s__('ValueStreamAnalytics|%{value}w'), {
+ value: roundToNearestHalf(weeks),
+ });
+ } else if (days) {
+ return sprintf(s__('ValueStreamAnalytics|%{value}d'), {
+ value: roundToNearestHalf(days),
+ });
+ } else if (hours) {
+ return sprintf(s__('ValueStreamAnalytics|%{value}h'), { value: hours });
+ } else if (minutes) {
+ return sprintf(s__('ValueStreamAnalytics|%{value}m'), { value: minutes });
+ } else if (seconds) {
+ return unescape(sanitize(s__('ValueStreamAnalytics|<1m'), { ALLOWED_TAGS: [] }));
+ }
+ return '-';
+};
diff --git a/app/assets/javascripts/lib/utils/datetime_range.js b/app/assets/javascripts/lib/utils/datetime_range.js
index a2b161d144..840cc4600f 100644
--- a/app/assets/javascripts/lib/utils/datetime_range.js
+++ b/app/assets/javascripts/lib/utils/datetime_range.js
@@ -26,7 +26,17 @@ const isValidDateString = (dateString) => {
return false;
}
- return !Number.isNaN(Date.parse(dateformat(dateString, 'isoUtcDateTime')));
+ let isoFormatted;
+ try {
+ isoFormatted = dateformat(dateString, 'isoUtcDateTime');
+ } catch (e) {
+ if (e instanceof TypeError) {
+ // not a valid date string
+ return false;
+ }
+ throw e;
+ }
+ return !Number.isNaN(Date.parse(isoFormatted));
};
const handleRangeDirection = ({ direction = DEFAULT_DIRECTION, anchorDate, minDate, maxDate }) => {
diff --git a/app/assets/javascripts/lib/utils/is_navigating_away.js b/app/assets/javascripts/lib/utils/is_navigating_away.js
new file mode 100644
index 0000000000..7df00b4537
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/is_navigating_away.js
@@ -0,0 +1,23 @@
+let navigating = false;
+
+window.addEventListener('beforeunload', () => {
+ navigating = true;
+});
+
+/**
+ * To only be used for testing purposes. Allows the navigating state to be set
+ * to a given value.
+ *
+ * @param {boolean} value The value to set the navigating flag to.
+ */
+export const setNavigatingForTestsOnly = (value) => {
+ navigating = value;
+};
+
+/**
+ * Returns a boolean indicating whether the browser is in the process of
+ * navigating away from the current page.
+ *
+ * @returns {boolean}
+ */
+export const isNavigatingAway = () => navigating;
diff --git a/app/assets/javascripts/lib/utils/regexp.js b/app/assets/javascripts/lib/utils/regexp.js
index 25b60dcd14..f212bf80bd 100644
--- a/app/assets/javascripts/lib/utils/regexp.js
+++ b/app/assets/javascripts/lib/utils/regexp.js
@@ -1,6 +1,5 @@
/**
* Regexp utility for the convenience of working with regular expressions.
- *
*/
// Inspired by https://github.com/mishoo/UglifyJS/blob/2bc1d02363db3798d5df41fb5059a19edca9b7eb/lib/parse-js.js#L203
@@ -8,4 +7,9 @@
const unicodeLetters =
'\\u0041-\\u005A\\u0061-\\u007A\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376\\u0377\\u037A-\\u037D\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u0527\\u0531-\\u0556\\u0559\\u0561-\\u0587\\u05D0-\\u05EA\\u05F0-\\u05F2\\u0620-\\u064A\\u066E\\u066F\\u0671-\\u06D3\\u06D5\\u06E5\\u06E6\\u06EE\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4\\u07F5\\u07FA\\u0800-\\u0815\\u081A\\u0824\\u0828\\u0840-\\u0858\\u08A0\\u08A2-\\u08AC\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971-\\u0977\\u0979-\\u097F\\u0985-\\u098C\\u098F\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC\\u09DD\\u09DF-\\u09E1\\u09F0\\u09F1\\u0A05-\\u0A0A\\u0A0F\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32\\u0A33\\u0A35\\u0A36\\u0A38\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0\\u0AE1\\u0B05-\\u0B0C\\u0B0F\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99\\u0B9A\\u0B9C\\u0B9E\\u0B9F\\u0BA3\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C33\\u0C35-\\u0C39\\u0C3D\\u0C58\\u0C59\\u0C60\\u0C61\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDE\\u0CE0\\u0CE1\\u0CF1\\u0CF2\\u0D05-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D3A\\u0D3D\\u0D4E\\u0D60\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32\\u0E33\\u0E40-\\u0E46\\u0E81\\u0E82\\u0E84\\u0E87\\u0E88\\u0E8A\\u0E8D\\u0E94-\\u0E97\\u0E99-\\u0E9F\\u0EA1-\\u0EA3\\u0EA5\\u0EA7\\u0EAA\\u0EAB\\u0EAD-\\u0EB0\\u0EB2\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC-\\u0EDF\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8C\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA\\u10FC-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F4\\u1401-\\u166C\\u166F-\\u167F\\u1681-\\u169A\\u16A0-\\u16EA\\u16EE-\\u16F0\\u1700-\\u170C\\u170E-\\u1711\\u1720-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1877\\u1880-\\u18A8\\u18AA\\u18B0-\\u18F5\\u1900-\\u191C\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19AB\\u19C1-\\u19C7\\u1A00-\\u1A16\\u1A20-\\u1A54\\u1AA7\\u1B05-\\u1B33\\u1B45-\\u1B4B\\u1B83-\\u1BA0\\u1BAE\\u1BAF\\u1BBA-\\u1BE5\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1CE9-\\u1CEC\\u1CEE-\\u1CF1\\u1CF5\\u1CF6\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u209C\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2160-\\u2188\\u2C00-\\u2C2E\\u2C30-\\u2C5E\\u2C60-\\u2CE4\\u2CEB-\\u2CEE\\u2CF2\\u2CF3\\u2D00-\\u2D25\\u2D27\\u2D2D\\u2D30-\\u2D67\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005-\\u3007\\u3021-\\u3029\\u3031-\\u3035\\u3038-\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312D\\u3131-\\u318E\\u31A0-\\u31BA\\u31F0-\\u31FF\\u3400-\\u4DB5\\u4E00-\\u9FCC\\uA000-\\uA48C\\uA4D0-\\uA4FD\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A\\uA62B\\uA640-\\uA66E\\uA67F-\\uA697\\uA6A0-\\uA6EF\\uA717-\\uA71F\\uA722-\\uA788\\uA78B-\\uA78E\\uA790-\\uA793\\uA7A0-\\uA7AA\\uA7F8-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA8F2-\\uA8F7\\uA8FB\\uA90A-\\uA925\\uA930-\\uA946\\uA960-\\uA97C\\uA984-\\uA9B2\\uA9CF\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAA60-\\uAA76\\uAA7A\\uAA80-\\uAAAF\\uAAB1\\uAAB5\\uAAB6\\uAAB9-\\uAABD\\uAAC0\\uAAC2\\uAADB-\\uAADD\\uAAE0-\\uAAEA\\uAAF2-\\uAAF4\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uABC0-\\uABE2\\uAC00-\\uD7A3\\uD7B0-\\uD7C6\\uD7CB-\\uD7FB\\uF900-\\uFA6D\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40\\uFB41\\uFB43\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC';
-export default { unicodeLetters };
+/**
+ * A regex that matches all single quotes in a string
+ */
+export const allSingleQuotes = /'/g;
+
+export default { unicodeLetters, allSingleQuotes };
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 5ee00464a8..419afa0a0a 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -4,6 +4,7 @@ import {
TRUNCATE_WIDTH_DEFAULT_WIDTH,
TRUNCATE_WIDTH_DEFAULT_FONT_SIZE,
} from '~/lib/utils/constants';
+import { allSingleQuotes } from '~/lib/utils/regexp';
/**
* Adds a , to a string composed by numbers, at every 3 chars.
@@ -479,3 +480,17 @@ export const markdownConfig = {
ALLOWED_ATTR: ['class', 'style', 'href', 'src'],
ALLOW_DATA_ATTR: false,
};
+
+/**
+ * Escapes a string into a shell string, for example
+ * when you want to give a user the command to checkout
+ * a branch.
+ *
+ * It replaces all single-quotes with an escaped "'\''"
+ * that is interpreted by shell as a single-quote. It also
+ * encapsulates the string in single-quotes.
+ *
+ * If the branch is `fix-'bug-behavior'`, that should be
+ * escaped to `'fix-'\''bug-behavior'\'''`.
+ */
+export const escapeShellString = (str) => `'${str.replace(allSingleQuotes, () => "'\\''")}'`;
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index deedc210e5..c70d23d06e 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -1,3 +1,5 @@
+export const DASH_SCOPE = '-';
+
const PATH_SEPARATOR = '/';
const PATH_SEPARATOR_LEADING_REGEX = new RegExp(`^${PATH_SEPARATOR}+`);
const PATH_SEPARATOR_ENDING_REGEX = new RegExp(`${PATH_SEPARATOR}+$`);
@@ -606,3 +608,30 @@ export function isSameOriginUrl(url) {
return false;
}
}
+
+/**
+ * Returns a URL to WebIDE considering the current user's position in
+ * repository's tree. If not MR `iid` has been passed, the URL is fetched
+ * from the global `gl.webIDEPath`.
+ *
+ * @param sourceProjectFullPath Source project's full path. Used in MRs
+ * @param targetProjectFullPath Target project's full path. Used in MRs
+ * @param iid MR iid
+ * @returns {string}
+ */
+
+export function constructWebIDEPath({
+ sourceProjectFullPath,
+ targetProjectFullPath = '',
+ iid,
+} = {}) {
+ if (!iid || !sourceProjectFullPath) {
+ return window.gl?.webIDEPath;
+ }
+ return mergeUrlParams(
+ {
+ target_project: sourceProjectFullPath !== targetProjectFullPath ? targetProjectFullPath : '',
+ },
+ webIDEUrl(`/${sourceProjectFullPath}/merge_requests/${iid}`),
+ );
+}
diff --git a/app/assets/javascripts/logs/components/environment_logs.vue b/app/assets/javascripts/logs/components/environment_logs.vue
index 3db9fa0162..2a60825a42 100644
--- a/app/assets/javascripts/logs/components/environment_logs.vue
+++ b/app/assets/javascripts/logs/components/environment_logs.vue
@@ -214,7 +214,7 @@ export default {
diff --git a/app/assets/javascripts/logs/stores/state.js b/app/assets/javascripts/logs/stores/state.js
index 8308058936..ee17e8ecef 100644
--- a/app/assets/javascripts/logs/stores/state.js
+++ b/app/assets/javascripts/logs/stores/state.js
@@ -31,7 +31,7 @@ export default () => ({
},
/**
- * Logs including trace
+ * Jobs with logs
*/
logs: {
lines: [],
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index b96a260755..e422d9b1a3 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -1,5 +1,4 @@
/* global $ */
-/* eslint-disable import/order */
import jQuery from 'jquery';
import Cookies from 'js-cookie';
@@ -15,6 +14,7 @@ import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { initRails } from '~/lib/utils/rails_ujs';
import * as popovers from '~/popovers';
import * as tooltips from '~/tooltips';
+import { initHeaderSearchApp } from '~/header_search';
import initAlertHandler from './alert_handler';
import { removeFlashClickListener } from './flash';
import initTodoToggle from './header';
@@ -36,7 +36,6 @@ import GlFieldErrors from './gl_field_errors';
import initUserPopovers from './user_popovers';
import initBroadcastNotifications from './broadcast_notification';
import { initTopNav } from './nav';
-import { initHeaderSearchApp } from '~/header_search';
import 'ee_else_ce/main_ee';
import 'jh_else_ce/main_jh';
diff --git a/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue b/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
index 665e8ee69f..69137ce615 100644
--- a/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
+++ b/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
@@ -42,7 +42,7 @@ export default {
required: false,
default: false,
},
- oncallSchedules: {
+ userDeletionObstacles: {
type: Object,
required: false,
default: () => ({}),
@@ -61,7 +61,7 @@ export default {
memberPath: this.memberPath.replace(':id', this.memberId),
memberType: this.memberType,
message: this.message,
- oncallSchedules: this.oncallSchedules,
+ userDeletionObstacles: this.userDeletionObstacles,
};
},
},
diff --git a/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue b/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue
index 0c20f935d5..44d658c90a 100644
--- a/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue
+++ b/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue
@@ -1,5 +1,6 @@
-
-
- {{ s__('Members|No expiration set') }}
-
- {{ s__('Members|Expired') }}
-
-
- {{ inWords }}
-
-
-
-
diff --git a/app/assets/javascripts/members/components/table/members_table.vue b/app/assets/javascripts/members/components/table/members_table.vue
index debc3fc31f..202f3aa89e 100644
--- a/app/assets/javascripts/members/components/table/members_table.vue
+++ b/app/assets/javascripts/members/components/table/members_table.vue
@@ -5,12 +5,17 @@ import MembersTableCell from 'ee_else_ce/members/components/table/members_table_
import { canOverride, canRemove, canResend, canUpdate } from 'ee_else_ce/members/utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import initUserPopovers from '~/user_popovers';
-import { FIELDS, ACTIVE_TAB_QUERY_PARAM_NAME } from '../../constants';
+import {
+ FIELDS,
+ ACTIVE_TAB_QUERY_PARAM_NAME,
+ MEMBER_STATE_AWAITING,
+ USER_STATE_BLOCKED_PENDING_APPROVAL,
+ BADGE_LABELS_PENDING_OWNER_APPROVAL,
+} from '../../constants';
import RemoveGroupLinkModal from '../modals/remove_group_link_modal.vue';
import RemoveMemberModal from '../modals/remove_member_modal.vue';
import CreatedAt from './created_at.vue';
import ExpirationDatepicker from './expiration_datepicker.vue';
-import ExpiresAt from './expires_at.vue';
import MemberActionButtons from './member_action_buttons.vue';
import MemberAvatar from './member_avatar.vue';
import MemberSource from './member_source.vue';
@@ -24,7 +29,6 @@ export default {
GlPagination,
MemberAvatar,
CreatedAt,
- ExpiresAt,
MembersTableCell,
MemberSource,
MemberActionButtons,
@@ -131,6 +135,74 @@ export default {
window.location.href,
);
},
+ /**
+ * Returns whether it's a new or existing user
+ *
+ * If memberInviteMetadata doesn't exist, it means we're adding an existing user
+ * to the Group/Project, so `isNewUser` should be false.
+ * If memberInviteMetadata exists but `userState` has content,
+ * the user has registered but is awaiting root approval
+ *
+ * @param {object} memberInviteMetadata - MemberEntity.invite
+ * @see {@link ~/app/serializers/member_entity.rb}
+ * @returns {boolean}
+ */
+ isNewUser(memberInviteMetadata) {
+ return memberInviteMetadata && !memberInviteMetadata.userState;
+ },
+ /**
+ * Returns whether the user is awaiting root approval
+ *
+ * This checks User.state exposed via MemberEntity
+ *
+ * @param {object} memberInviteMetadata - MemberEntity.invite
+ * @see {@link ~/app/serializers/member_entity.rb}
+ * @returns {boolean}
+ */
+ isUserPendingRootApproval(memberInviteMetadata) {
+ return memberInviteMetadata?.userState === USER_STATE_BLOCKED_PENDING_APPROVAL;
+ },
+ /**
+ * Returns whether the member is awaiting owner approval
+ *
+ * This checks Member.state exposed via MemberEntity
+ *
+ * @param {Number} memberState - Member.state exposed via MemberEntity.state
+ * @see {@link ~/ee/app/models/ee/member.rb}
+ * @see {@link ~/app/serializers/member_entity.rb}
+ * @returns {boolean}
+ */
+ isMemberPendingOwnerApproval(memberState) {
+ return memberState === MEMBER_STATE_AWAITING;
+ },
+ isUserAwaiting(memberInviteMetadata, memberState) {
+ return (
+ this.isUserPendingRootApproval(memberInviteMetadata) ||
+ this.isMemberPendingOwnerApproval(memberState)
+ );
+ },
+ shouldAddPendingOwnerApprovalBadge(memberInviteMetadata, memberState) {
+ return (
+ this.isUserAwaiting(memberInviteMetadata, memberState) &&
+ !this.isNewUser(memberInviteMetadata)
+ );
+ },
+ /**
+ * Returns the string to be used in the invite badge
+ *
+ * @param {object} memberInviteMetadata - MemberEntity.invite
+ * @see {@link ~/app/serializers/member_entity.rb}
+ * @param {Number} memberState - Member.state exposed via MemberEntity.state
+ * @see {@link ~/ee/app/models/ee/member.rb}
+ * @returns {string}
+ */
+ inviteBadge(memberInviteMetadata, memberState) {
+ if (this.shouldAddPendingOwnerApprovalBadge(memberInviteMetadata, memberState)) {
+ return BADGE_LABELS_PENDING_OWNER_APPROVAL;
+ }
+
+ return '';
+ },
},
};
@@ -174,18 +246,17 @@ export default {
-
+
+ {{
+ inviteBadge(invite, state)
+ }}
-
-
-
-
diff --git a/app/assets/javascripts/members/constants.js b/app/assets/javascripts/members/constants.js
index 6f465245d2..f5ca881ab0 100644
--- a/app/assets/javascripts/members/constants.js
+++ b/app/assets/javascripts/members/constants.js
@@ -37,12 +37,6 @@ export const FIELDS = [
thClass: 'col-meta',
tdClass: 'col-meta',
},
- {
- key: 'expires',
- label: __('Access expires'),
- thClass: 'col-meta',
- tdClass: 'col-meta',
- },
{
key: 'maxRole',
label: __('Max role'),
@@ -95,6 +89,22 @@ export const TAB_QUERY_PARAM_VALUES = {
accessRequest: 'access_requests',
};
+/**
+ * This user state value comes from the User model
+ * see the state machine in app/models/user.rb
+ */
+export const USER_STATE_BLOCKED_PENDING_APPROVAL = 'blocked_pending_approval';
+
+/**
+ * This and following member state constants' values
+ * come from ee/app/models/ee/member.rb
+ */
+export const MEMBER_STATE_CREATED = 0;
+export const MEMBER_STATE_AWAITING = 1;
+export const MEMBER_STATE_ACTIVE = 2;
+
+export const BADGE_LABELS_PENDING_OWNER_APPROVAL = __('Pending owner approval');
+
export const DAYS_TO_EXPIRE_SOON = 7;
export const LEAVE_MODAL_ID = 'member-leave-modal';
diff --git a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.vue b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.vue
index a856d38c08..87eeb27265 100644
--- a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.vue
+++ b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.vue
@@ -35,7 +35,11 @@ export default {
{{ line.richText }}
-
diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.vue b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.vue
index 2c89b614c8..2c59e7bfa2 100644
--- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.vue
+++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.vue
@@ -35,7 +35,11 @@ export default {
{{ line.richText }}
-
+
{{ line.buttonTitle }}
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index ed32f26583..244cf1e150 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -2,6 +2,7 @@
import $ from 'jquery';
import createFlash from '~/flash';
+import toast from '~/vue_shared/plugins/global_toast';
import { __ } from '~/locale';
import eventHub from '~/vue_merge_request_widget/event_hub';
import axios from './lib/utils/axios_utils';
@@ -136,10 +137,9 @@ MergeRequest.hideCloseButton = function () {
MergeRequest.toggleDraftStatus = function (title, isReady) {
if (isReady) {
- createFlash({
- message: __('Marked as ready. Merging is now allowed.'),
- type: 'notice',
- });
+ toast(__('Marked as ready. Merging is now allowed.'));
+ } else {
+ toast(__('Marked as draft. Can only be merged when marked as ready.'));
}
const titleEl = document.querySelector('.merge-request .detail-page-description .title');
diff --git a/app/assets/javascripts/mr_popover/index.js b/app/assets/javascripts/mr_popover/index.js
index 714cf67e0b..6e46c5d3c1 100644
--- a/app/assets/javascripts/mr_popover/index.js
+++ b/app/assets/javascripts/mr_popover/index.js
@@ -48,7 +48,12 @@ export default (elements) => {
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(),
+ defaultClient: createDefaultClient(
+ {},
+ {
+ assumeImmutableResults: true,
+ },
+ ),
});
const listenerAddedAttr = 'data-mr-listener-added';
diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js
deleted file mode 100644
index af7a600d1a..0000000000
--- a/app/assets/javascripts/namespace_select.js
+++ /dev/null
@@ -1,58 +0,0 @@
-import $ from 'jquery';
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import { parseBoolean } from '~/lib/utils/common_utils';
-import Api from './api';
-import { mergeUrlParams } from './lib/utils/url_utility';
-import { __ } from './locale';
-
-export default class NamespaceSelect {
- constructor(opts) {
- const isFilter = parseBoolean(opts.dropdown.dataset.isFilter);
- const fieldName = opts.dropdown.dataset.fieldName || 'namespace_id';
-
- initDeprecatedJQueryDropdown($(opts.dropdown), {
- filterable: true,
- selectable: true,
- filterRemote: true,
- search: {
- fields: ['path'],
- },
- fieldName,
- toggleLabel(selected) {
- if (selected.id == null) {
- return selected.text;
- }
- return `${selected.kind}: ${selected.full_path}`;
- },
- data(term, dataCallback) {
- return Api.namespaces(term, (namespaces) => {
- if (isFilter) {
- const anyNamespace = {
- text: __('Any namespace'),
- id: null,
- };
- namespaces.unshift(anyNamespace);
- namespaces.splice(1, 0, { type: 'divider' });
- }
- return dataCallback(namespaces);
- });
- },
- text(namespace) {
- if (namespace.id == null) {
- return namespace.text;
- }
- return `${namespace.kind}: ${namespace.full_path}`;
- },
- renderRow: this.renderRow,
- clicked(options) {
- if (!isFilter) {
- const { e } = options;
- e.preventDefault();
- }
- },
- url(namespace) {
- return mergeUrlParams({ [fieldName]: namespace.id }, window.location.href);
- },
- });
- }
-}
diff --git a/app/assets/javascripts/notebook/cells/markdown.vue b/app/assets/javascripts/notebook/cells/markdown.vue
index 1384c9c40b..073b27605b 100644
--- a/app/assets/javascripts/notebook/cells/markdown.vue
+++ b/app/assets/javascripts/notebook/cells/markdown.vue
@@ -1,6 +1,7 @@
-
+
diff --git a/app/assets/javascripts/notes/components/comment_type_dropdown.vue b/app/assets/javascripts/notes/components/comment_type_dropdown.vue
index 663a912999..30ea5d3532 100644
--- a/app/assets/javascripts/notes/components/comment_type_dropdown.vue
+++ b/app/assets/javascripts/notes/components/comment_type_dropdown.vue
@@ -96,7 +96,11 @@ export default {
data-track-action="click_button"
@click="$emit('click')"
>
-
+
{{ $options.i18n.submitButton.comment }}
{{ commentDescription }}
@@ -105,7 +109,7 @@ export default {
is-check-item
:is-checked="isNoteTypeDiscussion"
data-qa-selector="discussion_menu_item"
- @click="setNoteTypeToDiscussion"
+ @click.stop.prevent="setNoteTypeToDiscussion"
>
{{ $options.i18n.submitButton.startThread }}
{{ startDiscussionDescription }}
diff --git a/app/assets/javascripts/notes/components/discussion_notes.vue b/app/assets/javascripts/notes/components/discussion_notes.vue
index 0892276ff3..6fcfa66ea4 100644
--- a/app/assets/javascripts/notes/components/discussion_notes.vue
+++ b/app/assets/javascripts/notes/components/discussion_notes.vue
@@ -47,6 +47,11 @@ export default {
required: false,
default: '',
},
+ isOverviewTab: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
...mapGetters(['userCanReply']),
@@ -127,6 +132,7 @@ export default {
:show-reply-button="userCanReply"
:discussion-root="true"
:discussion-resolve-path="discussion.resolve_path"
+ :is-overview-tab="isOverviewTab"
@handleDeleteNote="$emit('deleteNote')"
@startReplying="$emit('startReplying')"
>
@@ -176,6 +182,7 @@ export default {
:line="diffLine"
:discussion-root="index === 0"
:discussion-resolve-path="discussion.resolve_path"
+ :is-overview-tab="isOverviewTab"
@handleDeleteNote="$emit('deleteNote')"
>
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index 44d0c741d5..e2a2edd734 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -257,7 +257,7 @@ export default {
{{ __('Author') }}
@@ -265,7 +265,7 @@ export default {
{{ accessLevel }}
@@ -273,7 +273,7 @@ export default {
{{ __('Contributor') }}
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index 93f7127612..1ce1696e33 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -51,7 +51,7 @@ export default {
},
},
computed: {
- ...mapGetters(['getDiscussion', 'suggestionsCount']),
+ ...mapGetters(['getDiscussion', 'suggestionsCount', 'getSuggestionsFilePaths']),
...mapGetters('diffs', ['suggestionCommitMessage']),
discussion() {
if (!this.note.isDraft) return {};
@@ -74,9 +74,10 @@ export default {
// Please see this issue comment for why these
// are hard-coded to 1:
// https://gitlab.com/gitlab-org/gitlab/-/issues/291027#note_468308022
- const suggestionsCount = 1;
- const filesCount = 1;
- const filePaths = this.file ? [this.file.file_path] : [];
+ const suggestionsCount = this.batchSuggestionsInfo.length || 1;
+ const batchFilePaths = this.getSuggestionsFilePaths();
+ const filePaths = batchFilePaths.length ? batchFilePaths : [this.file.file_path];
+ const filesCount = filePaths.length;
const suggestion = this.suggestionCommitMessage({
file_paths: filePaths.join(', '),
suggestions_count: suggestionsCount,
@@ -131,8 +132,8 @@ export default {
message,
}).then(callback);
},
- applySuggestionBatch({ flashContainer }) {
- return this.submitSuggestionBatch({ flashContainer });
+ applySuggestionBatch({ message, flashContainer }) {
+ return this.submitSuggestionBatch({ message, flashContainer });
},
addSuggestionToBatch(suggestionId) {
const { discussion_id: discussionId, id: noteId } = this.note;
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index a4f06a8d9f..b05643e5e1 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -348,6 +348,7 @@ export default {
id="note_note"
ref="textarea"
v-model="updatedNoteBody"
+ :disabled="isSubmitting"
:data-supports-quick-actions="!isEditing && !glFeatures.tributeAutocomplete"
name="note[note]"
class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form"
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index 4e686ce871..0925195d4b 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -1,10 +1,16 @@
-
-
-
- {{ __('Additional Metadata') }}
-
-
-
-
-
-
- {{
- packageEntity.nuget_metadatum.project_url
- }}
-
-
-
-
-
-
- {{
- packageEntity.nuget_metadatum.license_url
- }}
-
-
-
-
-
-
-
- {{ packageEntity.name }}
-
-
-
-
-
-
-
- {{ packageEntity.maven_metadatum.app_name }}
-
-
-
-
-
-
- {{ packageEntity.maven_metadatum.app_group }}
-
-
-
-
-
-
-
diff --git a/app/assets/javascripts/packages/details/components/app.vue b/app/assets/javascripts/packages/details/components/app.vue
deleted file mode 100644
index 59da32e666..0000000000
--- a/app/assets/javascripts/packages/details/components/app.vue
+++ /dev/null
@@ -1,292 +0,0 @@
-
-
-
-
-
-
-
-
-
- {{ __('Delete') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ __('Dependencies') }}
- {{
- packageDependencies.length
- }}
-
-
-
-
-
-
-
- {{ s__('PackageRegistry|This NuGet package has no dependencies.') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ s__('PackageRegistry|There are no other versions of this package.') }}
-
-
-
-
-
- {{ $options.i18n.deleteModalTitle }}
-
-
- {{ packageEntity.version }}
-
-
-
- {{ packageEntity.name }}
-
-
-
-
-
- {{ $options.i18n.deleteFileModalTitle }}
-
-
- {{ fileToDelete.file_name }}
-
-
-
-
-
diff --git a/app/assets/javascripts/packages/details/components/composer_installation.vue b/app/assets/javascripts/packages/details/components/composer_installation.vue
deleted file mode 100644
index bf1e5083e1..0000000000
--- a/app/assets/javascripts/packages/details/components/composer_installation.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
- {{ content }}
-
-
-
-
-
diff --git a/app/assets/javascripts/packages/details/components/conan_installation.vue b/app/assets/javascripts/packages/details/components/conan_installation.vue
deleted file mode 100644
index 1d855f6cf3..0000000000
--- a/app/assets/javascripts/packages/details/components/conan_installation.vue
+++ /dev/null
@@ -1,59 +0,0 @@
-
-
-
-
-
-
-
-
- {{ __('Registry setup') }}
-
-
-
-
- {{ content }}
-
-
-
-
diff --git a/app/assets/javascripts/packages/details/components/dependency_row.vue b/app/assets/javascripts/packages/details/components/dependency_row.vue
deleted file mode 100644
index 1a2202b23c..0000000000
--- a/app/assets/javascripts/packages/details/components/dependency_row.vue
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
-
-
- {{ dependency.name }}
- ({{ dependency.target_framework }})
-
-
-
- {{ dependency.version_pattern }}
-
-
-
diff --git a/app/assets/javascripts/packages/details/components/installation_commands.vue b/app/assets/javascripts/packages/details/components/installation_commands.vue
deleted file mode 100644
index ed55d7fe78..0000000000
--- a/app/assets/javascripts/packages/details/components/installation_commands.vue
+++ /dev/null
@@ -1,55 +0,0 @@
-
-
-
-
-
-
-
diff --git a/app/assets/javascripts/packages/details/components/installation_title.vue b/app/assets/javascripts/packages/details/components/installation_title.vue
deleted file mode 100644
index 43133bf782..0000000000
--- a/app/assets/javascripts/packages/details/components/installation_title.vue
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
-
-
- {{ __('Installation') }}
-
-
-
-
-
diff --git a/app/assets/javascripts/packages/details/components/maven_installation.vue b/app/assets/javascripts/packages/details/components/maven_installation.vue
deleted file mode 100644
index 6974de9934..0000000000
--- a/app/assets/javascripts/packages/details/components/maven_installation.vue
+++ /dev/null
@@ -1,153 +0,0 @@
-
-
-
-
-
-
-
-
-
-
- {{ content }}
-
-
-
-
-
-
-
-
- {{ s__('PackageRegistry|Registry setup') }}
-
-
-
- {{ content }}
-
-
-
-
-
-
- {{ content }}
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/assets/javascripts/packages/details/components/npm_installation.vue b/app/assets/javascripts/packages/details/components/npm_installation.vue
deleted file mode 100644
index 6b0fcf5e4f..0000000000
--- a/app/assets/javascripts/packages/details/components/npm_installation.vue
+++ /dev/null
@@ -1,103 +0,0 @@
-
-
-
-
-
-
-
-
-
-
- {{ __('Registry setup') }}
-
-
-
-
-
-
-
- {{ content }}
-
-
-
-
diff --git a/app/assets/javascripts/packages/details/components/nuget_installation.vue b/app/assets/javascripts/packages/details/components/nuget_installation.vue
deleted file mode 100644
index d5e64722f2..0000000000
--- a/app/assets/javascripts/packages/details/components/nuget_installation.vue
+++ /dev/null
@@ -1,58 +0,0 @@
-
-
-
-
-
-
-
- {{ __('Registry setup') }}
-
-
-
-
- {{ content }}
-
-
-
-
diff --git a/app/assets/javascripts/packages/details/components/package_title.vue b/app/assets/javascripts/packages/details/components/package_title.vue
deleted file mode 100644
index d02a7b3ec2..0000000000
--- a/app/assets/javascripts/packages/details/components/package_title.vue
+++ /dev/null
@@ -1,113 +0,0 @@
-
-
-
-
-
-
-
-
- {{ packageEntity.version }}
-
-
-
-
- {{ timeFormatted(packageEntity.created_at) }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ tag.name }}
-
-
-
-
-
-
-
-
diff --git a/app/assets/javascripts/packages/details/components/pypi_installation.vue b/app/assets/javascripts/packages/details/components/pypi_installation.vue
deleted file mode 100644
index fe4709d5fe..0000000000
--- a/app/assets/javascripts/packages/details/components/pypi_installation.vue
+++ /dev/null
@@ -1,71 +0,0 @@
-
-
-
-
-
-
-
-
- {{ __('Registry setup') }}
-
-
-
- {{ content }}
-
-
-
-
-
-
-
- {{ content }}
-
-
-
-
diff --git a/app/assets/javascripts/packages/details/constants.js b/app/assets/javascripts/packages/details/constants.js
deleted file mode 100644
index cd34b1ad45..0000000000
--- a/app/assets/javascripts/packages/details/constants.js
+++ /dev/null
@@ -1,55 +0,0 @@
-import { s__ } from '~/locale';
-
-export const TrackingLabels = {
- CODE_INSTRUCTION: 'code_instruction',
- CONAN_INSTALLATION: 'conan_installation',
- MAVEN_INSTALLATION: 'maven_installation',
- NPM_INSTALLATION: 'npm_installation',
- NUGET_INSTALLATION: 'nuget_installation',
- PYPI_INSTALLATION: 'pypi_installation',
- COMPOSER_INSTALLATION: 'composer_installation',
-};
-
-export const TrackingActions = {
- INSTALLATION: 'installation',
- REGISTRY_SETUP: 'registry_setup',
-
- COPY_CONAN_COMMAND: 'copy_conan_command',
- COPY_CONAN_SETUP_COMMAND: 'copy_conan_setup_command',
-
- COPY_MAVEN_XML: 'copy_maven_xml',
- COPY_MAVEN_COMMAND: 'copy_maven_command',
- COPY_MAVEN_SETUP: 'copy_maven_setup_xml',
-
- COPY_NPM_INSTALL_COMMAND: 'copy_npm_install_command',
- COPY_NPM_SETUP_COMMAND: 'copy_npm_setup_command',
-
- COPY_YARN_INSTALL_COMMAND: 'copy_yarn_install_command',
- COPY_YARN_SETUP_COMMAND: 'copy_yarn_setup_command',
-
- COPY_NUGET_INSTALL_COMMAND: 'copy_nuget_install_command',
- COPY_NUGET_SETUP_COMMAND: 'copy_nuget_setup_command',
-
- 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',
-
- COPY_GRADLE_INSTALL_COMMAND: 'copy_gradle_install_command',
- COPY_GRADLE_ADD_TO_SOURCE_COMMAND: 'copy_gradle_add_to_source_command',
-
- COPY_KOTLIN_INSTALL_COMMAND: 'copy_kotlin_install_command',
- COPY_KOTLIN_ADD_TO_SOURCE_COMMAND: 'copy_kotlin_add_to_source_command',
-};
-
-export const NpmManager = {
- NPM: 'npm',
- YARN: 'yarn',
-};
-
-export const FETCH_PACKAGE_VERSIONS_ERROR = s__(
- 'PackageRegistry|Unable to fetch package version information.',
-);
-
-export const HISTORY_PIPELINES_LIMIT = 5;
diff --git a/app/assets/javascripts/packages/details/index.js b/app/assets/javascripts/packages/details/index.js
deleted file mode 100644
index 5b9d58a386..0000000000
--- a/app/assets/javascripts/packages/details/index.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import Vue from 'vue';
-import Translate from '~/vue_shared/translate';
-import PackagesApp from './components/app.vue';
-import createStore from './store';
-
-Vue.use(Translate);
-
-export default () => {
- const el = document.querySelector('#js-vue-packages-detail');
- const { package: packageJson, canDelete: canDeleteStr, ...rest } = el.dataset;
- const packageEntity = JSON.parse(packageJson);
- const canDelete = canDeleteStr === 'true';
-
- const store = createStore({
- packageEntity,
- packageFiles: packageEntity.package_files,
- canDelete,
- ...rest,
- });
-
- // eslint-disable-next-line no-new
- new Vue({
- el,
- components: {
- PackagesApp,
- },
- store,
- render(createElement) {
- return createElement('packages-app');
- },
- });
-};
diff --git a/app/assets/javascripts/packages/details/store/getters.js b/app/assets/javascripts/packages/details/store/getters.js
deleted file mode 100644
index ae273e26d6..0000000000
--- a/app/assets/javascripts/packages/details/store/getters.js
+++ /dev/null
@@ -1,140 +0,0 @@
-import { PackageType } from '../../shared/constants';
-import { getPackageTypeLabel } from '../../shared/utils';
-import { NpmManager } from '../constants';
-
-export const packagePipeline = ({ packageEntity }) => {
- return packageEntity?.pipeline || null;
-};
-
-export const packageTypeDisplay = ({ packageEntity }) => {
- return getPackageTypeLabel(packageEntity.package_type);
-};
-
-export const packageIcon = ({ packageEntity }) => {
- if (packageEntity.package_type === PackageType.NUGET) {
- return packageEntity.nuget_metadatum?.icon_url || null;
- }
-
- return null;
-};
-
-export const conanInstallationCommand = ({ packageEntity }) => {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- return `conan install ${packageEntity.name} --remote=gitlab`;
-};
-
-export const conanSetupCommand = ({ conanPath }) =>
- // eslint-disable-next-line @gitlab/require-i18n-strings
- `conan remote add gitlab ${conanPath}`;
-
-export const mavenInstallationXml = ({ packageEntity = {} }) => {
- const {
- app_group: appGroup = '',
- app_name: appName = '',
- app_version: appVersion = '',
- } = packageEntity.maven_metadatum;
-
- return `
- ${appGroup}
- ${appName}
- ${appVersion}
- `;
-};
-
-export const mavenInstallationCommand = ({ packageEntity = {} }) => {
- const {
- app_group: group = '',
- app_name: name = '',
- app_version: version = '',
- } = packageEntity.maven_metadatum;
-
- return `mvn dependency:get -Dartifact=${group}:${name}:${version}`;
-};
-
-export const mavenSetupXml = ({ mavenPath }) => `
-
- gitlab-maven
- ${mavenPath}
-
-
-
-
-
- gitlab-maven
- ${mavenPath}
-
-
-
- gitlab-maven
- ${mavenPath}
-
- `;
-
-export const npmInstallationCommand = ({ packageEntity }) => (type = NpmManager.NPM) => {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- const instruction = type === NpmManager.NPM ? 'npm i' : 'yarn add';
-
- return `${instruction} ${packageEntity.name}`;
-};
-
-export const npmSetupCommand = ({ packageEntity, npmPath }) => (type = NpmManager.NPM) => {
- const scope = packageEntity.name.substring(0, packageEntity.name.indexOf('/'));
-
- if (type === NpmManager.NPM) {
- return `echo ${scope}:registry=${npmPath}/ >> .npmrc`;
- }
-
- return `echo \\"${scope}:registry\\" \\"${npmPath}/\\" >> .yarnrc`;
-};
-
-export const nugetInstallationCommand = ({ packageEntity }) =>
- `nuget install ${packageEntity.name} -Source "GitLab"`;
-
-export const nugetSetupCommand = ({ nugetPath }) =>
- `nuget source Add -Name "GitLab" -Source "${nugetPath}" -UserName -Password `;
-
-export const pypiPipCommand = ({ pypiPath, packageEntity }) =>
- // eslint-disable-next-line @gitlab/require-i18n-strings
- `pip install ${packageEntity.name} --extra-index-url ${pypiPath}`;
-
-export const pypiSetupCommand = ({ pypiSetupPath }) => `[gitlab]
-repository = ${pypiSetupPath}
-username = __token__
-password = `;
-
-export const composerRegistryInclude = ({ composerPath, composerConfigRepositoryName }) =>
- // eslint-disable-next-line @gitlab/require-i18n-strings
- `composer config repositories.${composerConfigRepositoryName} '{"type": "composer", "url": "${composerPath}"}'`;
-
-export const composerPackageInclude = ({ packageEntity }) =>
- // eslint-disable-next-line @gitlab/require-i18n-strings
- `composer req ${[packageEntity.name]}:${packageEntity.version}`;
-
-export const gradleGroovyInstalCommand = ({ packageEntity }) => {
- const {
- app_group: group = '',
- app_name: name = '',
- app_version: version = '',
- } = packageEntity.maven_metadatum;
- // eslint-disable-next-line @gitlab/require-i18n-strings
- return `implementation '${group}:${name}:${version}'`;
-};
-
-export const gradleGroovyAddSourceCommand = ({ mavenPath }) =>
- // eslint-disable-next-line @gitlab/require-i18n-strings
- `maven {
- url '${mavenPath}'
-}`;
-
-export const gradleKotlinInstalCommand = ({ packageEntity }) => {
- const {
- app_group: group = '',
- app_name: name = '',
- app_version: version = '',
- } = packageEntity.maven_metadatum;
- return `implementation("${group}:${name}:${version}")`;
-};
-
-export const gradleKotlinAddSourceCommand = ({ mavenPath }) => `maven("${mavenPath}")`;
-
-export const groupExists = ({ groupListUrl }) => groupListUrl.length > 0;
diff --git a/app/assets/javascripts/packages/details/utils.js b/app/assets/javascripts/packages/details/utils.js
deleted file mode 100644
index 27cc95566d..0000000000
--- a/app/assets/javascripts/packages/details/utils.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import { TrackingActions } from './constants';
-
-export const trackInstallationTabChange = {
- methods: {
- trackInstallationTabChange(tabIndex) {
- const action = tabIndex === 0 ? TrackingActions.INSTALLATION : TrackingActions.REGISTRY_SETUP;
- this.track(action, { label: this.trackingLabel });
- },
- },
-};
diff --git a/app/assets/javascripts/packages/shared/constants.js b/app/assets/javascripts/packages/shared/constants.js
index f15c31b85c..c284b8358b 100644
--- a/app/assets/javascripts/packages/shared/constants.js
+++ b/app/assets/javascripts/packages/shared/constants.js
@@ -1,5 +1,4 @@
-/* eslint-disable @gitlab/require-string-literal-i18n-helpers */
-import { __, s__ } from '~/locale';
+import { s__ } from '~/locale';
export const PackageType = {
CONAN: 'conan',
@@ -38,7 +37,7 @@ export const DELETE_PACKAGE_ERROR_MESSAGE = s__(
'PackageRegistry|Something went wrong while deleting the package.',
);
export const DELETE_PACKAGE_FILE_ERROR_MESSAGE = s__(
- __('PackageRegistry|Something went wrong while deleting the package file.'),
+ 'PackageRegistry|Something went wrong while deleting the package file.',
);
export const DELETE_PACKAGE_FILE_SUCCESS_MESSAGE = s__(
'PackageRegistry|Package file deleted successfully',
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue b/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue
new file mode 100644
index 0000000000..73fb3656af
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+ {{ $options.i18n.proxyNotAvailableText }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ group.dependencyProxyBlobCount }}
+ {{ group.dependencyProxyTotalSize }}
+
+
+
+
+
+
+ {{ $options.i18n.proxyDisabledText }}
+
+
+
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/index.js b/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/index.js
new file mode 100644
index 0000000000..16152eb81f
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/index.js
@@ -0,0 +1,14 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+
+Vue.use(VueApollo);
+
+export const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(
+ {},
+ {
+ assumeImmutableResults: true,
+ },
+ ),
+});
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql b/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql
new file mode 100644
index 0000000000..9058d349bf
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql
@@ -0,0 +1,10 @@
+query getDependencyProxyDetails($fullPath: ID!) {
+ group(fullPath: $fullPath) {
+ dependencyProxyBlobCount
+ dependencyProxyTotalSize
+ dependencyProxyImagePrefix
+ dependencyProxySetting {
+ enabled
+ }
+ }
+}
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/index.js b/app/assets/javascripts/packages_and_registries/dependency_proxy/index.js
new file mode 100644
index 0000000000..dc73470e07
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/index.js
@@ -0,0 +1,26 @@
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import app from '~/packages_and_registries/dependency_proxy/app.vue';
+import { apolloProvider } from '~/packages_and_registries/dependency_proxy/graphql';
+import Translate from '~/vue_shared/translate';
+
+Vue.use(Translate);
+
+export const initDependencyProxyApp = () => {
+ const el = document.getElementById('js-dependency-proxy');
+ if (!el) {
+ return null;
+ }
+ const { dependencyProxyAvailable, ...dataset } = el.dataset;
+ return new Vue({
+ el,
+ apolloProvider,
+ provide: {
+ dependencyProxyAvailable: parseBoolean(dependencyProxyAvailable),
+ ...dataset,
+ },
+ render(createElement) {
+ return createElement(app);
+ },
+ });
+};
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue
new file mode 100644
index 0000000000..6016757c1b
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue
@@ -0,0 +1,240 @@
+
+
+
+
+
+
+
+
+
+ {{ __('Delete') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ s__('PackageRegistry|There are no other versions of this package.') }}
+
+
+
+
+
+ {{ $options.i18n.deleteModalTitle }}
+
+
+ {{ packageEntity.version }}
+
+
+
+ {{ packageEntity.name }}
+
+
+
+
+
+ {{ $options.i18n.deleteFileModalTitle }}
+
+
+ {{ fileToDelete.file_name }}
+
+
+
+
+
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/components/details_title.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/details_title.vue
similarity index 100%
rename from app/assets/javascripts/packages_and_registries/infrastructure_registry/components/details_title.vue
rename to app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/details_title.vue
diff --git a/app/assets/javascripts/packages/details/components/file_sha.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/file_sha.vue
similarity index 100%
rename from app/assets/javascripts/packages/details/components/file_sha.vue
rename to app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/file_sha.vue
diff --git a/app/assets/javascripts/packages/details/components/package_files.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/package_files.vue
similarity index 98%
rename from app/assets/javascripts/packages/details/components/package_files.vue
rename to app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/package_files.vue
index 0563b612d0..ab4cfccd02 100644
--- a/app/assets/javascripts/packages/details/components/package_files.vue
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/package_files.vue
@@ -3,10 +3,10 @@ import { GlLink, GlTable, GlDropdownItem, GlDropdown, GlIcon, GlButton } from '@
import { last } from 'lodash';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { __ } from '~/locale';
-import FileSha from '~/packages/details/components/file_sha.vue';
import Tracking from '~/tracking';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import FileSha from './file_sha.vue';
export default {
name: 'PackageFiles',
diff --git a/app/assets/javascripts/packages/details/components/package_history.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/package_history.vue
similarity index 88%
rename from app/assets/javascripts/packages/details/components/package_history.vue
rename to app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/package_history.vue
index 27d2f208a4..e5be98b87f 100644
--- a/app/assets/javascripts/packages/details/components/package_history.vue
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/package_history.vue
@@ -1,10 +1,9 @@
@@ -116,6 +126,12 @@ export default {
{{ __('Registry setup') }}
+
+
-/* eslint-disable @gitlab/require-string-literal-i18n-helpers */
import { GlLink, GlSprintf } from '@gitlab/ui';
import { first } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { truncateSha } from '~/lib/utils/text_utility';
import { s__, n__ } from '~/locale';
-import { HISTORY_PIPELINES_LIMIT } from '~/packages/details/constants';
+import { HISTORY_PIPELINES_LIMIT } from '~/packages_and_registries/shared/constants';
import HistoryItem from '~/vue_shared/components/registry/history_item.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -21,8 +20,6 @@ export default {
combinedUpdateText: s__(
'PackageRegistry|Package updated by commit %{link} on branch %{branch}, built by pipeline %{pipeline}, and published to the registry %{datetime}',
),
- archivedPipelineMessageSingular: s__('PackageRegistry|Package has %{number} archived update'),
- archivedPipelineMessagePlural: s__('PackageRegistry|Package has %{number} archived updates'),
},
components: {
GlLink,
@@ -58,14 +55,14 @@ export default {
showPipelinesInfo() {
return Boolean(this.firstPipeline?.id);
},
- archiviedLines() {
+ archivedLines() {
return Math.max(this.pipelines.length - HISTORY_PIPELINES_LIMIT - 1, 0);
},
archivedPipelineMessage() {
return n__(
- this.$options.i18n.archivedPipelineMessageSingular,
- this.$options.i18n.archivedPipelineMessagePlural,
- this.archiviedLines,
+ 'PackageRegistry|Package has %{updatesCount} archived update',
+ 'PackageRegistry|Package has %{updatesCount} archived updates',
+ this.archivedLines,
);
},
},
@@ -135,10 +132,10 @@ export default {
-
+
-
- {{ archiviedLines }}
+
+ {{ archivedLines }}
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/app.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/app.vue
new file mode 100644
index 0000000000..08481ac565
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/app.vue
@@ -0,0 +1,134 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue
new file mode 100644
index 0000000000..195ff7af58
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue
@@ -0,0 +1,151 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ packageEntity.version }}
+
+
+
+ {{ pipelineUser }}
+
+
+
+
+ {{ packageType }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue
index 280d292ce0..836df59ca5 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue
@@ -1,10 +1,14 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ content }}
-
-
-
-
-
-
-
-
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/publish_method.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/publish_method.vue
new file mode 100644
index 0000000000..8ecf433f3a
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/publish_method.vue
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+ {{ pipeline.ref }}
+
+
+ {{
+ packageShaShort
+ }}
+
+
+
+
+
+
+
+ {{ $options.i18n.MANUALLY_PUBLISHED }}
+
+
+
+
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/constants.js b/app/assets/javascripts/packages_and_registries/package_registry/constants.js
index f023b4481a..6a88880fa9 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/constants.js
+++ b/app/assets/javascripts/packages_and_registries/package_registry/constants.js
@@ -1,5 +1,4 @@
-/* eslint-disable @gitlab/require-string-literal-i18n-helpers */
-import { __, s__ } from '~/locale';
+import { s__ } from '~/locale';
export const PACKAGE_TYPE_CONAN = 'CONAN';
export const PACKAGE_TYPE_MAVEN = 'MAVEN';
@@ -71,7 +70,7 @@ export const DELETE_PACKAGE_ERROR_MESSAGE = s__(
'PackageRegistry|Something went wrong while deleting the package.',
);
export const DELETE_PACKAGE_FILE_ERROR_MESSAGE = s__(
- __('PackageRegistry|Something went wrong while deleting the package file.'),
+ 'PackageRegistry|Something went wrong while deleting the package file.',
);
export const DELETE_PACKAGE_FILE_SUCCESS_MESSAGE = s__(
'PackageRegistry|Package file deleted successfully',
@@ -87,3 +86,10 @@ export const PACKAGE_PROCESSING_STATUS = 'PROCESSING';
export const NPM_PACKAGE_MANAGER = 'npm';
export const YARN_PACKAGE_MANAGER = 'yarn';
+
+export const PROJECT_PACKAGE_ENDPOINT_TYPE = 'project';
+export const INSTANCE_PACKAGE_ENDPOINT_TYPE = 'instance';
+
+export const PROJECT_RESOURCE_TYPE = 'project';
+export const GROUP_RESOURCE_TYPE = 'group';
+export const LIST_QUERY_DEBOUNCE_TIME = 50;
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql
new file mode 100644
index 0000000000..aaf0eb54af
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql
@@ -0,0 +1,27 @@
+fragment PackageData on Package {
+ id
+ name
+ version
+ packageType
+ createdAt
+ status
+ tags {
+ nodes {
+ name
+ }
+ }
+ pipelines {
+ nodes {
+ sha
+ ref
+ commitPath
+ user {
+ name
+ }
+ }
+ }
+ project {
+ fullPath
+ webUrl
+ }
+}
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql
new file mode 100644
index 0000000000..74e6de8786
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql
@@ -0,0 +1,27 @@
+#import "~/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql"
+
+query getPackages(
+ $fullPath: ID!
+ $isGroupPage: Boolean!
+ $sort: PackageSort
+ $groupSort: PackageGroupSort
+ $packageName: String
+ $packageType: PackageTypeEnum
+) {
+ project(fullPath: $fullPath) @skip(if: $isGroupPage) {
+ packages(sort: $sort, packageName: $packageName, packageType: $packageType) {
+ count
+ nodes {
+ ...PackageData
+ }
+ }
+ }
+ group(fullPath: $fullPath) @include(if: $isGroupPage) {
+ packages(sort: $groupSort, packageName: $packageName, packageType: $packageType) {
+ count
+ nodes {
+ ...PackageData
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/list.js b/app/assets/javascripts/packages_and_registries/package_registry/pages/list.js
index 1e01b75aab..d797a0a532 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/pages/list.js
+++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/list.js
@@ -1,14 +1,22 @@
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
-import PackagesListApp from '../components/list/packages_list_app.vue';
+import { apolloProvider } from '~/packages_and_registries/package_registry/graphql/index';
+import PackagesListApp from '../components/list/app.vue';
Vue.use(Translate);
export default () => {
const el = document.getElementById('js-vue-packages-list');
+ const isGroupPage = el.dataset.pageType === 'groups';
+
return new Vue({
el,
+ apolloProvider,
+ provide: {
+ ...el.dataset,
+ isGroupPage,
+ },
render(createElement) {
return createElement(PackagesListApp);
},
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/bundle.js b/app/assets/javascripts/packages_and_registries/settings/group/bundle.js
index 5cd8261ac2..9b5a0d221b 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/bundle.js
+++ b/app/assets/javascripts/packages_and_registries/settings/group/bundle.js
@@ -19,6 +19,7 @@ export default () => {
apolloProvider,
provide: {
defaultExpanded: parseBoolean(el.dataset.defaultExpanded),
+ dependencyProxyAvailable: parseBoolean(el.dataset.dependencyProxyAvailable),
groupPath: el.dataset.groupPath,
},
render(createElement) {
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue
new file mode 100644
index 0000000000..2dbe36def0
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue
@@ -0,0 +1,110 @@
+
+
+
+
+ {{ $options.i18n.DEPENDENCY_PROXY_HEADER }}
+
+
+
+
+ {{ content }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/duplicates_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/duplicates_settings.vue
index d66a30e7e8..b0088838ac 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/components/duplicates_settings.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/duplicates_settings.vue
@@ -86,6 +86,7 @@ export default {
:label="$options.i18n.DUPLICATES_TOGGLE_LABEL"
label-position="hidden"
:value="duplicatesAllowed"
+ :disabled="loading"
@change="update(modelNames.allowed, $event)"
/>
@@ -108,6 +109,7 @@ export default {
>
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue
index ec3be43196..b45cedcdd6 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue
@@ -1,108 +1,66 @@
+
+
+
+ {{ $options.i18n.PACKAGE_SETTINGS_HEADER }}
+
+
+
+
+ {{
+ content
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/constants.js b/app/assets/javascripts/packages_and_registries/settings/group/constants.js
index d29489a0b3..ee92245799 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/constants.js
+++ b/app/assets/javascripts/packages_and_registries/settings/group/constants.js
@@ -18,9 +18,9 @@ export const DUPLICATES_SETTINGS_EXCEPTION_LEGEND = s__(
'PackageRegistry|Publish packages if their name or version matches this regex.',
);
-export const SUCCESS_UPDATING_SETTINGS = s__('PackageRegistry|Settings saved successfully');
-export const ERROR_UPDATING_SETTINGS = s__(
- 'PackageRegistry|An error occurred while saving the settings',
+export const DEPENDENCY_PROXY_HEADER = s__('DependencyProxy|Dependency Proxy');
+export const DEPENDENCY_PROXY_SETTINGS_DESCRIPTION = s__(
+ 'DependencyProxy|Create a local proxy for storing frequently used upstream images. %{docLinkStart}Learn more%{docLinkEnd} about dependency proxies.',
);
// Parameters
@@ -28,3 +28,5 @@ export const ERROR_UPDATING_SETTINGS = s__(
export const PACKAGES_DOCS_PATH = helpPagePath('user/packages');
export const MAVEN_DUPLICATES_ALLOWED = 'mavenDuplicatesAllowed';
export const MAVEN_DUPLICATE_EXCEPTION_REGEX = 'mavenDuplicateExceptionRegex';
+
+export const DEPENDENCY_PROXY_DOCS_PATH = helpPagePath('user/packages/dependency_proxy/index');
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_dependency_proxy_settings.mutation.graphql b/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_dependency_proxy_settings.mutation.graphql
new file mode 100644
index 0000000000..d24a645fec
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_dependency_proxy_settings.mutation.graphql
@@ -0,0 +1,8 @@
+mutation updateDependencyProxySettings($input: UpdateDependencyProxySettingsInput!) {
+ updateDependencyProxySettings(input: $input) {
+ dependencyProxySetting {
+ enabled
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql b/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql
index a1c0130089..d3edebfbe2 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql
@@ -1,5 +1,8 @@
query getGroupPackagesSettings($fullPath: ID!) {
group(fullPath: $fullPath) {
+ dependencyProxySetting {
+ enabled
+ }
packageSettings {
mavenDuplicatesAllowed
mavenDuplicateExceptionRegex
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/cache_update.js b/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/cache_update.js
index fb06f557d6..fe94203f51 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/cache_update.js
+++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/cache_update.js
@@ -9,9 +9,16 @@ export const updateGroupPackageSettings = (fullPath) => (client, { data: updated
const sourceData = client.readQuery(queryAndParams);
const data = produce(sourceData, (draftState) => {
- draftState.group.packageSettings = {
- ...updatedData.updateNamespacePackageSettings.packageSettings,
- };
+ if (updatedData.updateNamespacePackageSettings) {
+ draftState.group.packageSettings = {
+ ...updatedData.updateNamespacePackageSettings.packageSettings,
+ };
+ }
+ if (updatedData.updateDependencyProxySettings) {
+ draftState.group.dependencyProxySetting = {
+ ...updatedData.updateDependencyProxySettings.dependencyProxySetting,
+ };
+ }
});
client.writeQuery({
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/optimistic_responses.js b/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/optimistic_responses.js
index f2c8de85bf..a30d8ca0b8 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/optimistic_responses.js
+++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/optimistic_responses.js
@@ -9,3 +9,15 @@ export const updateGroupPackagesSettingsOptimisticResponse = (changes) => ({
},
},
});
+
+export const updateGroupDependencyProxySettingsOptimisticResponse = (changes) => ({
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ __typename: 'Mutation',
+ updateDependencyProxySettings: {
+ __typename: 'UpdateDependencyProxySettingsPayload',
+ errors: [],
+ dependencyProxySetting: {
+ ...changes,
+ },
+ },
+});
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue
index bf286c84d5..7be3bba7ca 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue
@@ -95,7 +95,7 @@ export default {
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/constants.js b/app/assets/javascripts/packages_and_registries/settings/project/constants.js
index 165c4aae3c..4d477fbd05 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/constants.js
+++ b/app/assets/javascripts/packages_and_registries/settings/project/constants.js
@@ -73,6 +73,7 @@ export const OLDER_THAN_OPTIONS = [
{ key: 'SEVEN_DAYS', variable: 7, default: false },
{ key: 'FOURTEEN_DAYS', variable: 14, default: false },
{ key: 'THIRTY_DAYS', variable: 30, default: false },
+ { key: 'SIXTY_DAYS', variable: 60, default: false },
{ key: 'NINETY_DAYS', variable: 90, default: true },
];
diff --git a/app/assets/javascripts/packages_and_registries/shared/constants.js b/app/assets/javascripts/packages_and_registries/shared/constants.js
index 55b5816cc5..7d2971bd8c 100644
--- a/app/assets/javascripts/packages_and_registries/shared/constants.js
+++ b/app/assets/javascripts/packages_and_registries/shared/constants.js
@@ -1 +1,3 @@
export const FILTERED_SEARCH_TERM = 'filtered-search-term';
+export const FILTERED_SEARCH_TYPE = 'type';
+export const HISTORY_PIPELINES_LIMIT = 5;
diff --git a/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/usage_statistics.js b/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/usage_statistics.js
index 4c312a008c..68849857d0 100644
--- a/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/usage_statistics.js
+++ b/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/usage_statistics.js
@@ -1,7 +1,7 @@
import { __ } from '~/locale';
export const HELPER_TEXT_SERVICE_PING_DISABLED = __(
- 'To enable Registration Features, make sure "Enable service ping" is checked.',
+ 'To enable Registration Features, first enable Service Ping.',
);
export const HELPER_TEXT_SERVICE_PING_ENABLED = __(
diff --git a/app/assets/javascripts/pages/admin/projects/components/namespace_select.vue b/app/assets/javascripts/pages/admin/projects/components/namespace_select.vue
new file mode 100644
index 0000000000..c75c031b0b
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/projects/components/namespace_select.vue
@@ -0,0 +1,143 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $options.i18n.anyNamespace }}
+
+
+
+
+
+
+
+ {{ getNamespaceString(namespace) }}
+
+
+
+
+
+
diff --git a/app/assets/javascripts/pages/admin/projects/index.js b/app/assets/javascripts/pages/admin/projects/index.js
index b07ca815f1..3098d06510 100644
--- a/app/assets/javascripts/pages/admin/projects/index.js
+++ b/app/assets/javascripts/pages/admin/projects/index.js
@@ -1,8 +1,38 @@
-import NamespaceSelect from '~/namespace_select';
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import { mergeUrlParams } from '~/lib/utils/url_utility';
import ProjectsList from '~/projects_list';
+import NamespaceSelect from './components/namespace_select.vue';
new ProjectsList(); // eslint-disable-line no-new
-document
- .querySelectorAll('.js-namespace-select')
- .forEach((dropdown) => new NamespaceSelect({ dropdown }));
+function mountNamespaceSelect() {
+ const el = document.querySelector('.js-namespace-select');
+ if (!el) {
+ return false;
+ }
+
+ const { showAny, fieldName, placeholder, updateLocation } = el.dataset;
+
+ return new Vue({
+ el,
+ render(createComponent) {
+ return createComponent(NamespaceSelect, {
+ props: {
+ showAny: parseBoolean(showAny),
+ fieldName,
+ placeholder,
+ },
+ on: {
+ setNamespace(newNamespace) {
+ if (fieldName && updateLocation) {
+ window.location = mergeUrlParams({ [fieldName]: newNamespace }, window.location.href);
+ }
+ },
+ },
+ });
+ },
+ });
+}
+
+mountNamespaceSelect();
diff --git a/app/assets/javascripts/pages/admin/serverless/domains/index.js b/app/assets/javascripts/pages/admin/serverless/domains/index.js
deleted file mode 100644
index 4fab7a1d9c..0000000000
--- a/app/assets/javascripts/pages/admin/serverless/domains/index.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import initSettingsPanels from '~/settings_panels';
-
-// Initialize expandable settings panels
-initSettingsPanels();
-
-const domainCard = document.querySelector('.js-domain-cert-show');
-const domainForm = document.querySelector('.js-domain-cert-inputs');
-const domainReplaceButton = document.querySelector('.js-domain-cert-replace-btn');
-const domainSubmitButton = document.querySelector('.js-serverless-domain-submit');
-
-if (domainReplaceButton && domainCard && domainForm) {
- domainReplaceButton.addEventListener('click', () => {
- domainCard.classList.add('hidden');
- domainForm.classList.remove('hidden');
- domainSubmitButton.removeAttribute('disabled');
- });
-}
diff --git a/app/assets/javascripts/pages/admin/topics/edit/index.js b/app/assets/javascripts/pages/admin/topics/edit/index.js
new file mode 100644
index 0000000000..c4e05bbd09
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/topics/edit/index.js
@@ -0,0 +1,8 @@
+import $ from 'jquery';
+import GLForm from '~/gl_form';
+import initFilePickers from '~/file_pickers';
+import ZenMode from '~/zen_mode';
+
+new GLForm($('.js-project-topic-form')); // eslint-disable-line no-new
+initFilePickers();
+new ZenMode(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/admin/topics/new/index.js b/app/assets/javascripts/pages/admin/topics/new/index.js
new file mode 100644
index 0000000000..c4e05bbd09
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/topics/new/index.js
@@ -0,0 +1,8 @@
+import $ from 'jquery';
+import GLForm from '~/gl_form';
+import initFilePickers from '~/file_pickers';
+import ZenMode from '~/zen_mode';
+
+new GLForm($('.js-project-topic-form')); // eslint-disable-line no-new
+initFilePickers();
+new ZenMode(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/groups/dependency_proxies/index.js b/app/assets/javascripts/pages/groups/dependency_proxies/index.js
index 77c885d385..862ba46829 100644
--- a/app/assets/javascripts/pages/groups/dependency_proxies/index.js
+++ b/app/assets/javascripts/pages/groups/dependency_proxies/index.js
@@ -1,13 +1,3 @@
-import $ from 'jquery';
-import initDependencyProxy from '~/dependency_proxy';
+import { initDependencyProxyApp } from '~/packages_and_registries/dependency_proxy/';
-initDependencyProxy();
-
-const form = document.querySelector('form.edit_dependency_proxy_group_setting');
-const toggleInput = $('input.js-project-feature-toggle-input');
-
-if (form && toggleInput) {
- toggleInput.on('trigger-change', () => {
- form.submit();
- });
-}
+initDependencyProxyApp();
diff --git a/app/assets/javascripts/pages/groups/group_members/index.js b/app/assets/javascripts/pages/groups/group_members/index.js
index 0137ff8797..01a371920f 100644
--- a/app/assets/javascripts/pages/groups/group_members/index.js
+++ b/app/assets/javascripts/pages/groups/group_members/index.js
@@ -11,7 +11,7 @@ import { MEMBER_TYPES } from '~/members/constants';
import { groupLinkRequestFormatter } from '~/members/utils';
import UsersSelect from '~/users_select';
-const SHARED_FIELDS = ['account', 'expires', 'maxRole', 'expiration', 'actions'];
+const SHARED_FIELDS = ['account', 'maxRole', 'expiration', 'actions'];
initMembersApp(document.querySelector('.js-group-members-list-app'), {
[MEMBER_TYPES.user]: {
diff --git a/app/assets/javascripts/pages/groups/packages/index/index.js b/app/assets/javascripts/pages/groups/packages/index/index.js
index 1c4a10fd65..95522573b5 100644
--- a/app/assets/javascripts/pages/groups/packages/index/index.js
+++ b/app/assets/javascripts/pages/groups/packages/index/index.js
@@ -1,5 +1,10 @@
-import initPackageList from '~/packages/list/packages_list_app_bundle';
+(async function packageApp() {
+ if (window.gon.features.packageListApollo) {
+ const newPackageList = await import('~/packages_and_registries/package_registry/pages/list');
-if (document.getElementById('js-vue-packages-list')) {
- initPackageList();
-}
+ newPackageList.default();
+ } else {
+ const packageList = await import('~/packages/list/packages_list_app_bundle');
+ packageList.default();
+ }
+})();
diff --git a/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue b/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue
new file mode 100644
index 0000000000..ec3cf4a8a9
--- /dev/null
+++ b/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue
@@ -0,0 +1,176 @@
+
+
+
+
+
+
+
+ {{ s__('BulkImport|Group import history') }}
+
+
+
+
+
+
+
+
+ {{ getDestinationUrl(item) }}
+
+
+
+
+
+
+
+ {{ __('Details') }}
+
+
+ {{ item.failures }}
+
+
+
+
+
+
diff --git a/app/assets/javascripts/pages/import/bulk_imports/history/index.js b/app/assets/javascripts/pages/import/bulk_imports/history/index.js
new file mode 100644
index 0000000000..5a67aa99ba
--- /dev/null
+++ b/app/assets/javascripts/pages/import/bulk_imports/history/index.js
@@ -0,0 +1,15 @@
+import Vue from 'vue';
+import BulkImportHistoryApp from './components/bulk_imports_history_app.vue';
+
+function mountImportHistoryApp(mountElement) {
+ if (!mountElement) return undefined;
+
+ return new Vue({
+ el: mountElement,
+ render(createElement) {
+ return createElement(BulkImportHistoryApp);
+ },
+ });
+}
+
+mountImportHistoryApp(document.querySelector('#import-history-mount-element'));
diff --git a/app/assets/javascripts/pages/import/bulk_imports/history/utils/error_messages.js b/app/assets/javascripts/pages/import/bulk_imports/history/utils/error_messages.js
new file mode 100644
index 0000000000..24669e22ad
--- /dev/null
+++ b/app/assets/javascripts/pages/import/bulk_imports/history/utils/error_messages.js
@@ -0,0 +1,3 @@
+import { __ } from '~/locale';
+
+export const DEFAULT_ERROR = __('Something went wrong on our end.');
diff --git a/app/assets/javascripts/pages/profiles/index.js b/app/assets/javascripts/pages/profiles/index.js
index 80bc32dd43..6afb363699 100644
--- a/app/assets/javascripts/pages/profiles/index.js
+++ b/app/assets/javascripts/pages/profiles/index.js
@@ -2,6 +2,7 @@ import $ from 'jquery';
import '~/profile/gl_crop';
import Profile from '~/profile/profile';
import initSearchSettings from '~/search_settings';
+import initPasswordPrompt from './password_prompt';
// eslint-disable-next-line func-names
$(document).on('input.ssh_key', '#key_key', function () {
@@ -19,3 +20,4 @@ $(document).on('input.ssh_key', '#key_key', function () {
new Profile(); // eslint-disable-line no-new
initSearchSettings();
+initPasswordPrompt();
diff --git a/app/assets/javascripts/pages/profiles/password_prompt/constants.js b/app/assets/javascripts/pages/profiles/password_prompt/constants.js
new file mode 100644
index 0000000000..99b8442c92
--- /dev/null
+++ b/app/assets/javascripts/pages/profiles/password_prompt/constants.js
@@ -0,0 +1,9 @@
+import { __, s__ } from '~/locale';
+
+export const I18N_PASSWORD_PROMPT_TITLE = s__('PasswordPrompt|Confirm password to continue');
+export const I18N_PASSWORD_PROMPT_FORM_LABEL = s__(
+ 'PasswordPrompt|Please enter your password to confirm',
+);
+export const I18N_PASSWORD_PROMPT_ERROR_MESSAGE = s__('PasswordPrompt|Password is required');
+export const I18N_PASSWORD_PROMPT_CONFIRM_BUTTON = s__('PasswordPrompt|Confirm password');
+export const I18N_PASSWORD_PROMPT_CANCEL_BUTTON = __('Cancel');
diff --git a/app/assets/javascripts/pages/profiles/password_prompt/index.js b/app/assets/javascripts/pages/profiles/password_prompt/index.js
new file mode 100644
index 0000000000..2064511289
--- /dev/null
+++ b/app/assets/javascripts/pages/profiles/password_prompt/index.js
@@ -0,0 +1,58 @@
+import Vue from 'vue';
+import Translate from '~/vue_shared/translate';
+import PasswordPromptModal from './password_prompt_modal.vue';
+
+Vue.use(Translate);
+
+const emailFieldSelector = '#user_email';
+const editFormSelector = '.js-password-prompt-form';
+const passwordPromptFieldSelector = '.js-password-prompt-field';
+const passwordPromptBtnSelector = '.js-password-prompt-btn';
+
+const passwordPromptModalId = 'password-prompt-modal';
+
+const getEmailValue = () => document.querySelector(emailFieldSelector).value.trim();
+const passwordPromptButton = document.querySelector(passwordPromptBtnSelector);
+const field = document.querySelector(passwordPromptFieldSelector);
+const form = document.querySelector(editFormSelector);
+
+const handleConfirmPassword = (pw) => {
+ // update the validation_password field
+ field.value = pw;
+ // submit the form
+ form.submit();
+};
+
+export default () => {
+ const passwordPromptModalEl = document.getElementById(passwordPromptModalId);
+
+ if (passwordPromptModalEl && field) {
+ return new Vue({
+ el: passwordPromptModalEl,
+ data() {
+ return {
+ initialEmail: '',
+ };
+ },
+ mounted() {
+ this.initialEmail = getEmailValue();
+ passwordPromptButton.addEventListener('click', this.handleSettingsUpdate);
+ },
+ methods: {
+ handleSettingsUpdate(ev) {
+ const email = getEmailValue();
+ if (email !== this.initialEmail) {
+ ev.preventDefault();
+ this.$root.$emit('bv::show::modal', passwordPromptModalId, passwordPromptBtnSelector);
+ }
+ },
+ },
+ render(createElement) {
+ return createElement(PasswordPromptModal, {
+ props: { handleConfirmPassword },
+ });
+ },
+ });
+ }
+ return null;
+};
diff --git a/app/assets/javascripts/pages/profiles/password_prompt/password_prompt_modal.vue b/app/assets/javascripts/pages/profiles/password_prompt/password_prompt_modal.vue
new file mode 100644
index 0000000000..44728ea9cd
--- /dev/null
+++ b/app/assets/javascripts/pages/profiles/password_prompt/password_prompt_modal.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/assets/javascripts/pages/projects/cluster_agents/show/index.js b/app/assets/javascripts/pages/projects/cluster_agents/show/index.js
new file mode 100644
index 0000000000..4ed3e2f7be
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/cluster_agents/show/index.js
@@ -0,0 +1,3 @@
+import loadClusterAgentVues from '~/clusters/agents';
+
+loadClusterAgentVues();
diff --git a/app/assets/javascripts/pages/projects/clusters/index/index.js b/app/assets/javascripts/pages/projects/clusters/index/index.js
index 2b5451bd18..a1ba920b32 100644
--- a/app/assets/javascripts/pages/projects/clusters/index/index.js
+++ b/app/assets/javascripts/pages/projects/clusters/index/index.js
@@ -1,4 +1,4 @@
-import initClustersListApp from 'ee_else_ce/clusters_list';
+import initClustersListApp from '~/clusters_list';
import PersistentUserCallout from '~/persistent_user_callout';
const callout = document.querySelector('.gcp-signup-offer');
diff --git a/app/assets/javascripts/pages/projects/new/components/new_project_url_select.vue b/app/assets/javascripts/pages/projects/new/components/new_project_url_select.vue
deleted file mode 100644
index ba8858c985..0000000000
--- a/app/assets/javascripts/pages/projects/new/components/new_project_url_select.vue
+++ /dev/null
@@ -1,98 +0,0 @@
-
-
-
-
- {{ rootUrl }}
-
-
-
-
- {{ __('Groups') }}
-
- {{ group.fullPath }}
-
- {{ __('Users') }}
-
- {{ userNamespace.fullPath }}
-
-
-
-
-
-
-
diff --git a/app/assets/javascripts/pages/projects/new/index.js b/app/assets/javascripts/pages/projects/new/index.js
index ed816e3be9..d89b4d0e0a 100644
--- a/app/assets/javascripts/pages/projects/new/index.js
+++ b/app/assets/javascripts/pages/projects/new/index.js
@@ -1,66 +1,6 @@
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import createDefaultClient from '~/lib/graphql';
-import { parseBoolean } from '~/lib/utils/common_utils';
-import initProjectVisibilitySelector from '../../../project_visibility';
-import initProjectNew from '../../../projects/project_new';
-import NewProjectCreationApp from './components/app.vue';
-import NewProjectUrlSelect from './components/new_project_url_select.vue';
-
-function initNewProjectCreation() {
- const el = document.querySelector('.js-new-project-creation');
-
- const {
- pushToCreateProjectCommand,
- workingWithProjectsHelpPath,
- newProjectGuidelines,
- hasErrors,
- isCiCdAvailable,
- } = el.dataset;
-
- const props = {
- hasErrors: parseBoolean(hasErrors),
- isCiCdAvailable: parseBoolean(isCiCdAvailable),
- newProjectGuidelines,
- };
-
- const provide = {
- workingWithProjectsHelpPath,
- pushToCreateProjectCommand,
- };
-
- return new Vue({
- el,
- provide,
- render(h) {
- return h(NewProjectCreationApp, { props });
- },
- });
-}
-
-function initNewProjectUrlSelect() {
- const el = document.querySelector('.js-vue-new-project-url-select');
-
- if (!el) {
- return undefined;
- }
-
- Vue.use(VueApollo);
-
- return new Vue({
- el,
- apolloProvider: new VueApollo({
- defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
- }),
- provide: {
- namespaceFullPath: el.dataset.namespaceFullPath,
- namespaceId: el.dataset.namespaceId,
- rootUrl: el.dataset.rootUrl,
- trackLabel: el.dataset.trackLabel,
- },
- render: (createElement) => createElement(NewProjectUrlSelect),
- });
-}
+import { initNewProjectCreation, initNewProjectUrlSelect } from '~/projects/new';
+import initProjectVisibilitySelector from '~/project_visibility';
+import initProjectNew from '~/projects/project_new';
initProjectVisibilitySelector();
initProjectNew.bindEvents();
diff --git a/app/assets/javascripts/pages/projects/packages/packages/index/index.js b/app/assets/javascripts/pages/projects/packages/packages/index/index.js
index c94782fdf1..95522573b5 100644
--- a/app/assets/javascripts/pages/projects/packages/packages/index/index.js
+++ b/app/assets/javascripts/pages/projects/packages/packages/index/index.js
@@ -1,3 +1,10 @@
-import initPackageList from '~/packages/list/packages_list_app_bundle';
+(async function packageApp() {
+ if (window.gon.features.packageListApollo) {
+ const newPackageList = await import('~/packages_and_registries/package_registry/pages/list');
-initPackageList();
+ newPackageList.default();
+ } else {
+ const packageList = await import('~/packages/list/packages_list_app_bundle');
+ packageList.default();
+ }
+})();
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js
index 16c4a6191b..e92b9b30fa 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js
@@ -27,19 +27,22 @@ export const findTimezoneByIdentifier = (tzList = [], identifier = null) => {
};
export default class TimezoneDropdown {
- constructor({ $dropdownEl, $inputEl, onSelectTimezone, displayFormat } = defaults) {
+ constructor({
+ $dropdownEl,
+ $inputEl,
+ onSelectTimezone,
+ displayFormat,
+ allowEmpty = false,
+ } = defaults) {
this.$dropdown = $dropdownEl;
this.$dropdownToggle = this.$dropdown.find('.dropdown-toggle-text');
this.$input = $inputEl;
- this.timezoneData = this.$dropdown.data('data');
+ this.timezoneData = this.$dropdown.data('data') || [];
this.onSelectTimezone = onSelectTimezone;
this.displayFormat = displayFormat || defaults.displayFormat;
+ this.allowEmpty = allowEmpty;
- this.initialTimezone =
- findTimezoneByIdentifier(this.timezoneData, this.$input.val()) || defaultTimezone;
-
- this.initDefaultTimezone();
this.initDropdown();
}
@@ -52,24 +55,25 @@ export default class TimezoneDropdown {
search: {
fields: ['name'],
},
- clicked: (cfg) => this.updateInputValue(cfg),
+ clicked: (cfg) => this.handleDropdownChange(cfg),
text: (item) => formatTimezone(item),
});
- this.setDropdownToggle(this.displayFormat(this.initialTimezone));
- }
+ const initialTimezone = findTimezoneByIdentifier(this.timezoneData, this.$input.val());
- initDefaultTimezone() {
- if (!this.$input.val()) {
- this.$input.val(defaultTimezone.name);
+ if (initialTimezone !== null) {
+ this.setDropdownValue(initialTimezone);
+ } else if (!this.allowEmpty) {
+ this.setDropdownValue(defaultTimezone);
}
}
- setDropdownToggle(dropdownText) {
- this.$dropdownToggle.text(dropdownText);
+ setDropdownValue(timezone) {
+ this.$dropdownToggle.text(this.displayFormat(timezone));
+ this.$input.val(timezone.name);
}
- updateInputValue({ selectedObj, e }) {
+ handleDropdownChange({ selectedObj, e }) {
e.preventDefault();
this.$input.val(selectedObj.identifier);
if (this.onSelectTimezone) {
diff --git a/app/assets/javascripts/pages/projects/project_members/index.js b/app/assets/javascripts/pages/projects/project_members/index.js
index 0b662c945c..947bbdacf2 100644
--- a/app/assets/javascripts/pages/projects/project_members/index.js
+++ b/app/assets/javascripts/pages/projects/project_members/index.js
@@ -26,7 +26,7 @@ initInviteMembersForm();
new UsersSelect(); // eslint-disable-line no-new
-const SHARED_FIELDS = ['account', 'expires', 'maxRole', 'expiration', 'actions'];
+const SHARED_FIELDS = ['account', 'maxRole', 'expiration', 'actions'];
initMembersApp(document.querySelector('.js-project-members-list-app'), {
[MEMBER_TYPES.user]: {
tableFields: SHARED_FIELDS.concat(['source', 'granted']),
diff --git a/app/assets/javascripts/pages/projects/wikis/diff/index.js b/app/assets/javascripts/pages/projects/wikis/diff/index.js
new file mode 100644
index 0000000000..73440db761
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/wikis/diff/index.js
@@ -0,0 +1,3 @@
+import { initDiffStatsDropdown } from '~/init_diff_stats_dropdown';
+
+initDiffStatsDropdown();
diff --git a/app/assets/javascripts/pages/projects/wikis/edit/index.js b/app/assets/javascripts/pages/projects/wikis/edit/index.js
new file mode 100644
index 0000000000..b2288c2655
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/wikis/edit/index.js
@@ -0,0 +1,3 @@
+import { mountApplications } from '~/pages/shared/wikis/edit';
+
+mountApplications();
diff --git a/app/assets/javascripts/pages/projects/wikis/git_access/index.js b/app/assets/javascripts/pages/projects/wikis/git_access/index.js
new file mode 100644
index 0000000000..b1f3006bc1
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/wikis/git_access/index.js
@@ -0,0 +1,3 @@
+import initClonePanel from '~/clone_panel';
+
+initClonePanel();
diff --git a/app/assets/javascripts/pages/projects/wikis/index.js b/app/assets/javascripts/pages/projects/wikis/index.js
index 2c1f9e634a..83fcd348dd 100644
--- a/app/assets/javascripts/pages/projects/wikis/index.js
+++ b/app/assets/javascripts/pages/projects/wikis/index.js
@@ -1,5 +1,3 @@
-import { initDiffStatsDropdown } from '~/init_diff_stats_dropdown';
-import initWikis from '~/pages/shared/wikis';
+import Wikis from '~/pages/shared/wikis/wikis';
-initWikis();
-initDiffStatsDropdown();
+export default new Wikis();
diff --git a/app/assets/javascripts/pages/projects/wikis/show/index.js b/app/assets/javascripts/pages/projects/wikis/show/index.js
new file mode 100644
index 0000000000..c08a10122b
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/wikis/show/index.js
@@ -0,0 +1,3 @@
+import { mountApplications as mountEditApplications } from '~/pages/shared/wikis/async_edit';
+
+mountEditApplications();
diff --git a/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js b/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js
index 8d2d5d41f6..ee48543f0d 100644
--- a/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js
+++ b/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js
@@ -20,7 +20,7 @@ export default class OAuthRememberMe {
toggleRememberMe(event) {
const rememberMe = $(event.target).is(':checked');
- $('.oauth-login', this.container).each((i, element) => {
+ $('.js-oauth-login', this.container).each((i, element) => {
const $form = $(element).parent('form');
const href = $form.attr('action');
diff --git a/app/assets/javascripts/pages/shared/mount_runner_instructions.js b/app/assets/javascripts/pages/shared/mount_runner_instructions.js
index e83c73edfd..1cb7259be6 100644
--- a/app/assets/javascripts/pages/shared/mount_runner_instructions.js
+++ b/app/assets/javascripts/pages/shared/mount_runner_instructions.js
@@ -9,7 +9,12 @@ export function initInstallRunner(componentId = 'js-install-runner') {
const installRunnerEl = document.getElementById(componentId);
if (installRunnerEl) {
- const defaultClient = createDefaultClient();
+ const defaultClient = createDefaultClient(
+ {},
+ {
+ assumeImmutableResults: true,
+ },
+ );
const apolloProvider = new VueApollo({
defaultClient,
diff --git a/app/assets/javascripts/pages/shared/wikis/async_edit.js b/app/assets/javascripts/pages/shared/wikis/async_edit.js
new file mode 100644
index 0000000000..4536a07656
--- /dev/null
+++ b/app/assets/javascripts/pages/shared/wikis/async_edit.js
@@ -0,0 +1,11 @@
+export const mountApplications = async () => {
+ const el = document.querySelector('.js-wiki-edit-page');
+
+ if (el) {
+ const { mountApplications: mountEditApplications } = await import(
+ /* webpackChunkName: 'wiki_edit' */ './edit'
+ );
+
+ mountEditApplications();
+ }
+};
diff --git a/app/assets/javascripts/pages/shared/wikis/edit.js b/app/assets/javascripts/pages/shared/wikis/edit.js
new file mode 100644
index 0000000000..beeabfde1a
--- /dev/null
+++ b/app/assets/javascripts/pages/shared/wikis/edit.js
@@ -0,0 +1,88 @@
+import $ from 'jquery';
+import Vue from 'vue';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import csrf from '~/lib/utils/csrf';
+import Translate from '~/vue_shared/translate';
+import GLForm from '../../../gl_form';
+import ZenMode from '../../../zen_mode';
+import deleteWikiModal from './components/delete_wiki_modal.vue';
+import wikiAlert from './components/wiki_alert.vue';
+import wikiForm from './components/wiki_form.vue';
+
+const createModalVueApp = () => {
+ const deleteWikiModalWrapperEl = document.getElementById('delete-wiki-modal-wrapper');
+
+ if (deleteWikiModalWrapperEl) {
+ Vue.use(Translate);
+
+ const { deleteWikiUrl, pageTitle } = deleteWikiModalWrapperEl.dataset;
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: deleteWikiModalWrapperEl,
+ data() {
+ return {
+ deleteWikiUrl: '',
+ };
+ },
+ render(createElement) {
+ return createElement(deleteWikiModal, {
+ props: {
+ pageTitle,
+ deleteWikiUrl,
+ csrfToken: csrf.token,
+ },
+ });
+ },
+ });
+ }
+};
+
+const createAlertVueApp = () => {
+ const el = document.getElementById('js-wiki-error');
+ if (el) {
+ const { error, wikiPagePath } = el.dataset;
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ render(createElement) {
+ return createElement(wikiAlert, {
+ props: {
+ error,
+ wikiPagePath,
+ },
+ });
+ },
+ });
+ }
+};
+
+const createWikiFormApp = () => {
+ const el = document.getElementById('js-wiki-form');
+
+ if (el) {
+ const { pageInfo, formatOptions } = el.dataset;
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ provide: {
+ formatOptions: JSON.parse(formatOptions),
+ pageInfo: convertObjectPropsToCamelCase(JSON.parse(pageInfo)),
+ },
+ render(createElement) {
+ return createElement(wikiForm);
+ },
+ });
+ }
+};
+
+export const mountApplications = () => {
+ new ZenMode(); // eslint-disable-line no-new
+ new GLForm($('.wiki-form')); // eslint-disable-line no-new
+
+ createModalVueApp();
+ createAlertVueApp();
+ createWikiFormApp();
+};
diff --git a/app/assets/javascripts/pages/shared/wikis/index.js b/app/assets/javascripts/pages/shared/wikis/index.js
deleted file mode 100644
index 42aefe8132..0000000000
--- a/app/assets/javascripts/pages/shared/wikis/index.js
+++ /dev/null
@@ -1,92 +0,0 @@
-import $ from 'jquery';
-import Vue from 'vue';
-import ShortcutsWiki from '~/behaviors/shortcuts/shortcuts_wiki';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import csrf from '~/lib/utils/csrf';
-import Translate from '~/vue_shared/translate';
-import GLForm from '../../../gl_form';
-import ZenMode from '../../../zen_mode';
-import deleteWikiModal from './components/delete_wiki_modal.vue';
-import wikiAlert from './components/wiki_alert.vue';
-import wikiForm from './components/wiki_form.vue';
-import Wikis from './wikis';
-
-const createModalVueApp = () => {
- new Wikis(); // eslint-disable-line no-new
- new ShortcutsWiki(); // eslint-disable-line no-new
- new ZenMode(); // eslint-disable-line no-new
- new GLForm($('.wiki-form')); // eslint-disable-line no-new
-
- const deleteWikiModalWrapperEl = document.getElementById('delete-wiki-modal-wrapper');
-
- if (deleteWikiModalWrapperEl) {
- Vue.use(Translate);
-
- const { deleteWikiUrl, pageTitle } = deleteWikiModalWrapperEl.dataset;
-
- // eslint-disable-next-line no-new
- new Vue({
- el: deleteWikiModalWrapperEl,
- data() {
- return {
- deleteWikiUrl: '',
- };
- },
- render(createElement) {
- return createElement(deleteWikiModal, {
- props: {
- pageTitle,
- deleteWikiUrl,
- csrfToken: csrf.token,
- },
- });
- },
- });
- }
-};
-
-const createAlertVueApp = () => {
- const el = document.getElementById('js-wiki-error');
- if (el) {
- const { error, wikiPagePath } = el.dataset;
-
- // eslint-disable-next-line no-new
- new Vue({
- el,
- render(createElement) {
- return createElement(wikiAlert, {
- props: {
- error,
- wikiPagePath,
- },
- });
- },
- });
- }
-};
-
-const createWikiFormApp = () => {
- const el = document.getElementById('js-wiki-form');
-
- if (el) {
- const { pageInfo, formatOptions } = el.dataset;
-
- // eslint-disable-next-line no-new
- new Vue({
- el,
- provide: {
- formatOptions: JSON.parse(formatOptions),
- pageInfo: convertObjectPropsToCamelCase(JSON.parse(pageInfo)),
- },
- render(createElement) {
- return createElement(wikiForm);
- },
- });
- }
-};
-
-export default () => {
- createModalVueApp();
- createAlertVueApp();
- createWikiFormApp();
-};
diff --git a/app/assets/javascripts/pages/shared/wikis/wikis.js b/app/assets/javascripts/pages/shared/wikis/wikis.js
index 7d0b0c90c8..8d0105bc68 100644
--- a/app/assets/javascripts/pages/shared/wikis/wikis.js
+++ b/app/assets/javascripts/pages/shared/wikis/wikis.js
@@ -1,6 +1,7 @@
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import Tracking from '~/tracking';
import showToast from '~/vue_shared/plugins/global_toast';
+import ShortcutsWiki from '~/behaviors/shortcuts/shortcuts_wiki';
const TRACKING_EVENT_NAME = 'view_wiki_page';
const TRACKING_CONTEXT_SCHEMA = 'iglu:com.gitlab/wiki_page_context/jsonschema/1-0-1';
@@ -20,6 +21,7 @@ export default class Wikis {
Wikis.trackPageView();
Wikis.showToasts();
+ Wikis.initShortcuts();
}
handleToggleSidebar(e) {
@@ -64,4 +66,8 @@ export default class Wikis {
const toasts = document.querySelectorAll('.js-toast-message');
toasts.forEach((toast) => showToast(toast.dataset.message));
}
+
+ static initShortcuts() {
+ new ShortcutsWiki(); // eslint-disable-line no-new
+ }
}
diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
index f204f0ebfa..ed30198244 100644
--- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
+++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
@@ -1,6 +1,7 @@
+
+
+
+
+
+
diff --git a/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue b/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue
index ec240854be..a1fa214799 100644
--- a/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue
+++ b/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue
@@ -10,6 +10,8 @@ import {
toggleQueryPollingByVisibility,
} from '~/pipelines/components/graph/utils';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import PipelineEditorMiniGraph from './pipeline_editor_mini_graph.vue';
const POLL_INTERVAL = 10000;
export const i18n = {
@@ -30,7 +32,9 @@ export default {
GlLink,
GlLoadingIcon,
GlSprintf,
+ PipelineEditorMiniGraph,
},
+ mixins: [glFeatureFlagMixin()],
inject: ['projectFullPath'],
props: {
commitSha: {
@@ -55,12 +59,15 @@ export default {
};
},
update(data) {
- const { id, commitPath = '', detailedStatus = {} } = data.project?.pipeline || {};
+ const { id, commitPath = '', detailedStatus = {}, stages, status } =
+ data.project?.pipeline || {};
return {
id,
commitPath,
detailedStatus,
+ stages,
+ status,
};
},
result(res) {
@@ -111,9 +118,7 @@ export default {
-
+
@@ -129,19 +134,12 @@ export default {
-
+
-
- {{ content }}{{ pipelineId }}
+ {{ content }}{{ pipelineId }}
{{ status.text }}
@@ -157,8 +155,13 @@ export default {
-
+
+
-
+
{{ $options.i18n.btnText }}
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline.graphql
index d3a7387ad2..0c3653a288 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline.graphql
+++ b/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline.graphql
@@ -11,6 +11,25 @@ query getPipeline($fullPath: ID!, $sha: String!) {
group
text
}
+ stages {
+ edges {
+ node {
+ id
+ name
+ status
+ detailedStatus {
+ detailsPath
+ group
+ hasDetails
+ icon
+ id
+ label
+ text
+ tooltip
+ }
+ }
+ }
+ }
}
}
}
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
index 4324c64ab3..ba56702394 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
@@ -1,5 +1,4 @@
+
+
+
+ {{ rootUrl }}
+
+
+
+
+
+ {{ __('Groups') }}
+
+ {{ group.fullPath }}
+
+
+
+ {{ __('Users') }}
+
+ {{ userNamespace.fullPath }}
+
+
+ {{ __('No matches found') }}
+
+
+
+
+
+
diff --git a/app/assets/javascripts/projects/new/event_hub.js b/app/assets/javascripts/projects/new/event_hub.js
new file mode 100644
index 0000000000..e31806ad19
--- /dev/null
+++ b/app/assets/javascripts/projects/new/event_hub.js
@@ -0,0 +1,3 @@
+import createEventHub from '~/helpers/event_hub_factory';
+
+export default createEventHub();
diff --git a/app/assets/javascripts/projects/new/index.js b/app/assets/javascripts/projects/new/index.js
new file mode 100644
index 0000000000..572d3276e4
--- /dev/null
+++ b/app/assets/javascripts/projects/new/index.js
@@ -0,0 +1,66 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import NewProjectCreationApp from './components/app.vue';
+import NewProjectUrlSelect from './components/new_project_url_select.vue';
+
+export function initNewProjectCreation() {
+ const el = document.querySelector('.js-new-project-creation');
+
+ const {
+ pushToCreateProjectCommand,
+ workingWithProjectsHelpPath,
+ newProjectGuidelines,
+ hasErrors,
+ isCiCdAvailable,
+ } = el.dataset;
+
+ const props = {
+ hasErrors: parseBoolean(hasErrors),
+ isCiCdAvailable: parseBoolean(isCiCdAvailable),
+ newProjectGuidelines,
+ };
+
+ const provide = {
+ workingWithProjectsHelpPath,
+ pushToCreateProjectCommand,
+ };
+
+ return new Vue({
+ el,
+ provide,
+ render(h) {
+ return h(NewProjectCreationApp, { props });
+ },
+ });
+}
+
+export function initNewProjectUrlSelect() {
+ const elements = document.querySelectorAll('.js-vue-new-project-url-select');
+
+ if (!elements.length) {
+ return;
+ }
+
+ Vue.use(VueApollo);
+
+ elements.forEach(
+ (el) =>
+ new Vue({
+ el,
+ apolloProvider: new VueApollo({
+ defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
+ }),
+ provide: {
+ namespaceFullPath: el.dataset.namespaceFullPath,
+ namespaceId: el.dataset.namespaceId,
+ rootUrl: el.dataset.rootUrl,
+ trackLabel: el.dataset.trackLabel,
+ userNamespaceFullPath: el.dataset.userNamespaceFullPath,
+ userNamespaceId: el.dataset.userNamespaceId,
+ },
+ render: (createElement) => createElement(NewProjectUrlSelect),
+ }),
+ );
+}
diff --git a/app/assets/javascripts/pages/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql b/app/assets/javascripts/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql
similarity index 100%
rename from app/assets/javascripts/pages/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql
rename to app/assets/javascripts/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql
diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js
index ebd20583a1..b350db0c83 100644
--- a/app/assets/javascripts/projects/project_new.js
+++ b/app/assets/javascripts/projects/project_new.js
@@ -1,5 +1,7 @@
import $ from 'jquery';
+import { debounce } from 'lodash';
import DEFAULT_PROJECT_TEMPLATES from 'ee_else_ce/projects/default_project_templates';
+import axios from '../lib/utils/axios_utils';
import {
convertToTitleCase,
humanize,
@@ -9,6 +11,23 @@ import {
let hasUserDefinedProjectPath = false;
let hasUserDefinedProjectName = false;
+const invalidInputClass = 'gl-field-error-outline';
+
+const validateImportCredentials = (url, user, password) => {
+ const endpoint = `${gon.relative_url_root}/import/url/validate`;
+ return axios
+ .post(endpoint, {
+ url,
+ user,
+ password,
+ })
+ .then(({ data }) => data)
+ .catch(() => ({
+ // intentionally reporting success in case of validation error
+ // we do not want to block users from trying import in case of validation exception
+ success: true,
+ }));
+};
const onProjectNameChange = ($projectNameInput, $projectPathInput) => {
const slug = slugify(convertUnicodeToAscii($projectNameInput.val()));
@@ -85,7 +104,10 @@ const bindHowToImport = () => {
const bindEvents = () => {
const $newProjectForm = $('#new_project');
const $projectImportUrl = $('#project_import_url');
- const $projectImportUrlWarning = $('.js-import-url-warning');
+ const $projectImportUrlUser = $('#project_import_url_user');
+ const $projectImportUrlPassword = $('#project_import_url_password');
+ const $projectImportUrlError = $('.js-import-url-error');
+ const $projectImportForm = $('.project-import form');
const $projectPath = $('.tab-pane.active #project_path');
const $useTemplateBtn = $('.template-button > input');
const $projectFieldsForm = $('.project-fields-form');
@@ -139,12 +161,15 @@ const bindEvents = () => {
$projectPath.val($projectPath.val().trim());
});
- function updateUrlPathWarningVisibility() {
- const url = $projectImportUrl.val();
- const URL_PATTERN = /(?:git|https?):\/\/.*\/.*\.git$/;
- const isUrlValid = URL_PATTERN.test(url);
- $projectImportUrlWarning.toggleClass('hide', isUrlValid);
- }
+ const updateUrlPathWarningVisibility = debounce(async () => {
+ const { success: isUrlValid } = await validateImportCredentials(
+ $projectImportUrl.val(),
+ $projectImportUrlUser.val(),
+ $projectImportUrlPassword.val(),
+ );
+ $projectImportUrl.toggleClass(invalidInputClass, !isUrlValid);
+ $projectImportUrlError.toggleClass('hide', isUrlValid);
+ }, 500);
let isProjectImportUrlDirty = false;
$projectImportUrl.on('blur', () => {
@@ -153,9 +178,22 @@ const bindEvents = () => {
});
$projectImportUrl.on('keyup', () => {
deriveProjectPathFromUrl($projectImportUrl);
- // defer error message till first input blur
- if (isProjectImportUrlDirty) {
- updateUrlPathWarningVisibility();
+ });
+
+ [$projectImportUrl, $projectImportUrlUser, $projectImportUrlPassword].forEach(($f) => {
+ $f.on('input', () => {
+ if (isProjectImportUrlDirty) {
+ updateUrlPathWarningVisibility();
+ }
+ });
+ });
+
+ $projectImportForm.on('submit', (e) => {
+ const $invalidFields = $projectImportForm.find(`.${invalidInputClass}`);
+ if ($invalidFields.length > 0) {
+ $invalidFields[0].focus();
+ e.preventDefault();
+ e.stopPropagation();
}
});
diff --git a/app/assets/javascripts/projects/settings/access_dropdown.js b/app/assets/javascripts/projects/settings/access_dropdown.js
index a5e53ee392..7fb7a416dc 100644
--- a/app/assets/javascripts/projects/settings/access_dropdown.js
+++ b/app/assets/javascripts/projects/settings/access_dropdown.js
@@ -2,8 +2,8 @@
import { escape, find, countBy } from 'lodash';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import createFlash from '~/flash';
-import axios from '~/lib/utils/axios_utils';
import { n__, s__, __, sprintf } from '~/locale';
+import { getUsers, getGroups, getDeployKeys } from './api/access_dropdown_api';
import { LEVEL_TYPES, LEVEL_ID_PROP, ACCESS_LEVELS, ACCESS_LEVEL_NONE } from './constants';
export default class AccessDropdown {
@@ -16,9 +16,6 @@ export default class AccessDropdown {
this.accessLevelsData = accessLevelsData.roles;
this.$dropdown = $dropdown;
this.$wrap = this.$dropdown.closest(`.${this.accessLevel}-container`);
- this.usersPath = '/-/autocomplete/users.json';
- this.groupsPath = '/-/autocomplete/project_groups.json';
- this.deployKeysPath = '/-/autocomplete/deploy_keys_with_owners.json';
this.defaultLabel = this.$dropdown.data('defaultLabel');
this.setSelectedItems([]);
@@ -318,9 +315,9 @@ export default class AccessDropdown {
getData(query, callback) {
if (this.hasLicense) {
Promise.all([
- this.getDeployKeys(query),
- this.getUsers(query),
- this.groupsData ? Promise.resolve(this.groupsData) : this.getGroups(),
+ getDeployKeys(query),
+ getUsers(query),
+ this.groupsData ? Promise.resolve(this.groupsData) : getGroups(),
])
.then(([deployKeysResponse, usersResponse, groupsResponse]) => {
this.groupsData = groupsResponse;
@@ -332,7 +329,7 @@ export default class AccessDropdown {
createFlash({ message: __('Failed to load groups, users and deploy keys.') });
});
} else {
- this.getDeployKeys(query)
+ getDeployKeys(query)
.then((deployKeysResponse) => callback(this.consolidateData(deployKeysResponse.data)))
.catch(() => createFlash({ message: __('Failed to load deploy keys.') }));
}
@@ -473,46 +470,6 @@ export default class AccessDropdown {
return consolidatedData;
}
- getUsers(query) {
- return axios.get(this.buildUrl(gon.relative_url_root, this.usersPath), {
- params: {
- search: query,
- per_page: 20,
- active: true,
- project_id: gon.current_project_id,
- push_code: true,
- },
- });
- }
-
- getGroups() {
- return axios.get(this.buildUrl(gon.relative_url_root, this.groupsPath), {
- params: {
- project_id: gon.current_project_id,
- },
- });
- }
-
- getDeployKeys(query) {
- return axios.get(this.buildUrl(gon.relative_url_root, this.deployKeysPath), {
- params: {
- search: query,
- per_page: 20,
- active: true,
- project_id: gon.current_project_id,
- push_code: true,
- },
- });
- }
-
- buildUrl(urlRoot, url) {
- let newUrl;
- if (urlRoot != null) {
- newUrl = urlRoot.replace(/\/$/, '') + url;
- }
- return newUrl;
- }
-
renderRow(item) {
let criteria = {};
let groupRowEl;
diff --git a/app/assets/javascripts/projects/settings/api/access_dropdown_api.js b/app/assets/javascripts/projects/settings/api/access_dropdown_api.js
new file mode 100644
index 0000000000..10f6c28a7b
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/api/access_dropdown_api.js
@@ -0,0 +1,45 @@
+import axios from '~/lib/utils/axios_utils';
+
+const USERS_PATH = '/-/autocomplete/users.json';
+const GROUPS_PATH = '/-/autocomplete/project_groups.json';
+const DEPLOY_KEYS_PATH = '/-/autocomplete/deploy_keys_with_owners.json';
+
+const buildUrl = (urlRoot, url) => {
+ let newUrl;
+ if (urlRoot != null) {
+ newUrl = urlRoot.replace(/\/$/, '') + url;
+ }
+ return newUrl;
+};
+
+export const getUsers = (query) => {
+ return axios.get(buildUrl(gon.relative_url_root || '', USERS_PATH), {
+ params: {
+ search: query,
+ per_page: 20,
+ active: true,
+ project_id: gon.current_project_id,
+ push_code: true,
+ },
+ });
+};
+
+export const getGroups = () => {
+ return axios.get(buildUrl(gon.relative_url_root || '', GROUPS_PATH), {
+ params: {
+ project_id: gon.current_project_id,
+ },
+ });
+};
+
+export const getDeployKeys = (query) => {
+ return axios.get(buildUrl(gon.relative_url_root || '', DEPLOY_KEYS_PATH), {
+ params: {
+ search: query,
+ per_page: 20,
+ active: true,
+ project_id: gon.current_project_id,
+ push_code: true,
+ },
+ });
+};
diff --git a/app/assets/javascripts/projects/settings/components/access_dropdown.vue b/app/assets/javascripts/projects/settings/components/access_dropdown.vue
new file mode 100644
index 0000000000..9823b0229a
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/components/access_dropdown.vue
@@ -0,0 +1,409 @@
+
+
+
+
+
+
+
+
+ {{
+ $options.i18n.rolesSectionHeader
+ }}
+
+ {{ role.text }}
+
+
+
+
+
+ {{
+ $options.i18n.groupsSectionHeader
+ }}
+
+ {{ group.name }}
+
+
+
+
+
+ {{
+ $options.i18n.usersSectionHeader
+ }}
+
+ {{ user.name }}
+
+
+
+
+
+ {{
+ $options.i18n.deployKeysSectionHeader
+ }}
+
+ {{ key.title }}
+
+
+
+
+ {{ key.fullname }} ({{ key.username }})
+
+
+
+
+
diff --git a/app/assets/javascripts/projects/settings/init_access_dropdown.js b/app/assets/javascripts/projects/settings/init_access_dropdown.js
new file mode 100644
index 0000000000..11272652b6
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/init_access_dropdown.js
@@ -0,0 +1,39 @@
+import * as Sentry from '@sentry/browser';
+import Vue from 'vue';
+import AccessDropdown from './components/access_dropdown.vue';
+
+export const initAccessDropdown = (el, options) => {
+ if (!el) {
+ return false;
+ }
+
+ const { accessLevelsData, accessLevel } = options;
+ const { label, disabled, preselectedItems } = el.dataset;
+ let preselected = [];
+ try {
+ preselected = JSON.parse(preselectedItems);
+ } catch (e) {
+ Sentry.captureException(e);
+ }
+
+ return new Vue({
+ el,
+ render(createElement) {
+ const vm = this;
+ return createElement(AccessDropdown, {
+ props: {
+ accessLevel,
+ accessLevelsData: accessLevelsData.roles,
+ preselectedItems: preselected,
+ label,
+ disabled,
+ },
+ on: {
+ select(selected) {
+ vm.$emit('select', selected);
+ },
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/prometheus_alerts/components/reset_key.vue b/app/assets/javascripts/prometheus_alerts/components/reset_key.vue
index eecb357304..befbca4873 100644
--- a/app/assets/javascripts/prometheus_alerts/components/reset_key.vue
+++ b/app/assets/javascripts/prometheus_alerts/components/reset_key.vue
@@ -1,8 +1,16 @@
-