diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 64c9fe5..470de62 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -34,6 +34,20 @@ jobs: name: build path: build + deploy_storybook: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2.1.3 + 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 gh-pages@3.1.0 gh-pages -d ./build_storybook --dest storybook --add -u "github-actions-bot " + check_if_version_upgraded: name: Check if version upgrade runs-on: ubuntu-latest @@ -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: | @@ -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: | diff --git a/.gitignore b/.gitignore index d5c8599..7c5d5ec 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,5 @@ jspm_packages /dist /build_keycloak -/build \ No newline at end of file +/build +/storybook-static \ No newline at end of file diff --git a/.storybook/main.js b/.storybook/main.js new file mode 100644 index 0000000..bf619ea --- /dev/null +++ b/.storybook/main.js @@ -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'] +} \ No newline at end of file diff --git a/.storybook/preview.js b/.storybook/preview.js new file mode 100644 index 0000000..b6de5f6 --- /dev/null +++ b/.storybook/preview.js @@ -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}), + }, +} \ No newline at end of file diff --git a/.storybook/util.tsx b/.storybook/util.tsx new file mode 100644 index 0000000..4180dee --- /dev/null +++ b/.storybook/util.tsx @@ -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>; + + const Template = (mockData: MockData) => { + const finalMockData = { + message: undefined, + pageId, + ...mockData + } as MockData + if (!("message" in mockData)) mockData["message"] = undefined + const {kcContext} = getKcContext({mockPageId: pageId, mockData: [finalMockData]}) + return }/> + } + + return (args: MockData) => Object.assign(Template.bind({}), {args}) +} + diff --git a/package.json b/package.json index f7cf4e2..9d2b655 100755 --- a/package.json +++ b/package.json @@ -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": [ @@ -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", @@ -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": [ diff --git a/src/keycloak-theme/Template.stories.tsx b/src/keycloak-theme/Template.stories.tsx new file mode 100644 index 0000000..d8660ed --- /dev/null +++ b/src/keycloak-theme/Template.stories.tsx @@ -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; + + +export const Default = bind({}) +export const InFrench = bind({locale: {currentLanguageTag: 'fr'}}) +export const RealmDisplayNameIsHtml = bind({ + realm: { + displayNameHtml: 'my realm' + } +}) + +export const NoInternationalization = bind({ + realm: { + internationalizationEnabled: false, + } +}) + +export const WithGlobalError = bind({ + message: {type: "error", summary: "This is an error"} +}) diff --git a/src/keycloak-theme/kcContext.ts b/src/keycloak-theme/kcContext.ts index ad7ab6d..078efeb 100644 --- a/src/keycloak-theme/kcContext.ts +++ b/src/keycloak-theme/kcContext.ts @@ -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 @@ -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({ // Uncomment to test the login page for development. //mockPageId: "login.ftl", mockData: [ @@ -83,12 +84,12 @@ export const { kcContext } = getKcContext< ], // Simulate we got an error with the email field messagesPerField: { - printIfExists: (fieldName: string, className: T) => { console.log({ fieldName}); return fieldName === "email" ? className : undefined; }, - existsError: (fieldName: string)=> fieldName === "email", + printIfExists: (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" }, - + } ] }); diff --git a/src/keycloak-theme/pages/Error.stories.tsx b/src/keycloak-theme/pages/Error.stories.tsx new file mode 100644 index 0000000..768a6d1 --- /dev/null +++ b/src/keycloak-theme/pages/Error.stories.tsx @@ -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; + +export const Default = bind({message: {type: 'error', summary: 'Something went wrong'}}) diff --git a/src/keycloak-theme/pages/Error.tsx b/src/keycloak-theme/pages/Error.tsx new file mode 100644 index 0000000..b28ff06 --- /dev/null +++ b/src/keycloak-theme/pages/Error.tsx @@ -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, I18n>) { + const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props; + + const { message, client } = kcContext; + + const { msg } = i18n; + + return ( +