Skip to content

Commit

Permalink
First version for the Header component using Clarity (#1831)
Browse files Browse the repository at this point in the history
  • Loading branch information
Andres Martinez Gotor committed Jul 1, 2020
1 parent b5ed425 commit dc58023
Show file tree
Hide file tree
Showing 13 changed files with 439 additions and 61 deletions.
24 changes: 16 additions & 8 deletions dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
"private": true,
"homepage": "./",
"dependencies": {
"@clr/city": "^1.1.0",
"@clr/core": "^3.1.4",
"@clr/ui": "^2.3.4",
"@types/diff": "^4.0.2",
"@types/js-yaml": "^3.10.1",
"@types/json-schema": "^7.0.3",
Expand All @@ -12,9 +15,9 @@
"@types/moxios": "^0.4.8",
"@types/pako": "^1.0.0",
"@types/qs": "^6.5.1",
"@types/react": "^16.9.11",
"@types/react": "^16.9.41",
"@types/react-jsonschema-form": "^1.0.4",
"@types/react-router-dom": "^5.1.2",
"@types/react-router-dom": "^5.1.5",
"@types/react-router-hash-link": "^1.2.1",
"@types/react-select": "^1.2.6",
"@types/react-tabs": "^2.3.1",
Expand All @@ -40,27 +43,29 @@
"lodash": "^4.17.11",
"mem": "^4.0.0",
"moniker-native": "^0.1.6",
"prop-types": "15.7.2",
"protobufjs": "^6.8.4",
"qs": "^6.5.2",
"raf": "^3.4.0",
"react": "^16.11.0",
"react": "^16.13.0",
"react-ace": "^8.0.0",
"react-compound-slider": "^2.3.0",
"react-dom": "^16.11.0",
"react-dom": "^16.13.0",
"react-feather": "^1.0.8",
"react-jsonschema-form": "^1.0.3",
"react-markdown": "^4.2.2",
"react-minimal-pie-chart": "^6.0.1",
"react-modal": "^3.1.11",
"react-redux": "^7.1.9",
"react-router": "^5.1.2",
"react-router-dom": "^5.1.2",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-router-hash-link": "^1.2.2",
"react-select": "^1.2.1",
"react-switch": "^5.0.1",
"react-tabs": "^3.0.0",
"react-test-renderer": "^16.2.0",
"react-tooltip": "3.11.1",
"react-transition-group": "4.3.0",
"redux": "^4.0.0",
"redux-devtools-extension": "^2.13.5",
"redux-thunk": "^2.2.0",
Expand Down Expand Up @@ -109,10 +114,10 @@
"@types/enzyme-adapter-react-16": "^1.0.1",
"@types/jest": "^26.0.3",
"@types/node": "^14.0.14",
"@types/react-dom": "^16.9.4",
"@types/react-dom": "^16.9.5",
"@types/react-modal": "^3.1.1",
"@types/react-redux": "^7.1.9",
"@types/react-router": "^4.0.20",
"@types/react-router": "^5.1.5",
"@types/react-test-renderer": "^16.0.0",
"@types/redux-mock-store": "^1.0.0",
"enzyme": "^3.6.0",
Expand Down Expand Up @@ -141,6 +146,9 @@
"collectCoverageFrom": [
"src/**/*",
"!src/**/*.d.ts"
],
"transformIgnorePatterns": [
"node_modules/(?!@clr|lit-element|lit-html|ramda|.*css)"
]
},
"eslintConfig": {
Expand Down
14 changes: 14 additions & 0 deletions dashboard/src/components/Clarity/clarity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Code from: https://stackblitz.com/edit/react-ts-wrapper-5w8nhf
import { CdsButton as Button } from "@clr/core/button";
import { CdsIcon as Icon, ClarityIcons as ClrIcons } from "@clr/core/icon-shapes";

import "@clr/core/button";
import "@clr/core/icon";
import { createReactComponent } from "./converter/reactWrapper";

type CdsIconType = Icon;
export const CdsIcon = createReactComponent<CdsIconType>("cds-icon");
export const ClarityIcons = ClrIcons;

type CdsButtonType = Button & HTMLButtonElement;
export const CdsButton = createReactComponent<CdsButtonType>("cds-button");
75 changes: 75 additions & 0 deletions dashboard/src/components/Clarity/converter/reactWrapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Code from: https://stackblitz.com/edit/react-ts-wrapper-5w8nhf
import React from "react";

export function createReactComponent<BaseComponent extends HTMLElement>(elementName: string) {
return class ReactWrapperComponent extends React.Component<
Partial<Omit<BaseComponent, "children"> & React.DOMAttributes<HTMLElement>>
> {
public ref: React.RefObject<BaseComponent>;

get _customPropsList() {
return Object.keys(this.props).filter(prop => !this._propIsReservedReactProp(prop));
}

get nativeElement(): Promise<BaseComponent> {
return (this.ref.current as any).updateComplete.then(() => this.ref.current);
}

constructor(props: any) {
super(props);
this.ref = React.createRef(); // need to document minimum version of react needed
}

public componentDidMount() {
this._customPropsList.forEach(prop => {
if (this._propIsFunction(prop)) {
this._createCustomElementEvent(prop);
} else {
this._updateCustomElementProperty(prop);
}
});

if (this.ref.current) {
this.ref.current.focus = this.ref.current.focus;
}
}

public componentDidUpdate(prevProps: any) {
this._customPropsList
.filter(prop => !this._propIsFunction(prop) && prevProps[prop] !== this.props[prop])
.forEach(prop => this._updateCustomElementProperty(prop));
}

public render() {
return React.createElement(elementName, { ref: this.ref }, this.props.children);
}

public _propIsReservedReactProp(prop: any) {
const reactProperties = ["children", "localName", "ref", "style", "className"];
return reactProperties.indexOf(prop) !== -1;
}

public _propIsFunction(prop: any) {
return typeof this.props[prop] === "function";
}

public _createCustomElementEvent(prop: any) {
if (this.ref.current) {
let eventName = prop.substring(2);
eventName = eventName.charAt(0).toLowerCase() + eventName.slice(1);
this.ref.current.addEventListener(eventName, e => this.props[prop](e));
}
}

public _updateCustomElementProperty(prop: any) {
if (this.ref.current) {
this.ref.current[prop] = this.props[prop];

// if prop value is a string we assume to set the attribute on custom element
if (typeof this.props[prop] === "string") {
this.ref.current.setAttribute(prop, this.props[prop]);
}
}
}
};
}
2 changes: 1 addition & 1 deletion dashboard/src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import "./Header.css";
import HeaderLink from "./HeaderLink";
import NamespaceSelector from "./NamespaceSelector";

interface IHeaderProps {
export interface IHeaderProps {
authenticated: boolean;
fetchNamespaces: () => void;
logout: () => void;
Expand Down
24 changes: 24 additions & 0 deletions dashboard/src/components/Header/Header.v2.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.kubeapps__logo {
max-width: 10em;
}

.kubeapps-nav-link {
outline: none !important;
background-color: transparent;
border: none;
cursor: pointer;
}

.kubeapps-align-center {
display: flex;
align-items: center;
}

.kubeapps-dropdown {
opacity: 75%;
}

.kubeapps-dropdown-text {
margin-left: 0.3em;
margin-right: 0.3em;
}
47 changes: 47 additions & 0 deletions dashboard/src/components/Header/Header.v2.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { shallow } from "enzyme";
import * as React from "react";
import { IClustersState } from "../../reducers/cluster";
import { app } from "../../shared/url";
import Header from "./Header.v2";

const defaultProps = {
authenticated: true,
fetchNamespaces: jest.fn(),
logout: jest.fn(),
clusters: {
currentCluster: "default",
clusters: {
default: {
currentNamespace: "default",
namespaces: ["default", "other"],
},
},
} as IClustersState,
defaultNamespace: "kubeapps-user",
pathname: "",
push: jest.fn(),
setNamespace: jest.fn(),
createNamespace: jest.fn(),
getNamespace: jest.fn(),
featureFlags: { operators: false, additionalClusters: [], ui: "hex" },
};

it("renders the header links and titles", () => {
const wrapper = shallow(<Header {...defaultProps} />);
const items = wrapper.find(".nav-link");
const expectedItems = [
{ children: "Applications", to: app.apps.list("default", "default") },
{ children: "Catalog", to: app.catalog("default") },
];
expect(items.length).toEqual(expectedItems.length);
expectedItems.forEach((expectedItem, index) => {
expect(expectedItem.children).toBe(items.at(index).text());
expect(expectedItem.to).toBe(items.at(index).prop("to"));
});
});

it("should skip the links if it's not authenticated", () => {
const wrapper = shallow(<Header {...defaultProps} authenticated={false} />);
const items = wrapper.find(".nav-link");
expect(items).not.toExist();
});
101 changes: 101 additions & 0 deletions dashboard/src/components/Header/Header.v2.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import {
angleIcon,
applicationsIcon,
ClarityIcons,
clusterIcon,
fileGroupIcon,
} from "@clr/core/icon-shapes";
import * as React from "react";
import { NavLink } from "react-router-dom";
import { CdsIcon } from "../Clarity/clarity";

import logo from "../../logo.svg";
import { IClustersState } from "../../reducers/cluster";
import { app } from "../../shared/url";
import "./Header.v2.css";

ClarityIcons.addIcons(applicationsIcon, clusterIcon, fileGroupIcon, angleIcon);

interface IHeaderProps {
authenticated: boolean;
fetchNamespaces: () => void;
logout: () => void;
clusters: IClustersState;
defaultNamespace: string;
push: (path: string) => void;
setNamespace: (ns: string) => void;
createNamespace: (ns: string) => Promise<boolean>;
getNamespace: (ns: string) => void;
}

function Header(props: IHeaderProps) {
const { clusters, authenticated: showNav } = props;
const cluster = clusters.clusters[clusters.currentCluster];

const routesToRender = [
{
title: "Applications",
path: app.apps.list(clusters.currentCluster, cluster.currentNamespace),
external: false,
},
{ title: "Catalog", path: app.catalog(cluster.currentNamespace), external: false },
];
return (
<section>
<div className="container">
<header className="header header-7">
<div className="branding">
<NavLink to="/">
<img src={logo} alt="Kubeapps logo" className="kubeapps__logo" />
</NavLink>
</div>
{showNav && (
<nav className="header-nav">
{routesToRender.map(route => {
const { path, title } = route;
return (
<NavLink
key={path}
to={path}
activeClassName="active"
className="nav-link nav-text"
>
{title}
</NavLink>
);
})}
</nav>
)}
{showNav && (
<section className="header-actions">
<div className="dropdown bottom-right kubeapps-align-center kubeapps-nav-link kubeapps-dropdown">
<div className="clr-row">
<div className="clr-col-10">
<span>Current Context</span>
<div>
<CdsIcon size="sm" shape="cluster" inverse={true} />
<span className="kubeapps-dropdown-text">{clusters.currentCluster}</span>
<CdsIcon size="sm" shape="file-group" inverse={true} />
<span className="kubeapps-dropdown-text">{cluster.currentNamespace}</span>
</div>
</div>
<div className="clr-col-2 kubeapps-align-center">
<CdsIcon shape="angle" inverse={true} />
</div>
</div>
</div>
<button
className="kubeapps-nav-link nav-icon"
aria-label="VMware Cloud services configuration"
>
<CdsIcon size="lg" shape="applications" solid={true} />
</button>
</section>
)}
</header>
</div>
</section>
);
}

export default Header;
19 changes: 17 additions & 2 deletions dashboard/src/components/Header/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
import Header from "./Header";
import * as React from "react";

export default Header;
import { IHeaderProps } from "./Header";

const Header = React.lazy(() => import("./Header"));
const HeaderV2 = React.lazy(() => import("./Header.v2"));

interface IHeaderSelectorProps extends IHeaderProps {
UI: string;
}

const HeaderSelector: React.FC<IHeaderSelectorProps> = props => (
<React.Suspense fallback={null}>
{props.UI === "clarity" ? <HeaderV2 {...props} /> : <Header {...props} />}
</React.Suspense>
);

export default HeaderSelector;
6 changes: 6 additions & 0 deletions dashboard/src/components/Layout/UISelector/Clarity.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@import "node_modules/@clr/core/styles/global.min.css";
@import "node_modules/@clr/city/css/bundles/default.min.css";

body {
font-family: "Clarity City", "Avenir Next", "Helvetica Neue", Arial, sans-serif;
}

0 comments on commit dc58023

Please sign in to comment.