diff --git a/chronograf/ui/src/index.tsx b/chronograf/ui/src/index.tsx index 7ff48a3b59..86319b70da 100644 --- a/chronograf/ui/src/index.tsx +++ b/chronograf/ui/src/index.tsx @@ -106,7 +106,7 @@ class Root extends PureComponent<{}, State> { - + diff --git a/chronograf/ui/src/page_layout/containers/Nav.tsx b/chronograf/ui/src/page_layout/containers/Nav.tsx index b2ec0f7180..b399d9cd3f 100644 --- a/chronograf/ui/src/page_layout/containers/Nav.tsx +++ b/chronograf/ui/src/page_layout/containers/Nav.tsx @@ -91,7 +91,7 @@ class SideNav extends PureComponent { { type: NavItemType.Avatar, title: 'My Profile', - link: `/user`, + link: `/user/settings/${this.sourceParam}`, image: LeroyJenkins.avatar, location: location.pathname, highlightWhen: ['user'], diff --git a/chronograf/ui/src/shared/components/profile_page/ProfilePage.scss b/chronograf/ui/src/shared/components/profile_page/ProfilePage.scss new file mode 100644 index 0000000000..9d5d2f0e2e --- /dev/null +++ b/chronograf/ui/src/shared/components/profile_page/ProfilePage.scss @@ -0,0 +1,81 @@ +/* + Styles for Profile Page + ---------------------------------------------------------------------------- +*/ + +.profile { + display: flex; + align-items: stretch; + background-color: rgba($g3-castle, 0.5); +} + +.profile-nav { + display: flex; + flex-direction: column; + align-items: stretch; + flex: 0 1 0; + padding-left: $ix-marg-d; +} + +.profile-nav--header { + @include no-user-select(); + margin: $ix-marg-d; + margin-left: 0; + display: flex; + flex-direction: column; + align-items: center; + text-align: center +} + +.profile-nav--name { + font-size: 19px; + line-height: 22px; + color: $g15-platinum; + margin: 0; + margin-top: 16px; +} + +.profile-nav--description { + font-weight: 600; + font-size: 13px; + line-height: 15px; + margin: 0; + margin-top: $ix-marg-a + $ix-marg-b; + color: $g11-sidewalk; +} + +.profile-nav--tabs { + @include no-user-select(); + display: flex; + flex-direction: column; + align-items: stretch; +} + +.profile-nav--tab { + border-radius: $radius 0 0 $radius; + font-size: 17px; + height: $nav-size; + line-height: $nav-size; + padding: 0 17px; + color: $g11-sidewalk; + transition: background-color 0.25s ease, color 0.25s ease; + + &:hover { + background-color: rgba($g3-castle, 0.5); + color: $g16-pearl; + cursor: pointer; + } + + &.active { + background-color: $g3-castle; + color: $g18-cloud; + } +} +.profile-content { + flex: 1 0 0; + background-color: $g3-castle; + border-radius: $radius; + min-height: 500px; + padding: $ix-marg-d; +} + diff --git a/chronograf/ui/src/shared/components/profile_page/ProfilePage.tsx b/chronograf/ui/src/shared/components/profile_page/ProfilePage.tsx new file mode 100644 index 0000000000..bad0b95045 --- /dev/null +++ b/chronograf/ui/src/shared/components/profile_page/ProfilePage.tsx @@ -0,0 +1,111 @@ +// Libraries +import React, {Component, ReactElement, ReactNode} from 'react' +import {withRouter, InjectedRouter} from 'react-router' + +// Components +import ProfilePageSection from 'src/shared/components/profile_page/ProfilePageSection' +import ProfilePageTab from 'src/shared/components/profile_page/ProfilePageTab' +import Avatar from 'src/shared/components/avatar/Avatar' + +// Decorators +import {ErrorHandling} from 'src/shared/decorators/errors' + +interface Props { + name: string + avatar: string + description?: string + children: ReactNode[] + activeTabUrl: string + router: InjectedRouter + parentUrl: string +} + +@ErrorHandling +class ProfilePage extends Component { + public static Section = ProfilePageSection + + constructor(props) { + super(props) + } + + public render() { + this.validateChildTypes() + + return ( +
+
+ {this.profileNavHeader} + {this.profileNavTabs} +
+
{this.activeSectionComponent}
+
+ ) + } + + private get profileNavTabs(): JSX.Element { + const {children, activeTabUrl} = this.props + + return ( +
+ {React.Children.map(children, (child: JSX.Element) => ( + + ))} +
+ ) + } + + private get profileNavHeader(): JSX.Element { + const {avatar, name, description} = this.props + + return ( +
+ +

{name}

+ {description && ( +

{description}

+ )} +
+ ) + } + + private get activeSectionComponent(): JSX.Element[] { + const {children, activeTabUrl} = this.props + + // Using ReactElement as type to ensure children have props + return React.Children.map(children, (child: ReactElement) => { + if (child.props.url === activeTabUrl) { + return child.props.children + } + }) + } + + public handleTabClick = (url: string) => (): void => { + const {router, parentUrl} = this.props + router.push(`${parentUrl}/${url}/`) + } + + private validateChildTypes = (): void => { + const {children} = this.props + + React.Children.forEach(children, (child: JSX.Element) => { + if (child.type !== ProfilePageSection) { + throw new Error( + ' expected children of type ' + ) + } + }) + } +} + +export default withRouter(ProfilePage) diff --git a/chronograf/ui/src/shared/components/profile_page/ProfilePageSection.tsx b/chronograf/ui/src/shared/components/profile_page/ProfilePageSection.tsx new file mode 100644 index 0000000000..1f57c95920 --- /dev/null +++ b/chronograf/ui/src/shared/components/profile_page/ProfilePageSection.tsx @@ -0,0 +1,21 @@ +// Libraries +import React, {Component} from 'react' + +// Decorators +import {ErrorHandling} from 'src/shared/decorators/errors' + +interface Props { + id: string + title: string + url: string + children: JSX.Element +} + +@ErrorHandling +class ProfilePageSection extends Component { + public render() { + return
{this.props.children}
+ } +} + +export default ProfilePageSection diff --git a/chronograf/ui/src/shared/components/profile_page/ProfilePageTab.tsx b/chronograf/ui/src/shared/components/profile_page/ProfilePageTab.tsx new file mode 100644 index 0000000000..89db6078ff --- /dev/null +++ b/chronograf/ui/src/shared/components/profile_page/ProfilePageTab.tsx @@ -0,0 +1,22 @@ +// Libraries +import React, {SFC} from 'react' +import classnames from 'classnames' + +interface Props { + id: string + title: string + active: boolean + url: string + onClick: (url: string) => () => void +} + +const ProfilePageTab: SFC = ({title, active, url, onClick}) => ( +
+ {title} +
+) + +export default ProfilePageTab diff --git a/chronograf/ui/src/style/chronograf.scss b/chronograf/ui/src/style/chronograf.scss index dc6418c031..cc1c0c41e6 100644 --- a/chronograf/ui/src/style/chronograf.scss +++ b/chronograf/ui/src/style/chronograf.scss @@ -22,6 +22,7 @@ // Components // TODO: Import these styles into their respective components instead of this stylesheet @import 'src/page_layout/PageLayout'; +@import 'src/shared/components/profile_page/ProfilePage'; @import 'src/shared/components/avatar/Avatar'; @import 'src/shared/components/fancy_scrollbar/FancyScrollbar'; @import 'src/shared/components/notifications/Notifications'; diff --git a/chronograf/ui/src/user/components/TokenManager.tsx b/chronograf/ui/src/user/components/TokenManager.tsx new file mode 100644 index 0000000000..6ce7994950 --- /dev/null +++ b/chronograf/ui/src/user/components/TokenManager.tsx @@ -0,0 +1,14 @@ +// Libraries +import React, {Component} from 'react' + +interface Props { + token: string +} + +class TokenManager extends Component { + public render() { + return
{this.props.token}
+ } +} + +export default TokenManager diff --git a/chronograf/ui/src/user/components/UserSettings.tsx b/chronograf/ui/src/user/components/UserSettings.tsx new file mode 100644 index 0000000000..c873a79674 --- /dev/null +++ b/chronograf/ui/src/user/components/UserSettings.tsx @@ -0,0 +1,14 @@ +// Libraries +import React, {Component} from 'react' + +interface Props { + blargh: string +} + +class UserSettings extends Component { + public render() { + return
{this.props.blargh}
+ } +} + +export default UserSettings diff --git a/chronograf/ui/src/user/containers/UserPage.tsx b/chronograf/ui/src/user/containers/UserPage.tsx index 6ab31c3242..9247188cb7 100644 --- a/chronograf/ui/src/user/containers/UserPage.tsx +++ b/chronograf/ui/src/user/containers/UserPage.tsx @@ -3,6 +3,9 @@ import React, {PureComponent} from 'react' /// Components import {Page} from 'src/page_layout' +import ProfilePage from 'src/shared/components/profile_page/ProfilePage' +import UserSettings from 'src/user/components/UserSettings' +import TokenManager from 'src/user/components/TokenManager' // Decorators import {ErrorHandling} from 'src/shared/decorators/errors' @@ -26,15 +29,20 @@ interface User { interface Props { user?: User + params: { + tab: string + } } @ErrorHandling -export class FluxPage extends PureComponent { +export class UserPage extends PureComponent { public static defaultProps: Partial = { user: LeroyJenkins, } public render() { + const {user, params} = this.props + return ( @@ -44,11 +52,33 @@ export class FluxPage extends PureComponent { -
fsfsfsdfs
+
+ + + + + + + + +
) } } -export default FluxPage +export default UserPage