Skip to content

Commit

Permalink
Merge pull request #2 from lordvlad/lordvlad/issue125
Browse files Browse the repository at this point in the history
feat(devx): introduce storybook
  • Loading branch information
lordvlad authored Mar 17, 2023
2 parents cf96e59 + 502f467 commit 8c61339
Show file tree
Hide file tree
Showing 50 changed files with 9,067 additions and 227 deletions.
18 changes: 16 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,20 @@ jobs:
name: build
path: build

deploy_storybook:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/[email protected]
with:
node-version: '16'
- uses: bahmutov/npm-install@v1
- run: yarn build-storybook -o ./build_storybook
- run: git remote set-url origin https://git:${GITHUB_TOKEN}@github.com/${{github.repository}}.git
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: npx -y -p [email protected] gh-pages -d ./build_storybook --dest storybook --add -u "github-actions-bot <[email protected]>"

check_if_version_upgraded:
name: Check if version upgrade
runs-on: ubuntu-latest
Expand All @@ -53,7 +67,7 @@ jobs:
needs:
- check_if_version_upgraded
- build
# We publish the the docker image only if it's a push on the default branch or if it's a PR from a
# We publish the docker image only if it's a push on the default branch or if it's a PR from a
# branch (meaning not a PR from a fork). It would be more straightforward to test if secrets.DOCKERHUB_TOKEN is
# defined but GitHub Action don't allow it.
if: |
Expand All @@ -80,7 +94,7 @@ jobs:
- check_if_version_upgraded
- build
runs-on: ubuntu-latest
# We publish the the docker image only if it's a push on the default branch or if it's a PR from a
# We publish the docker image only if it's a push on the default branch or if it's a PR from a
# branch (meaning not a PR from a fork). It would be more straightforward to test if secrets.DOCKERHUB_TOKEN is
# defined but GitHub Action don't allow it.
if: |
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,5 @@ jspm_packages
/dist

/build_keycloak
/build
/build
/storybook-static
16 changes: 16 additions & 0 deletions .storybook/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module.exports = {
"stories": [
"../src/keycloak-theme/**/*.stories.tsx",
],
"addons": [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
"@storybook/preset-create-react-app"
],
"framework": "@storybook/react",
"core": {
"builder": "@storybook/builder-webpack5"
},
"staticDirs": ['../public']
}
13 changes: 13 additions & 0 deletions .storybook/preview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const parameters = {
actions: {argTypesRegex: "^on[A-Z].*"},
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
options: {
storySort: (a, b) =>
a[1].kind === b[1].kind ? 0 : a[1].id.localeCompare(b[1].id, undefined, {numeric: true}),
},
}
41 changes: 41 additions & 0 deletions .storybook/util.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type {KcContextExtension} from "keycloak-theme/kcContext";
import KcApp from "../src/keycloak-theme/KcApp";
import {KcContextBase} from "keycloakify";
import {getKcContext} from "keycloakify/lib/getKcContext";
import {ExtendsKcContextBase} from "keycloakify/src/lib/getKcContext/getKcContextFromWindow";
import {DeepPartial} from "keycloakify/src/lib/tools/DeepPartial";


export const socialProviders = [
{loginUrl: 'google', alias: 'google', providerId: 'google', displayName: 'Google'},
{loginUrl: 'microsoft', alias: 'microsoft', providerId: 'microsoft', displayName: 'Microsoft'},
{loginUrl: 'facebook', alias: 'facebook', providerId: 'facebook', displayName: 'Facebook'},
{loginUrl: 'instagram', alias: 'instagram', providerId: 'instagram', displayName: 'Instagram'},
{loginUrl: 'twitter', alias: 'twitter', providerId: 'twitter', displayName: 'Twitter'},
{loginUrl: 'linkedin', alias: 'linkedin', providerId: 'linkedin', displayName: 'LinkedIn'},
{loginUrl: 'stackoverflow', alias: 'stackoverflow', providerId: 'stackoverflow', displayName: 'Stackoverflow'},
{loginUrl: 'github', alias: 'github', providerId: 'github', displayName: 'Github'},
{loginUrl: 'gitlab', alias: 'gitlab', providerId: 'gitlab', displayName: 'Gitlab'},
{loginUrl: 'bitbucket', alias: 'bitbucket', providerId: 'bitbucket', displayName: 'Bitbucket'},
{loginUrl: 'paypal', alias: 'paypal', providerId: 'paypal', displayName: 'PayPal'},
{loginUrl: 'openshift', alias: 'openshift', providerId: 'openshift', displayName: 'OpenShift'},
]

type PageId = (KcContextExtension | KcContextBase)['pageId']
export const template = (pageId: PageId) => {
type MockData = DeepPartial<ExtendsKcContextBase<KcContextExtension>>;

const Template = (mockData: MockData) => {
const finalMockData = {
message: undefined,
pageId,
...mockData
} as MockData
if (!("message" in mockData)) mockData["message"] = undefined
const {kcContext} = getKcContext<KcContextExtension>({mockPageId: pageId, mockData: [finalMockData]})
return <KcApp kcContext={kcContext as NonNullable<typeof kcContext>}/>
}

return (args: MockData) => Object.assign(Template.bind({}), {args})
}

28 changes: 25 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
"start": "react-scripts start",
"build": "react-scripts build",
"build-keycloak-theme": "yarn build && keycloakify",
"download-builtin-keycloak-theme": "download-builtin-keycloak-theme"
"download-builtin-keycloak-theme": "download-builtin-keycloak-theme",
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"
},
"keycloakify": {
"extraPages": [
Expand All @@ -26,13 +28,23 @@
"evt": "^2.4.15",
"jwt-decode": "^3.1.2",
"keycloak-js": "^21.0.1",
"keycloakify": "^6.12.7",
"keycloakify": "^6.13.1",
"powerhooks": "^0.26.2",
"react": "18.1.0",
"react-dom": "18.1.0",
"tsafe": "^1.4.3"
},
"devDependencies": {
"@storybook/addon-actions": "^6.5.16",
"@storybook/addon-essentials": "^6.5.16",
"@storybook/addon-interactions": "^6.5.16",
"@storybook/addon-links": "^6.5.16",
"@storybook/builder-webpack5": "^6.5.16",
"@storybook/manager-webpack5": "^6.5.16",
"@storybook/node-logger": "^6.5.16",
"@storybook/preset-create-react-app": "^4.1.2",
"@storybook/react": "^6.5.16",
"@storybook/testing-library": "^0.0.13",
"@types/node": "^15.3.1",
"@types/react": "18.0.9",
"@types/react-dom": "18.0.4",
Expand All @@ -48,7 +60,17 @@
"react-hooks/exhaustive-deps": "off",
"@typescript-eslint/no-redeclare": "off",
"no-labels": "off"
}
},
"overrides": [
{
"files": [
"**/*.stories.*"
],
"rules": {
"import/no-anonymous-default-export": "off"
}
}
]
},
"browserslist": {
"production": [
Expand Down
32 changes: 32 additions & 0 deletions src/keycloak-theme/Template.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type {ComponentMeta} from '@storybook/react';
import KcApp from './KcApp';
import {template} from '../../.storybook/util'

const bind = template('my-extra-page-1.ftl');

export default {
title: 'Theme/Template',
component: KcApp,
parameters: {
layout: 'fullscreen',
},
} as ComponentMeta<typeof KcApp>;


export const Default = bind({})
export const InFrench = bind({locale: {currentLanguageTag: 'fr'}})
export const RealmDisplayNameIsHtml = bind({
realm: {
displayNameHtml: '<marquee>my realm</marquee>'
}
})

export const NoInternationalization = bind({
realm: {
internationalizationEnabled: false,
}
})

export const WithGlobalError = bind({
message: {type: "error", summary: "This is an error"}
})
23 changes: 12 additions & 11 deletions src/keycloak-theme/kcContext.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import { getKcContext } from "keycloakify/lib/getKcContext";

//NOTE: In most of the cases you do not need to overload the KcContext, you can
// just call getKcContext(...) without type arguments.
// You want to overload the KcContext only if:
// - You have custom plugins that add some values to the context (like https://github.com/micedre/keycloak-mail-whitelisting that adds authorizedMailDomains)
// - You want to add support for extra pages that are not yey featured by default, see: https://docs.keycloakify.dev/contributing#adding-support-for-a-new-page
export const { kcContext } = getKcContext<
export type KcContextExtension =
// NOTE: A 'keycloakify' field must be added
// in the package.json to generate theses extra pages
// https://docs.keycloakify.dev/build-options#keycloakify.extrapages
Expand All @@ -14,8 +9,14 @@ export const { kcContext } = getKcContext<
// NOTE: register.ftl is deprecated in favor of register-user-profile.ftl
// but let's say we use it anyway and have this plugin enabled: https://github.com/micedre/keycloak-mail-whitelisting
// keycloak-mail-whitelisting define the non standard ftl global authorizedMailDomains, we declare it here.
| { pageId: "register.ftl"; authorizedMailDomains: string[]; }
>({
| { pageId: "register.ftl"; authorizedMailDomains: string[]; };

//NOTE: In most of the cases you do not need to overload the KcContext, you can
// just call getKcContext(...) without type arguments.
// You want to overload the KcContext only if:
// - You have custom plugins that add some values to the context (like https://github.com/micedre/keycloak-mail-whitelisting that adds authorizedMailDomains)
// - You want to add support for extra pages that are not yey featured by default, see: https://docs.keycloakify.dev/contributing#adding-support-for-a-new-page
export const { kcContext } = getKcContext<KcContextExtension>({
// Uncomment to test the login page for development.
//mockPageId: "login.ftl",
mockData: [
Expand Down Expand Up @@ -83,12 +84,12 @@ export const { kcContext } = getKcContext<
],
// Simulate we got an error with the email field
messagesPerField: {
printIfExists: <T>(fieldName: string, className: T) => { console.log({ fieldName}); return fieldName === "email" ? className : undefined; },
existsError: (fieldName: string)=> fieldName === "email",
printIfExists: <T>(fieldName: string, className: T) => { console.log({ fieldName }); return fieldName === "email" ? className : undefined; },
existsError: (fieldName: string) => fieldName === "email",
get: (fieldName: string) => `Fake error for ${fieldName}`,
exists: (fieldName: string) => fieldName === "email"
},

}
]
});
Expand Down
16 changes: 16 additions & 0 deletions src/keycloak-theme/pages/Error.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {ComponentMeta} from '@storybook/react';
import KcApp from '../KcApp';
import {template} from '../../../.storybook/util'

const bind = template('error.ftl');

export default {
kind: 'Page',
title: 'Theme/Pages/Notification/Error',
component: KcApp,
parameters: {
layout: 'fullscreen',
},
} as ComponentMeta<typeof KcApp>;

export const Default = bind({message: {type: 'error', summary: 'Something went wrong'}})
34 changes: 34 additions & 0 deletions src/keycloak-theme/pages/Error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// copied and adapted from https://github.com/InseeFrLab/keycloakify/blob/main/src/lib/pages/Error.tsx
import React from "react";
import type { PageProps } from "keycloakify"
import type { KcContext } from "../kcContext";
import type { I18n } from "../i18n";


export default function Error(props: PageProps<Extract<KcContext, { pageId: "error.ftl" }>, I18n>) {
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;

const { message, client } = kcContext;

const { msg } = i18n;

return (
<Template
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
displayMessage={false}
headerNode={msg("errorTitle")}
formNode={
<div id="kc-error-message">
<p className="instruction">{message.summary}</p>
{client !== undefined && client.baseUrl !== undefined && (
<p>
<a id="backToApplication" href={client.baseUrl}>
{msg("backToApplication")}
</a>
</p>
)}
</div>
}
/>
);
}
17 changes: 17 additions & 0 deletions src/keycloak-theme/pages/IdpReviewUserProfile.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ComponentMeta } from '@storybook/react';
import KcApp from '../KcApp';
import { template } from '../../../.storybook/util'

const bind = template('idp-review-user-profile.ftl');

export default {
kind: 'Page',
title: 'Theme/Pages/IDP/Review User Profile',
component: KcApp,
parameters: {
layout: 'fullscreen',
},
} as ComponentMeta<typeof KcApp>;

export const Default = bind({})

47 changes: 47 additions & 0 deletions src/keycloak-theme/pages/IdpReviewUserProfile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React, { useState } from "react";
import { clsx } from "keycloakify/lib/tools/clsx";
import { UserProfileFormFields } from "keycloakify/lib/pages/shared/UserProfileCommons";
import type { PageProps } from "keycloakify";
import type { KcContext } from "../kcContext";
import type { I18n } from "../i18n";

export default function IdpReviewUserProfile(props: PageProps<Extract<KcContext, { pageId: "idp-review-user-profile.ftl" }>, I18n>) {
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;

const { msg, msgStr } = i18n;

const { url } = kcContext;

const [isFomSubmittable, setIsFomSubmittable] = useState(false);

return (
<Template
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
headerNode={msg("loginIdpReviewProfileTitle")}
formNode={
<form id="kc-idp-review-profile-form" className={clsx(kcProps.kcFormClass)} action={url.loginAction} method="post">
<UserProfileFormFields kcContext={kcContext} onIsFormSubmittableValueChange={setIsFomSubmittable} i18n={i18n} {...kcProps} />

<div className={clsx(kcProps.kcFormGroupClass)}>
<div id="kc-form-options" className={clsx(kcProps.kcFormOptionsClass)}>
<div className={clsx(kcProps.kcFormOptionsWrapperClass)} />
</div>
<div id="kc-form-buttons" className={clsx(kcProps.kcFormButtonsClass)}>
<input
className={clsx(
kcProps.kcButtonClass,
kcProps.kcButtonPrimaryClass,
kcProps.kcButtonBlockClass,
kcProps.kcButtonLargeClass
)}
type="submit"
value={msgStr("doSubmit")}
disabled={!isFomSubmittable}
/>
</div>
</div>
</form>
}
/>
);
}
Loading

0 comments on commit 8c61339

Please sign in to comment.