Bootstrap ui sdk development with Login component (#735)

* Push sdk folder

* Ignore `sdk` from root tsconfig

* Add postcss config

* Tweaks to the vite setup

* Cleanup and add `Login` component

* Test

* Undo test commit

* Turn off next linter rule for sdk

* Tweaks to component doc

* Add dom libs to tsconfig

* html id generation

* Component WIP

* Update tsconfig and lock file

* Refactor

* Tweak props and handle error display

* Simplify `useId` hook

* Update JSDOC

* Remove ErrorDisplayComponent prop

* Minor refactor

* Refactor id generation, add jsdoc

* Support styling via props and add default styles

* Sync lock files

* More default styles for input

* Sync lock file

* Tweak box-shadow

* Minor tweak comment

* WIP

* Update package name and lock file

* Update vite config fileName

* Exclude type emit for internal components

* Tweak package.json

* Rename `react-ui`

* Add repo and bugs fields

* Fix import name and jsdoc

* Support `unstyled` prop

* Tweak README and docs

* Minor tweaks

* Tweak README and update favicon

* Add workflow for npm publish

* Update build script and use node 16

* Add trigger on push

* Add dev dependencies to fix failing build

* Add missing dev dependency

* set access to public

* Move react & others to dev dependencies

* Fix main,module,exports in package.json

* Include types in package.json

* Fix types path

* Fix return type for `forwardTenant`

* Include react as a peer dependency

* Add react plugin to vite

* Handle `undefined`

* Inject defaultStyles into build

* Update tsconfig

* Update homepage

* Update type to void for no return

* Update usage

* [skip ci] update lock file

* [skip ci] Merge branch 'main' into 702-react-sdk-login

* WIP tweak component

* Handle submission state, align focus styles

* Sync lock file

* Swap ::set-output with `$GITHUB_OUTPUT`

* Use ci job output for getting npm version and publish tag

* trigger sdk jobs on `workflow_call`

* Use sdk workflow

* Fix workflow path

* Replace version in sdk package.json

* Move condition to sdk workflow

* Update vite to v4 and cleanup dependencies

* Publish a beta version for testing

* Revert removal of autoprefixer

* Update node version and inherit secrets

* Tweaks

* Update SDK README

* Revert changes related to testing

* Align border-radius with boxyhq ui

* Temporarily allow publish

* Style alignment with boxyhq ui

* - Update default styling to that of a b/w look
- Support style attr for container
- Tweak types
- Support CSS vars on container element to
style input and button outlines plus button hover

* Accept any color format

* Tweak README and demos

* Support spreading props for input/button

* Tweak style injection so that userland classes
load later and override the default styles

* Try non injection of css

* Add style.css to exports and files

* Formatting change

* Add styling section

* Bump up version

* - Use defaultClasses only if custom classes
are not set
- Remove `unstyled` prop

* Update demos

* Add style for disabled state

* Tweak README

* Bring back css injection

* Remove style from files and exports fields

* Scope css properties to appropriate inner elements

* Remove css properties

* Bump version

* Support HTMLAttributes for container and label also

* Prep for release

* Export types from entry point file

* Tweak for local usage via npm linking

* Replace package.json fields before publish

* Tweak demos

* updated dependabot for new package.json

* fixed yaml

Co-authored-by: Deepak Prabhakara <deepak@boxyhq.com>
This commit is contained in:
Aswin V 2023-01-12 03:46:23 +05:30 committed by GitHub
parent f5582374d4
commit a5ac299bbb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 10310 additions and 63 deletions

View File

@ -23,5 +23,11 @@ module.exports = {
'@typescript-eslint/no-var-requires': 'off',
},
},
{
files: ['sdk/**/*'],
rules: {
'@next/next/no-img-element': 'off',
},
},
],
};

View File

@ -13,3 +13,7 @@ updates:
directory: '/npm'
schedule:
interval: 'weekly'
- package-ecosystem: 'react-sdk'
directory: '/sdk/ui/react'
schedule:
interval: 'weekly'

View File

@ -23,6 +23,9 @@ on:
jobs:
ci:
runs-on: ubuntu-latest
outputs:
NPM_VERSION: ${{ steps.version.outputs.NPM_VERSION }}
PUBLISH_TAG: ${{ steps.version.outputs.PUBLISH_TAG }}
env:
NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
NEXTAUTH_URL: http://localhost:5225
@ -118,7 +121,8 @@ jobs:
run: npx playwright install chromium
- name: e2e tests
run: npx ts-node --log-error e2e/seedAuthDb.ts && npx playwright test
- run: |
- id: version
run: |
npm install --legacy-peer-deps
npm run build
npm install -g json
@ -135,47 +139,25 @@ jobs:
JACKSON_VERSION="${JACKSON_VERSION}-beta.${GITHUB_RUN_NUMBER}"
fi
echo ${JACKSON_VERSION} > npmversion.txt
echo ${publishTag} > publishTag.txt
echo "NPM_VERSION=${JACKSON_VERSION}" >> $GITHUB_OUTPUT
echo "PUBLISH_TAG=${publishTag}" >> $GITHUB_OUTPUT
echo $(cat npmversion.txt)
echo $(cat publishTag.txt)
working-directory: ./npm
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Upload saml-jackson npm version
uses: actions/upload-artifact@v3
with:
name: npmversion
path: ./npm/npmversion.txt
- name: Upload saml-jackson publish tag
uses: actions/upload-artifact@v3
with:
name: publishTag
path: ./npm/publishTag.txt
build:
needs: ci
runs-on: ubuntu-latest
steps:
- name: Check Out Repo
uses: actions/checkout@v3
- name: Download saml-jackson npm version
uses: actions/download-artifact@v3
with:
name: npmversion
- name: Get saml-jackson npm version
id: npmversion
run: echo "::set-output name=npmversion::$(cat npmversion.txt)"
- run: echo ${{ steps.npmversion.outputs.npmversion }}
- run: echo ${{ needs.ci.outputs.NPM_VERSION }}
- name: Get short SHA
id: slug
run: echo "::set-output name=sha7::$(echo ${GITHUB_SHA} | cut -c1-7)"
run: echo "{sha7}={$(echo ${GITHUB_SHA} | cut -c1-7)}" >> $GITHUB_OUTPUT
- name: Set up Docker Buildx
id: buildx
@ -200,7 +182,7 @@ jobs:
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ github.repository }}:latest,${{ github.repository }}:${{ steps.slug.outputs.sha7 }},${{ github.repository }}:${{ steps.npmversion.outputs.npmversion }}
tags: ${{ github.repository }}:latest,${{ github.repository }}:${{ steps.slug.outputs.sha7 }},${{ github.repository }}:${{ needs.ci.outputs.NPM_VERSION }}
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
@ -318,13 +300,6 @@ jobs:
rm results.sarif || true
rm ./_docker/sbom.json || true
mv ./_docker/sbom.xml ./_docker/sbom.cyclonedx || true
- name: Download saml-jackson npm version
uses: actions/download-artifact@v3
with:
name: npmversion
- name: Get saml-jackson npm version
id: _npmversion
run: echo "::set-output name=npmversion::$(cat npmversion.txt)"
- name: ORAS Setup & Push SBOM reports to GitHub Container Registry
if: github.ref == 'refs/heads/release'
run: |
@ -333,24 +308,24 @@ jobs:
curl -LO "https://cdn.bundle.bar/clients/oras/${ORAS_VERSION}/${ORAS_FILENAME}"
mkdir oras_install
tar -xvf "${ORAS_FILENAME}" -C oras_install
./oras_install/oras push ghcr.io/${{github.repository_owner}}/jackson/sbom:service-${{ steps._npmversion.outputs.npmversion }} ./sbom.*
./oras_install/oras push ghcr.io/${{github.repository_owner}}/jackson/sbom:service-${{ needs.ci.outputs.NPM_VERSION }} ./sbom.*
cd _docker
../oras_install/oras push ghcr.io/${{github.repository_owner}}/jackson/sbom:docker-${{ steps._npmversion.outputs.npmversion }} ./sbom.*
../oras_install/oras push ghcr.io/${{github.repository_owner}}/jackson/sbom:docker-${{ needs.ci.outputs.NPM_VERSION }} ./sbom.*
cd ..
cd npm
../oras_install/oras push ghcr.io/${{github.repository_owner}}/jackson/sbom:npm-${{ steps._npmversion.outputs.npmversion }} ./sbom.*
../oras_install/oras push ghcr.io/${{github.repository_owner}}/jackson/sbom:npm-${{ needs.ci.outputs.NPM_VERSION }} ./sbom.*
cd ..
- name: Sign the sbom images
if: github.ref == 'refs/heads/release'
run: |
cosign sign --key /tmp/cosign.key ghcr.io/${{github.repository_owner}}/jackson/sbom:service-${{ steps._npmversion.outputs.npmversion }} || true
cosign sign --key /tmp/cosign.key ghcr.io/${{github.repository_owner}}/jackson/sbom:docker-${{ steps._npmversion.outputs.npmversion }} || true
cosign sign --key /tmp/cosign.key ghcr.io/${{github.repository_owner}}/jackson/sbom:npm-${{ steps._npmversion.outputs.npmversion }} || true
cosign sign --key /tmp/cosign.key ghcr.io/${{github.repository_owner}}/jackson/sbom:service-${{ needs.ci.outputs.NPM_VERSION }} || true
cosign sign --key /tmp/cosign.key ghcr.io/${{github.repository_owner}}/jackson/sbom:docker-${{ needs.ci.outputs.NPM_VERSION }} || true
cosign sign --key /tmp/cosign.key ghcr.io/${{github.repository_owner}}/jackson/sbom:npm-${{ needs.ci.outputs.NPM_VERSION }} || true
env:
COSIGN_PASSWORD: ${{secrets.COSIGN_PASSWORD}}
publish:
needs: build
needs: [ci, build]
runs-on: ubuntu-latest
strategy:
@ -360,23 +335,8 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Download saml-jackson npm version
uses: actions/download-artifact@v3
with:
name: npmversion
- name: Get saml-jackson npm version
id: npmversion
run: echo "::set-output name=npmversion::$(cat npmversion.txt)"
- run: echo ${{ steps.npmversion.outputs.npmversion }}
- name: Download saml-jackson publish tag
uses: actions/download-artifact@v3
with:
name: publishTag
- name: Get saml-jackson npm version
id: publishTag
run: echo "::set-output name=publishTag::$(cat publishTag.txt)"
- run: echo ${{ steps.publishTag.outputs.publishTag }}
- run: echo ${{ needs.ci.outputs.NPM_VERSION }}
- run: echo ${{ needs.ci.outputs.PUBLISH_TAG }}
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
@ -393,12 +353,21 @@ jobs:
if: github.ref == 'refs/heads/release' || contains(github.ref, 'refs/tags/beta-v')
run: |
npm install -g json
JACKSON_VERSION=${{ steps.npmversion.outputs.npmversion }}
JACKSON_VERSION=${{ needs.ci.outputs.NPM_VERSION }}
json -I -f package.json -e "this.main=\"dist/index.js\""
json -I -f package.json -e "this.types=\"dist/index.d.ts\""
json -I -f package.json -e "this.version=\"${JACKSON_VERSION}\""
npm publish --tag ${{ steps.publishTag.outputs.publishTag }} --access public
npm publish --tag ${{ needs.ci.outputs.PUBLISH_TAG }} --access public
working-directory: ./npm
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
publish_sdk:
needs: [ci, build]
name: Publish SDK for React
uses: ./.github/workflows/sdk.yml
with:
npmversion: ${{ needs.ci.outputs.NPM_VERSION }}
publishtag: ${{ needs.ci.outputs.PUBLISH_TAG }}
secrets: inherit

42
.github/workflows/sdk.yml vendored Normal file
View File

@ -0,0 +1,42 @@
name: Publish React component SDK to npmjs
on:
workflow_call:
inputs:
npmversion:
required: true
type: string
publishtag:
required: true
type: string
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./sdk/ui/react
steps:
- uses: actions/checkout@v3
# Setup .npmrc file to publish to npm
- uses: actions/setup-node@v3
with:
node-version: '16.x'
registry-url: 'https://registry.npmjs.org'
- run: npm ci
- run: npm run build
- name: Publish NPM
if: github.ref == 'refs/heads/release' || contains(github.ref, 'refs/tags/beta-v')
run: |
npm install -g json
PKG_VERSION=${{ inputs.npmversion }}
json -I -f package.json -e "this.main=\"./dist/@boxyhq/react-ui.umd.cjs\""
json -I -f package.json -e "this.module=\"./dist/@boxyhq/react-ui.js\""
json -I -f package.json -e "this.types=\"./dist/dist/types/index.d.ts\""
json -I -f package.json -e "this.exports['.']['import']=\"./dist/@boxyhq/react-ui.js\""
json -I -f package.json -e "this.exports['.']['require']=\"./dist/@boxyhq/react-ui.umd.cjs\""
json -I -f package.json -e "this.version=\"${PKG_VERSION}\""
npm publish --tag ${{ inputs.publishtag }} --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@ -95,4 +95,4 @@
"engines": {
"node": ">=14.18.1 <=18.x"
}
}
}

5
sdk/ui/react/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
node_modules
.DS_Store
dist
docs-dist
*.local

79
sdk/ui/react/README.md Normal file
View File

@ -0,0 +1,79 @@
# @boxyhq/react-ui
UI components from [BoxyHQ](https://boxyhq.com/) for plug-and-play enterprise features.
## Installation
`npm install @boxyhq/react-ui --save`
## Usage
### SSO Login Component
There are mainly 2 ways of using the SSO Login Component as outlined below:
#### Preset value for `ssoIdentifier`
If a value is passed for `ssoIdentifier`, it would render a button that on click calls the passed-in handler (onSubmit) with the `ssoIdentifier` value. The handler can then initiate a redirect to the SSO service forwarding the value for ssoIdentifier.
```tsx
import { Login as SSOLogin } from '@boxyhq/react-ui';
const onSSOSubmit = async (ssoIdentifier: string) => {
// Below calls signIn from next-auth. Replace this with whatever auth lib that you are using.
await signIn('boxyhq-saml', undefined, { client_id: ssoIdentifier });
};
<SSOLogin
buttonText={'Login with SSO'}
ssoIdentifier={`tenant=${tenant}&product=${product}`}
onSubmit={onSSOSubmit}
classNames={{
container: 'mt-2',
button: 'btn-primary btn-block btn rounded-md active:-scale-95',
}}
/>;
```
#### Accept input from the user for `ssoIdentifier`
If a value is not passed for `ssoIdentifier`, it would render an input field for the user to enter the `ssoIdentifier` value. And then on submit, the value gets passed to the handler. The handler can then initiate a redirect to the SSO service forwarding the value for ssoIdentifier.
```tsx
import { Login as SSOLogin } from '@boxyhq/react-ui';
const onSSOSubmit = async (ssoIdentifier: string) => {
// Below calls signIn from next-auth. Replace this with whatever auth lib that you are using.
await signIn('boxyhq-saml', undefined, { client_id: ssoIdentifier });
};
<SSOLogin
buttonText={'Login with SSO'}
onSubmit={onSSOSubmit}
classNames={{
container: 'mt-2',
label: 'text-gray-400',
button: 'btn-primary btn-block btn rounded-md active:-scale-95',
input: 'input-bordered input mb-5 mt-2 w-full rounded-md',
}}
/>;
```
#### Styling
If the classNames prop is passed in, we can override the default styling for each inner element. In case an inner element is omitted from the classNames prop, default styles will be set for the element. For example, In the below snippet, all the inner elements are styled by passing in the classNames for each inner one.
```tsx
<SSOLogin
buttonText={'Login with SSO'}
onSubmit={onSSOSubmit}
classNames={{
container: 'mt-2',
label: 'text-gray-400',
button: 'btn-primary btn-block btn rounded-md active:-scale-95',
input: 'input-bordered input mb-5 mt-2 w-full rounded-md',
}}
/>
```
Styling via style attribute is also supported for each inner element.

View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@boxyhq/react-ui</title>
<link rel="icon" type="image/x-icon" href="/favicon.ico">
</head>
<body>
<div id="root"></div>
<script type="module" src="/@pages-infra/main.js"></script>
</body>
</html>

View File

@ -0,0 +1,7 @@
---
title: BoxyHQ React SDK
---
import README from '../../README.md';
<README />

View File

@ -0,0 +1,7 @@
import { Navigate } from 'react-router-dom';
const Component404 = () => {
return <Navigate to='/' />;
};
export default Component404;

View File

@ -0,0 +1,45 @@
import { createTheme, defaultSideNavs } from 'vite-pages-theme-doc';
import Component404 from './404';
import logo from './logo.png';
export default createTheme({
logo: (
<div style={{ fontSize: '20px', display: 'flex', alignItems: 'center', gap: '1rem' }}>
<img src={logo} alt='BoxyHQ logo' width='40' height='40' />
BoxyHQ React SDK
</div>
),
topNavs: [
{
label: 'SDK',
path: '/',
activeIfMatch: {
// match all first-level paths
path: '/:foo',
},
},
{
label: 'Components',
path: '/components/demos/Login',
activeIfMatch: '/components',
},
],
sideNavs: (ctx) => {
return defaultSideNavs(ctx, {
groupConfig: {
components: {
demos: {
label: 'Demos (dev only)',
order: -1,
},
sso: {
label: 'SSO',
order: 1,
},
},
},
});
},
Component404,
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,66 @@
import { defineConfig } from 'vite';
import * as path from 'path';
import react from '@vitejs/plugin-react';
import pages, { DefaultPageStrategy } from 'vite-plugin-react-pages';
export default defineConfig({
plugins: [
react(),
pages({
pagesDir: path.join(__dirname, 'pages'),
pageStrategy: new DefaultPageStrategy({
extraFindPages: async (pagesDir, helpers) => {
const srcPath = path.join(__dirname, '../src');
if (String(process.env.SHOW_ALL_COMPONENT_DEMOS) === 'true') {
// show all component demos during dev
// put them in page `/components/demos/${componentName}`
helpers.watchFiles(
srcPath,
'*/demos/**/*.{[tj]sx,md?(x)}',
async function fileHandler(file, api) {
const { relative, path: absolute } = file;
const match = relative.match(/(.*)\/demos\/(.*)\.([tj]sx|mdx?)$/);
if (!match) throw new Error('unexpected file: ' + absolute);
const [_, componentName, demoName] = match;
const pageId = `/components/demos/${componentName}`;
// register page data
api.addPageData({
pageId,
key: demoName,
// register demo runtime data path
// the ?demo query will wrap the module with useful demoInfo
// that will be consumed by theme-doc
dataPath: `${absolute}?demo`,
// register demo static data
staticData: await helpers.extractStaticData(file),
});
}
);
}
// find all component README
helpers.watchFiles(srcPath, '*/README.md?(x)', async function fileHandler(file, api) {
const { relative, path: absolute } = file;
const match = relative.match(/(.*)\/README\.mdx?$/);
if (!match) throw new Error('unexpected file: ' + absolute);
const [_, componentName] = match;
const pageId = `/components/${componentName}`;
// register page data
api.addPageData({
pageId,
// register demo runtime data path
dataPath: absolute,
// register demo static data
staticData: await helpers.extractStaticData(file),
});
});
},
}),
}),
],
resolve: {
alias: {
'@boxyhq/react-ui': path.join(__dirname, '../src'),
},
},
});

9441
sdk/ui/react/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

61
sdk/ui/react/package.json Normal file
View File

@ -0,0 +1,61 @@
{
"name": "@boxyhq/react-ui",
"description": "React UI components from BoxyHQ",
"version": "do-not-change",
"type": "module",
"keywords": [
"react",
"boxyhq",
"sso",
"enterprise-features"
],
"files": [
"dist"
],
"homepage": "https://github.com/boxyhq/jackson/tree/main/sdk/ui/react#readme",
"bugs": {
"url": "https://github.com/boxyhq/jackson/issues?q=is%3Aopen+is%3Aissue+label%3Asdk"
},
"repository": {
"type": "git",
"url": "https://github.com/boxyhq/jackson",
"directory": "sdk/ui/react"
},
"main": "./src/index.ts",
"module": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": {
"import": "./src/index.ts",
"require": "./src/index.ts"
}
},
"scripts": {
"dev": "SHOW_ALL_COMPONENT_DEMOS=true vite serve docs",
"build-docs": "rimraf docs/dist && vite build docs && serve -s docs/dist",
"ssr-docs": "rimraf docs/dist && vite-pages ssr docs && serve docs/dist",
"build": "vite build"
},
"devDependencies": {
"@mdx-js/react": "2.2.1",
"@rollup/plugin-typescript": "10.0.1",
"@types/node": "18.11.18",
"@types/react": "18.0.26",
"@types/react-router-dom": "5.3.3",
"@vitejs/plugin-react": "3.0.0",
"autoprefixer": "10.4.13",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-router-dom": "6.6.1",
"rimraf": "3.0.2",
"serve": "14.1.2",
"vite": "4.0.4",
"vite-pages-theme-doc": "4.0.1",
"vite-plugin-css-injected-by-js": "2.2.0",
"vite-plugin-react-pages": "4.0.2"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
}

View File

@ -0,0 +1,7 @@
// Had to create this file and install autoprefixer dev dependency to fix vite error: "[plugin:vite:css] [postcss] Cannot read properties of undefined (reading 'config')"
const postcss = {
plugins: {
autoprefixer: {},
},
};
export default postcss;

View File

@ -0,0 +1,33 @@
# Login
## Installation
```bash
npm install @boxyhq/react-ui
```
## Usage
```jsx
import { Login } from '@boxyhq/react-ui';
// Inside render
<Login onSubmit={async function(ssoIdentifier) {
// Use the ssoIdentifier to resolve the SSO connection for the SSO service
}}
inputLabel='Tenant'
buttonText='Sign-in with SSO'
placeholder='contoso@boxyhq.com'
styles={{
container: {'--btn-outline-color': '219 14% 22%'}
button: {color: '#fff'},
input: {'margin-top': '2px'},
label: {'font-size': '1.5rem'}}},
classNames={{
container: 'mt-2',
button: 'btn-primary btn-block btn rounded-md',
input: 'input-bordered input mb-5 mt-2 w-full rounded-md',
}}
buttonText="Sign-in with SSO"
/>;
```

View File

@ -0,0 +1,34 @@
.btn {
background: #0070f3;
color: #fff;
padding: 0.5rem 1rem;
cursor: pointer;
border-radius: 5px;
border: 1px solid #0070f3;
font-weight: 500;
font-size: 1rem;
transition: box-shadow 0.15s ease;
}
.btn:focus-visible {
box-shadow: 0 0 0 1px #fff, 0 0 0 3px #0070f3;
}
.btn:disabled {
opacity: 0.6;
pointer-events: none;
}
.inp {
border-radius: 5px;
appearance: none;
height: 40px;
padding: 0 12px;
border: 1px solid #f5f5f5;
margin-bottom: 1rem;
outline: none;
}
.inp:focus {
border-color: #666;
}

View File

@ -0,0 +1,29 @@
/**
* @title Login Component with custom styling
* @description Refer the code below to see the passed props. Also supported is the passing of style attribute for each inner element (Note that inline style will override other styles).
* @order 1
*/
import { Login } from '@boxyhq/react-ui';
import './demo1.css';
const Demo1 = () => {
return (
<Login
onSubmit={async (ssoIdentifier) => {
// initiate the SSO flow here
}}
styles={{
input: { borderColor: '#ebedf0' },
button: { padding: '.85rem' },
}}
classNames={{ button: 'btn', input: 'inp' }}
placeholder='contoso@boxyhq.com'
inputLabel='Team Domain *'
buttonText='Login with SSO'
innerProps={{ input: { type: 'email' } }}
/>
);
};
export default Demo1;

View File

@ -0,0 +1,22 @@
/**
* @title Login Component with default styles
* @description If classNames prop is not passed in, then default styling will be applied. Also supported is the passing of style attribute for each inner element (Note that inline style will override other styles).
* @order 2
*/
import { Login } from '@boxyhq/react-ui';
const Demo2 = () => {
return (
<Login
onSubmit={async (ssoIdentifier) => {
// initiate the SSO flow here
}}
styles={{ input: { border: '1px solid darkcyan' } }}
inputLabel='Team domain *'
placeholder='contoso@boxyhq.com'
/>
);
};
export default Demo2;

View File

@ -0,0 +1,21 @@
/**
* @title Login Component without input display
* @description Here we pass the ssoIdentifier directly instead of taking a user input.
* @order 3
*/
import { Login } from '@boxyhq/react-ui';
const Demo3 = () => {
return (
<Login
onSubmit={async (ssoIdentifier) => {
// initiate the SSO flow here
}}
ssoIdentifier='some-identifier'
buttonText='SIGN IN WITH SSO'
/>
);
};
export default Demo3;

View File

@ -0,0 +1,23 @@
/**
* @title Login Component with failing onSubmit
* @description If error object is returned with the error message, the message would be displayed inline.
* @order 4
*/
import { Login } from '@boxyhq/react-ui';
const Demo4 = () => {
return (
<Login
onSubmit={async (ssoIdentifier) => ({
error: {
message: 'Invalid team domain',
},
})}
inputLabel='Team domain *'
placeholder='contoso@boxyhq.com'
/>
);
};
export default Demo4;

View File

@ -0,0 +1,62 @@
.container {
display: flex;
flex-direction: column;
max-width: 28rem;
}
.label {
margin-bottom: 0.375rem;
}
.input {
font-family: inherit;
appearance: none;
background-color: #fff;
border-radius: 0.375rem;
border-width: 1px;
border-color: #ebedf0;
border-style: solid;
padding: 0.5rem 0.75rem;
font-size: 1rem;
margin-top: 1px;
outline: none;
margin-bottom: 1rem;
}
.input:focus {
outline: 2px solid hsl(0 0% 20%/ 0.2);
outline-offset: 2px;
}
.button {
cursor: pointer;
font-family: inherit;
background-image: none;
border-radius: 0.25rem;
padding: 0.5rem 1rem;
background-color: transparent;
border: 1px solid rgb(51, 51, 51);
color: currentColor;
}
.button:disabled {
background: hsl(219 14% 28% / 0.2);
color: hsl(0 0% 20% / 0.2);
border-color: currentColor;
pointer-events: none;
}
.button:hover:not(:disabled) {
background-color: hsl(0 0% 20%);
color: hsl(0 0% 100%);
}
.button:focus-visible {
outline: 2px solid hsl(219 14% 22%);
outline-offset: 2px;
}
.button:active:hover,
.button:active:focus {
transform: scale(0.95);
}

View File

@ -0,0 +1,94 @@
import { useState, type ChangeEventHandler, type FormEvent } from 'react';
import type { LoginProps } from './types';
import useId from '../hooks/useId';
import cssClassAssembler from '../utils/cssClassAssembler';
import defaultClasses from './index.module.css';
const COMPONENT = 'sso';
const Login = ({
ssoIdentifier = '',
onSubmit,
inputLabel = 'Tenant',
placeholder = '',
buttonText = 'Sign-in with SSO',
styles,
classNames,
innerProps,
}: LoginProps) => {
// Generate stable html id attributes for input/span elements
const inputId = useId(COMPONENT, 'input');
const errorSpanId = useId(COMPONENT, 'span');
// States for ssoIdentifier input and error message
const [_ssoIdentifier, _setSsoIdentifier] = useState('');
const [errMsg, setErrMsg] = useState('');
// input event listener
const handleChange: ChangeEventHandler<HTMLInputElement> = (e) => {
setErrMsg(''); // clear error if any
_setSsoIdentifier(e.currentTarget.value);
};
// state for button submission
const [isProcessing, setIsProcessing] = useState(false);
// call onSubmit passing the _ssoIdentifier or the preset ssoIdentifier from props
const onButtonClick = async (e: FormEvent) => {
e.preventDefault();
setIsProcessing(true);
const {
error: { message },
} = (await onSubmit(_ssoIdentifier || ssoIdentifier)) || { error: {} };
setIsProcessing(false);
if (typeof message === 'string' && message) {
setErrMsg(message);
}
};
const isError = !!errMsg;
// if preset ssoIdentifier not passed in, then render input UI
const shouldRenderInput = !ssoIdentifier;
const inputUI = shouldRenderInput ? (
<>
<label
htmlFor={inputId}
style={styles?.label}
className={cssClassAssembler(classNames?.label, defaultClasses.label)}
{...innerProps?.label}>
{inputLabel}
</label>
<input
id={inputId}
value={_ssoIdentifier}
placeholder={placeholder}
onChange={handleChange}
style={styles?.input}
className={cssClassAssembler(classNames?.input, defaultClasses.input)}
aria-invalid={isError}
aria-describedby={errorSpanId}
{...innerProps?.input}
/>
{isError && <span id={errorSpanId}>{errMsg}</span>}
</>
) : null;
const disableButton = !(_ssoIdentifier || ssoIdentifier) || isProcessing;
return (
<div
className={cssClassAssembler(classNames?.container, defaultClasses.container)}
style={styles?.container}
{...innerProps?.container}>
{inputUI}
<button
disabled={disableButton}
type='button'
onClick={onButtonClick}
style={styles?.button}
className={cssClassAssembler(classNames?.button, defaultClasses.button)}
{...innerProps?.button}>
{buttonText}
</button>
</div>
);
};
export default Login;

View File

@ -0,0 +1,54 @@
import type {
ButtonHTMLAttributes,
CSSProperties,
HTMLAttributes,
InputHTMLAttributes,
LabelHTMLAttributes,
} from 'react';
export interface LoginProps {
/**
* Could be email, tenant or anything that can help to resolve the SSO connection. Use this if you want to set the value directly instead of taking a user input
*/
ssoIdentifier?: string;
/**
* Function to be passed into the component, takes in a value (ssoIdentifier) that can be used to resolve the SSO Connection in the Jackson SSO service.
* @param {string} ssoIdentifier Could be email, tenant or anything that can help to resolve the SSO connection.
* @returns {Promise} Any error raised while trying to resolve the ssoIdentifier. This could be displayed inline in the component. In case the error is handled upstream by means of a toast or a UI notification, nothing needs to be returned.
*/
onSubmit: (ssoIdentifier: string) => Promise<{ error: { message: string } } | void>;
/**
* Label for the input field that can accept the ssoIdentifier value
* @defaultValue Tenant
*/
inputLabel?: string;
/**
* Placeholder for the input field that can accept the ssoIdentifier value
* @defaultValue ''
*/
placeholder?: string;
/**
* Text/Name of the login button
* @defaultValue Sign-in with SSO
*/
buttonText?: string;
/**
* Styles for each inner component that Login is made up of.
*/
styles?: {
container?: CSSProperties;
button?: CSSProperties;
input?: CSSProperties;
label?: CSSProperties;
};
/**
* Classnames for each inner components that Login is made up of.
*/
classNames?: { container?: string; button?: string; input?: string; label?: string };
innerProps?: {
input?: InputHTMLAttributes<HTMLInputElement>;
button?: ButtonHTMLAttributes<HTMLButtonElement>;
label?: LabelHTMLAttributes<HTMLLabelElement>;
container?: HTMLAttributes<HTMLDivElement>;
};
}

View File

@ -0,0 +1,20 @@
import { useEffect, useState } from 'react';
import htmlIdGenerator from '../utils/htmlIdGenerator';
/**
*
* @param component Pass the SDK component name here (e.g., sso)
* @param elementType Pass the HTML element type for which the Id is to be generated (e.g., input)
* @returns {string} Id that is gauranteed to be unique suitable for use as HTML id attributes
*/
const useId = (component: string, elementType?: string) => {
const [id, setId] = useState('');
useEffect(() => {
setId(htmlIdGenerator(component, elementType));
}, [component, elementType]);
return id;
};
export default useId;

View File

@ -0,0 +1,2 @@
export { default as Login } from './Login';
export * from './Login/types'

View File

@ -0,0 +1,4 @@
const cssClassAssembler = (customClasses = '', defaultClasses: string) =>
customClasses ? customClasses : defaultClasses;
export default cssClassAssembler;

View File

@ -0,0 +1,13 @@
/**
* Util function that helps to generate unique HTML ids that are namespaced by
* the prefix and element type
* @param prefix Pass anything that needs to be prefixed to the id
* @param elementType Pass the HTML element type here (e.g., input)
* @returns {string} Id that is gauranteed to be unique and suitable for html id attributes across various components
*/
const htmlIdGenerator = (prefix: string, elementType?: string) => {
const uniqueId = Math.floor(1e6 + Math.random() * 1e6).toString();
return elementType ? `boxyhq-${prefix}-${elementType}-${uniqueId}` : `boxyhq-${prefix}-${uniqueId}`;
};
export default htmlIdGenerator;

1
sdk/ui/react/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -0,0 +1,24 @@
// This file was originally from https://github.com/vitejs/vite-plugin-react-pages/blob/main/packages/create-project/template-lib/tsconfig.json
// Modified by referring https://github.com/vitejs/vite/blob/main/packages/create-vite/template-react-ts/tsconfig.json
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"allowJs": false,
"skipLibCheck": true,
"baseUrl": ".",
"jsx": "react-jsx",
"module": "ESNext",
"moduleResolution": "Node",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"paths": {
"@boxyhq/react-ui": ["./src"]
},
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true
},
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@ -0,0 +1,9 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@ -0,0 +1,47 @@
// this is the build config for this demo library source, not the playground
// the build config for the library playground (document) is located at docs/vite.config.ts
import { resolve } from 'path';
import { defineConfig } from 'vite';
import typescript from '@rollup/plugin-typescript';
import react from '@vitejs/plugin-react';
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js';
export default defineConfig({
build: {
// use vite library mode to build the package
// https://vitejs.dev/guide/build.html#library-mode
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: 'BoxyHQUI',
// the proper extensions will be added
fileName: '@boxyhq/react-ui',
},
rollupOptions: {
// make sure to externalize deps that shouldn't be bundled
// into your library
external: ['react'],
output: {
// Provide global variables to use in the UMD build
// for externalized deps
globals: {
react: 'React',
},
},
},
},
plugins: [
react(),
cssInjectedByJsPlugin(),
// use @rollup/plugin-typescript to generate .d.ts files
// https://github.com/rollup/plugins/tree/master/packages/typescript#noforceemit
typescript({
declaration: true,
emitDeclarationOnly: true,
noForceEmit: true,
declarationDir: resolve(__dirname, 'dist/types'),
rootDir: resolve(__dirname, 'src'),
exclude: ['**/demos/*', '**/utils/*', '**/hooks/*'],
}),
],
});

View File

@ -27,7 +27,7 @@
}
},
"include": ["next-env.d.ts", "types/*.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules", "npm/typeorm.ts"],
"exclude": ["node_modules", "npm/typeorm.ts", "sdk"],
"ts-node": {
"compilerOptions": {
"module": "CommonJS"