diff --git a/public/menu/install.svg b/public/menu/install.svg new file mode 100644 index 00000000..fc7d41c6 --- /dev/null +++ b/public/menu/install.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/menu/instance.svg b/public/menu/instance.svg new file mode 100644 index 00000000..460b513c --- /dev/null +++ b/public/menu/instance.svg @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/menu/runtime.png b/public/menu/runtime.png new file mode 100644 index 00000000..bf1f4096 Binary files /dev/null and b/public/menu/runtime.png differ diff --git a/public/menu/store.svg b/public/menu/store.svg new file mode 100644 index 00000000..67eaca1a --- /dev/null +++ b/public/menu/store.svg @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/Base/Button/index.scss b/src/components/Base/Button/index.scss index 36d00501..006af9b9 100644 --- a/src/components/Base/Button/index.scss +++ b/src/components/Base/Button/index.scss @@ -3,7 +3,7 @@ .button { height: 32px; padding: 7px 12px; - line-height: 16px; + line-height: 18px; font-size: $body-size; border-radius: 2px; border: 0 none; diff --git a/src/components/Header/index.jsx b/src/components/Header/index.jsx index bec6fa2f..3cc9ce2a 100644 --- a/src/components/Header/index.jsx +++ b/src/components/Header/index.jsx @@ -4,20 +4,27 @@ import { withRouter, Link, NavLink } from 'react-router-dom'; import { observer, inject } from 'mobx-react'; import { withTranslation } from 'react-i18next'; -import { Popover, Icon } from 'components/Base'; +import { Popover, Icon, Button } from 'components/Base'; import MenuLayer from 'components/MenuLayer'; import routes, { toRoute } from 'routes'; +import MenuIntroduction from 'components/MenuIntroduction'; +import { getCookie, setCookie } from 'utils'; +import menus from './menus'; import styles from './index.scss'; -const LinkItem = ({ to, title, path }) => { +const LinkItem = ({ + to, title, path, isIntroduction +}) => { const isActive = to === '/' ? ['/', '/apps/:appId'].includes(path) : path.startsWith(to); - return ( {title} @@ -32,9 +39,62 @@ const LinkItem = ({ to, title, path }) => { })) @observer export class Header extends Component { - renderMenus = () => { + state = { + activeIndex: 0 + }; + + changeIntroduction = isKnow => { + const index = this.state.activeIndex; + this.setState({ + activeIndex: this.state.activeIndex + 1 + }); + if (index >= menus.length || isKnow) { + const { user } = this.props; + setCookie(`${user.user_id}_no_introduction`, true); + } + }; + + rederStartModal() { + const { t } = this.props; + + return ( +
+
+
+ this.changeIntroduction(true)} + className={styles.close} + name="close" + size={36} + type="white" + /> +
+
+
{t('WELCOME_TO_OPENPITRIX')}
+
+ {t('WELCOME_TO_OPENPITRIX_DESC')} +
+ +
+
+
+
+ ); + } + + renderMenus() { const { t, user, match } = this.props; const { path } = match; + const { activeIndex } = this.state; + const introduction = (menus[activeIndex - 1] || {}).introduction; + const len = menus.length; + const noIntroduction = getCookie(`${user.user_id}_no_introduction`); if (!user.isLoggedIn()) { return null; @@ -42,25 +102,33 @@ export class Header extends Component { return (
- - - - + {menus.map((item, index) => ( + + ))} + + {!noIntroduction && activeIndex === 0 && this.rederStartModal()} + + {!noIntroduction && activeIndex > 0 && ( + + )}
); - }; + } renderMenuBtns() { const { t, user } = this.props; diff --git a/src/components/Header/index.scss b/src/components/Header/index.scss index 61166ea5..25568e17 100644 --- a/src/components/Header/index.scss +++ b/src/components/Header/index.scss @@ -179,6 +179,7 @@ $profile-hover-color: #efe6f8; .menusHeader { .menus { + position: relative; display: inline-block; margin-left: 8px; a { @@ -197,6 +198,15 @@ $profile-hover-color: #efe6f8; &.active { color: $N10; } + + &.introduction { + position: relative; + z-index: 1; + margin: 0; + padding: 8px; + color: $N10; + background: $N400; + } } } @@ -227,3 +237,78 @@ $profile-hover-color: #efe6f8; } } } + +.startModal { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 2; + width: 626px; + height: 480px; + border-radius: 2px; + box-shadow: 0 1px 4px 0 rgba(73, 33, 173, 0.06), + 0 4px 8px 0 rgba(35, 35, 36, 0.04); + background-color: $N0; + + .banner { + position: relative; + width: 626px; + height: 181px; + border-radius: 2px; + background-image: radial-gradient(circle at 9% 0, $P75, $N500); + + .close { + position: absolute; + right: 12px; + top: 24px; + } + + .circle { + width: 308px; + height: 308px; + border-radius: 50%; + background-image: radial-gradient( + circle at 50% 0, + $Y200, + rgba(252, 217, 102, 0) + ); + } + } + + .word { + width: 368px; + margin: 0 auto; + text-align: center; + + .title { + @include title-font($line-height: 36px, $font-size: 24px); + margin-top: 40px; + margin-bottom: 8px; + } + + .description { + @include note-font($line-height: 28px, $font-size: 14px); + margin-bottom: 32px; + } + + button { + padding: 7px 24px; + } + } +} + +.modalShadow { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + background-color: rgba(52, 57, 69, 0.7); +} + +:lang(en) { + .startModal .word { + width: 420px; + } +} diff --git a/src/components/Header/menus.js b/src/components/Header/menus.js new file mode 100644 index 00000000..c3f5c879 --- /dev/null +++ b/src/components/Header/menus.js @@ -0,0 +1,38 @@ +export default [ + { + name: 'App Store', + link: '', + introduction: { + title: 'App Store', + description: 'MENU_APP_INTRO', + image: '/menu/store.svg' + } + }, + { + name: 'Purchased', + link: 'apps', + introduction: { + title: 'Purchased', + description: 'MENU_PURCHASED_INTRO', + image: '/menu/install.svg' + } + }, + { + name: 'My Instances', + link: 'clusters', + introduction: { + title: 'My Instances', + description: 'MENU_CLUSTER_INTRO', + image: '/menu/instance.svg' + } + }, + { + name: 'My Runtimes', + link: 'runtimes', + introduction: { + title: 'My Runtimes', + description: 'MENU_RUNTIME_INTRO', + image: '/menu/runtime.png' + } + } +]; diff --git a/src/components/MenuIntroduction/index.jsx b/src/components/MenuIntroduction/index.jsx new file mode 100644 index 00000000..b27c950f --- /dev/null +++ b/src/components/MenuIntroduction/index.jsx @@ -0,0 +1,78 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import { withTranslation } from 'react-i18next'; + +import { Button } from 'components/Base'; +import styles from './index.scss'; + +@withTranslation() +export default class MenuIntroduction extends PureComponent { + static propTypes = { + activeIndex: PropTypes.number, + changeIntroduction: PropTypes.func, + className: PropTypes.string, + description: PropTypes.string, + image: PropTypes.string, + title: PropTypes.string, + total: PropTypes.number + }; + + render() { + const { + className, + title, + description, + image, + activeIndex, + total, + changeIntroduction, + t + } = this.props; + const dots = Array.from(Array(total).keys()); + const left = (activeIndex - 1) * 75; + + return ( +
+
+
+
+
{title}
+
{description}
+
+ + +
+ +
+ {dots.map((dot, index) => ( +
+
+
+
+ ); + } +} diff --git a/src/components/MenuIntroduction/index.scss b/src/components/MenuIntroduction/index.scss new file mode 100644 index 00000000..3148bcd9 --- /dev/null +++ b/src/components/MenuIntroduction/index.scss @@ -0,0 +1,93 @@ +@import '~scss/vars'; + +.menuIntroduction { + position: absolute; + left: 0; + top: 100%; + z-index: 2; + width: 445px; + border-radius: 2px; + background-color: $N0; + + &::after { + content: ''; + position: absolute; + top: -6px; + left: 24px; + border-bottom: 6px solid $N0; + border-right: 5px solid transparent; + border-left: 5px solid transparent; + } + + .content { + padding-left: 24px; + + .title { + @include title-font($line-height: 32px, $font-size: 20px); + padding: 28px 0 12px; + } + + .description { + @include normal-font; + width: 265px; + padding-bottom: 24px; + } + + .word { + display: inline-block; + } + + .image { + float: right; + max-height: 135px; + } + } + + .footer { + padding: 0 24px; + height: 48px; + line-height: 48px; + border-radius: 2px; + background-color: $N10; + + .dot { + display: inline-block; + width: 6px; + height: 6px; + border-radius: 50%; + background-color: $N30; + margin-right: 16px; + + &.active { + background-color: $P75; + } + } + + .skip { + display: inline-block; + margin-right: 12px; + line-height: 20px; + font-size: 12px; + color: $N75; + text-align: center; + cursor: pointer; + } + + button { + height: 24px; + font-size: 12px; + padding: 4px 8px; + cursor: pointer; + } + } +} + +.shadow { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + border-radius: 2px; + background-color: rgba(52, 57, 69, 0.7); +} diff --git a/src/locales/en/base.json b/src/locales/en/base.json index 6b14ce36..111ff348 100644 --- a/src/locales/en/base.json +++ b/src/locales/en/base.json @@ -191,5 +191,12 @@ "TIP_HELM_NAMESPACE": "The input characters for the namespace contain only lowercase letters and numbers", "MY_INSTANCES_DESCRIPTION": "List of instances created based on the application.", - "SPECIFICATION_LINK_TITLE": "{{type}} specification and application development" + "SPECIFICATION_LINK_TITLE": "{{type}} specification and application development", + + "WELCOME_TO_OPENPITRIX": "Welcome to the OpenPitrix App Center", + "WELCOME_TO_OPENPITRIX_DESC": "You just created an account, let us introduce you how to quickly find the application you need and deploy and manage it.", + "MENU_APP_INTRO": "There are many types of applications including SaaS class, container, VM class, etc., more than 1200 models, you can purchase and deploy at any time.", + "MENU_PURCHASED_INTRO": "All deployed or purchased apps can be viewed and managed here.", + "MENU_CLUSTER_INTRO": "Includes all deployed instances and important monitoring information for the instances, supporting different dimensions of search and filtering.", + "MENU_RUNTIME_INTRO": "Supports environment access, unified management, and comprehensive monitoring of multiple cloud platforms." } diff --git a/src/locales/zh/base.json b/src/locales/zh/base.json index 6d30b802..5a716363 100644 --- a/src/locales/zh/base.json +++ b/src/locales/zh/base.json @@ -499,5 +499,16 @@ "Back home": " 返回首页", "Sorry, the page you visited does not exist": "抱歉,您访问的页面不存在。", - "Invalid operation": "非法操作" + "Invalid operation": "非法操作", + + "WELCOME_TO_OPENPITRIX": "欢迎进入 OpenPitrix 应用中心", + "WELCOME_TO_OPENPITRIX_DESC": "你刚刚创建了账户,接下来让我们来为你介绍一下如何快速找到你需要的应用并部署和管理。", + "Let's start": "开始吧", + "MENU_APP_INTRO": "包含 SaaS 类,容器器,VM 类等众多类型的应用,超过 1200 款,可以随时购买与部署。", + "MENU_PURCHASED_INTRO": "所有部署或购买过的应用都可以在此统一查看和管理。", + "MENU_CLUSTER_INTRO": "包括所有部署的实例以及实例的重要监控信息,支持不同维度的搜索和过滤。", + "MENU_RUNTIME_INTRO": "支持多种云平台的环境接入,统一管理,全面监控。", + "Skip": "跳过", + "Learn all": "全部了解", + "To understanding": "了解" } diff --git a/src/pages/Login/index.jsx b/src/pages/Login/index.jsx index 1ec4fa41..4687e2ca 100644 --- a/src/pages/Login/index.jsx +++ b/src/pages/Login/index.jsx @@ -40,7 +40,9 @@ export default class Login extends Component { await store.fetchDetail(res.user.sub, true); } - const defaultUrl = toRoute(routes.portal.apps, user.defaultPortal); + const defaultUrl = user.isNormal + ? toRoute(routes.home) + : toRoute(routes.portal.apps, user.defaultPortal); if (!(res && res.err)) { localStorage.removeItem('menuApps'); // clear newest visited menu apps diff --git a/test/components/MenuIntroduction.test.js b/test/components/MenuIntroduction.test.js new file mode 100644 index 00000000..f8672077 --- /dev/null +++ b/test/components/MenuIntroduction.test.js @@ -0,0 +1,16 @@ +import MenuIntroduction from 'components/MenuIntroduction'; + +describe('Layout/MenuIntroduction', () => { + it('basic render', () => { + const wrapper = render( + + ); + + expect(toJson(wrapper)).toMatchSnapshot(); + }); +}); diff --git a/test/components/__snapshots__/MenuIntroduction.test.js.snap b/test/components/__snapshots__/MenuIntroduction.test.js.snap new file mode 100644 index 00000000..ef809ff6 --- /dev/null +++ b/test/components/__snapshots__/MenuIntroduction.test.js.snap @@ -0,0 +1,66 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Layout/MenuIntroduction basic render 1`] = ` +
+ +
+
+`;