From 43a25d93ebdabea52f99b05e15b06250cd8f07d7 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Wed, 17 May 2023 16:05:49 +0000 Subject: Add latest changes from gitlab-org/gitlab@16-0-stable-ee --- .../lib/apollo/indexed_db_persistent_storage.js | 97 ++++++++++++++ app/assets/javascripts/lib/apollo/local_db.js | 14 ++ app/assets/javascripts/lib/graphql.js | 69 +++++++--- app/assets/javascripts/lib/mermaid.js | 11 +- app/assets/javascripts/lib/mousetrap.js | 59 +++++++++ app/assets/javascripts/lib/swagger.js | 9 ++ app/assets/javascripts/lib/utils/chart_utils.js | 38 ++++++ app/assets/javascripts/lib/utils/color_utils.js | 28 +--- .../utils/confirm_via_gl_modal/confirm_action.js | 2 + .../utils/confirm_via_gl_modal/confirm_modal.vue | 7 +- app/assets/javascripts/lib/utils/constants.js | 5 + app/assets/javascripts/lib/utils/css_utils.js | 25 ++++ .../javascripts/lib/utils/datetime/constants.js | 7 + .../lib/utils/datetime/date_format_utility.js | 40 ++---- .../lib/utils/datetime/time_spent_utility.js | 13 ++ .../lib/utils/datetime/timeago_utility.js | 40 ++++-- .../javascripts/lib/utils/datetime_utility.js | 2 + app/assets/javascripts/lib/utils/dom_utils.js | 2 +- app/assets/javascripts/lib/utils/error_message.js | 15 +++ app/assets/javascripts/lib/utils/file_upload.js | 7 + app/assets/javascripts/lib/utils/keys.js | 4 + app/assets/javascripts/lib/utils/number_utils.js | 49 +++++-- app/assets/javascripts/lib/utils/ref_validator.js | 145 +++++++++++++++++++++ .../javascripts/lib/utils/resize_observer.js | 10 +- .../javascripts/lib/utils/secret_detection.js | 45 +++++++ app/assets/javascripts/lib/utils/sticky.js | 60 --------- .../javascripts/lib/utils/tappable_promise.js | 49 +++++++ app/assets/javascripts/lib/utils/text_markdown.js | 57 +++++++- app/assets/javascripts/lib/utils/text_utility.js | 45 ++++++- app/assets/javascripts/lib/utils/url_utility.js | 40 +++--- .../utils/vue3compat/compat_functional_mixin.js | 14 ++ .../vue3compat/get_instance_from_directive.js | 9 ++ .../javascripts/lib/utils/vue3compat/mark_raw.js | 9 ++ .../lib/utils/vue3compat/normalize_children.js | 11 ++ .../javascripts/lib/utils/vue3compat/vue_apollo.js | 78 +++++++++++ .../javascripts/lib/utils/vue3compat/vue_router.js | 115 ++++++++++++++++ .../javascripts/lib/utils/vue3compat/vuex.js | 38 ++++++ .../javascripts/lib/utils/web_ide_navigator.js | 24 ++++ 38 files changed, 1110 insertions(+), 182 deletions(-) create mode 100644 app/assets/javascripts/lib/apollo/indexed_db_persistent_storage.js create mode 100644 app/assets/javascripts/lib/apollo/local_db.js create mode 100644 app/assets/javascripts/lib/mousetrap.js create mode 100644 app/assets/javascripts/lib/utils/datetime/constants.js create mode 100644 app/assets/javascripts/lib/utils/datetime/time_spent_utility.js create mode 100644 app/assets/javascripts/lib/utils/error_message.js create mode 100644 app/assets/javascripts/lib/utils/ref_validator.js create mode 100644 app/assets/javascripts/lib/utils/secret_detection.js delete mode 100644 app/assets/javascripts/lib/utils/sticky.js create mode 100644 app/assets/javascripts/lib/utils/tappable_promise.js create mode 100644 app/assets/javascripts/lib/utils/vue3compat/compat_functional_mixin.js create mode 100644 app/assets/javascripts/lib/utils/vue3compat/get_instance_from_directive.js create mode 100644 app/assets/javascripts/lib/utils/vue3compat/mark_raw.js create mode 100644 app/assets/javascripts/lib/utils/vue3compat/normalize_children.js create mode 100644 app/assets/javascripts/lib/utils/vue3compat/vue_apollo.js create mode 100644 app/assets/javascripts/lib/utils/vue3compat/vue_router.js create mode 100644 app/assets/javascripts/lib/utils/vue3compat/vuex.js create mode 100644 app/assets/javascripts/lib/utils/web_ide_navigator.js (limited to 'app/assets/javascripts/lib') diff --git a/app/assets/javascripts/lib/apollo/indexed_db_persistent_storage.js b/app/assets/javascripts/lib/apollo/indexed_db_persistent_storage.js new file mode 100644 index 00000000000..5d2a002bf85 --- /dev/null +++ b/app/assets/javascripts/lib/apollo/indexed_db_persistent_storage.js @@ -0,0 +1,97 @@ +/* eslint-disable no-underscore-dangle */ +/* eslint-disable class-methods-use-this */ +import { db } from './local_db'; + +/** + * IndexedDB implementation of apollo-cache-persist [PersistentStorage][1] + * + * [1]: https://github.com/apollographql/apollo-cache-persist/blob/d536c741d1f2828a0ef9abda343a9186dd8dbff2/src/types/index.ts#L15 + */ +export class IndexedDBPersistentStorage { + static async create() { + await db.open(); + + return new IndexedDBPersistentStorage(); + } + + async getItem(queryId) { + const resultObj = {}; + const selectedQuery = await db.table('queries').get(queryId); + const tableNames = new Set(db.tables.map((table) => table.name)); + + if (selectedQuery) { + resultObj.ROOT_QUERY = selectedQuery; + + const lookupTable = []; + + const parseObjectsForRef = async (selObject) => { + const ops = Object.values(selObject).map(async (child) => { + if (!child) { + return; + } + + if (child.__ref) { + const pathId = child.__ref; + const [refType, ...refKeyParts] = pathId.split(':'); + const refKey = refKeyParts.join(':'); + + if ( + !resultObj[pathId] && + !lookupTable.includes(pathId) && + tableNames.has(refType.toLowerCase()) + ) { + lookupTable.push(pathId); + const selectedEntity = await db.table(refType.toLowerCase()).get(refKey); + if (selectedEntity) { + await parseObjectsForRef(selectedEntity); + resultObj[pathId] = selectedEntity; + } + } + } else if (typeof child === 'object') { + await parseObjectsForRef(child); + } + }); + + return Promise.all(ops); + }; + + await parseObjectsForRef(resultObj.ROOT_QUERY); + } + + return resultObj; + } + + async setItem(key, value) { + await this.#setQueryResults(key, JSON.parse(value)); + } + + async removeItem() { + // apollo-cache-persist only ever calls this when we're removing everything, so let's blow it all away + // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113745#note_1329175993 + + await Promise.all( + db.tables.map((table) => { + return table.clear(); + }), + ); + } + + async #setQueryResults(queryId, results) { + await Promise.all( + Object.keys(results).map((id) => { + const objectType = id.split(':')[0]; + if (objectType === 'ROOT_QUERY') { + return db.table('queries').put(results[id], queryId); + } + const key = objectType.toLowerCase(); + const tableExists = db.tables.some((table) => table.name === key); + if (tableExists) { + return db.table(key).put(results[id], id); + } + return new Promise((resolve) => { + resolve(); + }); + }), + ); + } +} diff --git a/app/assets/javascripts/lib/apollo/local_db.js b/app/assets/javascripts/lib/apollo/local_db.js new file mode 100644 index 00000000000..cda30ff9d42 --- /dev/null +++ b/app/assets/javascripts/lib/apollo/local_db.js @@ -0,0 +1,14 @@ +/* eslint-disable @gitlab/require-i18n-strings */ +import Dexie from 'dexie'; + +export const db = new Dexie('GLLocalCache'); +db.version(1).stores({ + pages: 'url, timestamp', + queries: '', + project: 'id', + group: 'id', + usercore: 'id', + issue: 'id, state, title', + label: 'id, title', + milestone: 'id', +}); diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js index c0e923b2670..a4c13f9e40e 100644 --- a/app/assets/javascripts/lib/graphql.js +++ b/app/assets/javascripts/lib/graphql.js @@ -1,7 +1,7 @@ import { ApolloClient, InMemoryCache, ApolloLink, HttpLink } from '@apollo/client/core'; import { BatchHttpLink } from '@apollo/client/link/batch-http'; import { createUploadLink } from 'apollo-upload-client'; -import { persistCacheSync, LocalStorageWrapper } from 'apollo3-cache-persist'; +import { persistCache } from 'apollo3-cache-persist'; import ActionCableLink from '~/actioncable_link'; import { apolloCaptchaLink } from '~/captcha/apollo_captcha_link'; import possibleTypes from '~/graphql_shared/possible_types.json'; @@ -53,6 +53,15 @@ export const typePolicies = { TreeEntry: { keyFields: ['webPath'], }, + Subscription: { + fields: { + aiCompletionResponse: { + read(value) { + return value ?? null; + }, + }, + }, + }, }; export const stripWhitespaceFromQuery = (url, path) => { @@ -104,7 +113,7 @@ Object.defineProperty(window, 'pendingApolloRequests', { }, }); -export default (resolvers = {}, config = {}) => { +function createApolloClient(resolvers = {}, config = {}) { const { baseUrl, batchMax = 10, @@ -113,8 +122,10 @@ export default (resolvers = {}, config = {}) => { typeDefs, path = '/api/graphql', useGet = false, - localCacheKey = null, } = config; + + const shouldUnbatch = gon.features?.unbatchGraphqlQueries; + let ac = null; let uri = `${gon.relative_url_root || ''}${path}`; @@ -152,7 +163,7 @@ export default (resolvers = {}, config = {}) => { }; const requestLink = ApolloLink.split( - () => useGet, + () => useGet || shouldUnbatch, new HttpLink({ ...httpOptions, fetch: fetchIntervention }), new BatchHttpLink(httpOptions), ); @@ -171,6 +182,7 @@ export default (resolvers = {}, config = {}) => { config: { url: httpResponse.url, operationName: operation.operationName, + method: operation.getContext()?.fetchOptions?.method || 'POST', // If method is not explicitly set, we default to POST request }, headers: { 'x-request-id': httpResponse.headers.get('x-request-id'), @@ -237,16 +249,6 @@ export default (resolvers = {}, config = {}) => { }, }); - if (localCacheKey) { - persistCacheSync({ - cache: newCache, - // we leave NODE_ENV here temporarily for visibility so developers can easily see caching happening in dev mode - debug: process.env.NODE_ENV === 'development', - storage: new LocalStorageWrapper(window.localStorage), - persistenceMapper, - }); - } - ac = new ApolloClient({ typeDefs, link: appLink, @@ -262,5 +264,42 @@ export default (resolvers = {}, config = {}) => { acs.push(ac); - return ac; + return { client: ac, cache: newCache }; +} + +export async function createApolloClientWithCaching(resolvers = {}, config = {}) { + const { localCacheKey = null } = config; + const { client, cache } = createApolloClient(resolvers, config); + + if (localCacheKey) { + let storage; + + // Test that we can use IndexedDB. If not, no persisting for you! + try { + const { IndexedDBPersistentStorage } = await import( + /* webpackChunkName: 'indexed_db_persistent_storage' */ './apollo/indexed_db_persistent_storage' + ); + + storage = await IndexedDBPersistentStorage.create(); + } catch (error) { + return client; + } + + await persistCache({ + cache, + // we leave NODE_ENV here temporarily for visibility so developers can easily see caching happening in dev mode + debug: process.env.NODE_ENV === 'development', + storage, + key: localCacheKey, + persistenceMapper, + }); + } + + return client; +} + +export default (resolvers = {}, config = {}) => { + const { client } = createApolloClient(resolvers, config); + + return client; }; diff --git a/app/assets/javascripts/lib/mermaid.js b/app/assets/javascripts/lib/mermaid.js index c72561ce69d..bbc1d8ae1e1 100644 --- a/app/assets/javascripts/lib/mermaid.js +++ b/app/assets/javascripts/lib/mermaid.js @@ -6,17 +6,20 @@ const setIframeRenderedSize = (h, w) => { window.parent.postMessage({ h, w }, origin); }; -const drawDiagram = (source) => { +const drawDiagram = async (source) => { const element = document.getElementById('app'); const insertSvg = (svgCode) => { // eslint-disable-next-line no-unsanitized/property element.innerHTML = svgCode; - const height = parseInt(element.firstElementChild.getAttribute('height'), 10); - const width = parseInt(element.firstElementChild.style.maxWidth, 10); + element.firstElementChild.removeAttribute('height'); + const { height, width } = element.firstElementChild.getBoundingClientRect(); + setIframeRenderedSize(height, width); }; - mermaid.mermaidAPI.render('mermaid', source, insertSvg); + + const { svg } = await mermaid.mermaidAPI.render('mermaid', source); + insertSvg(svg); }; const darkModeEnabled = () => getParameterByName('darkMode') === 'true'; diff --git a/app/assets/javascripts/lib/mousetrap.js b/app/assets/javascripts/lib/mousetrap.js new file mode 100644 index 00000000000..ef3f54ec314 --- /dev/null +++ b/app/assets/javascripts/lib/mousetrap.js @@ -0,0 +1,59 @@ +// This is the only file allowed to import directly from the package. +// eslint-disable-next-line no-restricted-imports +import Mousetrap from 'mousetrap'; + +const additionalStopCallbacks = []; +const originalStopCallback = Mousetrap.prototype.stopCallback; + +Mousetrap.prototype.stopCallback = function customStopCallback(e, element, combo) { + for (const callback of additionalStopCallbacks) { + const returnValue = callback.call(this, e, element, combo); + if (returnValue !== undefined) return returnValue; + } + + return originalStopCallback.call(this, e, element, combo); +}; + +/** + * Add a stop callback to Mousetrap. + * + * This allows overriding the default behaviour of Mousetrap#stopCallback, + * which is to stop the bound key handler/callback from being called if the key + * combo is pressed inside form fields (input, select, textareas, etc). See + * https://craig.is/killing/mice#api.stopCallback. + * + * The stopCallback registered here has the same signature as + * Mousetrap#stopCallback, with the one difference being that the callback + * should return `undefined` if it has no opinion on whether the current key + * combo should be stopped or not, and the next stop callback should be + * consulted instead. If a boolean is returned, no other stop callbacks are + * called. + * + * Note: This approach does not always work as expected when coupled with + * Mousetrap's pause plugin, which is used for enabling/disabling all keyboard + * shortcuts. That plugin assumes it's the first to execute and overwrite + * Mousetrap's `stopCallback` method, whereas to work correctly with this, it + * must execute last. This is not guaranteed or even attempted. + * + * To work correctly, we may need to reimplement the pause plugin here. + * + * @param {(e: Event, element: Element, combo: string) => boolean|undefined} + * stopCallback The additional stop callback function to add to the chain + * of stop callbacks. + * @returns {void} + */ +export const addStopCallback = (stopCallback) => { + // Unshift, since we want to iterate through them in reverse order, so that + // the most recently added handler is called first, and the original + // stopCallback method is called last. + additionalStopCallbacks.unshift(stopCallback); +}; + +/** + * Clear additionalStopCallbacks. Used only for tests. + */ +export const clearStopCallbacksForTests = () => { + additionalStopCallbacks.length = 0; +}; + +export { Mousetrap }; diff --git a/app/assets/javascripts/lib/swagger.js b/app/assets/javascripts/lib/swagger.js index ed646176604..fcdab18c623 100644 --- a/app/assets/javascripts/lib/swagger.js +++ b/app/assets/javascripts/lib/swagger.js @@ -1,6 +1,13 @@ import { SwaggerUIBundle } from 'swagger-ui-dist'; import { safeLoad } from 'js-yaml'; import { isObject } from '~/lib/utils/type_utility'; +import { getParameterByName } from '~/lib/utils/url_utility'; +import { resetServiceWorkersPublicPath } from '~/lib/utils/webpack'; + +const resetWebpackPublicPath = () => { + window.gon = { relative_url_root: getParameterByName('relativeRootPath') }; + resetServiceWorkersPublicPath(); +}; const renderSwaggerUI = (value) => { /* SwaggerUIBundle accepts openapi definition @@ -12,6 +19,8 @@ const renderSwaggerUI = (value) => { spec = safeLoad(spec, { json: true }); } + resetWebpackPublicPath(); + Promise.all([import(/* webpackChunkName: 'openapi' */ 'swagger-ui-dist/swagger-ui.css')]) .then(() => { SwaggerUIBundle({ diff --git a/app/assets/javascripts/lib/utils/chart_utils.js b/app/assets/javascripts/lib/utils/chart_utils.js index 7da3bab0a4b..520d7f627f6 100644 --- a/app/assets/javascripts/lib/utils/chart_utils.js +++ b/app/assets/javascripts/lib/utils/chart_utils.js @@ -1,3 +1,6 @@ +import { getSvgIconPathContent } from '~/lib/utils/icon_utils'; +import { __ } from '~/locale'; + const commonTooltips = () => ({ mode: 'x', intersect: false, @@ -98,3 +101,38 @@ export const firstAndLastY = (data) => { return [firstY, lastY]; }; + +const toolboxIconSvgPath = async (name) => { + return `path://${await getSvgIconPathContent(name)}`; +}; + +export const getToolboxOptions = async () => { + const promises = ['marquee-selection', 'redo', 'repeat', 'download'].map(toolboxIconSvgPath); + + try { + const [marqueeSelectionPath, redoPath, repeatPath, downloadPath] = await Promise.all(promises); + + return { + toolbox: { + feature: { + dataZoom: { + icon: { zoom: marqueeSelectionPath, back: redoPath }, + }, + restore: { + icon: repeatPath, + }, + saveAsImage: { + icon: downloadPath, + }, + }, + }, + }; + } catch (e) { + if (process.env.NODE_ENV !== 'production') { + // eslint-disable-next-line no-console + console.warn(__('SVG could not be rendered correctly: '), e); + } + + return {}; + } +}; diff --git a/app/assets/javascripts/lib/utils/color_utils.js b/app/assets/javascripts/lib/utils/color_utils.js index 3d8df4fde05..a9f4257e28b 100644 --- a/app/assets/javascripts/lib/utils/color_utils.js +++ b/app/assets/javascripts/lib/utils/color_utils.js @@ -8,7 +8,7 @@ const colorValidatorEl = document.createElement('div'); * 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. + * @param {String} colorExpression color expression rgba, hex, hsla, etc. */ export const isValidColorExpression = (colorExpression) => { colorValidatorEl.style.color = ''; @@ -17,32 +17,6 @@ export const isValidColorExpression = (colorExpression) => { return colorValidatorEl.style.color.length > 0; }; -/** - * Convert hex color to rgb array - * - * @param hex string - * @returns array|null - */ -export const hexToRgb = (hex) => { - // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") - const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; - const fullHex = hex.replace(shorthandRegex, (_m, r, g, b) => r + r + g + g + b + b); - - const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(fullHex); - return result - ? [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)] - : null; -}; - -export const textColorForBackground = (backgroundColor) => { - const [r, g, b] = hexToRgb(backgroundColor); - - if (r + g + b > 500) { - return '#333333'; - } - return '#FFFFFF'; -}; - /** * Check whether a color matches the expected hex format * diff --git a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_action.js b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_action.js index 3bfbfea7f22..a6081303bf8 100644 --- a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_action.js +++ b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_action.js @@ -12,6 +12,7 @@ export function confirmAction( modalHtmlMessage, title, hideCancel, + size, } = {}, ) { return new Promise((resolve) => { @@ -36,6 +37,7 @@ export function confirmAction( title, modalHtmlMessage, hideCancel, + size, }, on: { confirmed() { diff --git a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue index ea91ccec546..24be1485379 100644 --- a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue +++ b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue @@ -56,6 +56,11 @@ export default { required: false, default: false, }, + size: { + type: String, + required: false, + default: 'sm', + }, }, computed: { primaryAction() { @@ -103,9 +108,9 @@ export default {