diff --git a/app/assets/javascripts/entrypoints/graphql_explorer.js b/app/assets/javascripts/entrypoints/graphql_explorer.js index 801b3c8056a0e5ebcfebcc49422f5b713150a557..6a5b8ddea671b4cf8abf75a317b87b07016e9c58 100644 --- a/app/assets/javascripts/entrypoints/graphql_explorer.js +++ b/app/assets/javascripts/entrypoints/graphql_explorer.js @@ -1,14 +1,16 @@ import '~/webpack'; - import gitlabLogo from '@gitlab/svgs/dist/illustrations/gitlab_logo.svg?raw'; import { gql } from '@apollo/client'; import { GraphiQL } from 'graphiql'; /* eslint-disable no-restricted-imports */ import React from 'react'; import { createRoot } from 'react-dom/client'; +import { Mousetrap } from '~/lib/mousetrap'; import { DOCS_URL_IN_EE_DIR } from 'jh_else_ce/lib/utils/url_utility'; /* eslint-enable no-restricted-imports */ import createDefaultClient, { fetchPolicies } from '~/lib/graphql'; +import { keysFor, TOGGLE_PERFORMANCE_BAR } from '~/behaviors/shortcuts/keybindings'; +import Shortcuts from '~/behaviors/shortcuts/shortcuts'; const apolloClient = createDefaultClient( {}, @@ -107,3 +109,5 @@ function apolloFetcher(graphQLParams, { headers }) { createRoot(graphiqlContainer).render( React.createElement(GraphiQL, { defaultQuery, fetcher: apolloFetcher }, GraphiQLLogo), ); + +Mousetrap.bind(keysFor(TOGGLE_PERFORMANCE_BAR), Shortcuts.onTogglePerfBar); diff --git a/app/assets/javascripts/entrypoints/performance_bar.js b/app/assets/javascripts/entrypoints/performance_bar.js index d1be41cbed10093677de21cf61db6e94cb24c41e..de7dce8f697a48adbd9620688e90853a75389063 100644 --- a/app/assets/javascripts/entrypoints/performance_bar.js +++ b/app/assets/javascripts/entrypoints/performance_bar.js @@ -1,3 +1,5 @@ import '~/webpack'; import '~/commons'; -import '~/performance_bar'; +import initPerformanceBarAndLog from '~/performance_bar'; + +initPerformanceBarAndLog(); diff --git a/app/assets/javascripts/performance_bar/components/detailed_metric.vue b/app/assets/javascripts/performance_bar/components/detailed_metric.vue index c62c3b5e17d59ccbcd5f6289b06fa11199b5dbf3..00acc3d263c8321da169cdc127e4f7502e2032e8 100644 --- a/app/assets/javascripts/performance_bar/components/detailed_metric.vue +++ b/app/assets/javascripts/performance_bar/components/detailed_metric.vue @@ -166,6 +166,7 @@ export default { :title="header" size="lg" scrollable + :static="true" hide-backdrop hide-footer no-focus-on-show diff --git a/app/assets/javascripts/performance_bar/components/info_modal/info_app.vue b/app/assets/javascripts/performance_bar/components/info_modal/info_app.vue index a16474bbc5002df7dd7c2f4de12f6bd2874216f0..ecdabda93f3c24fd2afcfc470c02e599d0b2af95 100644 --- a/app/assets/javascripts/performance_bar/components/info_modal/info_app.vue +++ b/app/assets/javascripts/performance_bar/components/info_modal/info_app.vue @@ -51,6 +51,7 @@ export default { modal-id="environment-info" :title="s__('PerformanceBar|Debugging information')" size="sm" + :static="true" hide-backdrop hide-footer no-focus-on-show diff --git a/app/assets/javascripts/performance_bar/index.js b/app/assets/javascripts/performance_bar/index.js index 77047e126683f15705958acbef668a1e49f005a7..11f9c2ee75360625256fb20e1b2022c4b78c0e25 100644 --- a/app/assets/javascripts/performance_bar/index.js +++ b/app/assets/javascripts/performance_bar/index.js @@ -5,6 +5,7 @@ import { numberToHumanSize } from '~/lib/utils/number_utils'; import { s__ } from '~/locale'; import Translate from '~/vue_shared/translate'; +import { transformRootToHostRules } from '~/performance_bar/transform_root_to_host_rules'; import initPerformanceBarLog from './performance_bar_log'; import PerformanceBarService from './services/performance_bar_service'; import PerformanceBarStore from './stores/performance_bar_store'; @@ -163,7 +164,18 @@ const initPerformanceBar = (el) => { }); }; -initPerformanceBar(document.querySelector('#js-peek')); -initPerformanceBarLog(); +export function getPerformanceBarNode() { + const shadowDomRoot = document.querySelector('#performance-bar-root')?.shadowRoot; + if (shadowDomRoot) { + transformRootToHostRules(shadowDomRoot); + return shadowDomRoot.querySelector('#js-peek'); + } + return document.querySelector('#js-peek'); +} + +export default function initPerformanceBarAndLog() { + const app = initPerformanceBar(getPerformanceBarNode()); + initPerformanceBarLog(); -export default initPerformanceBar; + return app; +} diff --git a/app/assets/javascripts/performance_bar/transform_root_to_host_rules.js b/app/assets/javascripts/performance_bar/transform_root_to_host_rules.js new file mode 100644 index 0000000000000000000000000000000000000000..20863001e0995b44c492e167a25fc6d65bcb3c4d --- /dev/null +++ b/app/assets/javascripts/performance_bar/transform_root_to_host_rules.js @@ -0,0 +1,19 @@ +export function transformRootToHostRules(shadowDomRoot) { + if (!shadowDomRoot?.styleSheets?.length) { + return; + } + let cssText = ''; + + for (const sheet of shadowDomRoot.styleSheets) { + for (const rule of sheet.cssRules) { + if (rule instanceof CSSStyleRule && [':root', 'body'].includes(rule.selectorText)) { + cssText += `:host {${rule.style.cssText}}\n`; + } + } + } + + const newStyleSheet = new CSSStyleSheet(); + newStyleSheet.replaceSync(cssText); + + shadowDomRoot.adoptedStyleSheets.push(newStyleSheet); +} diff --git a/app/assets/stylesheets/page_bundles/graphql_explorer.scss b/app/assets/stylesheets/page_bundles/graphql_explorer.scss index fd9e259b9c8d5d44094cb42dbe1ed6d4acf65213..56b47c19e1e9fbd99c4dca8cccf610138e2b39d2 100644 --- a/app/assets/stylesheets/page_bundles/graphql_explorer.scss +++ b/app/assets/stylesheets/page_bundles/graphql_explorer.scss @@ -1,4 +1,5 @@ @use 'graphiql/graphiql.css'; +@use 'framework/variables'; html, body, #graphiql-container { height: 100%; @@ -7,6 +8,11 @@ html, body, #graphiql-container { width: 100%; } +.with-performance-bar #graphiql-container { + padding-top: variables.$performance-bar-height; + height: calc(100% - variables.$performance-bar-height); +} + .graphiql-container .graphiql-logo { height: 24px; padding: 0 15px; diff --git a/app/controllers/api/graphql/graphql_explorer_controller.rb b/app/controllers/api/graphql/graphql_explorer_controller.rb index 8e873cd85d7d0642f2821852c9fe0fa7074a9205..5f12f07043bffef9ee48c41f4205ec0d9fef2e17 100644 --- a/app/controllers/api/graphql/graphql_explorer_controller.rb +++ b/app/controllers/api/graphql/graphql_explorer_controller.rb @@ -4,6 +4,7 @@ module API module Graphql class GraphqlExplorerController < BaseActionController include Gitlab::GonHelper + include WithPerformanceBar def show # We need gon to setup gon.relative_url_root which is used by our Apollo client diff --git a/app/views/api/graphql/graphql_explorer/show.html.haml b/app/views/api/graphql/graphql_explorer/show.html.haml index cb02422f993f2b4fd48cf300afbf9c51e8439482..71241bfdc973d2e931ee151e3046b20bca3907a3 100644 --- a/app/views/api/graphql/graphql_explorer/show.html.haml +++ b/app/views/api/graphql/graphql_explorer/show.html.haml @@ -9,5 +9,14 @@ = universal_stylesheet_link_tag 'page_bundles/graphql_explorer' = webpack_bundle_tag 'graphql_explorer' - %body + %body{ class: ('with-performance-bar' if performance_bar_enabled?) } + - if performance_bar_enabled? + #performance-bar-root + %template{ :shadowrootmode => "open" } + = render 'peek/bar' + = universal_stylesheet_link_tag 'application' + = universal_stylesheet_link_tag 'application_utilities' + = universal_stylesheet_link_tag 'tailwind' + = universal_stylesheet_link_tag 'performance_bar' + = webpack_bundle_tag 'performance_bar' #graphiql-container Loading... diff --git a/spec/frontend/performance_bar/index_spec.js b/spec/frontend/performance_bar/index_spec.js index cfc752655bd1a425db8e5e58fdfc70615869c6fe..43abd05b9bba4a981006bac2ddd75a11f6c4677c 100644 --- a/spec/frontend/performance_bar/index_spec.js +++ b/spec/frontend/performance_bar/index_spec.js @@ -1,23 +1,35 @@ -import Vue from 'vue'; import MockAdapter from 'axios-mock-adapter'; -import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; +import { resetHTMLFixture, setHTMLFixture } from 'helpers/fixtures'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; -import '~/performance_bar/components/performance_bar_app.vue'; -import performanceBar from '~/performance_bar'; +import initPerformanceBarAndLog from '~/performance_bar'; import PerformanceBarService from '~/performance_bar/services/performance_bar_service'; -Vue.config.ignoredElements = ['gl-emoji']; - jest.mock('~/performance_bar/performance_bar_log'); -describe('performance bar wrapper', () => { +function setupNormalDOM() { + setHTMLFixture('<div id="js-peek"></div>'); + return document.getElementById('js-peek'); +} + +function setupShadowDOM() { + setHTMLFixture(`<div id="performance-bar-root"></div>`); + const host = document.querySelector('#performance-bar-root'); + const shadow = host.attachShadow({ mode: 'open' }); + const peekWrapper = document.createElement('div'); + shadow.appendChild(peekWrapper); + return peekWrapper; +} + +describe.each([ + ['normal DOM', setupNormalDOM], + ['shadow DOM', setupShadowDOM], +])('Performance Bar – Using %s', (_, setupFn) => { let mock; let vm; beforeEach(() => { - setHTMLFixture('<div id="js-peek"></div>'); - const peekWrapper = document.getElementById('js-peek'); + const peekWrapper = setupFn(); performance.getEntriesByType = jest.fn().mockReturnValue([]); peekWrapper.setAttribute('id', 'js-peek'); @@ -47,12 +59,13 @@ describe('performance bar wrapper', () => { {}, ); - vm = performanceBar(peekWrapper); + vm = initPerformanceBarAndLog(); }); afterEach(() => { vm.$destroy(); - document.getElementById('js-peek').remove(); + document?.getElementById('performance-bar-root')?.remove?.(); + document?.getElementById('js-peek')?.remove?.(); mock.restore(); resetHTMLFixture(); });