Added github actions to automate build, test, and linting (#41)

Fixes issue #15.  Builds, test, lints, and checks format when a PR is created using github actions.
This commit is contained in:
Donald Labaj 2020-09-01 10:51:59 -04:00 committed by GitHub
parent 3959d2c47e
commit adbb2c3d3f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 172 additions and 139 deletions

33
.github/workflows/node.js.yml vendored Normal file
View file

@ -0,0 +1,33 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Node.js CI
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm install -g yarn
- run: yarn install
- run: yarn format:check
- run: yarn build
- run: yarn lint
- run: yarn test

View file

@ -11,6 +11,7 @@
"build": "snowpack build",
"test": "jest",
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx}\"",
"format:check": "prettier --check \"src/**/*.{js,jsx,ts,tsx}\"",
"lint": "eslint ./src/**/*.ts*",
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"

View file

@ -1,5 +1,5 @@
import React, { useContext, useState } from 'react';
import { useTranslation } from 'react-i18next';
import React, { useContext, useState } from "react";
import { useTranslation } from "react-i18next";
import {
Avatar,
Button,
@ -14,10 +14,10 @@ import {
PageHeaderTools,
PageHeaderToolsItem,
PageHeaderToolsGroup,
} from '@patternfly/react-core';
import { HelpIcon } from '@patternfly/react-icons';
import { KeycloakContext } from './auth/KeycloakContext';
import { Link } from 'react-router-dom';
} from "@patternfly/react-core";
import { HelpIcon } from "@patternfly/react-icons";
import { KeycloakContext } from "./auth/KeycloakContext";
import { Link } from "react-router-dom";
export const Header = () => {
return (
@ -39,7 +39,7 @@ const ManageAccountDropdownItem = () => {
const { t } = useTranslation();
return (
<DropdownItem key="manage account" onClick={() => keycloak?.account()}>
{t('Manage account')}
{t("Manage account")}
</DropdownItem>
);
};
@ -49,19 +49,19 @@ const SignOutDropdownItem = () => {
const { t } = useTranslation();
return (
<DropdownItem key="sign out" onClick={() => keycloak?.logout()}>
{t('Sign out')}
{t("Sign out")}
</DropdownItem>
);
};
const ServerInfoDropdownItem = () => {
const { t } = useTranslation();
return <DropdownItem key="server info">{t('Server info')}</DropdownItem>;
return <DropdownItem key="server info">{t("Server info")}</DropdownItem>;
};
const HelpDropdownItem = () => {
const { t } = useTranslation();
const help = t('Help');
const help = t("Help");
return (
<DropdownItem>
<HelpIcon />
@ -90,8 +90,8 @@ const headerTools = () => {
<PageHeaderTools>
<PageHeaderToolsGroup
visibility={{
default: 'hidden',
md: 'visible',
default: "hidden",
md: "visible",
}} /** the settings and help icon buttons are only visible on desktop sizes and replaced by a kebab dropdown for other sizes */
>
<PageHeaderToolsItem>
@ -104,15 +104,15 @@ const headerTools = () => {
<PageHeaderToolsGroup>
<PageHeaderToolsItem
visibility={{
md: 'hidden',
md: "hidden",
}} /** this kebab dropdown replaces the icon buttons and is hidden for desktop sizes */
>
<KebabDropdown />
</PageHeaderToolsItem>
<PageHeaderToolsItem
visibility={{
default: 'hidden',
md: 'visible',
default: "hidden",
md: "visible",
}} /** this user dropdown is hidden on mobile sizes */
>
<UserDropdown />

View file

@ -1,6 +1,6 @@
import React from 'react';
import { Nav, NavItem, NavList, PageSidebar } from '@patternfly/react-core';
import { RealmSelector } from './components/realm-selector/RealmSelector';
import React from "react";
import { Nav, NavItem, NavList, PageSidebar } from "@patternfly/react-core";
import { RealmSelector } from "./components/realm-selector/RealmSelector";
export const PageNav = () => {
return (
@ -8,7 +8,7 @@ export const PageNav = () => {
nav={
<Nav>
<NavList>
<RealmSelector realm="Master" realmList={['Photoz']} />
<RealmSelector realm="Master" realmList={["Photoz"]} />
<NavItem id="default-link1" to="/default-link1" itemId={0}>
Link 1
</NavItem>

View file

@ -1 +1 @@
module.exports = 'test-file-stub';
module.exports = "test-file-stub";

View file

@ -1,5 +1,5 @@
import * as React from 'react';
import { KeycloakService } from './keycloak.service';
import * as React from "react";
import { KeycloakService } from "./keycloak.service";
export const KeycloakContext = React.createContext<KeycloakService | undefined>(
undefined

View file

@ -1,5 +1,5 @@
import { KeycloakLoginOptions } from 'keycloak-js';
import { useTranslation } from 'react-i18next';
import { KeycloakLoginOptions } from "keycloak-js";
import { useTranslation } from "react-i18next";
export type KeycloakClient = Keycloak.KeycloakInstance;
@ -26,7 +26,7 @@ export class KeycloakService {
this.keycloakAuth.login(options);
}
public logout(redirectUri: string = ''): void {
public logout(redirectUri: string = ""): void {
this.keycloakAuth.logout({ redirectUri: redirectUri });
}
@ -36,9 +36,9 @@ export class KeycloakService {
public authServerUrl(): string | undefined {
const authServerUrl = this.keycloakAuth.authServerUrl;
return authServerUrl!.charAt(authServerUrl!.length - 1) === '/'
return authServerUrl!.charAt(authServerUrl!.length - 1) === "/"
? authServerUrl
: authServerUrl + '/';
: authServerUrl + "/";
}
public realm(): string | undefined {
@ -51,13 +51,13 @@ export class KeycloakService {
}
private loggedInUserName = (t: Function, tokenParsed: Token) => {
let userName = t('unknownUser');
let userName = t("unknownUser");
if (tokenParsed) {
const givenName = tokenParsed.given_name;
const familyName = tokenParsed.family_name;
const preferredUsername = tokenParsed.preferred_username;
if (givenName && familyName) {
userName = t('fullName', { givenName, familyName });
userName = t("fullName", { givenName, familyName });
} else {
userName = givenName || familyName || preferredUsername || userName;
}
@ -74,10 +74,10 @@ export class KeycloakService {
resolve(this.keycloakAuth.token as string);
})
.catch(() => {
reject('Failed to refresh token');
reject("Failed to refresh token");
});
} else {
reject('Not logged in');
reject("Not logged in");
}
});
}

View file

@ -1,9 +1,9 @@
import Keycloak, { KeycloakInstance } from 'keycloak-js';
import Keycloak, { KeycloakInstance } from "keycloak-js";
const keycloak: KeycloakInstance = Keycloak();
export default async function (): Promise<KeycloakInstance> {
await keycloak.init({ onLoad: 'check-sso', pkceMethod: 'S256' }).catch(() => {
alert('failed to initialize keycloak');
await keycloak.init({ onLoad: "check-sso", pkceMethod: "S256" }).catch(() => {
alert("failed to initialize keycloak");
});
return keycloak;
}

View file

@ -1,10 +1,10 @@
import React from 'react';
import { render } from '@testing-library/react';
import React from "react";
import { render } from "@testing-library/react";
import clientMock from './mock-clients.json';
import { ClientList } from './ClientList';
import clientMock from "./mock-clients.json";
import { ClientList } from "./ClientList";
test('renders ClientList', () => {
test("renders ClientList", () => {
const { getByText } = render(
<ClientList clients={clientMock} baseUrl="http://blog.nerdin.ch" />
);

View file

@ -1,4 +1,4 @@
import React from 'react';
import React from "react";
import {
Table,
TableBody,
@ -6,11 +6,11 @@ import {
TableVariant,
IFormatter,
IFormatterValueType,
} from '@patternfly/react-table';
import { Badge } from '@patternfly/react-core';
} from "@patternfly/react-table";
import { Badge } from "@patternfly/react-core";
import { ExternalLink } from '../components/external-link/ExternalLink';
import { ClientRepresentation } from '../model/client-model';
import { ExternalLink } from "../components/external-link/ExternalLink";
import { ClientRepresentation } from "../model/client-model";
type ClientListProps = {
clients?: ClientRepresentation[];
@ -18,17 +18,17 @@ type ClientListProps = {
};
const columns: (keyof ClientRepresentation)[] = [
'clientId',
'protocol',
'description',
'baseUrl',
"clientId",
"protocol",
"description",
"baseUrl",
];
export const ClientList = ({ baseUrl, clients }: ClientListProps) => {
const enabled = (): IFormatter => (data?: IFormatterValueType) => {
const field = data!.toString();
const value = field.substring(0, field.indexOf('#'));
return field.indexOf('true') !== -1 ? (
const value = field.substring(0, field.indexOf("#"));
return field.indexOf("true") !== -1 ? (
<>{value}</>
) : (
<>
@ -38,7 +38,7 @@ export const ClientList = ({ baseUrl, clients }: ClientListProps) => {
};
const emptyFormatter = (): IFormatter => (data?: IFormatterValueType) => {
return data ? data : '—';
return data ? data : "—";
};
const externalLink = (): IFormatter => (data?: IFormatterValueType) => {
@ -50,13 +50,13 @@ export const ClientList = ({ baseUrl, clients }: ClientListProps) => {
const replaceBaseUrl = (r: ClientRepresentation) =>
r.rootUrl &&
r.rootUrl
.replace('${authBaseUrl}', baseUrl)
.replace('${authAdminUrl}', baseUrl) +
(r.baseUrl ? r.baseUrl.substr(1) : '');
.replace("${authBaseUrl}", baseUrl)
.replace("${authAdminUrl}", baseUrl) +
(r.baseUrl ? r.baseUrl.substr(1) : "");
const data = clients!
.map((r) => {
r.clientId = r.clientId + '#' + r.enabled;
r.clientId = r.clientId + "#" + r.enabled;
r.baseUrl = replaceBaseUrl(r);
return r;
})
@ -67,11 +67,11 @@ export const ClientList = ({ baseUrl, clients }: ClientListProps) => {
<Table
variant={TableVariant.compact}
cells={[
{ title: 'Client ID', cellFormatters: [enabled()] },
'Type',
{ title: 'Description', cellFormatters: [emptyFormatter()] },
{ title: "Client ID", cellFormatters: [enabled()] },
"Type",
{ title: "Description", cellFormatters: [emptyFormatter()] },
{
title: 'Home URL',
title: "Home URL",
cellFormatters: [externalLink(), emptyFormatter()],
},
]}

View file

@ -1,11 +1,11 @@
import React from 'react';
import { Button, AlertVariant } from '@patternfly/react-core';
import { mount } from 'enzyme';
import EnzymeToJson from 'enzyme-to-json';
import { act } from 'react-dom/test-utils';
import React from "react";
import { Button, AlertVariant } from "@patternfly/react-core";
import { mount } from "enzyme";
import EnzymeToJson from "enzyme-to-json";
import { act } from "react-dom/test-utils";
import { AlertPanel } from './AlertPanel';
import { useAlerts } from './Alerts';
import { AlertPanel } from "./AlertPanel";
import { useAlerts } from "./Alerts";
jest.useFakeTimers();
@ -14,23 +14,23 @@ const WithButton = () => {
return (
<>
<AlertPanel alerts={alerts} onCloseAlert={hide} />
<Button onClick={() => add('Hello', AlertVariant.default)}>Add</Button>
<Button onClick={() => add("Hello", AlertVariant.default)}>Add</Button>
</>
);
};
it('renders global alerts', () => {
it("renders global alerts", () => {
const empty = EnzymeToJson(
mount(<AlertPanel alerts={[]} onCloseAlert={() => {}} />)
);
expect(empty).toMatchSnapshot();
const tree = mount(<WithButton />);
const button = tree.find('button');
const button = tree.find("button");
expect(button).not.toBeNull();
act(() => {
button!.simulate('click');
button!.simulate("click");
});
expect(EnzymeToJson(tree)).toMatchSnapshot();

View file

@ -1,6 +1,6 @@
import { useState } from 'react';
import { AlertType } from './AlertPanel';
import { AlertVariant } from '@patternfly/react-core';
import { useState } from "react";
import { AlertType } from "./AlertPanel";
import { AlertVariant } from "@patternfly/react-core";
export function useAlerts(): [
(message: string, type: AlertVariant) => void,

View file

@ -77,9 +77,7 @@ exports[`renders global alerts 2`] = `
>
<span
class="pf-u-screen-reader"
>
Default alert:
</span>
/>
Hello
</h4>
<div

View file

@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Spinner } from '@patternfly/react-core';
import React, { useEffect, useState } from "react";
import { Spinner } from "@patternfly/react-core";
type DataLoaderProps<T> = {
loader: () => Promise<T>;
@ -26,7 +26,7 @@ export function DataLoader<T>(props: DataLoaderProps<T>) {
return props.children;
}
return (
<div style={{ textAlign: 'center' }}>
<div style={{ textAlign: "center" }}>
<Spinner />
</div>
);

View file

@ -1,5 +1,5 @@
import React from 'react';
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
import React from "react";
import { ExternalLinkAltIcon } from "@patternfly/react-icons";
export const ExternalLink = ({
title,
@ -8,8 +8,8 @@ export const ExternalLink = ({
}: React.HTMLProps<HTMLAnchorElement>) => {
return (
<a href={href} {...rest}>
{title ? title : href}{' '}
{href?.startsWith('http') && <ExternalLinkAltIcon />}
{title ? title : href}{" "}
{href?.startsWith("http") && <ExternalLinkAltIcon />}
</a>
);
};

View file

@ -1,13 +1,13 @@
import React, { useState } from 'react';
import { useHistory } from 'react-router-dom';
import React, { useState } from "react";
import { useHistory } from "react-router-dom";
import {
Dropdown,
DropdownToggle,
DropdownItem,
Button,
} from '@patternfly/react-core';
} from "@patternfly/react-core";
import style from './realm-selector.module.css';
import style from "./realm-selector.module.css";
type RealmSelectorProps = {
realm: string;
@ -39,7 +39,7 @@ export const RealmSelector = ({ realm, realmList }: RealmSelectorProps) => {
dropdownItems={[
...dropdownItems,
<DropdownItem component="div" key="add">
<Button onClick={() => history.push('/add-realm')}>Add Realm</Button>
<Button onClick={() => history.push("/add-realm")}>Add Realm</Button>
</DropdownItem>,
]}
/>

View file

@ -1,7 +1,7 @@
import React from 'react';
import { Title } from '@patternfly/react-core';
import React from "react";
import { Title } from "@patternfly/react-core";
import style from './form-panel.module.css';
import style from "./form-panel.module.css";
interface FormPanelProps extends React.HTMLProps<HTMLFormElement> {
title: string;

View file

@ -1,8 +1,8 @@
import React, { Children, useEffect, useState } from 'react';
import { Form, Grid, GridItem, Title } from '@patternfly/react-core';
import React, { Children, useEffect, useState } from "react";
import { Form, Grid, GridItem, Title } from "@patternfly/react-core";
import { FormPanel } from './FormPanel';
import style from './scroll-form.module.css';
import { FormPanel } from "./FormPanel";
import style from "./scroll-form.module.css";
type ScrollFormProps = {
sections: string[];
@ -26,7 +26,7 @@ export const ScrollForm = ({ sections, children }: ScrollFormProps) => {
const [active, setActive] = useState(sections[0]);
useEffect(() => {
window.addEventListener('scroll', () => {
window.addEventListener("scroll", () => {
const active = getCurrentSection();
if (active) {
setActive(active);
@ -44,7 +44,7 @@ export const ScrollForm = ({ sections, children }: ScrollFormProps) => {
{sections.map((cat) => (
<li
className={
'pf-c-tabs__item' + (active === cat ? ' pf-m-current' : '')
"pf-c-tabs__item" + (active === cat ? " pf-m-current" : "")
}
key={cat}
>
@ -54,7 +54,7 @@ export const ScrollForm = ({ sections, children }: ScrollFormProps) => {
onClick={() =>
document
.getElementById(cat)
?.scrollIntoView({ behavior: 'smooth' })
?.scrollIntoView({ behavior: "smooth" })
}
>
<span className="pf-c-tabs__item-text">{cat}</span>

View file

@ -1,4 +1,4 @@
import React from 'react';
import React from "react";
import {
ToggleTemplateProps,
Toolbar,
@ -8,8 +8,8 @@ import {
TextInput,
Button,
Pagination,
} from '@patternfly/react-core';
import { SearchIcon } from '@patternfly/react-icons';
} from "@patternfly/react-core";
import { SearchIcon } from "@patternfly/react-icons";
type TableToolbarProps = {
count: number;
@ -33,7 +33,7 @@ export const TableToolbar = ({
children,
}: TableToolbarProps) => {
const page = first / max;
const pagination = (variant: 'top' | 'bottom' = 'top') => (
const pagination = (variant: "top" | "bottom" = "top") => (
<Pagination
isCompact
toggleTemplate={({ firstIndex, lastIndex }: ToggleTemplateProps) => (
@ -63,15 +63,13 @@ export const TableToolbar = ({
</Button>
</InputGroup>
</ToolbarItem>
{ toolbarItem && <ToolbarItem>
{ toolbarItem }
</ToolbarItem>}
{toolbarItem && <ToolbarItem>{toolbarItem}</ToolbarItem>}
<ToolbarItem variant="pagination">{pagination()}</ToolbarItem>
</ToolbarContent>
</Toolbar>
{children}
<Toolbar>
<ToolbarItem>{pagination('bottom')}</ToolbarItem>
<ToolbarItem>{pagination("bottom")}</ToolbarItem>
</Toolbar>
</>
);

View file

@ -1,4 +1,4 @@
import React from 'react';
import React from "react";
import {
Text,
PageSection,

View file

@ -1,5 +1,5 @@
import { createContext } from 'react';
import { HttpClient } from './http-client';
import { createContext } from "react";
import { HttpClient } from "./http-client";
export const HttpClientContext = createContext<HttpClient | undefined>(
undefined

View file

@ -1,4 +1,4 @@
import { KeycloakService } from '../auth/keycloak.service';
import { KeycloakService } from "../auth/keycloak.service";
type ConfigResolve = (config: RequestInit) => void;
@ -31,14 +31,14 @@ export class HttpClient {
endpoint: string,
config?: RequestInitWithParams
): Promise<HttpResponse<T>> {
return this.doRequest(endpoint, { ...config, method: 'get' });
return this.doRequest(endpoint, { ...config, method: "get" });
}
public async doDelete<T>(
endpoint: string,
config?: RequestInitWithParams
): Promise<HttpResponse<T>> {
return this.doRequest(endpoint, { ...config, method: 'delete' });
return this.doRequest(endpoint, { ...config, method: "delete" });
}
public async doPost<T>(
@ -49,7 +49,7 @@ export class HttpClient {
return this.doRequest(endpoint, {
...config,
body: JSON.stringify(body),
method: 'post',
method: "post",
});
}
@ -61,7 +61,7 @@ export class HttpClient {
return this.doRequest(endpoint, {
...config,
body: JSON.stringify(body),
method: 'put',
method: "put",
});
}
@ -103,14 +103,14 @@ export class HttpClient {
private makeUrl(url: string, config?: RequestInitWithParams): string {
const searchParams = new URLSearchParams();
// add request params
if (config && {}.hasOwnProperty.call(config, 'params')) {
if (config && {}.hasOwnProperty.call(config, "params")) {
const params: { [name: string]: string } = (config.params as {}) || {};
Object.keys(params).forEach((key) =>
searchParams.append(key, params[key])
);
}
return url + '?' + searchParams.toString();
return url + "?" + searchParams.toString();
}
private makeConfig(config: RequestInit = {}): Promise<RequestInit> {
@ -121,9 +121,9 @@ export class HttpClient {
resolve({
...config,
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
...config.headers,
Authorization: 'Bearer ' + token,
Authorization: "Bearer " + token,
},
});
})
@ -135,7 +135,7 @@ export class HttpClient {
}
window.addEventListener(
'unhandledrejection',
"unhandledrejection",
(event: PromiseRejectionEvent) => {
event.promise.catch((error) => {
if (error instanceof AccountServiceError) {

View file

@ -1,13 +1,13 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
// import backend from "i18next-http-backend";
import messages from './messages.json';
import messages from "./messages.json";
const initOptions = {
resources: messages,
lng: 'en',
fallbackLng: 'en',
lng: "en",
fallbackLng: "en",
saveMissing: true,
interpolation: {

View file

@ -1,14 +1,14 @@
import React from 'react';
import ReactDom from 'react-dom';
import { I18nextProvider } from 'react-i18next';
import { i18n } from './i18n';
import React from "react";
import ReactDom from "react-dom";
import { I18nextProvider } from "react-i18next";
import { i18n } from "./i18n";
import { App } from './App';
import init from './auth/keycloak';
import { KeycloakContext } from './auth/KeycloakContext';
import { KeycloakService } from './auth/keycloak.service';
import { HttpClientContext } from './http-service/HttpClientContext';
import { HttpClient } from './http-service/http-client';
import { App } from "./App";
import init from "./auth/keycloak";
import { KeycloakContext } from "./auth/KeycloakContext";
import { KeycloakService } from "./auth/keycloak.service";
import { HttpClientContext } from "./http-service/HttpClientContext";
import { HttpClient } from "./http-service/http-client";
init().then((keycloak) => {
const keycloakService = new KeycloakService(keycloak);
@ -20,10 +20,10 @@ init().then((keycloak) => {
</HttpClientContext.Provider>
</KeycloakContext.Provider>
</I18nextProvider>,
document.getElementById('app')
document.getElementById("app")
);
});
(document.getElementById('favicon') as HTMLAnchorElement).href = `${
(document.getElementById("favicon") as HTMLAnchorElement).href = `${
import.meta.env.SNOWPACK_PUBLIC_FAVICON
}`;

View file

@ -1,14 +1,17 @@
import { ProviderRepresentation } from "./model/server-info";
export const sortProvider = (a: [string, ProviderRepresentation], b: [string, ProviderRepresentation]) => {
export const sortProvider = (
a: [string, ProviderRepresentation],
b: [string, ProviderRepresentation]
) => {
let s1, s2;
if (a[1].order != b[1].order) {
s1 = b[1].order;
s2 = a[1].order;
} else {
} else {
s1 = a[0];
s2 = b[0];
}
}
if (s1 < s2) {
return -1;
} else if (s1 > s2) {