Small header (#1721)

Co-authored-by: René Pfeuffer <rene.pfeuffer@cloudogu.com>
This commit is contained in:
Eduard Heimbuch
2021-07-07 16:26:58 +02:00
committed by GitHub
parent 123bf5c862
commit cd6e624e61
20 changed files with 656 additions and 182 deletions

View File

@@ -32,8 +32,14 @@ const Wrapper = styled.div`
height: 100%;
`;
storiesOf("Logo", module).add("Default", () => (
<Wrapper>
<Logo />
</Wrapper>
));
storiesOf("Logo", module)
.add("Default", () => (
<Wrapper>
<Logo />
</Wrapper>
))
.add("WithoutText", () => (
<Wrapper>
<Logo withText={false} />
</Wrapper>
));

View File

@@ -21,15 +21,22 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React from "react";
import { WithTranslation, withTranslation } from "react-i18next";
import React, { FC } from "react";
import { useTranslation } from "react-i18next";
import Image from "./Image";
class Logo extends React.Component<WithTranslation> {
render() {
const { t } = this.props;
return <Image src="/images/logo.png" alt={t("logo.alt")} />;
}
}
type Props = {
withText?: boolean;
className?: string;
};
export default withTranslation("commons")(Logo);
const Logo: FC<Props> = ({ withText = true, className }) => {
const [t] = useTranslation("commons");
if (withText) {
return <Image src="/images/logo.png" alt={t("logo.alt")} className={className} />;
}
return <Image src="/images/scmLogo.svg" alt={t("logo.alt")} className={className} />;
};
export default Logo;

View File

@@ -53430,6 +53430,17 @@ exports[`Storyshots Logo Default 1`] = `
</div>
`;
exports[`Storyshots Logo WithoutText 1`] = `
<div
className="Logostories__Wrapper-sc-14nnt4j-0 brMuIC"
>
<img
alt="logo.alt"
src="/images/scmLogo.svg"
/>
</div>
`;
exports[`Storyshots MarkdownView Code without Lang 1`] = `
<div
className="MarkdownViewstories__Spacing-sc-1lofakk-0 iOWSJJ"

View File

@@ -30,20 +30,20 @@ export type Device = {
export const devices = {
mobile: {
width: 768
width: 768,
},
tablet: {
width: 769
width: 769,
},
desktop: {
width: 1024
width: 1024,
},
widescreen: {
width: 1216
width: 1216,
},
fullhd: {
width: 1408
}
width: 1408,
},
};
export type DeviceType = keyof typeof devices;

View File

@@ -21,33 +21,42 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { ReactNode } from "react";
import React, { FC, ReactNode } from "react";
import Logo from "./../Logo";
import { Links } from "@scm-manager/ui-types";
type Props = {
children?: ReactNode;
links: Links;
authenticated: boolean;
children: ReactNode;
};
class Header extends React.Component<Props> {
render() {
const { children } = this.props;
return (
<section className="hero is-dark is-small">
<div className="hero-body">
<div className="container">
<div className="columns is-vcentered">
<div className="column">
<Logo />
</div>
const SmallHeader: FC<{ children: ReactNode }> = ({ children }) => {
return <div className="has-scm-background">{children}</div>;
};
const LargeHeader: FC = () => {
return (
<section className="hero has-scm-background is-small">
<div className="hero-body">
<div className="container">
<div className="columns is-vcentered">
<div className="column">
<Logo />
</div>
</div>
</div>
<div className="hero-foot">
<div className="container">{children}</div>
</div>
</section>
);
</div>
</section>
);
};
const Header: FC<Props> = ({ authenticated, children, links }) => {
if (authenticated) {
return <SmallHeader>{children}</SmallHeader>;
} else {
return <LargeHeader />;
}
}
};
export default Header;

View File

@@ -21,86 +21,59 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { ReactNode } from "react";
import { WithTranslation, withTranslation } from "react-i18next";
import React, { FC, ReactNode } from "react";
import PrimaryNavigationLink from "./PrimaryNavigationLink";
import { Links } from "@scm-manager/ui-types";
import { binder, ExtensionPoint } from "@scm-manager/ui-extensions";
import { urls } from "@scm-manager/ui-api";
import { withRouter, RouteComponentProps } from "react-router-dom";
import { useLocation } from "react-router-dom";
import { useTranslation } from "react-i18next";
type Props = RouteComponentProps & WithTranslation & {
type Props = {
links: Links;
};
type Appender = (to: string, match: string, label: string, linkName: string) => void;
class PrimaryNavigation extends React.Component<Props> {
createNavigationAppender = (navigationItems: ReactNode[]): Appender => {
const { t, links } = this.props;
const PrimaryNavigation: FC<Props> = ({ links }) => {
const [t] = useTranslation("commons");
const location = useLocation();
const createNavigationAppender = (navItems: ReactNode[]): Appender => {
return (to: string, match: string, label: string, linkName: string) => {
const link = links[linkName];
if (link) {
const navigationItem = <PrimaryNavigationLink testId={label.replace(".", "-")} to={to} match={match} label={t(label)} key={linkName} />;
navigationItems.push(navigationItem);
const navigationItem = (
<PrimaryNavigationLink
testId={label.replace(".", "-")}
to={to}
match={match}
label={t(label)}
key={linkName}
className="navbar-item"
/>
);
navItems.push(navigationItem);
}
};
};
appendLogout = (navigationItems: ReactNode[], append: Appender) => {
const { t, links } = this.props;
const createNavigationItems = () => {
const navItems: ReactNode[] = [];
const props = {
const extensionProps = {
links,
label: t("primary-navigation.logout")
label: t("primary-navigation.first-menu"),
};
if (binder.hasExtension("primary-navigation.logout", props)) {
navigationItems.push(
<ExtensionPoint key="primary-navigation.logout" name="primary-navigation.logout" props={props} />
);
} else {
append("/logout", "/logout", "primary-navigation.logout", "logout");
}
};
appendLogin = (navigationItems: ReactNode[], append: Appender) => {
const { t, links, location } = this.props;
const from = location.pathname;
const loginPath = "/login";
const to = `${loginPath}?from=${encodeURIComponent(from)}`;
const props = {
links,
label: t("primary-navigation.login"),
loginUrl: urls.withContextPath(loginPath),
from
};
if (binder.hasExtension("primary-navigation.login", props)) {
navigationItems.push(
<ExtensionPoint key="primary-navigation.login" name="primary-navigation.login" props={props} />
);
} else {
append(to, "/login", "primary-navigation.login", "login");
}
};
createNavigationItems = () => {
const navigationItems: ReactNode[] = [];
const { t, links } = this.props;
const props = {
links,
label: t("primary-navigation.first-menu")
};
const append = this.createNavigationAppender(navigationItems);
if (binder.hasExtension("primary-navigation.first-menu", props)) {
navigationItems.push(
<ExtensionPoint key="primary-navigation.first-menu" name="primary-navigation.first-menu" props={props} />
const append = createNavigationAppender(navItems);
if (binder.hasExtension("primary-navigation.first-menu", extensionProps)) {
navItems.push(
<ExtensionPoint
key="primary-navigation.first-menu"
name="primary-navigation.first-menu"
props={extensionProps}
/>
);
}
append("/repos/", "/(repo|repos)", "primary-navigation.repositories", "repositories");
@@ -108,32 +81,21 @@ class PrimaryNavigation extends React.Component<Props> {
append("/groups/", "/(group|groups)", "primary-navigation.groups", "groups");
append("/admin", "/admin", "primary-navigation.admin", "config");
navigationItems.push(
navItems.push(
<ExtensionPoint
key="primary-navigation"
name="primary-navigation"
renderAll={true}
props={{
links: this.props.links
links,
}}
/>
);
this.appendLogout(navigationItems, append);
this.appendLogin(navigationItems, append);
return navigationItems;
return navItems;
};
render() {
const navigationItems = this.createNavigationItems();
return <>{createNavigationItems()}</>;
};
return (
<nav className="tabs is-boxed mb-0">
<ul>{navigationItems}</ul>
</nav>
);
}
}
export default withTranslation("commons")(withRouter(PrimaryNavigation));
export default PrimaryNavigation;

View File

@@ -21,42 +21,31 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import * as React from "react";
import { Route, Link } from "react-router-dom";
import React, { FC } from "react";
import { useRouteMatch, Link } from "react-router-dom";
import { createAttributesForTesting } from "../devBuild";
import classNames from "classnames";
type Props = {
to: string;
match: string;
label: string;
match?: string;
activeOnlyWhenExact?: boolean;
testId?: string;
className?: string;
};
class PrimaryNavigationLink extends React.Component<Props> {
renderLink = (route: any) => {
const { to, label, testId } = this.props;
return (
<li className={route.match ? "is-active" : ""}>
<Link to={to} {...createAttributesForTesting(testId)}>
{label}
</Link>
</li>
);
};
const PrimaryNavigationLink: FC<Props> = ({ to, match, testId, label, className }) => {
const routeMatch = useRouteMatch({ path: match });
render() {
const { to, match, activeOnlyWhenExact, testId } = this.props;
const path = match ? match : to;
return (
<Route
path={path}
exact={activeOnlyWhenExact}
children={this.renderLink}
{...createAttributesForTesting(testId)}
/>
);
}
}
return (
<Link
to={to}
className={classNames(className, "navbar-item", { "is-active": routeMatch })}
{...createAttributesForTesting(testId)}
>
{label}
</Link>
);
};
export default PrimaryNavigationLink;

View File

@@ -10,7 +10,7 @@
},
"dependencies": {
"@fortawesome/fontawesome-free": "^5.11.2",
"bulma": "^0.9.0",
"bulma": "^0.9.3",
"bulma-popover": "^1.0.0",
"bulma-tooltip": "^3.0.0",
"react-diff-view": "^2.4.1"

View File

@@ -60,10 +60,6 @@ $family-monospace: "Courier New", Monaco, Menlo, "Ubuntu Mono", "source-code-pro
padding: 0 0 0 3.8em !important;
}
.main {
min-height: calc(100vh - 300px);
}
// shown in top section when pageactions set
hr.header-with-actions {
margin-top: -10px;
@@ -92,7 +88,6 @@ hr.header-with-actions {
}
footer.footer {
//height: 100px;
background-color: $white-ter;
padding: inherit;
@@ -716,6 +711,13 @@ form .field:not(.is-grouped) {
opacity: 1;
}
.has-scm-background {
background-image: url(images/scmManagerHero.jpg) !important;
background-size: cover;
background-position: top center;
background-color: #002e4b;
}
// hero
.hero.is-dark {
background-color: #002e4b;

View File

@@ -22,10 +22,11 @@
* SOFTWARE.
*/
import { Links } from "./hal";
import { Embedded, Links } from "./hal";
export type IndexResources = {
version: string;
initialization?: string;
_links: Links;
_embedded?: Embedded;
};

View File

@@ -0,0 +1,104 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.3.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 213.8 181.8" style="enable-background:new 0 0 213.8 181.8;" xml:space="preserve">
<style type="text/css">
.st0{fill:#C7C7C6;}
.st1{clip-path:url(#SVGID_2_);fill:url(#SVGID_3_);}
.st2{clip-path:url(#SVGID_5_);fill:url(#SVGID_6_);}
.st3{clip-path:url(#SVGID_8_);fill:url(#SVGID_9_);}
.st4{clip-path:url(#SVGID_11_);fill:url(#SVGID_12_);}
.st5{clip-path:url(#SVGID_14_);fill:url(#SVGID_15_);}
</style>
<g>
<path class="st0" d="M0.1,0L0,0.1C25,30,61.8,97.2,41,160.3c-0.4,3.2-1.4,6.1-2.8,8.7C76.6,99.9,25.3,19.6,14.6,4.2
C9.8,2.4,4.9,1,0.1,0"/>
<path class="st0" d="M40.5,165.2C75.9,104.5,41.6,35.4,25.2,8.5c4.8,2.3,9.6,4.9,14.1,7.7C54.8,47.3,76.1,107,45.6,157
C43.9,159.7,42.2,162.5,40.5,165.2"/>
<path class="st0" d="M49.3,151c7.7-13.3,31.3-53.6-0.4-128.3C53.2,26,57.3,29.4,61,33c13.8,44.1,11.6,80-6.6,109.8L49.3,151z"/>
<path class="st0" d="M58.8,135.6c7-12,23.4-40.1,11.3-93.2c3.2,3.8,6.1,7.7,8.5,11.7c2,21.9,1.5,45.9-14.7,73.2
C62.2,130.1,60.5,132.9,58.8,135.6"/>
<path class="st0" d="M67.6,121.5c5.1-8.8,15.8-27.1,15.6-58.7c0.6,1.3,1.1,2.7,1.6,4c1,2.7,1.7,5.3,2.1,7.7
c-2.5,13.8-5.6,24.1-14.2,38.6L67.6,121.5z"/>
<g>
<defs>
<path id="SVGID_1_" d="M75.7,181.8L75.7,181.8c0.6-2.7,1.7-5.2,3.3-7.7c24.8-61.6,96.4-89.3,134.8-96l0-0.2
c-4.3-2.4-9-4.4-13.8-6.2C182,76.6,91.3,104.8,75.7,181.8"/>
</defs>
<clipPath id="SVGID_2_">
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
</clipPath>
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="-306.7593" y1="394.7581" x2="-304.2159" y2="394.7581" gradientTransform="matrix(54.3221 0 0 -54.3221 16739.459 21570.8125)">
<stop offset="0" style="stop-color:#45BFEA"/>
<stop offset="0.5" style="stop-color:#2FA9E0"/>
<stop offset="1" style="stop-color:#2893D1"/>
</linearGradient>
<rect x="75.7" y="71.7" class="st1" width="138.2" height="110.1"/>
</g>
<g>
<defs>
<path id="SVGID_4_" d="M77.6,168.5c-0.5,3.2-1,6.4-1.4,9.6c12.2-69.2,83.1-99.9,113-109.9c-5.2-1.4-10.5-2.4-15.8-3.2
C141.4,78.9,86.6,110.7,77.6,168.5"/>
</defs>
<clipPath id="SVGID_5_">
<use xlink:href="#SVGID_4_" style="overflow:visible;"/>
</clipPath>
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="-305.2708" y1="395.9162" x2="-302.7246" y2="395.9162" gradientTransform="matrix(44.3853 0 0 -44.3853 13625.6709 17694.4355)">
<stop offset="0" style="stop-color:#45BFEA"/>
<stop offset="0.5" style="stop-color:#2FA9E0"/>
<stop offset="1" style="stop-color:#2893D1"/>
</linearGradient>
<rect x="76.1" y="65" class="st2" width="113" height="113.1"/>
</g>
<g>
<defs>
<path id="SVGID_7_" d="M145.9,63.8c-39,24.8-60.6,53.7-65.9,88.2l-1.4,9.6c2.7-15.1,10.8-61.1,83.2-97.8
c-2.8-0.2-5.5-0.2-8.3-0.2C151,63.6,148.5,63.7,145.9,63.8"/>
</defs>
<clipPath id="SVGID_8_">
<use xlink:href="#SVGID_7_" style="overflow:visible;"/>
</clipPath>
<linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="-302.3176" y1="398.1338" x2="-299.7714" y2="398.1338" gradientTransform="matrix(32.6903 0 0 -32.6903 9961.4385 13127.7422)">
<stop offset="0" style="stop-color:#45BFEA"/>
<stop offset="0.5" style="stop-color:#2FA9E0"/>
<stop offset="1" style="stop-color:#2893D1"/>
</linearGradient>
<rect x="78.6" y="63.6" class="st3" width="83.2" height="98"/>
</g>
<g>
<defs>
<path id="SVGID_10_" d="M118.8,68.7c-15.6,15.4-30.8,34.1-36.1,65.4c-0.5,3.2-1,6.4-1.4,9.6c2.4-13.7,8-45.7,51.6-78.5
C128,66.1,123.2,67.3,118.8,68.7"/>
</defs>
<clipPath id="SVGID_11_">
<use xlink:href="#SVGID_10_" style="overflow:visible;"/>
</clipPath>
<linearGradient id="SVGID_12_" gradientUnits="userSpaceOnUse" x1="-295.4908" y1="403.173" x2="-292.9446" y2="403.173" gradientTransform="matrix(20.2781 0 0 -20.2781 6073.2446 8280.0889)">
<stop offset="0" style="stop-color:#45BFEA"/>
<stop offset="0.5" style="stop-color:#2FA9E0"/>
<stop offset="1" style="stop-color:#2893D1"/>
</linearGradient>
<rect x="81.3" y="65.3" class="st4" width="51.6" height="78.5"/>
</g>
<g>
<defs>
<path id="SVGID_13_" d="M105.9,74.4c-2.5,1.4-4.7,2.9-6.6,4.5c-7,12.2-11.3,22-14.1,38.7l-1.4,9.6c1.8-10,5.4-30.9,26-54.9
C108.4,73,107.1,73.7,105.9,74.4"/>
</defs>
<clipPath id="SVGID_14_">
<use xlink:href="#SVGID_13_" style="overflow:visible;"/>
</clipPath>
<linearGradient id="SVGID_15_" gradientUnits="userSpaceOnUse" x1="-277.7022" y1="416.0602" x2="-275.1562" y2="416.0602" gradientTransform="matrix(10.2218 0 0 -10.2218 2922.3333 4352.7075)">
<stop offset="0" style="stop-color:#45BFEA"/>
<stop offset="0.5" style="stop-color:#2FA9E0"/>
<stop offset="1" style="stop-color:#2893D1"/>
</linearGradient>
<rect x="83.7" y="72.4" class="st5" width="26" height="54.9"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -24,11 +24,11 @@
import React, { FC } from "react";
import Main from "./Main";
import { useTranslation } from "react-i18next";
import { ErrorPage, Footer, Header, Loading, PrimaryNavigation } from "@scm-manager/ui-components";
import { ErrorPage, Footer, Header, Loading } from "@scm-manager/ui-components";
import { binder } from "@scm-manager/ui-extensions";
import Login from "./Login";
import { useIndex, useSubject } from "@scm-manager/ui-api";
import Notifications from "./Notifications";
import NavigationBar from "./NavigationBar";
const App: FC = () => {
const { data: index } = useIndex();
@@ -46,7 +46,7 @@ const App: FC = () => {
if (index?.initialization) {
const Extension = binder.getExtension(`initialization.step.${index.initialization}`);
content = <Extension data={index._embedded[index.initialization]} />;
content = <Extension data={index?._embedded ? index._embedded[index.initialization] : undefined} />;
} else if (!authenticated && !isLoading) {
content = <Login />;
} else if (isLoading) {
@@ -59,13 +59,8 @@ const App: FC = () => {
return (
<div className="App">
<Header>
{authenticated ? (
<div className="is-flex is-justify-content-space-between is-flex-wrap-nowrap ">
<PrimaryNavigation links={index._links} />
<Notifications />
</div>
) : null}
<Header authenticated={authenticated} links={index._links}>
<NavigationBar links={index._links} />
</Header>
{content}
{authenticated ? <Footer me={me} version={index.version} links={index._links} /> : null}

View File

@@ -0,0 +1,46 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC } from "react";
import Notifications from "./Notifications";
import LogoutButton from "./LogoutButton";
import { Links } from "@scm-manager/ui-types";
import LoginButton from "./LoginButton";
type Props = {
burgerMode: boolean;
links: Links;
};
const HeaderActions: FC<Props> = ({ burgerMode, links }) => {
return (
<>
{!burgerMode ? <Notifications className="navbar-item" /> : null}
<LogoutButton burgerMode={burgerMode} links={links} />
<LoginButton burgerMode={burgerMode} links={links} />
</>
);
};
export default HeaderActions;

View File

@@ -0,0 +1,86 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC } from "react";
import { devices, Icon } from "@scm-manager/ui-components";
import { binder, ExtensionPoint } from "@scm-manager/ui-extensions";
import { useTranslation } from "react-i18next";
import { Links } from "@scm-manager/ui-types";
import classNames from "classnames";
import { useHistory } from "react-router-dom";
import styled from "styled-components";
type Props = {
className?: string;
links?: Links;
burgerMode: boolean;
};
const StyledLogoutButton = styled.div`
@media screen and (max-width: ${devices.desktop.width}px) {
border-top: 1px solid white;
margin-top: 1rem;
padding-top: 1rem;
padding-bottom: 1rem;
}
@media screen and (min-width: ${devices.desktop.width}px) {
margin-left: 2rem;
}
`;
const LoginButton: FC<Props> = ({ burgerMode, links, className }) => {
const [t] = useTranslation("commons");
const history = useHistory();
const extensionProps = {
links,
label: t("primary-navigation.login"),
};
if (links?.login) {
if (binder.hasExtension("primary-navigation.login", extensionProps)) {
return <ExtensionPoint key="primary-navigation.login" name="primary-navigation.login" props={extensionProps} />;
} else {
return (
<StyledLogoutButton
data-testid="primary-navigation-login"
onClick={() => history.push({ pathname: "/login" })}
className={classNames("is-align-items-center", "navbar-item", className)}
>
<Icon
title={t("primary-navigation.login")}
name="sign-in-alt"
color="white"
className={burgerMode ? "is-size-5" : "is-size-4"}
/>
{" " + t("primary-navigation.login")}
</StyledLogoutButton>
);
}
}
return null;
};
export default LoginButton;

View File

@@ -0,0 +1,86 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC } from "react";
import { devices, Icon } from "@scm-manager/ui-components";
import { binder, ExtensionPoint } from "@scm-manager/ui-extensions";
import { useTranslation } from "react-i18next";
import { Links } from "@scm-manager/ui-types";
import classNames from "classnames";
import { useHistory } from "react-router-dom";
import styled from "styled-components";
type Props = {
className?: string;
links?: Links;
burgerMode: boolean;
};
const StyledLogoutButton = styled.div`
@media screen and (max-width: ${devices.desktop.width}px) {
border-top: 1px solid white;
margin-top: 1rem;
padding-top: 1rem;
padding-bottom: 1rem;
}
@media screen and (min-width: ${devices.desktop.width}px) {
margin-left: 2rem;
}
`;
const LogoutButton: FC<Props> = ({ burgerMode, links, className }) => {
const [t] = useTranslation("commons");
const history = useHistory();
const extensionProps = {
links,
label: t("primary-navigation.logout"),
};
if (links?.logout) {
if (binder.hasExtension("primary-navigation.logout", extensionProps)) {
return <ExtensionPoint key="primary-navigation.logout" name="primary-navigation.logout" props={extensionProps} />;
} else {
return (
<StyledLogoutButton
data-testid="primary-navigation-logout"
onClick={() => history.push({ pathname: "/logout" })}
className={classNames("is-align-items-center", "navbar-item", className)}
>
<Icon
title={t("primary-navigation.logout")}
name="sign-out-alt"
color="white"
className={burgerMode ? "is-size-5" : "is-size-4"}
/>
{" " + t("primary-navigation.logout")}
</StyledLogoutButton>
);
}
}
return null;
};
export default LogoutButton;

View File

@@ -48,6 +48,7 @@ import Profile from "./Profile";
import NamespaceRoot from "../repos/namespaces/containers/NamespaceRoot";
import ImportLog from "../repos/importlog/ImportLog";
import CreateRepositoryRoot from "../repos/containers/CreateRepositoryRoot";
import styled from "styled-components";
type Props = {
me: Me;
@@ -55,6 +56,14 @@ type Props = {
links: Links;
};
type StyledMainProps = {
isSmallHeader: boolean;
};
const StyledMain = styled.div.attrs((props) => ({}))<StyledMainProps>`
min-height: calc(100vh - ${(props) => (props.isSmallHeader ? 250 : 210)}px);
`;
class Main extends React.Component<Props> {
render() {
const { authenticated, me, links } = this.props;
@@ -71,7 +80,7 @@ class Main extends React.Component<Props> {
}
return (
<ErrorBoundary>
<div className="main">
<StyledMain className="main" isSmallHeader={!!links.logout}>
<Switch>
<Redirect exact from="/" to={url} />
<Route exact path="/login" component={Login} />
@@ -103,11 +112,11 @@ class Main extends React.Component<Props> {
props={{
me,
links,
authenticated
authenticated,
}}
/>
</Switch>
</div>
</StyledMain>
</ErrorBoundary>
);
}

View File

@@ -0,0 +1,144 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC, useEffect, useState } from "react";
import { Links } from "@scm-manager/ui-types";
import classNames from "classnames";
import styled from "styled-components";
import { devices, Logo, PrimaryNavigation } from "@scm-manager/ui-components";
import HeaderActions from "./HeaderActions";
import Notifications from "./Notifications";
const StyledMenuBar = styled.div`
background-color: transparent !important;
`;
const LogoItem = styled.a`
cursor: default !important;
`;
const StyledNavBar = styled.nav`
@media screen and (min-width: ${devices.desktop.width - 1}px) {
.navbar-burger-actions {
display: none;
}
}
.navbar-start .navbar-item {
border-bottom: solid 5px transparent;
&.is-active {
border-bottom: solid 5px #28b1e8;
}
}
.navbar-menu.is-active .navbar-start .navbar-item {
border-bottom: none;
border-left: solid 5px transparent;
&.is-active {
border-left: solid 5px #28b1e8;
}
}
.navbar-menu {
padding: 0;
}
.navbar-brand {
@media screen and (max-width: ${devices.desktop.width - 1}px) {
border-bottom: 1px solid white;
}
}
.navbar-menu.is-active .navbar-end .navbar-item {
border-left: solid 5px transparent;
}
.navbar-burger {
color: #fff !important;
}
.navbar-item {
:hover:not(.logo) {
background-color: rgba(10, 10, 10, 0.1) !important;
color: #fff;
}
color: #fff !important;
background-color: transparent !important;
}
color: #fff;
background-color: transparent !important;
`;
type Props = {
links: Links;
};
const BurgerActionBar: FC = () => (
<div className="navbar-burger-actions">
<Notifications className="navbar-item" direction="left" />
</div>
);
const NavigationBar: FC<Props> = ({ links }) => {
const [burgerActive, setBurgerActive] = useState(false);
useEffect(() => {
const close = () => {
if (burgerActive) {
setBurgerActive(false);
}
};
window.addEventListener("click", close);
return () => window.removeEventListener("click", close);
}, [burgerActive]);
return (
<StyledNavBar className="navbar mb-0 container" role="navigation" aria-label="main navigation">
<div className="navbar-brand">
<LogoItem className="navbar-item logo">
<Logo withText={false} className="image is-32x32" />
</LogoItem>
<BurgerActionBar />
<button
role="button"
className={classNames("navbar-burger", { "is-active": burgerActive })}
aria-expanded="true"
onClick={() => setBurgerActive((active) => !active)}
>
<span aria-hidden="true" />
<span aria-hidden="true" />
<span aria-hidden="true" />
</button>
</div>
<StyledMenuBar className={classNames("navbar-menu", { "is-active": burgerActive })}>
<div className="navbar-start">
<PrimaryNavigation links={links} />
</div>
<div className="navbar-end">
<HeaderActions burgerMode={burgerActive} links={links} />
</div>
</StyledMenuBar>
</StyledNavBar>
);
};
export default NavigationBar;

View File

@@ -33,14 +33,14 @@ import {
ToastType,
Loading,
DateFromNow,
devices
devices,
} from "@scm-manager/ui-components";
import styled from "styled-components";
import {
useClearNotifications,
useDismissNotification,
useNotifications,
useNotificationSubscription
useNotificationSubscription,
} from "@scm-manager/ui-api";
import { Notification, NotificationCollection } from "@scm-manager/ui-types";
import { useHistory, Link } from "react-router-dom";
@@ -54,13 +54,14 @@ const Bell = styled(Icon)`
const Container = styled.div`
display: flex;
cursor: pointer;
@media screen and (max-width: ${devices.desktop.width}px) {
padding-right: 1rem;
}
`;
const DropDownMenu = styled.div`
type DropDownProps = {
direction: "left" | "right";
};
// eslint-disable-next-line @typescript-eslint/no-empty-function
const DropDownMenu = styled.div.attrs((props) => {})<DropDownProps>`
min-width: 35rem;
@media screen and (max-width: ${devices.mobile.width}px) {
@@ -79,7 +80,7 @@ const DropDownMenu = styled.div`
height: 0;
width: 0;
top: 0;
right: 0.9rem;
${(props) => props.direction}: 1.25rem;
border-color: transparent;
border-bottom-color: white;
border-left-color: white;
@@ -273,6 +274,9 @@ const BellNotificationContainer = styled.div`
position: relative;
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
`;
type NotificationCounterProps = {
@@ -282,7 +286,7 @@ type NotificationCounterProps = {
const NotificationCounter = styled.span<NotificationCounterProps>`
position: absolute;
top: -0.5rem;
right: ${props => (props.count < 10 ? "0" : "-0.25")}rem;
right: ${(props) => (props.count < 10 ? "0" : "-0.25")}rem;
`;
type BellNotificationIconProps = {
@@ -317,7 +321,12 @@ const ErrorBox: FC<{ error: Error | null }> = ({ error }) => {
);
};
const Notifications: FC = () => {
type NotificationProps = {
className?: string;
direction?: "left" | "right";
};
const Notifications: FC<NotificationProps> = ({ className, direction = "right" }) => {
const { data, isLoading, error, refetch } = useNotifications();
const { notifications, remove, clear } = useNotificationSubscription(refetch, data);
@@ -332,15 +341,21 @@ const Notifications: FC = () => {
<>
<NotificationSubscription notifications={notifications} remove={remove} />
<div
className={classNames("is-align-self-flex-end", "dropdown", "is-right", "is-hoverable", {
"is-active": open
})}
onClick={e => e.stopPropagation()}
className={classNames(
"dropdown",
`is-${direction}`,
"is-hoverable",
{
"is-active": open,
},
className
)}
onClick={(e) => e.stopPropagation()}
>
<Container className="dropdown-trigger">
<BellNotificationIcon data={data} onClick={() => setOpen(o => !o)} />
<BellNotificationIcon data={data} onClick={() => setOpen((o) => !o)} />
</Container>
<DropDownMenu className="dropdown-menu" id="dropdown-menu" role="menu">
<DropDownMenu className="dropdown-menu" id="dropdown-menu" role="menu" direction={direction}>
<ErrorBox error={error} />
{isLoading ? <LoadingBox /> : null}
{data ? <NotificationDropDown data={data} remove={remove} clear={clear} /> : null}