From dcebd53902a9684af30a90e5c8478c663fbb7303 Mon Sep 17 00:00:00 2001 From: shadowusr <58862284+shadowusr@users.noreply.github.com> Date: Thu, 17 Oct 2024 02:43:44 +0300 Subject: [PATCH] feat: implement a button to switch between new and old UI (#609) * feat: implement a button to switch between new and old UI * fix: get rid of unnecessary invisible space * fix: disable settings button while loading * feat: add notification on how to switch back to old UI --- lib/constants/local-storage.ts | 8 ++ .../controls/browser-list/index.jsx | 5 +- lib/static/components/controls/controls.less | 101 ++++++++++++++++++ .../components/controls/report-info.jsx | 63 ++++++----- lib/static/hooks/useLocalStorage.js | 5 + lib/static/new-ui.css | 5 +- lib/static/new-ui/app/App.tsx | 6 +- .../components/AttemptPicker/index.module.css | 2 +- .../components/LoadingBar/index.module.css | 2 +- .../new-ui/components/MainLayout/Footer.tsx | 67 ++++++++++++ .../components/MainLayout/index.module.css | 13 +++ .../new-ui/components/MainLayout/index.tsx | 23 +++- .../components/SettingsPanel/index.module.css | 32 ++++++ .../new-ui/components/SettingsPanel/index.tsx | 56 ++++++++++ .../new-ui/components/SplitViewLayout.tsx | 2 + .../components/SuiteTitle/index.module.css | 6 -- .../new-ui/components/SuiteTitle/index.tsx | 1 - .../UiModeHintNotification/index.module.css | 91 ++++++++++++++++ .../UiModeHintNotification/index.tsx | 26 +++++ .../ScreenshotsTreeViewItem/index.module.css | 2 +- .../VisualChecksPage/index.module.css | 2 +- lib/static/template.html | 12 +++ 22 files changed, 486 insertions(+), 44 deletions(-) create mode 100644 lib/constants/local-storage.ts create mode 100644 lib/static/new-ui/components/MainLayout/Footer.tsx create mode 100644 lib/static/new-ui/components/SettingsPanel/index.module.css create mode 100644 lib/static/new-ui/components/SettingsPanel/index.tsx create mode 100644 lib/static/new-ui/components/UiModeHintNotification/index.module.css create mode 100644 lib/static/new-ui/components/UiModeHintNotification/index.tsx diff --git a/lib/constants/local-storage.ts b/lib/constants/local-storage.ts new file mode 100644 index 000000000..dd3d81081 --- /dev/null +++ b/lib/constants/local-storage.ts @@ -0,0 +1,8 @@ +export enum LocalStorageKey { + UIMode = 'ui-mode' +} + +export enum UiMode { + Old = 'old', + New = 'new', +} diff --git a/lib/static/components/controls/browser-list/index.jsx b/lib/static/components/controls/browser-list/index.jsx index 8b9361608..36023d59b 100644 --- a/lib/static/components/controls/browser-list/index.jsx +++ b/lib/static/components/controls/browser-list/index.jsx @@ -1,5 +1,7 @@ 'use strict'; +import {Button, Select, useSelectOptions} from '@gravity-ui/uikit'; +import classNames from 'classnames'; import React, {useState, useMemo, useEffect} from 'react'; import {compact} from 'lodash'; import PropTypes from 'prop-types'; @@ -8,7 +10,6 @@ import {mkBrowserIcon, buildComplexId} from './utils'; import 'react-checkbox-tree/lib/react-checkbox-tree.css'; import './index.styl'; -import {Button, Select, useSelectOptions} from '@gravity-ui/uikit'; const BrowserList = ({available, onChange, selected: selectedProp}) => { const getOptions = () => { @@ -138,7 +139,7 @@ const BrowserList = ({available, onChange, selected: selectedProp}) => {
{option.content}
- + ); }; diff --git a/lib/static/components/controls/controls.less b/lib/static/components/controls/controls.less index 27002d527..af8c7ef72 100644 --- a/lib/static/components/controls/controls.less +++ b/lib/static/components/controls/controls.less @@ -130,7 +130,108 @@ width: 200px; } +@property --gradient-angle { + syntax: ""; + inherits: false; + initial-value: 0turn; +} + +@property --from-color { + syntax: ""; + inherits: false; + initial-value: #00ffff00; +} + +@property --to-color { + syntax: ""; + inherits: false; + initial-value: #eee; +} + .report-info { + .new-ui-button { + margin-right: 1em; + transition: color 1s linear; + position: relative; + + @keyframes glow { + from {background-position: 0} + to {background-position: 400%} + } + + & .new-ui-button__glow { + opacity: 0; + left: 0; + filter: blur(10px); + transition: opacity 1s ease; + animation: 20s linear infinite glow; + position: absolute; + width: 100%; + height: 100%; + background-image: linear-gradient(90deg, #03a9f4, #f441a5, #ffeb3b, #03a9f4); + border-radius: 5px; + background-size: 400%; + z-index: -5; + } + + &:hover .new-ui-button__glow { + opacity: 1; + } + + @keyframes color-fade { + from {color: black} + to {color: white} + } + + &:hover { + animation: 1s color-fade ease forwards; + animation-delay: .6s; + } + + @keyframes pulse { + 0% { + --gradient-angle: 0deg; + --to-color: rgb(108,71,255); + } + + 90% { + --to-color: rgb(108,71,255); + --from-color: #00ffff00; + } + + 100% { + --gradient-angle: 180deg; + --from-color: rgb(108,71,255); + --to-color: rgb(108,71,255); + } + } + + &::before { + transition: none; + background-image: conic-gradient(from var(--gradient-angle) at -10% 100%, var(--from-color) 0%, var(--to-color) 100%); + } + + &:hover::before { + animation: 1s ease pulse forwards; + } + + @keyframes bg-fade { + from { background-color: #eee } + to { background-color: rgb(108,71,255) } + } + + &::after { + background-color: #eee; + margin: 2px; + border-radius: 4px; + } + + &:hover::after { + animation: 1s ease bg-fade forwards; + animation-delay: .7s; + } + } + .label { margin: 0 15px 0 0; } diff --git a/lib/static/components/controls/report-info.jsx b/lib/static/components/controls/report-info.jsx index 40a5af026..1fece14a5 100644 --- a/lib/static/components/controls/report-info.jsx +++ b/lib/static/components/controls/report-info.jsx @@ -1,36 +1,51 @@ -import React, {Component} from 'react'; +import {Flask} from '@gravity-ui/icons'; +import React from 'react'; import PropTypes from 'prop-types'; import {connect} from 'react-redux'; -import {Label} from '@gravity-ui/uikit'; +import {Button, Icon, Label} from '@gravity-ui/uikit'; import {isEmpty} from 'lodash'; import {version} from '../../../../package.json'; +import useLocalStorage from '@/static/hooks/useLocalStorage'; +import {LocalStorageKey, UiMode} from '@/constants/local-storage'; -class ReportInfo extends Component { - static propTypes = { - gui: PropTypes.bool.isRequired, - timestamp: PropTypes.number.isRequired +function ReportInfo(props) { + const {gui, timestamp} = props; + const lang = isEmpty(navigator.languages) ? navigator.language : navigator.languages[0]; + const date = new Date(timestamp).toLocaleString(lang); + + const [, setUiMode] = useLocalStorage(LocalStorageKey.UIMode, UiMode.New); + + const onNewUiButtonClick = () => { + setUiMode(UiMode.New); + + const targetUrl = new URL(window.location.href); + + targetUrl.pathname = targetUrl.pathname.replace(/\/(index\.html)?$/, (match, ending) => ending ? '/new-ui.html' : '/new-ui'); + targetUrl.searchParams.set('switched-from-old-ui', '1'); + + window.location.href = targetUrl.href; }; - render() { - const {gui, timestamp} = this.props; - const lang = isEmpty(navigator.languages) ? navigator.language : navigator.languages[0]; - const date = new Date(timestamp).toLocaleString(lang); - - return ( -
- - {!gui && } -
- ); - } + return ( +
+ + + {!gui && } +
+ ); } +ReportInfo.propTypes = { + gui: PropTypes.bool.isRequired, + timestamp: PropTypes.number.isRequired +}; + export default connect( ({gui, timestamp}) => ({gui, timestamp}) )(ReportInfo); diff --git a/lib/static/hooks/useLocalStorage.js b/lib/static/hooks/useLocalStorage.js index 4d5e59731..f6dd9bda7 100644 --- a/lib/static/hooks/useLocalStorage.js +++ b/lib/static/hooks/useLocalStorage.js @@ -2,6 +2,11 @@ import {useCallback, useEffect, useState} from 'react'; import useEventListener from './useEventListener'; import * as localStorageWrapper from '../modules/local-storage-wrapper'; +/** + * @param key + * @param initialValue + * @returns {[*, function]} An array containing the current state value and a function to update it. + */ export default function useLocalStorage(key, initialValue) { const readValue = useCallback(() => { return localStorageWrapper.getItem(key, initialValue); diff --git a/lib/static/new-ui.css b/lib/static/new-ui.css index 4f5b0b1be..7547b37c2 100644 --- a/lib/static/new-ui.css +++ b/lib/static/new-ui.css @@ -63,9 +63,12 @@ body { } } -.action-button { +.regular-button { font-size: 15px; font-weight: 450; +} + +.action-button { /* Sets spinner color */ --g-color-line-brand: var(--g-color-text-hint); } diff --git a/lib/static/new-ui/app/App.tsx b/lib/static/new-ui/app/App.tsx index bd7a6832d..7a4be1653 100644 --- a/lib/static/new-ui/app/App.tsx +++ b/lib/static/new-ui/app/App.tsx @@ -2,10 +2,9 @@ import {ThemeProvider} from '@gravity-ui/uikit'; import React, {ReactNode, StrictMode} from 'react'; import {MainLayout} from '../components/MainLayout'; import {HashRouter, Navigate, Route, Routes} from 'react-router-dom'; -import {CircleInfo, Eye, ListCheck} from '@gravity-ui/icons'; +import {Eye, ListCheck} from '@gravity-ui/icons'; import {SuitesPage} from '../features/suites/components/SuitesPage'; import {VisualChecksPage} from '../features/visual-checks/components/VisualChecksPage'; -import {InfoPage} from '../features/info/components/InfoPage'; import '@gravity-ui/uikit/styles/fonts.css'; import '@gravity-ui/uikit/styles/styles.css'; @@ -23,8 +22,7 @@ export function App(): ReactNode { element: , children: [} />] }, - {title: 'Visual Checks', url: '/visual-checks', icon: Eye, element: }, - {title: 'Info', url: '/info', icon: CircleInfo, element: } + {title: 'Visual Checks', url: '/visual-checks', icon: Eye, element: } ]; return diff --git a/lib/static/new-ui/components/AttemptPicker/index.module.css b/lib/static/new-ui/components/AttemptPicker/index.module.css index a2c76f5f0..1ae277d55 100644 --- a/lib/static/new-ui/components/AttemptPicker/index.module.css +++ b/lib/static/new-ui/components/AttemptPicker/index.module.css @@ -14,6 +14,6 @@ } .retry-button { - composes: action-button from global; + composes: regular-button from global, action-button from global; margin-left: auto; } diff --git a/lib/static/new-ui/components/LoadingBar/index.module.css b/lib/static/new-ui/components/LoadingBar/index.module.css index d2b4a0874..9d05a3c26 100644 --- a/lib/static/new-ui/components/LoadingBar/index.module.css +++ b/lib/static/new-ui/components/LoadingBar/index.module.css @@ -2,7 +2,7 @@ height: 30px; width: 100%; position: absolute; - z-index: 99; + z-index: 999999; display: flex; align-items: center; flex-direction: column; diff --git a/lib/static/new-ui/components/MainLayout/Footer.tsx b/lib/static/new-ui/components/MainLayout/Footer.tsx new file mode 100644 index 000000000..0a9f9da63 --- /dev/null +++ b/lib/static/new-ui/components/MainLayout/Footer.tsx @@ -0,0 +1,67 @@ +import {Gear} from '@gravity-ui/icons'; +import {FooterItem, MenuItem as GravityMenuItem} from '@gravity-ui/navigation'; +import {Icon} from '@gravity-ui/uikit'; +import classNames from 'classnames'; +import React, {ReactNode, useEffect, useState} from 'react'; +import {useSelector} from 'react-redux'; + +import {UiModeHintNotification} from '@/static/new-ui/components/UiModeHintNotification'; +import styles from '@/static/new-ui/components/MainLayout/index.module.css'; +import {getIsInitialized} from '@/static/new-ui/store/selectors'; +import useLocalStorage from '@/static/hooks/useLocalStorage'; +import {PanelId} from '@/static/new-ui/components/MainLayout/index'; + +interface FooterProps { + visiblePanel: PanelId | null; + onFooterItemClick: (item: GravityMenuItem) => void; +} + +export function Footer(props: FooterProps): ReactNode { + const isInitialized = useSelector(getIsInitialized); + const [isHintVisible, setIsHintVisible] = useState(null); + const [wasHintShownBefore, setWasHintShownBefore] = useLocalStorage('ui-mode-hint-shown', false); + + useEffect(() => { + const hasJustSwitched = new URL(window.location.href).searchParams.get('switched-from-old-ui') === '1'; + if (isInitialized && hasJustSwitched && !wasHintShownBefore) { + setIsHintVisible(true); + setWasHintShownBefore(true); + + const timeoutId = setTimeout(() => { + setIsHintVisible(false); + }, 20000); + + return () => { + clearTimeout(timeoutId); + }; + } + + return; + }, [isInitialized]); + + useEffect(() => { + if (isHintVisible && props.visiblePanel) { + setIsHintVisible(false); + } + }, [props.visiblePanel]); + + const isCurrent = props.visiblePanel === PanelId.Settings; + + return <> + setIsHintVisible(false)} /> + makeItem({ + ...params, + icon: + }) + }} /> + ; +} diff --git a/lib/static/new-ui/components/MainLayout/index.module.css b/lib/static/new-ui/components/MainLayout/index.module.css index d29e5520c..9171ac42f 100644 --- a/lib/static/new-ui/components/MainLayout/index.module.css +++ b/lib/static/new-ui/components/MainLayout/index.module.css @@ -6,3 +6,16 @@ background-color: var(--color-bg-dark); width: 100%; } + +:global(.gn-composite-bar-item):has(.footer-item:global(.disabled)) { + pointer-events: none; + opacity: .5; +} + +.footer-item { + color: var(--gn-aside-header-item-icon-color) !important; +} + +.footer-item--active { + color: var(--gn-aside-header-item-current-icon-color) !important; +} diff --git a/lib/static/new-ui/components/MainLayout/index.tsx b/lib/static/new-ui/components/MainLayout/index.tsx index d4e803229..3a04404c6 100644 --- a/lib/static/new-ui/components/MainLayout/index.tsx +++ b/lib/static/new-ui/components/MainLayout/index.tsx @@ -1,11 +1,18 @@ import {AsideHeader, MenuItem as GravityMenuItem} from '@gravity-ui/navigation'; import classNames from 'classnames'; -import React from 'react'; +import React, {ReactNode, useState} from 'react'; import {useSelector} from 'react-redux'; import {useNavigate, matchPath, useLocation} from 'react-router-dom'; + +import {getIsInitialized} from '@/static/new-ui/store/selectors'; +import {SettingsPanel} from '@/static/new-ui/components/SettingsPanel'; import TestplaneIcon from '../../../icons/testplane-mono.svg'; import styles from './index.module.css'; -import {getIsInitialized} from '@/static/new-ui/store/selectors'; +import {Footer} from './Footer'; + +export enum PanelId { + Settings = 'settings', +} interface MenuItem { title: string; @@ -32,6 +39,11 @@ export function MainLayout(props: MainLayoutProps): JSX.Element { const isInitialized = useSelector(getIsInitialized); + const [visiblePanel, setVisiblePanel] = useState(null); + const onFooterItemClick = (item: GravityMenuItem): void => { + visiblePanel ? setVisiblePanel(null) : setVisiblePanel(item.id as PanelId); + }; + return navigate('/suites')}} @@ -42,5 +54,12 @@ export function MainLayout(props: MainLayoutProps): JSX.Element { customBackgroundClassName={styles.asideHeaderBgWrapper} renderContent={(): React.ReactNode => props.children} hideCollapseButton={true} + renderFooter={(): ReactNode =>