diff --git a/public/i18n/en.yaml b/public/i18n/en.yaml index 464cf254c1..b23c60b329 100644 --- a/public/i18n/en.yaml +++ b/public/i18n/en.yaml @@ -217,6 +217,7 @@ command-palette: no-results-found: No results found search: remove-ns-context: Remove Namespace context + quick-navigation: Quick navigation common: ariaLabel: new-tab-link: This link will be opened in a new tab. diff --git a/src/command-pallette/CommandPaletteProvider.tsx b/src/command-pallette/CommandPaletteProvider.tsx index 0202056e3d..9fa39a538d 100644 --- a/src/command-pallette/CommandPaletteProvider.tsx +++ b/src/command-pallette/CommandPaletteProvider.tsx @@ -16,7 +16,9 @@ export const CommandPaletteProvider = ({ >(); const setShowDialog = (value: boolean) => { - const modalPresent = document.querySelector('ui5-dialog[open]'); + const modalPresent = + document.querySelector('ui5-dialog[open]') || + document.querySelector('.command-palette-ui'); // disable opening palette if other modal is present if (!modalPresent || !value) { _setShowDialog(value); @@ -37,8 +39,6 @@ export const CommandPaletteProvider = ({ setShowDialog(!showDialog); // [on Firefox] prevent opening the browser search bar via CMD/CTRL+K e.preventDefault(); - } else if (key === 'Escape') { - hide(); } }; diff --git a/src/command-pallette/CommandPalletteUI/CommandPaletteSearchBar.scss b/src/command-pallette/CommandPalletteUI/CommandPaletteSearchBar.scss new file mode 100644 index 0000000000..fb60f445bf --- /dev/null +++ b/src/command-pallette/CommandPalletteUI/CommandPaletteSearchBar.scss @@ -0,0 +1,13 @@ +.command-palette-search-bar { + width: var(--_ui5-v2-7-0_input_width) !important; +} + +.ui5-shellbar-mid-content { + flex-grow: 1; +} + +@media (max-width: 1040px) { + .command-palette-search-bar { + width: 100% !important; + } +} diff --git a/src/command-pallette/CommandPalletteUI/CommandPaletteSearchBar.tsx b/src/command-pallette/CommandPalletteUI/CommandPaletteSearchBar.tsx new file mode 100644 index 0000000000..f3982e1fb4 --- /dev/null +++ b/src/command-pallette/CommandPalletteUI/CommandPaletteSearchBar.tsx @@ -0,0 +1,98 @@ +import { useEffect, RefObject, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { createPortal } from 'react-dom'; +import { Icon, Input } from '@ui5/webcomponents-react'; +import { K8sResource } from 'types'; +import { useObjectState } from 'shared/useObjectState'; +import { CommandPaletteUI } from './CommandPaletteUI'; +import { useRecoilValue } from 'recoil'; +import { availableNodesSelector } from 'state/navigation/availableNodesSelector'; +import { SCREEN_SIZE_BREAKPOINT_M } from './types'; +import './CommandPaletteSearchBar.scss'; + +type CommandPaletteSearchBarProps = { + slot?: string; + shouldFocus?: boolean; + setShouldFocus?: Function; + shellbarRef?: RefObject; +}; + +export function CommandPaletteSearchBar({ + slot, + shouldFocus, + setShouldFocus, + shellbarRef, +}: CommandPaletteSearchBarProps) { + useRecoilValue(availableNodesSelector); // preload the values to prevent page rerenders + const { t } = useTranslation(); + const [open, setOpen] = useState(shouldFocus || false); + const [resourceCache, updateResourceCache] = useObjectState< + Record + >(); + const shouldShowDialog = shouldFocus ? shouldFocus : open; + + const setShowDialog = (value: boolean) => { + const modalPresent = + document.querySelector('ui5-dialog[open]') || + document.querySelector('.command-palette-ui'); + // disable opening palette if other modal is present + if (!modalPresent || !value) { + setOpen(value); + } + }; + + useEffect(() => { + const shellbarCurr = shellbarRef?.current; + const searchButton = shellbarCurr?.shadowRoot?.querySelector( + '.ui5-shellbar-search-button', + ) as HTMLElement; + const searchField = shellbarCurr?.shadowRoot?.querySelector( + '.ui5-shellbar-search-field', + ) as HTMLElement; + + if ( + searchButton && + searchField && + window.innerWidth > SCREEN_SIZE_BREAKPOINT_M + ) { + searchButton.style.display = 'none'; + + // search bar has to be always visible on big screen + shellbarCurr?.setAttribute('show-search-field', ''); + searchField.style.display = 'flex'; + } else if (searchButton && searchField) { + searchButton.style.display = 'inline-block'; + shellbarCurr?.removeAttribute('show-search-field'); + searchField.style.display = 'none'; + } + }, [window.innerWidth, shellbarRef?.current]); // eslint-disable-line react-hooks/exhaustive-deps + + return ( + <> + setOpen(true)} + onInput={e => e.preventDefault()} + showClearIcon + className="search-with-display-more command-palette-search-bar" + icon={} + slot={slot} + placeholder={t('command-palette.search.quick-navigation')} + /> + {shouldShowDialog && + createPortal( + { + setShowDialog(false); + if (setShouldFocus) setShouldFocus(false); + }} + resourceCache={resourceCache} + updateResourceCache={updateResourceCache} + />, + document.body, + )} + + ); +} diff --git a/src/command-pallette/CommandPalletteUI/CommandPaletteUI.scss b/src/command-pallette/CommandPalletteUI/CommandPaletteUI.scss index f47f28faee..f43dac476c 100644 --- a/src/command-pallette/CommandPalletteUI/CommandPaletteUI.scss +++ b/src/command-pallette/CommandPalletteUI/CommandPaletteUI.scss @@ -1,5 +1,6 @@ .command-palette-ui { position: fixed; + top: calc(var(--_ui5-v2-7-0_shellbar_root_height) + 0.5rem); width: 100%; height: 100%; @@ -7,13 +8,15 @@ justify-content: center; z-index: 1000000; - background-color: rgba(0, 0, 0, 0.6); + background-color: rgba(0, 0, 0, 0.3); &__wrapper { + display: flex; width: 60vw; max-width: 700px; position: fixed; - margin-top: 20vh; + top: 0.5rem; + opacity: 0%; //prevent visually jumping } &__content { @@ -23,5 +26,47 @@ background: var(--sapList_Background); padding: 10px; border-radius: 10px; + box-shadow: 0 0 0.125rem 0 + color-mix(in srgb, var(--sapContent_ShadowColor) 16%, transparent), + 0 0.5rem 1rem 0 + color-mix(in srgb, var(--sapContent_ShadowColor) 16%, transparent); + } + + .input-container { + display: flex; + flex-direction: row; + align-items: center; + + .input-back-button { + display: none; + } + } +} + +@media (max-width: 1040px) { + .command-palette-ui { + &__wrapper { + width: 100vw; + max-width: unset; + top: 0; + height: 100%; + opacity: 100%; + } + + .input-container { + display: flex; + flex-direction: row; + align-items: center; + + .search-with-display-more { + margin-right: 2.5rem; + } + + .input-back-button { + display: block; + padding: 0 0.25rem; + box-sizing: content-box; + } + } } } diff --git a/src/command-pallette/CommandPalletteUI/CommandPaletteUI.tsx b/src/command-pallette/CommandPalletteUI/CommandPaletteUI.tsx index 97f80617dc..0c8f0e6c1a 100644 --- a/src/command-pallette/CommandPalletteUI/CommandPaletteUI.tsx +++ b/src/command-pallette/CommandPalletteUI/CommandPaletteUI.tsx @@ -1,5 +1,8 @@ -import { ReactNode, useEffect, useState } from 'react'; +import { ReactNode, useEffect, useRef, useState } from 'react'; +import { useRecoilState, useRecoilValue } from 'recoil'; import { useEventListener } from 'hooks/useEventListener'; +import { addHistoryEntry, getHistoryEntries } from './search-history'; +import { activeNamespaceIdState } from 'state/activeNamespaceIdAtom'; import { CommandPalletteHelp, NamespaceContextDisplay, @@ -7,13 +10,14 @@ import { SuggestedQuery, } from './components/components'; import { ResultsList } from './ResultsList/ResultsList'; -import { addHistoryEntry, getHistoryEntries } from './search-history'; import { useSearchResults } from './useSearchResults'; -import './CommandPaletteUI.scss'; import { K8sResource } from 'types'; -import { useRecoilValue } from 'recoil'; -import { activeNamespaceIdState } from 'state/activeNamespaceIdAtom'; -import { Icon, Input } from '@ui5/webcomponents-react'; +import { Button, Icon, Input } from '@ui5/webcomponents-react'; +import './CommandPaletteUI.scss'; +import { handleActionIfFormOpen } from 'shared/components/UnsavedMessageBox/helpers'; +import { isResourceEditedState } from 'state/resourceEditedAtom'; +import { isFormOpenState } from 'state/formOpenAtom'; +import { SCREEN_SIZE_BREAKPOINT_M } from './types'; function Background({ hide, @@ -51,6 +55,10 @@ export function CommandPaletteUI({ updateResourceCache, }: CommandPaletteProps) { const namespace = useRecoilValue(activeNamespaceIdState); + const [isResourceEdited, setIsResourceEdited] = useRecoilState( + isResourceEditedState, + ); + const [isFormOpen, setIsFormOpen] = useRecoilState(isFormOpenState); const [query, setQuery] = useState(''); const [originalQuery, setOriginalQuery] = useState(''); @@ -62,6 +70,8 @@ export function CommandPaletteUI({ const [isHistoryMode, setHistoryMode] = useState(false); const [historyIndex, setHistoryIndex] = useState(0); + const commandPaletteRef = useRef(null); + const { results, suggestedQuery, @@ -77,9 +87,42 @@ export function CommandPaletteUI({ useEffect(() => setNamespaceContext(namespace), [namespace]); useEffect(() => { - document.getElementById('command-palette-search')?.focus(); + setTimeout( + () => document.getElementById('command-palette-search')?.focus(), + 100, + ); }, []); + useEffect(() => { + const headerInput = document.getElementById('command-palette-search-bar'); + const headerSlot = document + .querySelector('ui5-shellbar') + ?.shadowRoot?.querySelector('.ui5-shellbar-search-field') as HTMLElement; + const paletteCurrent = commandPaletteRef.current; + + if (!showCommandPalette || !headerSlot) return; + + //show search bar when Command Palette is open + document + .querySelector('ui5-shellbar') + ?.setAttribute('show-search-field', ''); + headerSlot.style.display = 'flex'; + + //position Command Palette + if ( + window.innerWidth > SCREEN_SIZE_BREAKPOINT_M && + headerInput && + paletteCurrent + ) { + const shellbarRect = headerInput.getBoundingClientRect(); + paletteCurrent.style.right = `${window.innerWidth - + shellbarRect.right}px`; + paletteCurrent.style.opacity = '100%'; //prevent visually jumping + } else if (paletteCurrent) { + paletteCurrent.style.right = '0px'; + } + }, [showCommandPalette, window.innerWidth]); // eslint-disable-line react-hooks/exhaustive-deps + const commandPaletteInput = document.getElementById('command-palette-search'); const handleQuerySelection = () => { @@ -104,8 +147,18 @@ export function CommandPaletteUI({ const historyEntries = getHistoryEntries(); if (key === 'Enter' && results[0]) { // choose current entry - addHistoryEntry(results[0].query); - results[0].onActivate(); + e.preventDefault(); + + handleActionIfFormOpen( + isResourceEdited, + setIsResourceEdited, + isFormOpen, + setIsFormOpen, + () => { + addHistoryEntry(results[0].query); + results[0].onActivate(); + }, + ); } else if (key === 'Tab') { e.preventDefault(); // fill search with active history entry @@ -170,6 +223,10 @@ export function CommandPaletteUI({ 'keydown', (e: Event) => { const { key } = e as KeyboardEvent; + if (key === 'Escape') { + hide(); + } + return !isHistoryMode ? keyDownInDropdownMode(key, e) : keyDownInHistoryMode(key, e); @@ -179,22 +236,37 @@ export function CommandPaletteUI({ return ( -
+
- setQuery((e.target as HTMLInputElement).value)} - showClearIcon - className="search-with-display-more full-width" - icon={} - /> +
+ + + setQuery((e.target as HTMLInputElement).value) + } + showClearIcon + className="search-with-display-more full-width" + icon={} + /> +
{!showHelp && ( <> +

       
-

+
); } diff --git a/src/command-pallette/CommandPalletteUI/types.ts b/src/command-pallette/CommandPalletteUI/types.ts index c2c8ad255b..81907614d9 100644 --- a/src/command-pallette/CommandPalletteUI/types.ts +++ b/src/command-pallette/CommandPalletteUI/types.ts @@ -4,6 +4,7 @@ import { useClustersInfoType } from 'state/utils/getClustersInfo'; import { K8sResource } from 'types'; export const LOADING_INDICATOR = 'LOADING_INDICATOR'; +export const SCREEN_SIZE_BREAKPOINT_M = 1040; export type CommandPaletteContext = { fetch: (relativeUrl: string) => Promise; diff --git a/src/header/Header.scss b/src/header/Header.scss index 78612db3e3..2e5d959ef9 100644 --- a/src/header/Header.scss +++ b/src/header/Header.scss @@ -38,122 +38,3 @@ ui5-shellbar.header { } } } - -.snowflake { - color: #fff; - font-size: 1em; - font-family: Arial; - text-shadow: 0 0 1px #000; -} - -@-webkit-keyframes snowflakes-fall { - 0% { - top: -10%; - } - 100% { - top: 100%; - } -} -@-webkit-keyframes snowflakes-shake { - 0% { - -webkit-transform: translateX(0px); - transform: translateX(0px); - } - 50% { - -webkit-transform: translateX(80px); - transform: translateX(80px); - } - 100% { - -webkit-transform: translateX(0px); - transform: translateX(0px); - } -} -@keyframes snowflakes-fall { - 0% { - top: -10%; - } - 100% { - top: 100%; - } -} -@keyframes snowflakes-shake { - 0% { - transform: translateX(0px); - } - 50% { - transform: translateX(80px); - } - 100% { - transform: translateX(0px); - } -} -.snowflake { - position: fixed; - top: -10%; - z-index: 9999; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - cursor: default; - -webkit-animation-name: snowflakes-fall, snowflakes-shake; - -webkit-animation-duration: 10s, 3s; - -webkit-animation-timing-function: linear, ease-in-out; - -webkit-animation-iteration-count: infinite, infinite; - -webkit-animation-play-state: running, running; - animation-name: snowflakes-fall, snowflakes-shake; - animation-duration: 10s, 3s; - animation-timing-function: linear, ease-in-out; - animation-iteration-count: infinite, infinite; - animation-play-state: running, running; -} -.snowflake:nth-of-type(0) { - left: 1%; - -webkit-animation-delay: 0s, 0s; - animation-delay: 0s, 0s; -} -.snowflake:nth-of-type(1) { - left: 10%; - -webkit-animation-delay: 1s, 1s; - animation-delay: 1s, 1s; -} -.snowflake:nth-of-type(2) { - left: 20%; - -webkit-animation-delay: 6s, 0.5s; - animation-delay: 6s, 0.5s; -} -.snowflake:nth-of-type(3) { - left: 30%; - -webkit-animation-delay: 4s, 2s; - animation-delay: 4s, 2s; -} -.snowflake:nth-of-type(4) { - left: 40%; - -webkit-animation-delay: 2s, 2s; - animation-delay: 2s, 2s; -} -.snowflake:nth-of-type(5) { - left: 50%; - -webkit-animation-delay: 8s, 3s; - animation-delay: 8s, 3s; -} -.snowflake:nth-of-type(6) { - left: 60%; - -webkit-animation-delay: 6s, 2s; - animation-delay: 6s, 2s; -} -.snowflake:nth-of-type(7) { - left: 70%; - -webkit-animation-delay: 2.5s, 1s; - animation-delay: 2.5s, 1s; -} -.snowflake:nth-of-type(8) { - left: 80%; - -webkit-animation-delay: 1s, 0s; - animation-delay: 1s, 0s; -} -.snowflake:nth-of-type(9) { - left: 90%; - -webkit-animation-delay: 3s, 1.5s; - animation-delay: 3s, 1.5s; -} diff --git a/src/header/Header.tsx b/src/header/Header.tsx index 7efb5cb5b8..e62a0e4c05 100644 --- a/src/header/Header.tsx +++ b/src/header/Header.tsx @@ -1,108 +1,62 @@ -import { useEffect, useState } from 'react'; +import { useRef, useState } from 'react'; import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; import { Avatar, - Menu, - MenuItem, - Ui5CustomEvent, - MenuDomRef, ShellBar, ShellBarItem, ListItemStandard, } from '@ui5/webcomponents-react'; -import { MenuItemClickEventDetail } from '@ui5/webcomponents/dist/Menu.js'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { useFeature } from 'hooks/useFeature'; +import { useAvailableNamespaces } from 'hooks/useAvailableNamespaces'; +import { useCheckSAPUser } from 'hooks/useCheckSAPUser'; import { clustersState } from 'state/clustersAtom'; import { clusterState } from 'state/clusterAtom'; -import { isPreferencesOpenState } from 'state/preferences/isPreferencesModalOpenAtom'; +import { showKymaCompanionState } from 'state/companion/showKymaCompanionAtom'; +import { isResourceEditedState } from 'state/resourceEditedAtom'; +import { isFormOpenState } from 'state/formOpenAtom'; import { Logo } from './Logo/Logo'; import { SidebarSwitcher } from './SidebarSwitcher/SidebarSwitcher'; -import { useAvailableNamespaces } from 'hooks/useAvailableNamespaces'; -import { useGetLegalLinks, LegalLink } from './SidebarMenu/useGetLegalLinks'; -import { useGetHelpLinks, GetHelpLink } from './SidebarMenu/useGetHelpLinks'; -import { useGetBusolaVersionDetails } from './SidebarMenu/useGetBusolaVersion'; +import { HeaderMenu } from './HeaderMenu'; +import { CommandPaletteSearchBar } from 'command-pallette/CommandPalletteUI/CommandPaletteSearchBar'; +import { SnowFeature } from './SnowFeature'; -import './Header.scss'; -import { isResourceEditedState } from 'state/resourceEditedAtom'; -import { isFormOpenState } from 'state/formOpenAtom'; import { handleActionIfFormOpen } from 'shared/components/UnsavedMessageBox/helpers'; -import { showKymaCompanionState } from 'state/companion/showKymaCompanionAtom'; import { configFeaturesNames } from 'state/types'; -import { themeState } from 'state/preferences/themeAtom'; -import { useCheckSAPUser } from 'hooks/useCheckSAPUser'; - -const SNOW_STORAGE_KEY = 'snow-animation'; +import './Header.scss'; export function Header() { useAvailableNamespaces(); const isSAPUser = useCheckSAPUser(); - const localStorageSnowEnabled = () => { - const snowStorage = localStorage.getItem(SNOW_STORAGE_KEY); - if (snowStorage && typeof JSON.parse(snowStorage) === 'boolean') { - return JSON.parse(snowStorage); - } - return true; - }; + const [isMenuOpen, setIsMenuOpen] = useState(false); - const [isSnowOpen, setIsSnowOpen] = useState(localStorageSnowEnabled()); + const [isSearchOpen, setIsSearchOpen] = useState(false); const { t } = useTranslation(); const navigate = useNavigate(); const { isEnabled: isFeedbackEnabled, link: feedbackLink } = useFeature( configFeaturesNames.FEEDBACK, ); - const { isEnabled: isSnowEnabled } = useFeature(configFeaturesNames.SNOW); - const { githubLink, busolaVersion } = useGetBusolaVersionDetails(); - const legalLinks = useGetLegalLinks(); - const getHelpLinks = useGetHelpLinks(); - - const setPreferencesOpen = useSetRecoilState(isPreferencesOpenState); const cluster = useRecoilValue(clusterState); const clusters = useRecoilValue(clustersState); const [isResourceEdited, setIsResourceEdited] = useRecoilState( isResourceEditedState, ); const [isFormOpen, setIsFormOpen] = useRecoilState(isFormOpenState); - const [theme] = useRecoilState(themeState); const { isEnabled: isKymaCompanionEnabled } = useFeature('KYMA_COMPANION'); const setShowCompanion = useSetRecoilState(showKymaCompanionState); - - useEffect(() => { - if (theme === 'sap_horizon_hcb' || theme === 'sap_horizon_hcw') { - setIsSnowOpen(false); - localStorage.setItem(SNOW_STORAGE_KEY, JSON.stringify(false)); - } - }, [theme]); + const shellbarRef = useRef(null); const inactiveClusterNames = Object.keys(clusters || {}).filter( name => name !== cluster?.name, ); - const nonBreakableSpaces = (number: number): string => { - let spaces = ''; - for (let i = 0; i < number; i++) { - spaces += '\u00a0'; - } - return spaces; - }; - - const handleSnowButtonClick = () => { - if (isSnowOpen) { - setIsSnowOpen(false); - localStorage.setItem(SNOW_STORAGE_KEY, JSON.stringify(false)); - } else { - setIsSnowOpen(true); - localStorage.setItem(SNOW_STORAGE_KEY, JSON.stringify(true)); - } - }; - const clustersList = [ ...inactiveClusterNames.map((name, index) => { return ( @@ -116,43 +70,8 @@ export function Header() { , ]; - const openNewWindow = (link: string) => { - const newWindow = window.open(link, '_blank', 'noopener, noreferrer'); - if (newWindow) newWindow.opener = null; - }; - - const handleMenuItemClick = ( - e: Ui5CustomEvent, - ) => { - const legalLinkUsed = legalLinks.find(x => x.label === e.detail.text); - const getHelpLinkUsed = getHelpLinks.find(x => x.label === e.detail.text); - - if (e.detail.text === t('navigation.preferences.title')) { - setPreferencesOpen(true); - } else if (e.detail.text === t('navigation.menu.give-feedback')) { - openNewWindow(feedbackLink); - } else if (legalLinkUsed) { - openNewWindow(legalLinkUsed.link); - } else if ( - e.detail.text === `${t('common.labels.version')} ${busolaVersion}` - ) { - openNewWindow(githubLink); - } else if (getHelpLinkUsed) { - openNewWindow(getHelpLinkUsed.link); - } - }; - return ( <> - {isSnowOpen && isSnowEnabled && ( - - )} } onProfileClick={() => setIsMenuOpen(true)} - > - {isSnowEnabled && ( - - )} + } + showSearchField + onSearchButtonClick={e => { + if (!e.detail.searchFieldVisible) { + setIsSearchOpen(true); + return; + } + setIsSearchOpen(false); + }} + ref={shellbarRef} + > + {isFeedbackEnabled && ( window.open(feedbackLink, '_blank')} @@ -249,60 +176,7 @@ export function Header() { /> )} - { - setIsMenuOpen(false); - }} - onItemClick={handleMenuItemClick} - > - setPreferencesOpen(true)} - key="preferences" - text={t('navigation.preferences.title')} - icon="wrench" - /> - - - {getHelpLinks.map((getHelpLint: GetHelpLink) => ( - - ))} - - - {legalLinks.map((legalLink: LegalLink) => ( - - ))} - { - window.open(githubLink, '_blank', 'noopener, noreferrer'); - }} - text={t('common.labels.version')} - additionalText={busolaVersion} - icon="inspect" - /> - - + ); } diff --git a/src/header/HeaderMenu.tsx b/src/header/HeaderMenu.tsx new file mode 100644 index 0000000000..57856caf36 --- /dev/null +++ b/src/header/HeaderMenu.tsx @@ -0,0 +1,125 @@ +import { Menu, MenuItem } from '@ui5/webcomponents-react'; +import { MenuDomRef, Ui5CustomEvent } from '@ui5/webcomponents-react'; +import { MenuItemClickEventDetail } from '@ui5/webcomponents/dist/Menu.js'; +import { useSetRecoilState } from 'recoil'; +import { isPreferencesOpenState } from 'state/preferences/isPreferencesModalOpenAtom'; +import { useGetBusolaVersionDetails } from './SidebarMenu/useGetBusolaVersion'; +import { useGetLegalLinks } from './SidebarMenu/useGetLegalLinks'; +import { useGetHelpLinks } from './SidebarMenu/useGetHelpLinks'; +import { useFeature } from 'hooks/useFeature'; +import { configFeaturesNames } from 'state/types'; +import { useTranslation } from 'react-i18next'; + +interface LegalLink { + label: string; + link: string; +} + +interface GetHelpLink { + label: string; + link: string; +} + +interface HeaderMenuProps { + isMenuOpen: boolean; + setIsMenuOpen: (value: boolean) => void; +} + +export function HeaderMenu({ isMenuOpen, setIsMenuOpen }: HeaderMenuProps) { + const { t } = useTranslation(); + const setPreferencesOpen = useSetRecoilState(isPreferencesOpenState); + const { githubLink, busolaVersion } = useGetBusolaVersionDetails(); + const legalLinks = useGetLegalLinks(); + const getHelpLinks = useGetHelpLinks(); + const { link: feedbackLink } = useFeature(configFeaturesNames.FEEDBACK); + + const nonBreakableSpaces = (number: number): string => { + let spaces = ''; + for (let i = 0; i < number; i++) { + spaces += '\u00a0'; + } + return spaces; + }; + + const openNewWindow = (link: string) => { + const newWindow = window.open(link, '_blank', 'noopener, noreferrer'); + if (newWindow) newWindow.opener = null; + }; + + const handleMenuItemClick = ( + e: Ui5CustomEvent, + ) => { + const legalLinkUsed = legalLinks.find(x => x.label === e.detail.text); + const getHelpLinkUsed = getHelpLinks.find(x => x.label === e.detail.text); + + if (e.detail.text === t('navigation.preferences.title')) { + setPreferencesOpen(true); + } else if (e.detail.text === t('navigation.menu.give-feedback')) { + openNewWindow(feedbackLink); + } else if (legalLinkUsed) { + openNewWindow(legalLinkUsed.link); + } else if ( + e.detail.text === `${t('common.labels.version')} ${busolaVersion}` + ) { + openNewWindow(githubLink); + } else if (getHelpLinkUsed) { + openNewWindow(getHelpLinkUsed.link); + } + }; + return ( + { + setIsMenuOpen(false); + }} + onItemClick={handleMenuItemClick} + > + setPreferencesOpen(true)} + key="preferences" + text={t('navigation.preferences.title')} + icon="wrench" + /> + + + {getHelpLinks.map((getHelpLint: GetHelpLink) => ( + + ))} + + + {legalLinks.map((legalLink: LegalLink) => ( + + ))} + { + window.open(githubLink, '_blank', 'noopener, noreferrer'); + }} + text={t('common.labels.version')} + additionalText={busolaVersion} + icon="inspect" + /> + + + ); +} diff --git a/src/header/SnowFeature.scss b/src/header/SnowFeature.scss new file mode 100644 index 0000000000..9b309347b8 --- /dev/null +++ b/src/header/SnowFeature.scss @@ -0,0 +1,118 @@ +.snowflake { + color: #fff; + font-size: 1em; + font-family: Arial; + text-shadow: 0 0 1px #000; +} + +@-webkit-keyframes snowflakes-fall { + 0% { + top: -10%; + } + 100% { + top: 100%; + } +} +@-webkit-keyframes snowflakes-shake { + 0% { + -webkit-transform: translateX(0px); + transform: translateX(0px); + } + 50% { + -webkit-transform: translateX(80px); + transform: translateX(80px); + } + 100% { + -webkit-transform: translateX(0px); + transform: translateX(0px); + } +} +@keyframes snowflakes-fall { + 0% { + top: -10%; + } + 100% { + top: 100%; + } +} +@keyframes snowflakes-shake { + 0% { + transform: translateX(0px); + } + 50% { + transform: translateX(80px); + } + 100% { + transform: translateX(0px); + } +} +.snowflake { + position: fixed; + top: -10%; + z-index: 9999; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + cursor: default; + -webkit-animation-name: snowflakes-fall, snowflakes-shake; + -webkit-animation-duration: 10s, 3s; + -webkit-animation-timing-function: linear, ease-in-out; + -webkit-animation-iteration-count: infinite, infinite; + -webkit-animation-play-state: running, running; + animation-name: snowflakes-fall, snowflakes-shake; + animation-duration: 10s, 3s; + animation-timing-function: linear, ease-in-out; + animation-iteration-count: infinite, infinite; + animation-play-state: running, running; +} +.snowflake:nth-of-type(0) { + left: 1%; + -webkit-animation-delay: 0s, 0s; + animation-delay: 0s, 0s; +} +.snowflake:nth-of-type(1) { + left: 10%; + -webkit-animation-delay: 1s, 1s; + animation-delay: 1s, 1s; +} +.snowflake:nth-of-type(2) { + left: 20%; + -webkit-animation-delay: 6s, 0.5s; + animation-delay: 6s, 0.5s; +} +.snowflake:nth-of-type(3) { + left: 30%; + -webkit-animation-delay: 4s, 2s; + animation-delay: 4s, 2s; +} +.snowflake:nth-of-type(4) { + left: 40%; + -webkit-animation-delay: 2s, 2s; + animation-delay: 2s, 2s; +} +.snowflake:nth-of-type(5) { + left: 50%; + -webkit-animation-delay: 8s, 3s; + animation-delay: 8s, 3s; +} +.snowflake:nth-of-type(6) { + left: 60%; + -webkit-animation-delay: 6s, 2s; + animation-delay: 6s, 2s; +} +.snowflake:nth-of-type(7) { + left: 70%; + -webkit-animation-delay: 2.5s, 1s; + animation-delay: 2.5s, 1s; +} +.snowflake:nth-of-type(8) { + left: 80%; + -webkit-animation-delay: 1s, 0s; + animation-delay: 1s, 0s; +} +.snowflake:nth-of-type(9) { + left: 90%; + -webkit-animation-delay: 3s, 1.5s; + animation-delay: 3s, 1.5s; +} diff --git a/src/header/SnowFeature.tsx b/src/header/SnowFeature.tsx new file mode 100644 index 0000000000..e4fc83b014 --- /dev/null +++ b/src/header/SnowFeature.tsx @@ -0,0 +1,66 @@ +import { ShellBarItem } from '@ui5/webcomponents-react'; +import { useFeature } from 'hooks/useFeature'; +import { useEffect, useState } from 'react'; +import { createPortal } from 'react-dom'; +import { useTranslation } from 'react-i18next'; +import { useRecoilState } from 'recoil'; +import { themeState } from 'state/preferences/themeAtom'; +import { configFeaturesNames } from 'state/types'; +import './SnowFeature.scss'; + +const SNOW_STORAGE_KEY = 'snow-animation'; + +export function SnowFeature() { + const { t } = useTranslation(); + const snowStorage = localStorage.getItem(SNOW_STORAGE_KEY); + const localStorageSnowEnabled = () => { + if (snowStorage && typeof JSON.parse(snowStorage) === 'boolean') { + return JSON.parse(snowStorage); + } + return true; + }; + const [isSnowOpen, setIsSnowOpen] = useState(localStorageSnowEnabled()); + const { isEnabled: isSnowEnabled } = useFeature(configFeaturesNames.SNOW); + const [theme] = useRecoilState(themeState); + + useEffect(() => { + if (theme === 'sap_horizon_hcb' || theme === 'sap_horizon_hcw') { + setIsSnowOpen(false); + localStorage.setItem(SNOW_STORAGE_KEY, JSON.stringify(false)); + } + }, [theme]); + const handleSnowButtonClick = () => { + if (isSnowOpen) { + setIsSnowOpen(false); + localStorage.setItem(SNOW_STORAGE_KEY, JSON.stringify(false)); + } else { + setIsSnowOpen(true); + localStorage.setItem(SNOW_STORAGE_KEY, JSON.stringify(true)); + } + }; + + return ( + <> + {isSnowOpen && + isSnowEnabled && + createPortal( + , + document.body, + )} + {isSnowEnabled && ( + + )} + + ); +} diff --git a/src/styles/index.scss b/src/styles/index.scss index bfdc96c71d..dcc45de980 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -185,10 +185,15 @@ ui5-tabcontainer::part(content) { .search-with-display-more { @include input-icon( url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiB3aWR0aD0iMjYiIGhlaWdodD0iMjYiPjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzRENUE2QyIgZD0iTTQyMiAwcTM4IDAgNjQgMjZ0MjYgNjR2MzMycTAgMzgtMjYgNjR0LTY0IDI2SDEzOHEtMjggMC01My41LTExdC00NC0yOS41LTI5LjUtNDRUMCAzNzRxMC0yOSAxMS01My41VDQwLjUgMjc3dDQ0LTI5LjVUMTM4IDIzN2gxMjdsLTMzLTMzcS04LTgtOC0xOCAwLTExIDcuNS0xOC41VDI1MCAxNjB0MTggN2w3NyA3N3E3IDkgNyAxOCAwIDExLTcgMThsLTc3IDc3cS04IDgtMTggOC0xMSAwLTE4LjUtNy41VDIyNCAzMzlxMC0xMCA4LTE4bDMzLTMzSDEzOHEtMTggMC0zNCA3dC0yNy41IDE4LjUtMTguNSAyNy03IDMzLjVxMCAzNiAyNS41IDYxLjVUMTM4IDQ2MWgyODRxMTcgMCAyOC0xMXQxMS0yOFY5MHEwLTE3LTExLTI4dC0yOC0xMUg5MHEtMTcgMC0yOCAxMVQ1MSA5MHY3NnEwIDExLTcgMTguNVQyNiAxOTJ0LTE4LjUtNy41VDAgMTY2VjkwcTAtMzggMjYtNjRUOTAgMGgzMzJ6IiAvPjwvc3ZnPg==), - 48px, - 16px center, + 2rem, + 0.75rem center, 16px ); + height: 2rem; + + ui5-icon { + align-self: center; + } } .combobox-with-dimension-icon { diff --git a/tests/integration/tests/cluster/test-command-palette.spec.js b/tests/integration/tests/cluster/test-command-palette.spec.js index 02eab41998..6c733693c7 100644 --- a/tests/integration/tests/cluster/test-command-palette.spec.js +++ b/tests/integration/tests/cluster/test-command-palette.spec.js @@ -1,6 +1,7 @@ /// function openCommandPalette() { + cy.wait(1000); cy.get('body').type( `${Cypress.platform === 'darwin' ? '{cmd}k' : '{ctrl}k'}`, { force: true }, diff --git a/tests/integration/tests/cluster/test-edit-cluster.spec.js b/tests/integration/tests/cluster/test-edit-cluster.spec.js index bd2683ae5c..4d6c26c311 100644 --- a/tests/integration/tests/cluster/test-edit-cluster.spec.js +++ b/tests/integration/tests/cluster/test-edit-cluster.spec.js @@ -20,6 +20,8 @@ context('Test edit cluster', () => { .eq(0) .then(el => (originalName = el.text())); + cy.wait(1000); + cy.get('ui5-button[data-testid="edit"]').click(); cy.get('ui5-input[data-testid="cluster-description"]') diff --git a/tests/integration/tests/extensibility/ext-test-pizzas.spec.js b/tests/integration/tests/extensibility/ext-test-pizzas.spec.js index 4c035ec78d..4d53bfb23d 100644 --- a/tests/integration/tests/extensibility/ext-test-pizzas.spec.js +++ b/tests/integration/tests/extensibility/ext-test-pizzas.spec.js @@ -67,6 +67,7 @@ context('Test Pizzas', () => { cy.loginAndSelectCluster(); cy.getLeftNav() + .find('ui5-side-navigation-item') .contains('Namespaces') .click(); diff --git a/tests/integration/tests/extensibility/ext-test-variables.spec.js b/tests/integration/tests/extensibility/ext-test-variables.spec.js index acee946462..826d4c68e3 100644 --- a/tests/integration/tests/extensibility/ext-test-variables.spec.js +++ b/tests/integration/tests/extensibility/ext-test-variables.spec.js @@ -61,6 +61,7 @@ context('Test extensibility variables', () => { cy.loginAndSelectCluster(); cy.getLeftNav() + .find('ui5-side-navigation-item') .contains('Namespaces') .click(); diff --git a/tests/integration/tests/namespace/test-custom-resources.spec.js b/tests/integration/tests/namespace/test-custom-resources.spec.js index 89947b13a2..37c5b27917 100644 --- a/tests/integration/tests/namespace/test-custom-resources.spec.js +++ b/tests/integration/tests/namespace/test-custom-resources.spec.js @@ -15,6 +15,8 @@ context('Test Custom Resources', () => { cy.loginAndSelectCluster(); cy.goToNamespaceDetails(); + cy.wait(1000); + cy.get('body').type( `${Cypress.platform === 'darwin' ? '{cmd}k' : '{ctrl}k'}`, );