diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100755 index 000000000..1b8538386 --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1,23 @@ +parser: babel-eslint + +parserOptions: + ecmaVersion: 6 + sourceType: module + ecmaFeatures: + jsx: true + experimentalObjectRestSpread: true + +env: + browser: true + es6: true + +globals: + global: true + +extends: + - eslint:recommended + - plugin:react/recommended + +settings: + react: + version: detect diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a469b4c84..46e7a981c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,4 +2,4 @@ # Each line is a file pattern followed by one or more owners. # These owners will be the default owners for everything in the repo. -* @aviadmizrachi @frontegg-david @MaxArnautFrontegg @TomerFrontegg @mariavlasov +* @aviadmizrachi @frontegg-david @eldad-frontegg @rotemzif1 @amirjaron @MaxArnaut diff --git a/.github/actions/trigger-e2e-test/action.yaml b/.github/actions/trigger-e2e-test/action.yaml deleted file mode 100644 index 1a7cbf4ca..000000000 --- a/.github/actions/trigger-e2e-test/action.yaml +++ /dev/null @@ -1,53 +0,0 @@ -name: Trigger E2E Action -description: Trigger E2E workflow for a specific version - -inputs: - version: - description: 'Version' - required: true - sha: - description: 'Commit SHA' - required: true - bot_app_id: - description: 'Bot App Id' - required: true - bot_app_key: - description: 'Bot App Key' - required: true - -runs: - using: "composite" - steps: - - id: create_bot_token - name: Create bot token - uses: actions/create-github-app-token@v1 - with: - app-id: ${{ inputs.bot_app_id }} - private-key: ${{ inputs.bot_app_key }} - owner: frontegg - repositories: e2e-system-tests - - name: "Trigger E2E tests" - uses: actions/github-script@v7 - env: - version: ${{ inputs.version }} - sha: ${{ inputs.sha }} - with: - github-token: ${{ steps.create_bot_token.outputs.token }} - script: | - const {sha, version} = process.env; - const repo = 'frontegg-react' - const owner = 'frontegg' - const e2eRepo = 'e2e-system-tests' - const workflow_id = 'frontegg-react-e2e-tests.yml' - const dispatch_id = `${repo}/${sha}` - - github.rest.actions.createWorkflowDispatch({ - owner, - repo: e2eRepo, - workflow_id, - ref: 'master', - inputs: { - version, - dispatch_id, - } - }) diff --git a/.github/scripts/generate-changelog.js b/.github/scripts/generate-changelog.js deleted file mode 100644 index 4afc2ee50..000000000 --- a/.github/scripts/generate-changelog.js +++ /dev/null @@ -1,47 +0,0 @@ -export default async ({context, github}) => { - const {default: fs} = await import('fs'); - let changelog = fs.readFileSync('./CHANGELOG.md', {encoding: 'utf8'}); - const {version} = JSON.parse(fs.readFileSync('./lerna.json', {encoding: "utf-8"})); - - - const {data: pullsData} = await github.rest.pulls.list({ - owner: context.repo.owner, - repo: context.repo.repo, - base: 'master', - state: 'closed', - sort: 'merged_at', - direction: 'desc' - }); - - let changelogStr = '' - - const mergedPulls = pullsData.filter(pull => pull.merged_at != null); - const lastReleaseIndex = mergedPulls.findIndex(pull => pull.head.ref === 'release/next') - const lastRelease = mergedPulls[lastReleaseIndex] - const pullsFromLastRelease = mergedPulls.slice(0, lastReleaseIndex); - - const reactChanges = pullsFromLastRelease.filter(pull => pull.head.ref !== 'upgrade-admin-portal') - const adminPortalChanges = pullsFromLastRelease.filter(pull => pull.head.ref === 'upgrade-admin-portal') - - - adminPortalChanges.forEach(pull => { - changelogStr += `${pull.body}\n` - }); - changelogStr += '\n'; - if (reactChanges.length > 0) { - changelogStr += `### React Wrapper ${version}:\n` - } - reactChanges.forEach(pull => { - changelogStr += `- ${pull.title}\n` - }); - - changelog = changelog.replace(/# Change Log\n/g, ''); - const dateNow = new Date(); - const date = `${dateNow.getFullYear()}-${dateNow.getMonth() + 1}-${dateNow.getDate()}` - let newChangelog = `# Change Log\n\n## [${version}](https://github.com/frontegg/frontegg-react/compare/${lastRelease.title}...v${version}) (${date})\n\n` - newChangelog += changelogStr - newChangelog += changelog.replace(/# Change Log\n/g, '\n'); - - fs.writeFileSync('./CHANGELOG.md', newChangelog, {encoding: 'utf8'}); - return changelogStr; -} diff --git a/.github/scripts/index.js b/.github/scripts/index.js deleted file mode 100644 index 820d182a2..000000000 --- a/.github/scripts/index.js +++ /dev/null @@ -1 +0,0 @@ -export {default as generateChangeLog} from './generate-changelog.js' diff --git a/.github/scripts/package.json b/.github/scripts/package.json deleted file mode 100644 index e90a67ac7..000000000 --- a/.github/scripts/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "scripts", - "version": "1.0.0", - "main": "index.js", - "license": "MIT", - "type": "module" -} diff --git a/.github/workflows/publish-alpha.yml b/.github/workflows/publish-alpha.yml deleted file mode 100644 index ee5d86320..000000000 --- a/.github/workflows/publish-alpha.yml +++ /dev/null @@ -1,77 +0,0 @@ -name: "(▶) Publish Alpha Version" -on: - workflow_dispatch: -jobs: - createAlphaVersion: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - with: - fetch-depth: "0" - - name: Read .nvmrc - run: echo "##[set-output name=NVMRC;]$(cat .nvmrc)" - id: nvm - - name: Use Node.js (.nvmrc) - uses: actions/setup-node@v1 - with: - node-version: "${{ steps.nvm.outputs.NVMRC }}" - - name: Install Dependencies and Build Packages - run: make init - - name: Git Identity - run: | - git config --global user.name 'frontegg' - git config --global user.email 'frontegg@users.noreply.github.com' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Authenticate with Registry - run: | - yarn logout - echo "init-author-name=Frontegg LTD" > .npmrc - echo "init-author-email=hello@frontegg.com" >> .npmrc - echo "init-author-url=https://frontegg.com" >> .npmrc - echo "init-license=MIT" >> .npmrc - echo "always-auth=true" >> .npmrc - echo "registry=https://registry.npmjs.org" >> .npmrc - echo "@frontegg:registry=https://registry.npmjs.org" >> .npmrc - echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> .npmrc - npm whoami - env: - NPM_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} - - name: Commit Version UP - run: | - yarn lerna version patch --no-git-tag-version --yes --no-push --force-publish - yarn update-version - yarn prettier-hook - - name: "Set incremented version" - uses: actions/github-script@v6 - id: 'incremented-version' - with: - result-encoding: string - script: | - const {default: fs} = await import('fs'); - const {version} = JSON.parse(fs.readFileSync('./lerna.json', {encoding: "utf-8"})); - return version; - - name: Publish Pre-Release version to NPM - id: publish_pre_release_version - run: | - version=$(node -p 'require("./lerna.json").version') - echo "::set-output name=LIB_VERSION::${version}" - echo "Publishing DEV version - v${version}-alpha.${{ github.run_id }}" - make move-package-json-to-dist - make prerelease-version-upgrade-${version}-alpha.${{ github.run_id }} - make pretty - make publish-packages-next - env: - NPM_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} - - name : Notify Slack on alpha version deployment - uses : rtCamp/action-slack-notify@v2 - env : - SLACK_CHANNEL : frontegg-alpha-versions - SLACK_COLOR : ${{ job.status }} - SLACK_ICON : https://avatars.githubusercontent.com/u/67857107?s=40&v=4 - SLACK_MESSAGE : '${{ steps.publish_pre_release_version.outputs.LIB_VERSION }}-alpha.${{ github.run_id }} has been released :rocket:' - SLACK_TITLE : 'A new @frontegg/react alpha version!' - SLACK_USERNAME : ${{ github.actor }} - SLACK_WEBHOOK : ${{ secrets.ROTEM_SLACK_WEBHOOK }} - MSG_MINIMAL : true diff --git a/.github/workflows/publish-prerelease.yml b/.github/workflows/publish-prerelease.yml index f6038ed28..99ad9aac1 100644 --- a/.github/workflows/publish-prerelease.yml +++ b/.github/workflows/publish-prerelease.yml @@ -9,20 +9,21 @@ jobs: createReleasePullRequest: if: "!contains(join(github.event.pull_request.labels.*.name, ','), 'Release') && github.event.pull_request.merged == true" runs-on: ubuntu-latest + container: cypress/browsers:node12.13.0-chrome80-ff74 steps: - name: Checkout uses: actions/checkout@v2 with: fetch-depth: "0" - - name: Read .nvmrc - run: echo "##[set-output name=NVMRC;]$(cat .nvmrc)" - id: nvm - - name: Use Node.js (.nvmrc) + - name: Setup Node.js uses: actions/setup-node@v1 with: - node-version: "${{ steps.nvm.outputs.NVMRC }}" + node-version: 12 + registry-url: 'https://npm.pkg.github.com' - name: Install Dependencies and Build Packages run: make init + - name: Run Components Test + run: make test-component - name: Git Identity run: | git config --global user.name 'frontegg' @@ -38,75 +39,50 @@ jobs: echo "init-license=MIT" >> .npmrc echo "always-auth=true" >> .npmrc echo "registry=https://registry.npmjs.org" >> .npmrc + echo "_authToken=$NPM_TOKEN" >> .npmrc echo "@frontegg:registry=https://registry.npmjs.org" >> .npmrc echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> .npmrc npm whoami env: NPM_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} - - - - name: set Patch release - id: version_type_patch - run: | - echo "VERSION_TYPE=patch" >> $GITHUB_ENV - - - name: Set Minor release - id: version_type_minor - if: contains(join(github.event.pull_request.labels.*.name, ','), 'Minor') - run: | - echo "VERSION_TYPE=minor" >> $GITHUB_ENV - - name: Commit Version UP run: | - yarn lerna version $VERSION_TYPE --no-git-tag-version --yes --no-push --force-publish - yarn update-version + yarn lerna version patch --no-git-tag-version --yes --no-push --force-publish yarn prettier-hook - - name: "Set Generated changelog" - uses: actions/github-script@v6 - id: 'generated-changelog' - with: - result-encoding: string - script: | - const { generateChangeLog } = await import(`${process.env.GITHUB_WORKSPACE}/.github/scripts/index.js`); - return generateChangeLog({context, github, core}) - - name: "Set incremented version" - uses: actions/github-script@v6 - id: 'incremented-version' - with: - result-encoding: string - script: | - const {default: fs} = await import('fs'); - const {version} = JSON.parse(fs.readFileSync('./lerna.json', {encoding: "utf-8"})); - return version; - - name: Commit changes + git add . && git commit -m "chore(release): publish `node -p 'require("./lerna.json").version'`" + - name: Set current CHANGELOG to output + id: changelog shell: bash -ex {0} - id: 'cpr_commit_sha' run: | - git add . && git commit -m "chore(release): publish ${{ steps.incremented-version.outputs.result }}" - echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT + version=$(node -p 'require("./lerna.json").version') + changelog=$(node -p "(function(){var changelog = require('fs').readFileSync('./CHANGELOG.md', {encoding: 'utf8'}); changelog = changelog.substring(changelog.indexOf('# [')); changelog = changelog.substring(changelog.indexOf('\n')).trim(); return changelog.substring(0, changelog.indexOf('# [') - 2)})()") + changelog="${changelog//'%'/'%25'}" + changelog="${changelog//$'\n'/'%0A'}" + changelog="${changelog//$'\r'/'%0D'}" + echo $changelog + echo "::set-output name=version::${version}" + echo "::set-output name=changelog::${changelog}" - name: Create Release Pull Request id: cpr uses: peter-evans/create-pull-request@v3.5.1 with: token: ${{ secrets.GITHUB_TOKEN }} path: ${{ secrets.GITHUB_WORKSPACE }} - commit-message: "Update v${{ steps.incremented-version.outputs.result }}" + commit-message: "Update v${{ steps.changelog.outputs.version }}" committer: GitHub author: "${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>" - title: 'v${{ steps.incremented-version.outputs.result }}' + title: 'v${{ steps.changelog.outputs.version }}' body: | - # v${{ steps.incremented-version.outputs.result }} + # v${{ steps.changelog.outputs.version }} - ${{steps.generated-changelog.outputs.result}} + ${{steps.changelog.outputs.changelog}} labels: "Type: Release" branch: "release/next" - name: Publish Pre-Release version to NPM - id: publish_pre_release_version run: | version=$(node -p 'require("./lerna.json").version') - echo "::set-output name=LIB_VERSION::${version}" echo "Publishing DEV version - v${version}-alpha.${{ github.run_id }}" make move-package-json-to-dist make prerelease-version-upgrade-${version}-alpha.${{ github.run_id }} @@ -116,30 +92,3 @@ jobs: make publish-packages-next env: NPM_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} - - - name: Notify Slack on alpha version deployment - uses: rtCamp/action-slack-notify@v2 - env: - SLACK_CHANNEL: frontegg-alpha-versions - SLACK_COLOR: ${{ job.status }} - SLACK_ICON: https://avatars.githubusercontent.com/u/67857107?s=40&v=4 - SLACK_MESSAGE: '${{ steps.publish_pre_release_version.outputs.LIB_VERSION }}-alpha.${{ github.run_id }} has been released :rocket:' - SLACK_TITLE: 'A new @frontegg/react alpha version!' - SLACK_USERNAME: ${{ github.actor }} - SLACK_WEBHOOK: ${{ secrets.ROTEM_SLACK_WEBHOOK }} - MSG_MINIMAL: true - - name: Wait until NPM registry finished indexing the new version - uses: actions/github-script@v6 - with: - script: | - const checkingVersion = '${{ steps.publish_pre_release_version.outputs.LIB_VERSION }}-alpha.${{ github.run_id }}'; - const checkNpmVersions = require('./scripts/wait-for-npm-indexing.js'); - await checkNpmVersions(github, ['@frontegg/react'], checkingVersion); - - - name: "Call trigger-e2e-test action" - uses: ./.github/actions/trigger-e2e-test - with: - version: ${{ steps.publish_pre_release_version.outputs.LIB_VERSION }}-alpha.${{ github.run_id }} - sha: ${{ steps.cpr_commit_sha.outputs.sha }} - bot_app_id: ${{ secrets.GH_FRONTEGG_BOT_APP_ID }} - bot_app_key: ${{ secrets.GH_FRONTEGG_BOT_APP_SECRET }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f820c73f2..5ffbdf91c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,18 +13,17 @@ jobs: name: Publish if: "contains(join(github.event.pull_request.labels.*.name, ','), 'Release') && github.event.pull_request.merged == true" runs-on: ubuntu-latest + container: cypress/browsers:node12.13.0-chrome80-ff74 steps: - name: Checkout uses: actions/checkout@v2 with: fetch-depth: "0" - - name: Read .nvmrc - run: echo "##[set-output name=NVMRC;]$(cat .nvmrc)" - id: nvm - - name: Use Node.js (.nvmrc) + - name: Setup Node.js uses: actions/setup-node@v1 with: - node-version: "${{ steps.nvm.outputs.NVMRC }}" + node-version: 12 + registry-url: 'https://npm.pkg.github.com' - name: Git Identity run: | git config --global user.name 'github-actions[bot]' @@ -34,6 +33,8 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Install Dependencies and Build Packages run: make init + - name: Run Components Test + run: make test-component - name: Set Current Version id: set_current_version if: startsWith(github.event.pull_request.title, 'v') @@ -57,7 +58,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Create Git Tag if: steps.tag_check.outputs.exists_tag == 'false' - uses: pkgdeps/git-tag-action@v2 + uses: azu/action-package-version-to-git-tag@v1 with: version: ${{ steps.set_current_version.outputs.CURRENT_VERSION }} github_token: ${{ secrets.GITHUB_TOKEN }} @@ -86,6 +87,7 @@ jobs: echo "init-license=MIT" >> .npmrc echo "always-auth=true" >> .npmrc echo "registry=https://registry.npmjs.org" >> .npmrc + echo "_authToken=$NPM_TOKEN" >> .npmrc echo "@frontegg:registry=https://registry.npmjs.org" >> .npmrc echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> .npmrc npm whoami @@ -94,46 +96,10 @@ jobs: - name: Publish run: | make move-package-json-to-dist - make publish-packages-latest + make publish-packages env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} - - name : Wait until NPM registry finished indexing the new version - uses : actions/github-script@v6 - with : - script : | - const checkingVersion = '${{ steps.set_current_version.outputs.CURRENT_VERSION }}'; - const checkNpmVersions = require('./scripts/wait-for-npm-indexing.js'); - await checkNpmVersions(github, ['@frontegg/react'], checkingVersion); - - name : "Create bot token for dispatch" - id : dispatch_bot_token - uses : actions/create-github-app-token@v3 - with : - client-id : ${{ secrets.GH_FRONTEGG_BOT_CLIENT_ID }} - private-key : ${{ secrets.GH_FRONTEGG_BOT_APP_SECRET }} - owner : frontegg - repositories : | - oauth-service - dashboard - - name : "Trigger Oauth and Dashboard Services Pipeline Workflow" - uses : actions/github-script@v5 - env : - PR_VERSION : '${{ steps.set_current_version.outputs.CURRENT_VERSION }}' - with : - github-token : ${{ steps.dispatch_bot_token.outputs.token }} - script : | - const fe_react_version = process.env.PR_VERSION; - const owner = 'frontegg'; - const dispatchActionsData = [{ repo: 'oauth-service', workflow_id: 'update-react-dependency.yaml' }, { repo: 'dashboard', workflow_id: 'update-frontegg-react-dependency.yaml' }]; - await Promise.all(dispatchActionsData.map(({ repo, workflow_id }) => github.rest.actions.createWorkflowDispatch({ - owner, - repo, - workflow_id, - ref: 'master', - inputs: { - fe_react_version, - }})) - ); - name: Notify Slack on deployment uses: rtCamp/action-slack-notify@v2 env: diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 12cf79c5b..81261bb32 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -11,16 +11,23 @@ jobs: init: name: 'Install, Build and Test' runs-on: ubuntu-latest + container: cypress/browsers:node12.13.0-chrome80-ff74 steps: - - uses: actions/checkout@v4 - - name: Read .nvmrc - run: echo "##[set-output name=NVMRC;]$(cat .nvmrc)" - id: nvm - - name: Use Node.js (.nvmrc) - uses: actions/setup-node@v4 + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 with: - node-version: "${{ steps.nvm.outputs.NVMRC }}" + node-version: 12 - run: make clean - run: make install - run: make build - - run: yarn test + - run: make test-component + - uses: actions/upload-artifact@v1 + if: failure() + with: + name: cypress-screenshots + path: cypress/screenshots + - uses: actions/upload-artifact@v1 + if: failure() + with: + name: cypress-videos + path: cypress/videos diff --git a/.github/workflows/sanity.yml b/.github/workflows/sanity.yml deleted file mode 100644 index 268b25a12..000000000 --- a/.github/workflows/sanity.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Sanity Check -on: - schedule: - - cron: "0 0 * * *" - -env: - CI: true -jobs: - sanity : - name : 'Sanity check' - runs-on : ubuntu-latest - steps : - - uses : actions/checkout@v2 - - name : Read .nvmrc - run : echo "##[set-output name=NVMRC;]$(cat .nvmrc)" - id : nvm - - name : Use Node.js (.nvmrc) - uses : actions/setup-node@v1 - with : - node-version : "${{ steps.nvm.outputs.NVMRC }}" - - run : cd packages/sanity-check && yarn run build && yarn run test - - diff --git a/.github/workflows/trigger-e2e-test.yml b/.github/workflows/trigger-e2e-test.yml deleted file mode 100644 index 7cab9238d..000000000 --- a/.github/workflows/trigger-e2e-test.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: "(▶) Trigger E2E tests Workflow" - -on: - workflow_dispatch: - inputs: - version: - description: 'Version' - required: true - sha: - description: 'Commit SHA' - required: true - -jobs: - trigger_e2e_tests: - name: "Trigger E2E tests Workflow" - runs-on: 'ubuntu-latest' - steps: - - uses: actions/checkout@v4 - - - name: "Print inputs" - run: | - echo "Received test request for @frontegg/react@${{ inputs.version }}" - echo "From: ${{ inputs.dispatch_id }}" - - - name: "Call trigger-e2e-test action" - uses: ./.github/actions/trigger-e2e-test - with: - version: ${{ inputs.version }} - sha: ${{ inputs.sha }} - bot_app_id: ${{ secrets.GH_FRONTEGG_BOT_APP_ID }} - bot_app_key: ${{ secrets.GH_FRONTEGG_BOT_APP_SECRET }} - - diff --git a/.gitignore b/.gitignore index 38633574f..c65f533b3 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,3 @@ cypress/videos .nyc_output cypress/screenshots .idea -packages/demo-saas/build diff --git a/.nvmrc b/.nvmrc index 6aab9b43f..82f87fa0a 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v18.18.0 +v12.18.4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ac481b1a..ea3d36199 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3314 +1,1304 @@ # Change Log -## [7.15.2](https://github.com/frontegg/frontegg-react/compare/v7.15.1...v7.15.2) (2026-8-17) +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. -- FR-26036 - Fixed tenant selection takes precedence over skipUserLoading - - ---- - -> [!NOTE] -> **Medium Risk** -> Dependency-only change, but it alters auth/session loading behavior in upstream packages (tenant vs. skipUserLoading), which can affect multi-tenant login flows. -> -> **Overview** -> **Bumps** `@frontegg/react`’s direct dependencies from **7.123.0** to **7.124.0** (`@frontegg/js`, `@frontegg/react-hooks`) and refreshes **`yarn.lock`** so the aligned **7.124.0** tree is pinned (`types`, `redux-store`, `rest-api`, etc.). -> -> Consumers of this package pick up upstream **Admin Portal / SDK** behavior from that release, including the fix where **tenant selection takes precedence over `skipUserLoading`** (FR-26036). No application source in this repo changes beyond the version pins. -> -> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 78b7c25ebb99a2e7cd06f52ff9e5d297f077318f. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). - - - -## [7.15.1](https://github.com/frontegg/frontegg-react/compare/v7.15.0...v7.15.1) (2026-8-17) - -- FR-26036 - Added tenantChoicePending to fix the hosted postlogin vs tenant-chooser race -- FR-26413 - Fixed the identifier field showing the default keyboard instead of the email keyboard on iOS -- FR-23291 - Added Admin Portal users filter by role UI - - ---- - -> [!NOTE] -> **Low Risk** -> Dependency version bump with no local source changes; risk is limited to upstream 7.123.0 release behavior. -> -> **Overview** -> Bumps **`@frontegg/react`**’s direct dependencies from **7.122.0** to **7.123.0** (`@frontegg/js`, `@frontegg/react-hooks`) and refreshes **`yarn.lock`** for the matching `@frontegg/*` tree (`types`, `redux-store`, `rest-api`). -> -> Consumers of this package pick up Admin Portal **7.123.0** behavior from upstream, including fixes for hosted post-login vs tenant-chooser timing (`tenantChoicePending`), iOS email keyboard on the identifier field, and Admin Portal users filtering by role. -> -> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 49e5ef73e5deb3e5100bf0130e5e5829b2d89819. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). - - - -## [7.15.0](https://github.com/frontegg/frontegg-react/compare/v7.14.0...v7.15.0) (2026-8-2) - -- FR-24113 - Added mobile-friendly authenticator setup-key enrollment UI -- FR-24965 - Added remember last used MFA factor preference -- FR-26112 - Fixed iOS Password AutoFill on the embedded login page - - ---- - -> [!NOTE] -> **Medium Risk** -> Transitive upgrade touches MFA enrollment, MFA preference, and embedded login autofill—auth-adjacent UX with no local code review in this PR. -> -> **Overview** -> Bumps **`@frontegg/react`**’s direct dependencies from **7.121.0** to **7.122.0**: `@frontegg/js` and `@frontegg/react-hooks`, with **`yarn.lock`** updated for the matching `@frontegg/types`, `@frontegg/redux-store`, and `@frontegg/rest-api` versions. -> -> There are no application code changes in this repo; consumers pick up **AdminPortal / SDK 7.122.0** behavior from those packages, including mobile-friendly authenticator setup-key enrollment, remembering the last used MFA factor, and an iOS Password AutoFill fix on the embedded login page. -> -> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 42b0c4c34075b25c9c8ff03c4175c8214e155dff. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). - - - -## [7.14.0](https://github.com/frontegg/frontegg-react/compare/v7.13.11...v7.14.0) (2026-7-27) - -- FR-25731 - Fixed chooser load-error state seeding when switchable list empties after opening - - -- FR-25731 - Added post-auth Choose Organization step in login-box (forward-port of #2864 to v7.120.x) - - ---- - -> [!NOTE] -> **Medium Risk** -> Although this diff is only version pins, 7.121.0 changes post-authentication login-box/org-chooser flow, which can affect how users complete sign-in. -> -> **Overview** -> Bumps `@frontegg/react`’s pinned `@frontegg/js` and `@frontegg/react-hooks` from **7.119.0** to **7.121.0**, with `yarn.lock` updated for the aligned `@frontegg/*` stack (`redux-store`, `rest-api`, `types`). -> -> No application source changes in this repo—the update pulls in upstream **7.121.0** login-box behavior described in the PR: a **post-auth Choose Organization** step and a fix for **chooser load-error state** when the switchable org list becomes empty after the chooser opens (FR-25731). -> -> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 6ad99258fdffdbb72a9742e2f207c1953d970bf0. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). - - - -## [7.13.11](https://github.com/frontegg/frontegg-react/compare/v7.13.10...v7.13.11) (2026-7-23) - -- FR-26014 - Added admin-box addressType redux branch (external instant-nav) [7.119.x] - - ---- - -> [!NOTE] -> **Low Risk** -> Version pin and lockfile-only change; risk is limited to regressions or behavior changes in the upstream 7.119.0 Admin Portal packages. -> -> **Overview** -> Bumps **`@frontegg/react`**’s pinned Frontegg Admin Portal stack from **7.118.0** to **7.119.0** by updating `@frontegg/js` and `@frontegg/react-hooks` in `packages/react/package.json` and refreshing **`yarn.lock`** for the related `@frontegg/*` packages (`redux-store`, `rest-api`, `types`). -> -> There is no application source change in this repo; consumers of `@frontegg/react` pick up upstream **7.119.x** behavior, including **FR-26014** (admin-box **`addressType`** redux branch for external instant navigation) as noted in the PR description. -> -> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 06addbf5b454469eb22259736809182a9f054299. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). - -- FR-24939 - Fixed native step-up challenge not rendering in the embedded login WebView -- FR-24853 - Removed identifiers flag - - ---- - -> [!NOTE] -> **Low Risk** -> Dependency version bump only; behavior changes come from published `@frontegg/*` packages, with no local code edits in this diff. -> -> **Overview** -> This PR **updates the React package’s Frontegg SDK dependencies** from **7.117.0** to **7.118.0**: `@frontegg/js` and `@frontegg/react-hooks` in `packages/react/package.json`, with matching lockfile entries for the full `@frontegg/*` tree (`types`, `redux-store`, `rest-api`, etc.). -> -> There is **no application source change** in the diff—consumers of `@frontegg/react` pick up upstream fixes described in the PR notes, including **native step-up challenge rendering in embedded login WebViews** (FR-24939) and **removal of the identifiers flag** (FR-24853). -> -> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 1f9716f8963bce14dc6f1a2cacddd2066324987f. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). - - -### React Wrapper 7.13.11: -- ci: upgrade create-github-app-token action to v3 - -## [7.13.10](https://github.com/frontegg/frontegg-react/compare/v7.13.9...v7.13.10) (2026-7-9) - -- FR-24939 - Fixed native step-up challenge not rendering in the embedded login WebView -- FR-24853 - Removed identifiers flag - - ---- - -> [!NOTE] -> **Low Risk** -> Dependency version bump only; behavior changes come from published `@frontegg/*` packages, with no local code edits in this diff. -> -> **Overview** -> This PR **updates the React package’s Frontegg SDK dependencies** from **7.117.0** to **7.118.0**: `@frontegg/js` and `@frontegg/react-hooks` in `packages/react/package.json`, with matching lockfile entries for the full `@frontegg/*` tree (`types`, `redux-store`, `rest-api`, etc.). -> -> There is **no application source change** in the diff—consumers of `@frontegg/react` pick up upstream fixes described in the PR notes, including **native step-up challenge rendering in embedded login WebViews** (FR-24939) and **removal of the identifiers flag** (FR-24853). -> -> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 1f9716f8963bce14dc6f1a2cacddd2066324987f. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). - - -### React Wrapper 7.13.10: -- ci: upgrade create-github-app-token action to v3 -- ci: use GitHub App token for cross-repo dispatch instead of expiring PAT - -## [7.13.9](https://github.com/frontegg/frontegg-react/compare/v7.13.8...v7.13.9) (2026-7-6) - -- FR-23757 - Fixed the actor of system audit logs to not be unknown - -- FR-25580 - Fixed token refresh resilience with retry backoff - - ---- - -> [!NOTE] -> **Medium Risk** -> Touches auth-related upstream behavior (token refresh); scope is limited to version pins with no local code edits. -> -> **Overview** -> Bumps **`@frontegg/js`** and **`@frontegg/react-hooks`** in `packages/react` from **7.115.0** to **7.117.0**, with **`yarn.lock`** updated for the aligned **`@frontegg/types`**, **`redux-store`**, and **`rest-api`** versions. -> -> This pulls in upstream AdminPortal/SDK fixes noted in the PR: **system audit logs** should show a proper actor instead of *unknown* (FR-23757), and **token refresh** is more resilient via retry backoff (FR-25580). No local React package source changes—only dependency pins and lockfile. -> -> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 850aa02375413c5df9654f60e18a43f18bfcee33. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). - - - -## [7.13.8](https://github.com/frontegg/frontegg-react/compare/v7.13.7...v7.13.8) (2026-7-6) - -- FR-24939 - Fixed step-up gate ignoring max_age when the token has no auth_time -- FR-24939 - Fixed mobile SDK step-up looping to a blank/error page instead of the MFA challenge - - ---- - -> [!NOTE] -> **Medium Risk** -> Changes upstream step-up and MFA behavior used by the React SDK; low diff size but auth-flow impact warrants careful regression on step-up and mobile MFA. -> -> **Overview** -> Bumps `@frontegg/react`’s pinned `@frontegg/js` and `@frontegg/react-hooks` from **7.114.0** to **7.115.0**, with matching `yarn.lock` entries for the related `@frontegg/*` packages. -> -> This pulls in upstream **7.115.0** fixes (FR-24939): step-up now respects **max_age** when the token lacks `auth_time`, and mobile SDK step-up no longer loops to a blank/error page instead of the MFA challenge. -> -> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 3ef175aa79d86b01eda9e240b54f80a2cc26ab8b. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). - - - -## [7.13.7](https://github.com/frontegg/frontegg-react/compare/v7.13.6...v7.13.7) (2026-6-25) - -- FR-24988 - Fixed hosted login box accessibility issues -- FR-25494 - fixed stale frontegg oauth stale - - ---- - -> [!NOTE] -> **Low Risk** -> Dependency-only version bump with no local code changes; risk is limited to behavior changes inside the 7.114.0 SDK packages. -> -> **Overview** -> Updates **`@frontegg/react`** to consume **Frontegg SDK 7.114.0** by bumping **`@frontegg/js`** and **`@frontegg/react-hooks`** from `7.113.0` to `7.114.0`, with matching lockfile entries for the transitive **`@frontegg/types`**, **`@frontegg/redux-store`**, and **`@frontegg/rest-api`** packages. -> -> No application source in this repo changes; consumers get upstream fixes from that release (per PR notes: hosted login box accessibility and Frontegg OAuth stale-session handling). -> -> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 7fa66ce0d59280ce82830e924242df4039f5882c. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). - - - -## [7.13.6](https://github.com/frontegg/frontegg-react/compare/v7.13.5...v7.13.6) (2026-6-22) - -- FR-24579 - Fixed admin-portal white screen in mobile SDK by refreshing session from native tokens - - ---- +## [2.8.14](https://github.com/frontegg/frontegg-react/compare/v2.8.13...v2.8.14) (2021-07-27) -> [!NOTE] -> **Medium Risk** -> Touches core auth/session packages without local code review in this diff; risk is mainly regression in admin portal or mobile SDK session handling from the upstream release. -> -> **Overview** -> Bumps **`@frontegg/react`**’s direct dependencies from **7.112.0** to **7.113.0**: `@frontegg/js` and `@frontegg/react-hooks`, with **`yarn.lock`** updated for the matching transitive **`@frontegg/types`**, **`redux-store`**, and **`rest-api`** packages. -> -> No application source changes in this PR—the update pulls in upstream **7.113.0** behavior, including the fix noted for **FR-24579** (admin portal white screen on mobile SDK by refreshing session from native tokens). -> -> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 455cb010cf87711c59f26aa901b9a897333966f3. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). - - - -## [7.13.5](https://github.com/frontegg/frontegg-react/compare/v7.13.4...v7.13.5) (2026-6-22) - -- FR-24579 - Fixed admin-portal redirect race on native auth handoff -- FR-24579 - Fixed admin-portal mobile bridge capability gating and stuck loading - - ---- - -> [!NOTE] -> **Low Risk** -> Lockfile and dependency version pins only; no application code changes in this repository. -> -> **Overview** -> Bumps **`@frontegg/react`**’s direct dependencies from **7.111.0** to **7.112.0**: `@frontegg/js` and `@frontegg/react-hooks`, with **`yarn.lock`** updated for the full `@frontegg/*` tree (`types`, `redux-store`, `rest-api`, etc.). -> -> This release line includes Admin Portal fixes referenced in **FR-24579** (redirect race on native auth handoff, mobile bridge capability gating / stuck loading); those changes live in the published packages, not in this repo’s source. -> -> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit e008404ff704c75ca374a83fe38b756b680c773d. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). - - - -## [7.13.4](https://github.com/frontegg/frontegg-react/compare/v7.13.3...v7.13.4) (2026-6-21) - -- FR-24579 - Added native token bridge for the admin portal (no second login) -- FR-20973 - Fixed sso with username -- FR-20975 - Fixed description username login with magic code -- FR-22194 - Fixed error massage of username already exists missing from UI -- FR-20977 - Fixed resend with username in magic link - - ---- - -> [!NOTE] -> **Medium Risk** -> Auth and admin-portal behavior shifts with the SDK bump even though this diff is only lockfile/package versions; regression risk is in login, SSO, and magic-link flows. -> -> **Overview** -> Bumps **`@frontegg/react`**’s pinned Frontegg SDK from **7.110.0** to **7.111.0** by updating `@frontegg/js` and `@frontegg/react-hooks` in `packages/react/package.json`, with matching **`yarn.lock`** entries for `@frontegg/js`, `@frontegg/react-hooks`, `@frontegg/redux-store`, `@frontegg/rest-api`, and `@frontegg/types`. -> -> No application source in this repo changes; consumers pick up **7.111.0** Admin Portal / auth behavior from the upstream packages (e.g. admin portal native token bridge and username/SSO/magic-link fixes noted in the PR description). -> -> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 29129cb8fc4d3d3dd756a5c545f6d64e2cff23b5. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). - - - -## [7.13.3](https://github.com/frontegg/frontegg-react/compare/v7.13.2...v7.13.3) (2026-6-4) - -- FR-25111 - Fixed tenant regex - - ---- - -> [!NOTE] -> **Low Risk** -> Dependency-only version bump with no local code changes; risk is limited to upstream 7.110.0 behavior in auth/admin flows. -> -> **Overview** -> Bumps the embedded Frontegg Admin Portal stack from **7.109.0** to **7.110.0** by updating `@frontegg/js` and `@frontegg/react-hooks` in `packages/react/package.json` and refreshing `yarn.lock` for the related `@frontegg/*` packages. -> -> There are no application source changes in this repo; behavior updates (including the **tenant regex** fix noted in FR-25111) come from the upgraded published packages. -> -> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit e036f6948f8a1b49c443a3026c9ab66aac74a545. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). - - - -## [7.13.2](https://github.com/frontegg/frontegg-react/compare/v7.13.1...v7.13.2) (2026-6-1) - -- FR-25022 - Changed phone validations - - ---- - -> [!NOTE] -> **Medium Risk** -> Validation behavior for phone numbers changes in a vendor auth/admin package, which can affect signup or profile flows without local code to review in this PR. -> -> **Overview** -> Bumps **`@frontegg/js`** and **`@frontegg/react-hooks`** in `packages/react` from **7.108.0** to **7.109.0**, with **`yarn.lock`** updated for the matching **`@frontegg/types`**, **`redux-store`**, and **`rest-api`** versions. There are **no application source changes** in this repo—the Admin Portal behavior (including **FR-25022** phone validation updates noted in the PR description) comes from the upgraded Frontegg packages. -> -> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit ba67d54dd118e133862c20cdffc7fc4935065d49. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). - - - -## [7.13.1](https://github.com/frontegg/frontegg-react/compare/v7.13.0...v7.13.1) (2026-5-19) - -- FR-23507 - Fixed custom login box favicon not displaying pulls from main login box instead - - ---- - -> [!NOTE] -> **Medium Risk** -> Updates core Frontegg SDK dependencies, so behavior changes come from upstream library code and could affect authentication/AdminPortal flows at runtime despite the small diff. -> -> **Overview** -> Updates `packages/react` to depend on `@frontegg/js` and `@frontegg/react-hooks` `7.108.0` (from `7.107.0`). -> -> Regenerates `yarn.lock` to pull the corresponding `7.108.0` Frontegg transitive packages (`@frontegg/types`, `@frontegg/redux-store`, `@frontegg/rest-api`). -> -> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 81cc59e22c86c4a0f2788d40a78481e6e941d04b. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). - - - -## [7.13.0](https://github.com/frontegg/frontegg-react/compare/v7.12.22...v7.13.0) (2026-5-17) - -- FR-24663 - Fixed country restriction dark theme input -- FR-24664 - Fixed country field background in modern theme -- FR-24693 - Fixed country restriction admin portal not full list of countries display for allow deny lists -- FR-24661 - Fixed country restriction tip counter updates -- FR-24667 - Added country restriction admin portal current country is not added to the list after enabling the counter restriction toggle - - ---- - -> [!NOTE] -> **Medium Risk** -> Updates core Frontegg runtime dependencies, which could change Admin Portal behavior at runtime despite being a small diff. Risk is limited to upstream package changes and lockfile resolution. -> -> **Overview** -> Bumps `@frontegg/react`'s Frontegg dependencies to `7.107.0` by updating `@frontegg/js` and `@frontegg/react-hooks`, along with the corresponding transitive packages in `yarn.lock` (e.g., `@frontegg/redux-store`, `@frontegg/rest-api`, `@frontegg/types`). -> -> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 0219802f3bad61bcc72c9dfa1b2ac7c424031aaf. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). - -- FR-24187 - Fixed CPU issues - - ---- - -> [!NOTE] -> **Low Risk** -> Low risk: this PR only updates dependency versions and lockfile entries, with no in-repo logic changes. Behavior changes, if any, come from the upstream Frontegg packages. -> -> **Overview** -> Updates `packages/react` to depend on `@frontegg/js` and `@frontegg/react-hooks` `7.106.0` (from `7.105.0`). -> -> Regenerates `yarn.lock` to pull the matching `7.106.0` versions of transitive Frontegg packages (`@frontegg/types`, `@frontegg/redux-store`, `@frontegg/rest-api`). -> -> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 64810cd988f72146c20dc7e4c9069f2c1f07a991. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). - - -### React Wrapper 7.13.0: -- fix - e2e trigger - -## [7.12.22](https://github.com/frontegg/frontegg-react/compare/v7.12.21...v7.12.22) (2026-5-5) - -- FR-24187 - Fixed CPU issues - - ---- - -> [!NOTE] -> **Low Risk** -> Low risk: this PR only updates dependency versions and lockfile entries, with no in-repo logic changes. Behavior changes, if any, come from the upstream Frontegg packages. -> -> **Overview** -> Updates `packages/react` to depend on `@frontegg/js` and `@frontegg/react-hooks` `7.106.0` (from `7.105.0`). -> -> Regenerates `yarn.lock` to pull the matching `7.106.0` versions of transitive Frontegg packages (`@frontegg/types`, `@frontegg/redux-store`, `@frontegg/rest-api`). -> -> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 64810cd988f72146c20dc7e4c9069f2c1f07a991. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). - -- FR-23435 - Added country restriction features to Security Center -- FR-23515 - Fixed wrong audit log tooltips -- FR-23524 - Added guidesCdnUrl to SSOPage - - ---- - -> [!NOTE] -> **Low Risk** -> Low risk lockfile/dependency-only update; behavior changes are limited to whatever is introduced in upstream `@frontegg/*` packages. -> -> **Overview** -> Updates `@frontegg/react` to depend on `@frontegg/js` and `@frontegg/react-hooks` `7.105.0` (from `7.104.0`). -> -> Regenerates `yarn.lock` to pull the `7.105.0` Frontegg dependency chain (`@frontegg/types`, `@frontegg/redux-store`, `@frontegg/rest-api`) with updated resolved artifacts and integrity hashes. -> -> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 25ced5838bb38eb844ac71bafd62158ed19e7500. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). - - -### React Wrapper 7.12.22: -- fix - e2e trigger - -## [7.12.21](https://github.com/frontegg/frontegg-react/compare/v7.12.20...v7.12.21) (2026-3-31) - -- FR-23900 - Added validation for reset password token and improved user feedback for expired links - - ---- - -> [!NOTE] -> **Low Risk** -> Low risk dependency-only bump; behavior changes (if any) come from upstream `@frontegg/*` packages rather than local code changes. -> -> **Overview** -> Updates `@frontegg/react` dependencies to `@frontegg/js` and `@frontegg/react-hooks` `7.104.0` (from `7.103.0`). -> -> Refreshes `yarn.lock` to pull the corresponding `7.104.0` transitive Frontegg packages (`@frontegg/types`, `@frontegg/redux-store`, `@frontegg/rest-api`). -> -> Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 0545b689269403c0a7af825229a20f1b5889e19b. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). - - - -## [7.12.20](https://github.com/frontegg/frontegg-react/compare/v7.12.19...v7.12.20) (2026-3-19) - -- FR-23610 - Added login completed GTM -- FR-23421 - Added support for CMC SCIM guide dialog and fix SSO guide - - ---- - -> [!NOTE] -> **Low Risk** -> Low risk: this PR only updates dependency versions and lockfile entries, with no direct source code changes. Any behavior change would come from the upstream `@frontegg/*` packages. -> -> **Overview** -> Updates `@frontegg/react` to depend on `@frontegg/js` and `@frontegg/react-hooks` `7.103.0` (from `7.102.0`). -> -> Refreshes `yarn.lock` to pull in the corresponding `7.103.0` releases for transitive Frontegg packages (`@frontegg/types`, `@frontegg/redux-store`, `@frontegg/rest-api`). -> -> Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 3d8629989a92e6a191d29e8a8b1702c67eac0095. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). - - - -## [7.12.19](https://github.com/frontegg/frontegg-react/compare/v7.12.18...v7.12.19) (2026-3-8) - -- FR-22979 - Changed callback in InviteUserForm to handle errors and reset form state - - ---- - -> [!NOTE] -> **Low Risk** -> Low risk dependency-only update (no source changes), but behavior may shift due to updated Frontegg SDK transitive packages. -> -> **Overview** -> Updates `packages/react/package.json` to depend on `@frontegg/js` and `@frontegg/react-hooks` `7.102.0` (from `7.101.0`). -> -> Regenerates `yarn.lock` to pull in the corresponding `7.102.0` releases and transitive bumps (notably `@frontegg/types`, `@frontegg/redux-store`, and `@frontegg/rest-api`). -> -> Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 37257c84d3616d1134cc172f49641eed44147c00. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). - - - -## [7.12.18](https://github.com/frontegg/frontegg-react/compare/v7.12.17...v7.12.18) (2026-2-16) - -- FR-22346 - Fixed enable session per tenant data mismatch between user jwt and sdk -- FR-22346 - Fixed enable session per tenant data mismatch between user jwt and sdk - - ---- - -> [!NOTE] -> **Low Risk** -> Dependency-only bump of Frontegg SDK packages; risk is limited to upstream behavior changes/regressions in auth/session handling introduced by the new versions. -> -> **Overview** -> Updates `@frontegg/react` to depend on `@frontegg/js` and `@frontegg/react-hooks` `7.101.0` (from `7.100.0`). -> -> Refreshes `yarn.lock` to pull the `7.101.0` Frontegg dependency chain (`@frontegg/types`, `@frontegg/redux-store`, `@frontegg/rest-api`) with no source code changes in this repo. -> -> Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 428af0c0ac5ae6afa6c4be281a3dc811657801b4. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). - +**Note:** Version bump only for package @fronteg/react -## [7.12.17](https://github.com/frontegg/frontegg-react/compare/v7.12.16...v7.12.17) (2026-2-10) -- FR-23484 - Added an option to render SSO guides outside of admin box - ---- -> [!NOTE] -> **Low Risk** -> Lockfile-only dependency bump with no application code changes; risk is limited to behavior changes introduced by the updated upstream Frontegg packages. -> -> **Overview** -> Updates `@frontegg/react` to depend on `@frontegg/js` and `@frontegg/react-hooks` `7.100.0` (from `7.99.0`). -> -> Refreshes `yarn.lock` accordingly, pulling in the `7.100.0` versions of Frontegg transitive packages (`@frontegg/types`, `@frontegg/redux-store`, `@frontegg/rest-api`). -> -> Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 28f2aa9a6f81391244e8b9928f8cafac3da023c0. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). - +## [2.8.13](https://github.com/frontegg/frontegg-react/compare/v2.8.12...v2.8.13) (2021-07-22) +**Note:** Version bump only for package @fronteg/react -## [7.12.16](https://github.com/frontegg/frontegg-react/compare/v7.12.15...v7.12.16) (2026-1-27) -- FR-22263 - Fixed publish - ---- -> [!NOTE] -> Updates Frontegg dependencies to the latest minor release. -> -> - Bumps `@frontegg/js` and `@frontegg/react-hooks` from `7.97.0` to `7.99.0` in `packages/react/package.json` -> - Refreshes `yarn.lock` to align transitive Frontegg packages (`@frontegg/types`, `@frontegg/redux-store`, `@frontegg/rest-api`) to `7.99.0` -> -> **Scope/Risk** -> - No source code changes; dependency-only update. -> -> Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 2cb0f6beb46b051fcd0aeec7ba89066b6e51dd7a. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). - +## [2.8.12](https://github.com/frontegg/frontegg-react/compare/v2.8.11...v2.8.12) (2021-07-20) -## [7.12.15](https://github.com/frontegg/frontegg-react/compare/v7.12.14...v7.12.15) (2026-1-14) +**Note:** Version bump only for package @fronteg/react -- FR-23141 - Added username edit config - ---- -> [!NOTE] -> Updates Frontegg dependencies across React package and lockfile. -> -> - Bumps `@frontegg/js` and `@frontegg/react-hooks` to `7.97.0` in `packages/react/package.json` -> - Refreshes `yarn.lock` to align transitive Frontegg packages (`redux-store`, `rest-api`, `types`) to `7.97.0` -> -> Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 651de40e846e34f524c75a7364b43ec4a86eb3f5. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). - -## [7.12.14](https://github.com/frontegg/frontegg-react/compare/v7.12.13...v7.12.14) (2026-1-5) +## [2.8.11](https://github.com/frontegg/frontegg-react/compare/v2.8.10...v2.8.11) (2021-07-11) -- FR-17084 - Fixed loader +**Note:** Version bump only for package @fronteg/react - ---- -> [!NOTE] -> Upgrades Frontegg dependencies to the latest minor version. -> -> - Bumps `@frontegg/js` and `@frontegg/react-hooks` to `7.96.0` in `packages/react/package.json` -> - Updates `yarn.lock` to align transitive Frontegg packages (`@frontegg/types`, `@frontegg/redux-store`, `@frontegg/rest-api`) to `7.96.0` -> -> No application/source code changes; dependency versions only. -> -> Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 6601f1aa7ae52e18ccc5e740e48db84332e68a48. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). - -## [7.12.13](https://github.com/frontegg/frontegg-react/compare/v7.12.12...v7.12.13) (2025-12-11) -- FR-21830 - Fixed IP location +## [2.8.10](https://github.com/frontegg/frontegg-react/compare/v2.8.9...v2.8.10) (2021-07-08) - ---- -> [!NOTE] -> Upgrade Frontegg dependencies to 7.95.0 and update lockfile/transitives accordingly. -> -> - **Dependencies**: -> - Bump `@frontegg/js` and `@frontegg/react-hooks` to `7.95.0` in `packages/react/package.json`. -> - Update `yarn.lock` to reflect `7.95.0` across transitive packages: `@frontegg/redux-store`, `@frontegg/rest-api`, `@frontegg/types`. -> -> Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit cac26563c989c32a9a6bfa828cc6de832f9f3517. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). - +### Bug Fixes +* **audits:** fix store conflict between old audits and new auditlogs state ([5e493fe](https://github.com/frontegg/frontegg-react/commit/5e493fec79dd73198186a6b2a94e8833e4600102)) +* **core:** add destroy store on unmount ([c117bf9](https://github.com/frontegg/frontegg-react/commit/c117bf9c853f0ecfbb3e6c4098dfac8e91dfd9b7)) -## [7.12.12](https://github.com/frontegg/frontegg-react/compare/v7.12.11...v7.12.12) (2025-12-7) -- FR-22289 - Added support new columns in users table - ---- -> [!NOTE] -> Updates `@frontegg/js` and `@frontegg/react-hooks` to 7.94.0 with corresponding lockfile updates for related Frontegg packages. -> -> - **Dependencies**: -> - Bump `@frontegg/js` and `@frontegg/react-hooks` in `packages/react/package.json` from `7.93.0` → `7.94.0`. -> - Update transitive Frontegg packages in `yarn.lock` to `7.94.0` (`@frontegg/types`, `@frontegg/redux-store`, `@frontegg/rest-api`). -> -> Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 99002cc3f0eca18303d736f7e3ab4b00f59a716b. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). - +## [2.8.9](https://github.com/frontegg/frontegg-react/compare/v2.8.8...v2.8.9) (2021-07-08) -## [7.12.11](https://github.com/frontegg/frontegg-react/compare/v7.12.10...v7.12.11) (2025-11-30) -- FR-22193 - Added admin portal support for searching by username, email, name, etc. in the users table +### Bug Fixes - ---- +* **auth:** add missing callback to sso file config saga ([9c895ab](https://github.com/frontegg/frontegg-react/commit/9c895abf5bb364ef4fabbd8b2961d17722f821c7)) -> [!NOTE] -> Upgrade Frontegg React dependencies to 7.93.0 with corresponding yarn.lock updates. -> -> - **Dependencies**: -> - Bump `@frontegg/js` and `@frontegg/react-hooks` to `7.93.0` in `packages/react/package.json`. -> - Update `yarn.lock` resolutions, cascading to `@frontegg/types`, `@frontegg/redux-store`, and `@frontegg/rest-api` at `7.93.0`. -> -> Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit d0f2997c7cd70efb1460efedf2eb02d1a7abcb13. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). - -## [7.12.10](https://github.com/frontegg/frontegg-react/compare/v7.12.9...v7.12.10) (2025-10-19) -- FR-22210 - Added Netherlands -- FR-22239 - Fixed share link design and texts - ---- +## [2.8.8](https://github.com/frontegg/frontegg-react/compare/v2.8.7...v2.8.8) (2021-07-06) -> [!NOTE] -> Bumps Frontegg React dependencies to 7.92.0 and aligns related packages. -> -> - **Dependencies**: -> - Update in `packages/react/package.json`: -> - `@frontegg/js` 7.91.0 → 7.92.0 -> - `@frontegg/react-hooks` 7.91.0 → 7.92.0 -> - Align transitive Frontegg packages to 7.92.0 (`@frontegg/types`, `@frontegg/redux-store`, `@frontegg/rest-api`). -> -> Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 55679109aef6611ac7b08b49d521c74f809e238c. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). - +### Bug Fixes -## [7.12.9](https://github.com/frontegg/frontegg-react/compare/v7.12.8...v7.12.9) (2025-9-29) +* **auth:** reload captcha after failed login ([f12be57](https://github.com/frontegg/frontegg-react/commit/f12be57a299de748adb991ad6cdc7d17c226903e)) -- FR-22258 - Added approval flow - ---- -> [!NOTE] -> Update Frontegg React dependencies to 7.91.0 and refresh lockfile. -> -> - **Dependencies**: -> - Bump `@frontegg/js` and `@frontegg/react-hooks` to `7.91.0` in `packages/react/package.json`. -> - Update `yarn.lock` to `7.91.0` for related Frontegg packages: `@frontegg/js`, `@frontegg/react-hooks`, `@frontegg/redux-store`, `@frontegg/rest-api`, `@frontegg/types`. -> -> Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 90adfd1541652322631a92f6c5893ae1bd1dc1dc. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). - -## [7.12.8](https://github.com/frontegg/frontegg-react/compare/v7.12.7...v7.12.8) (2025-9-25) +## [2.8.7](https://github.com/frontegg/frontegg-react/compare/v2.8.6...v2.8.7) (2021-07-01) -- FR-22238 - Added a 'Back' button when both link and form invitations … -- FR-22167 - Fixed username field to be hidden by default in the invite… +**Note:** Version bump only for package @fronteg/react -## [7.12.7](https://github.com/frontegg/frontegg-react/compare/v7.12.6...v7.12.7) (2025-9-16) -- FR-22185 - Fixed running afterAuthRedirect in social login callback -- FR-22201 - Added invitation user form state preservation for enhanced… +## [2.8.6](https://github.com/frontegg/frontegg-react/compare/v2.8.5...v2.8.6) (2021-06-30) -## [7.12.6](https://github.com/frontegg/frontegg-react/compare/v7.12.5...v7.12.6) (2025-9-16) +### Bug Fixes -- FR-22185 - Added support for whitelisting direct login action with metadataOverrides +* **redux-store:** fix add user saga ([#454](https://github.com/frontegg/frontegg-react/issues/454)) ([3c6aaea](https://github.com/frontegg/frontegg-react/commit/3c6aaea3f4b12805e2c5e1f6116c0f2c77cfba53)) -## [7.12.5](https://github.com/frontegg/frontegg-react/compare/v7.12.4...v7.12.5) (2025-9-14) -- FR-22130 - Removed autoFocus from invite user's email field +## [2.8.5](https://github.com/frontegg/frontegg-react/compare/v2.8.4...v2.8.5) (2021-06-30) -## [7.12.4](https://github.com/frontegg/frontegg-react/compare/v7.12.3...v7.12.4) (2025-9-10) +### Bug Fixes -- FR-22001 - Added Embedded Social Login in MobileSDK +* **redux-store:** fix addUser saga ([#452](https://github.com/frontegg/frontegg-react/issues/452)) ([f5b19a7](https://github.com/frontegg/frontegg-react/commit/f5b19a75a599aacfc2cd104393272860d4ff7b2e)) -## [7.12.3](https://github.com/frontegg/frontegg-react/compare/v7.12.2...v7.12.3) (2025-9-4) -- FR-22006 - Fixed signup login direct action with basename -- FR-21916 - Added username field to InviteUserForm with validation -- FR-21330 - Fixed empty names -- FR-22047 - Fixed user invite initial dialog to use inviteByEmailEnable... -- FR-21924 - Added roles to invitation link flow -- FR-21914 - Added smart navigation for invite dialog based on enabled … -- FR-21913 - Added dialog step management enum and logic for enhanced invite user modal +## [2.8.4](https://github.com/frontegg/frontegg-react/compare/v2.8.3...v2.8.4) (2021-06-29) -## [7.12.2](https://github.com/frontegg/frontegg-react/compare/v7.12.1...v7.12.2) (2025-9-3) +### Bug Fixes -- FR-21916 - Added username field to InviteUserForm with validation -- FR-21330 - Fixed empty names -- FR-22047 - Fixed user invite initial dialog to use inviteByEmailEnable... -- FR-21924 - Added roles to invitation link flow -- FR-21914 - Added smart navigation for invite dialog based on enabled … -- FR-21913 - Added dialog step management enum and logic for enhanced invite user modal +* **redux-store:** [FR-3203] - skip prelogin when saml is disabled ([#448](https://github.com/frontegg/frontegg-react/issues/448)) ([e6febcd](https://github.com/frontegg/frontegg-react/commit/e6febcd6f9305a6fc81a563baba875299eb331e8)) +* FR-3342, round number of total pages to bigger ([7a0fb1f](https://github.com/frontegg/frontegg-react/commit/7a0fb1f8410fb36469d1e3a0a2ad00b725c8bff7)) -- FR-22006 - Added signup to login direct action -- FR-21912 - Added InvitationLink component (basic version) -- FR-21911 - Added InviteUserViaLink component (basic version) -- FR-21910 - Added an InviteUserSelector component -- FR-21909 - Changed InviteUserForm component to separate file from InviteUserDialog -## [7.12.1](https://github.com/frontegg/frontegg-react/compare/v7.12.0...v7.12.1) (2025-8-12) -- FR-21734 - Added the applications that are free-access for this tenant to the user's application list -- FR-21350 - Added FREE-ACCESS application tenant to the user's application list -- FR-21495 - Changed Georgia phone country code as requested +## [2.8.3](https://github.com/frontegg/frontegg-react/compare/v2.8.1...v2.8.3) (2021-06-23) +### Bug Fixes -## [7.12.0](https://github.com/frontegg/frontegg-react/compare/v7.11.0...v7.12.0) (2025-7-29) +* align all dependencies versions ([f1d5c48](https://github.com/frontegg/frontegg-react/commit/f1d5c48aba34827ea06a554cfb61734f1a40c93b)) +* update libraries version ([77da072](https://github.com/frontegg/frontegg-react/commit/77da072c67cb11e6cccb86b044a3a1a22b09625c)) -- FR-21351 - Changed user loading methods to use the new V3 API -- FR-21350 - Added usage for tenant-application access types in hosted Admin box -## [7.11.0](https://github.com/frontegg/frontegg-react/compare/v7.10.3...v7.11.0) (2025-7-17) -- FR-21360 - Fixed CMCComponents React19 compatibility -- FR-20601 - Fixed metadata sync when using custom signup fields -- FR-20601 - Fixed custom signup fields visiblity -- FR-21194 - Fixed translations options to have all supported languages +## [2.8.2](https://github.com/frontegg/frontegg-react/compare/v2.8.1...v2.8.2) (2021-06-23) +### Bug Fixes -## [7.10.3](https://github.com/frontegg/frontegg-react/compare/v7.10.2...v7.10.3) (2025-6-25) +* update libraries version ([77da072](https://github.com/frontegg/frontegg-react/commit/77da072c67cb11e6cccb86b044a3a1a22b09625c)) -- FR-21242 - Added customization for select phone number country codes -## [7.10.2](https://github.com/frontegg/frontegg-react/compare/v7.10.1...v7.10.2) (2025-6-23) -- FR-20838 - Added search for role in the edit roles dialog of users page -- FR-21222 - Fixed reset password selector icons to be aligned to the center -- FR-21206 - Fixed user goes to "Forget password?" page after clicking on "Try another method" -- FR-21208 - Added support for dynamic redirect url after signup -- FR-21174 - Changed forgot password sms option translation -- FR-21173 - Fixed password reset success state handling +## [2.8.1](https://github.com/frontegg/frontegg-react/compare/v2.8.0...v2.8.1) (2021-06-22) +**Note:** Version bump only for package @fronteg/react -## [7.10.1](https://github.com/frontegg/frontegg-react/compare/v7.10.0...v7.10.1) (2025-6-16) -- FR-0000 - Fixed node version -- FR-21160 - Removed exclusion of set user email policy state -## [7.10.0](https://github.com/frontegg/frontegg-react/compare/v7.9.0...v7.10.0) (2025-6-12) +# [2.8.0](https://github.com/frontegg/frontegg-react/compare/v2.7.2...v2.8.0) (2021-06-21) -- FR-20862 - Fixed useLoginHint and add tests to simulate hosted prelogin flow -- FR-21121 - Added reset password with email/sms actions and a Forgot password sms otc page -- FR-21097 - Added suspense to renderCMCComponent -- FR-21120 - Added BaseOTC component and use it in login otc flows -- FR-21112 - Added password recovery selector and a determinePasswordRecoveryStrategy function -- FR-21118 - Fixed default language handle -- FR-20838 - Added search functionality to roles popper -- FR-20178 - Added generic IdentifierField and used it in login and forgot password flows -- FR-20945 - Added username in profile -- FR-20868 - Added usernames +### Features +* add connectivity redux hooks FR-2889 ([b8a5f98](https://github.com/frontegg/frontegg-react/commit/b8a5f98a6e173b239d8bfc9111c9f4794a322259)) +* move connectivity state to redux-store package FR-2889 ([55b80f8](https://github.com/frontegg/frontegg-react/commit/55b80f87181fa0283f187f7b36286cb0575586c9)) -## [7.9.0](https://github.com/frontegg/frontegg-react/compare/v7.8.0...v7.9.0) (2025-6-7) -- FR-21024 - Added callback to reset password cmc -- FR-20953 - Added Display specific countries for phone number -- FR-20733 - Fixed Screen flickering after forget password success -- FR-20104 - Fixed Issue with search term persistence in the Personal and non-Personal API Tokens modals -- FR-20811 - Fixed optional localizations deep partial type -- FR-20407 - Added localization support for 25 languages -## [7.8.0](https://github.com/frontegg/frontegg-react/compare/v7.7.4...v7.8.0) (2025-5-27) +## [2.7.2](https://github.com/frontegg/frontegg-react/compare/v2.7.1...v2.7.2) (2021-06-14) -- FR-20899 - Changed logout user session button -- FR-20901 - Fixed select actions menu item -- FR-20871 - Added support for default language -- FR-20894 - Added support regex in string input +**Note:** Version bump only for package @fronteg/react -- FR-19718 - Added username strategies -## [7.7.4](https://github.com/frontegg/frontegg-react/compare/v7.7.3...v7.7.4) (2025-5-20) +## [2.7.1](https://github.com/frontegg/frontegg-react/compare/v2.7.0...v2.7.1) (2021-06-10) -- FR-20337 - Fixed errors not displayed on submit +**Note:** Version bump only for package @fronteg/react -## [7.7.3](https://github.com/frontegg/frontegg-react/compare/v7.7.2...v7.7.3) (2025-5-15) -- FR-19738 - Added support for CMC components +# [2.7.0](https://github.com/frontegg/frontegg-react/compare/v2.6.0...v2.7.0) (2021-06-07) -## [7.7.2](https://github.com/frontegg/frontegg-react/compare/v7.7.1...v7.7.2) (2025-5-4) +### Bug Fixes -- FR-20047 - Added support for overrideSignupFields as string js code -- FR-19797 - Fixed bug duplicate request on double click -- FR-20479 - Added type error validation method to yup interfaces +* **audits:** fix ip cell crash ([#425](https://github.com/frontegg/frontegg-react/issues/425)) ([169c4b6](https://github.com/frontegg/frontegg-react/commit/169c4b67f038d53f4eee600f0c9973cc56855779)) +* fix owasp type error FR-3131 ([ad57394](https://github.com/frontegg/frontegg-react/commit/ad57394fe6ec8483f450be4b492490cf73f655e5)) +* **connectivity:** fix the documentation ([02946a9](https://github.com/frontegg/frontegg-react/commit/02946a928360e12045ad5f23ba4717d8cbbf499b)) +* **connectivity:** remove the fitContent property. fix scrolling of the container ([b2a7c8f](https://github.com/frontegg/frontegg-react/commit/b2a7c8f13425c90d51b22cce613c71819a9c9f64)) +* **core:** increase domain suffix length ([02af3e9](https://github.com/frontegg/frontegg-react/commit/02af3e9d379831e833adfae85003506146013da3)) +### Features -## [7.7.1](https://github.com/frontegg/frontegg-react/compare/v7.7.0...v7.7.1) (2025-4-29) +* **auth:** display error from strategy on activation form ([ad6a5c4](https://github.com/frontegg/frontegg-react/commit/ad6a5c43d18564cbb91d9ebb4901c33741c5a8ae)) -- FR-20482 - Added one of validation method to yup boolean interface -- FR-19402 - Fixed UI delete all checkbox -- FR-20398 - Added delete all option to SCIM deletion -- FR-20047 - Added Custom Signup Fields -## [7.7.0](https://github.com/frontegg/frontegg-react/compare/v7.6.7...v7.7.0) (2025-4-14) -- FR-18986 - Added tooltip functionality for individual chips in GroupsChipsList component -- FR-20385 - Fixed user impersonation with identifiers -- FR-20254 - Added localizations for activate with social logins -- FR-20280 - Added support for signup with phone number +# [2.6.0](https://github.com/frontegg/frontegg-react/compare/v2.5.2...v2.6.0) (2021-05-27) +### Features -## [7.6.7](https://github.com/frontegg/frontegg-react/compare/v7.6.6...v7.6.7) (2025-3-31) +* **auth:** force terms on social sign up FR-2869 ([#406](https://github.com/frontegg/frontegg-react/issues/406)) ([4462402](https://github.com/frontegg/frontegg-react/commit/4462402c8648a023eb7595c4153a9943c039f995)) +* **auth:** space for release ([#420](https://github.com/frontegg/frontegg-react/issues/420)) ([fd18c60](https://github.com/frontegg/frontegg-react/commit/fd18c60e41dcc76f713b32ed84b5bfd7e2f8c355)) -- FR-20267 - Added support for direction by metadata -- FR-20250 - Fixed min optional tests on password strength -- FR-19976 - Fixed pre step shouldn't appear when shouldSetPassword is not false -## [7.6.6](https://github.com/frontegg/frontegg-react/compare/v7.6.5...v7.6.6) (2025-3-23) -- FR-19299 - Fixed Hebrew translation of password rotation -- FR-17951 - Changed the api routes of profile page's phone number +## [2.5.2](https://github.com/frontegg/frontegg-react/compare/v2.5.1...v2.5.2) (2021-05-24) -- FR-20068 - Added password config custom tests +**Note:** Version bump only for package @fronteg/react -## [7.6.5](https://github.com/frontegg/frontegg-react/compare/v7.6.4...v7.6.5) (2025-3-16) -- FR-20068 - Added password config custom tests -- FR-13869 - Fixed autocomplete overrides input theme -- FR-19956 - Fixed missing email for unenreolled MFA users -- FR-20037 - Fixed input focus on user invite dialog +## [2.5.1](https://github.com/frontegg/frontegg-react/compare/v2.5.0...v2.5.1) (2021-05-23) +**Note:** Version bump only for package @fronteg/react -## [7.6.4](https://github.com/frontegg/frontegg-react/compare/v7.6.3...v7.6.4) (2025-3-5) -- FR-19452 - Added a "silent refresh" to switchTenant +# [2.5.0](https://github.com/frontegg/frontegg-react/compare/v2.4.0...v2.5.0) (2021-05-21) -## [7.6.3](https://github.com/frontegg/frontegg-react/compare/v7.6.2...v7.6.3) (2025-3-4) -- FR-19752 - Fixed error not shown in custom role creation -- FR-18568 - Added password rotation support +### Bug Fixes +* **connectivity:** add overflow auto to connectivity page FR-3005 ([fd4b341](https://github.com/frontegg/frontegg-react/commit/fd4b34177d0da2b386325b4144bba2bad4a235d8)) -## [7.6.2](https://github.com/frontegg/frontegg-react/compare/v7.6.1...v7.6.2) (2025-2-26) +### Features -- FR-19037 - Added fallback for skip user load -- FR-19648 - Added password expiration to login box +* **connectivity:** add new paraneter fitConntent ([d31c281](https://github.com/frontegg/frontegg-react/commit/d31c28122b09b2b41ebcab1cc89d5a5f0bc93d17)) -## [7.6.1](https://github.com/frontegg/frontegg-react/compare/v7.6.0...v7.6.1) (2025-2-25) -- FR-19037 - Added an option to skip user load +# [2.4.0](https://github.com/frontegg/frontegg-react/compare/v2.3.2...v2.4.0) (2021-05-19) -## [7.6.0](https://github.com/frontegg/frontegg-react/compare/v7.5.1...v7.6.0) (2025-2-20) -- FR-18630 - Fixed input transparent autofill for vite -- FR-15942 - Changed titles in SSO configuration guides -- FR-19747 - Added a pre step toggler to the dashboard that enables a confirmation mechanism on relevant emails, to prevent automated scanners from invalidating magic links +### Features +* **auth:** [FR-2731] remember MFA devices ([#404](https://github.com/frontegg/frontegg-react/issues/404)) ([7f135d2](https://github.com/frontegg/frontegg-react/commit/7f135d200657ffd19ab54bcf9fd2049c07db43b4)) -- FR-19677 - Added hebrew translations for the pre step screens -- FR-19547 - Changed pre step titles -## [7.5.1](https://github.com/frontegg/frontegg-react/compare/v7.5.0...v7.5.1) (2025-2-10) -- FR-19547 - Changed prestep component to get t instead of texts +## [2.3.2](https://github.com/frontegg/frontegg-react/compare/v2.3.1...v2.3.2) (2021-05-10) +### Bug Fixes -## [7.5.0](https://github.com/frontegg/frontegg-react/compare/v7.4.9...v7.5.0) (2025-2-10) +* **connectivity:** fix UI glitches ([a78c4f0](https://github.com/frontegg/frontegg-react/commit/a78c4f0587a606cc529909d35a24d98ab3e66f01)) -- FR-19547 - Added prestep support for auth flows -- FR-19048 - Changed sign-up with 6 digit code flow -- FR-19626 - Added support for social login eventually style -- FR-9045 - Fixed login form placeholder color -## [7.4.9](https://github.com/frontegg/frontegg-react/compare/v7.4.8...v7.4.9) (2025-2-5) -- FR-19618 - Fixed FRONTEGG_AFTER_AUTH_REDIRECT_URL usage on custom login -- FR-19509 - Fixed sms as main auth strategy validation +## [2.3.1](https://github.com/frontegg/frontegg-react/compare/v2.3.0...v2.3.1) (2021-05-10) +### Bug Fixes -## [7.4.8](https://github.com/frontegg/frontegg-react/compare/v7.4.7...v7.4.8) (2025-2-2) +* **connectivity:** fix changes not saved on swithcing connecticity context ([04758b5](https://github.com/frontegg/frontegg-react/commit/04758b5070da15f20f93010ddd24d9bd9b4f27ab)) +* **connectivity:** fix connectivity slack UI ([b214466](https://github.com/frontegg/frontegg-react/commit/b2144661bad8a6d827f4e6fc652fae1b9eae7dde)) -- FR-19573 - Fixed redundant required asterisk on optional field -- FR-19566 - Fixed navbar route title appearing without items -- FR-19351 - Fixed SMS input calling code replace -## [7.4.7](https://github.com/frontegg/frontegg-react/compare/v7.4.6...v7.4.7) (2025-1-26) -- FR-19351 - Fixed MFA with SMS error and inital state bugs -- FR-19269 - Changed initial requests to run in parallel +# [2.3.0](https://github.com/frontegg/frontegg-react/compare/v2.2.2...v2.3.0) (2021-05-07) +### Bug Fixes -## [7.4.6](https://github.com/frontegg/frontegg-react/compare/v7.4.5...v7.4.6) (2025-1-16) +* **audits:** fix position and behaviors of the User Agent logo ([3e067ef](https://github.com/frontegg/frontegg-react/commit/3e067efe8253c460174e6ea103774a5fc64016e9)) +* **auth:** remove duplicated useField destructure ([4efd3f1](https://github.com/frontegg/frontegg-react/commit/4efd3f1e477c29c7198d46fd3400ad5c7ec5f21f)) +* **connectivity:** fix save data in the slack configurattionn ([23ff145](https://github.com/frontegg/frontegg-react/commit/23ff1452cafc91debd4ee99ee473798e37e5d739)) -- FR-19387 - Fixed login with apple in iOS devices -- FR-18450 - Changed permissions tree components +### Features +* add random user for auditLogsDataDemo ([b7aaa9d](https://github.com/frontegg/frontegg-react/commit/b7aaa9d3c1367aa547ca62b7e878127ebd9cbfbd)) -## [7.4.5](https://github.com/frontegg/frontegg-react/compare/v7.4.4...v7.4.5) (2025-1-14) -- FR-19236 - Fixed error handling on OIDC failure -- FR-19301 - Fixed settings list item title -- FR-19121 - Fixed security center recommendations after policy changed -## [7.4.4](https://github.com/frontegg/frontegg-react/compare/v7.4.3...v7.4.4) (2025-1-6) +## [2.2.2](https://github.com/frontegg/frontegg-react/compare/v2.2.1...v2.2.2) (2021-04-29) -- FR-19272 - Fixed sso input +### Bug Fixes -- FR-19068 - Fixed MFA error appears after reset +* **auth:** FR-2591 call account strategy after logout ([#380](https://github.com/frontegg/frontegg-react/issues/380)) ([09fe728](https://github.com/frontegg/frontegg-react/commit/09fe728203009d23f53dc4a51deb36b392c86de8)) -## [7.4.3](https://github.com/frontegg/frontegg-react/compare/v7.4.2...v7.4.3) (2024-12-31) -- FR-19220 - Fixed roles selection for MSP users -- FR-19205 - Added filter country code by country calling code -- FR-18963 - Added SMS enhancements +## [2.2.1](https://github.com/frontegg/frontegg-react/compare/v2.2.0...v2.2.1) (2021-04-28) -## [7.4.2](https://github.com/frontegg/frontegg-react/compare/v7.4.1...v7.4.2) (2024-12-29) +**Note:** Version bump only for package @fronteg/react -- FR-19191 - Fixed infinite loop on get ip metadata -## [7.4.1](https://github.com/frontegg/frontegg-react/compare/v7.4.0...v7.4.1) (2024-12-24) -- FR-19079 - Added Suggest Save password in mobile sdk +# [2.2.0](https://github.com/frontegg/frontegg-react/compare/v2.1.0...v2.2.0) (2021-04-28) +### Bug Fixes -## [7.4.0](https://github.com/frontegg/frontegg-react/compare/v7.3.1...v7.4.0) (2024-12-23) +* **connectivity:** FR-2311 validate webhook secret key length and format error message ([#358](https://github.com/frontegg/frontegg-react/issues/358)) ([589fccb](https://github.com/frontegg/frontegg-react/commit/589fccbf8c34704cbf16f246cda96cdcd5b85f92)) +* Add react-redux to react-hooks dist file ([a1244e7](https://github.com/frontegg/frontegg-react/commit/a1244e7fe49fbad6c53e02331493a7a3aaef7d64)), closes [#FR-2682](https://github.com/frontegg/frontegg-react/issues/FR-2682) +* disable refresh token after activate user ([84db237](https://github.com/frontegg/frontegg-react/commit/84db2376901b34f53d7079108d989b7d1c5d57b7)), closes [#FR-2761](https://github.com/frontegg/frontegg-react/issues/FR-2761) +* fix rollup config for react-hooks ([7338e52](https://github.com/frontegg/frontegg-react/commit/7338e5229636754342e8cf4efa817095fd681b48)), closes [#FR-2682](https://github.com/frontegg/frontegg-react/issues/FR-2682) +* fix rollup config for react-hooks ([a13f343](https://github.com/frontegg/frontegg-react/commit/a13f3433cf44057a6479c3b4267d9116271a2a1a)), closes [#FR-2682](https://github.com/frontegg/frontegg-react/issues/FR-2682) +* remove encoding variable in audits ip filtering ([8f77b39](https://github.com/frontegg/frontegg-react/commit/8f77b39d6f059efc2031f58d0464af3b407c2013)) -- FR-19047 - Fixed permissions evalutaion when have more than one permission -- FR-18988 - Fixed missing email on MFA verification description -- FR-19068 - Fixed MFA error appears after clicking on verify another way -- FR-18917 - Fixed user roles deletion +### Features +* **auth:** add action to get public vendor config ([#367](https://github.com/frontegg/frontegg-react/issues/367)) ([48eb6ec](https://github.com/frontegg/frontegg-react/commit/48eb6ecf523def554fd9622609fffc5e2bef9692)) +* **auth:** get activate account config in order to determine if user should set password ([#370](https://github.com/frontegg/frontegg-react/issues/370)) ([b04d42a](https://github.com/frontegg/frontegg-react/commit/b04d42a8d84778bfdadcc3a7a872f9b24eb18028)) +* **connectivity:** FR-2586 format dates on webhook page ([#357](https://github.com/frontegg/frontegg-react/issues/357)) ([80a6832](https://github.com/frontegg/frontegg-react/commit/80a683273d49517583967ed04fc589da74e8d020)) +* add frontegg react library to support routing and sharing store ([2fed55f](https://github.com/frontegg/frontegg-react/commit/2fed55f61832c785d4ec99d7193226b9cf4f3a16)), closes [#FR-2761](https://github.com/frontegg/frontegg-react/issues/FR-2761) -## [7.3.1](https://github.com/frontegg/frontegg-react/compare/v7.3.0...v7.3.1) (2024-12-19) -- FR-18683 - Changed phone number selector input -## [7.3.0](https://github.com/frontegg/frontegg-react/compare/v7.2.2...v7.3.0) (2024-12-15) +# [2.1.0](https://github.com/frontegg/frontegg-react/compare/v2.0.0...v2.1.0) (2021-04-13) -- FR-18941 - Fixed permissions evaluation without permissions in JWT -- FR-18966 - Added override error message infrastructure +### Bug Fixes -- FR-18896 - Added RTL support +* remove live sagas from mock generator function ([ae3a366](https://github.com/frontegg/frontegg-react/commit/ae3a366633d1ab7f502437bec9413942e902104b)) +### Features -## [7.2.2](https://github.com/frontegg/frontegg-react/compare/v7.2.1...v7.2.2) (2024-12-5) +* add option to consume switch tenant callback ([74fd8c6](https://github.com/frontegg/frontegg-react/commit/74fd8c65a3e4dd624d0144d7a330b98ad8a09d43)) -- FR-18954 - Fixed MFA with email active state, step and missing erros -- FR-18973 - Added login to show error page only for 5xx errors -## [7.2.1](https://github.com/frontegg/frontegg-react/compare/v7.2.0...v7.2.1) (2024-12-3) -- FR-18925 - Fixed wrong ip presented in Audit logs table IP column +# [2.0.0](https://github.com/frontegg/frontegg-react/compare/v1.28.0...v2.0.0) (2021-04-11) -- FR-18682 - Changed country flags image +### Bug Fixes +* Wait for refresh token after switch tenant ([defadc1](https://github.com/frontegg/frontegg-react/commit/defadc1354b345bf7a07526b80e2d10eb16f0aaf)) +* **core:** fix server side rendering issue with loading animation ([d9bb7a3](https://github.com/frontegg/frontegg-react/commit/d9bb7a349052eb6fd2a6ab1d76a6e6a4bc225cb3)) +* FR-2312 - add success variant support for all elements libraries ([58b85b7](https://github.com/frontegg/frontegg-react/commit/58b85b7fe2f07a954a95ba87a17d44567efd946f)) +* **audits:** FR-2162 - remove expandable in case of nothing to show(audits) ([25df9ed](https://github.com/frontegg/frontegg-react/commit/25df9edb782ba00a49fcd87ebbd2cae05824f10f)) +* **auth:** duplicate profile picture timestamps ([1e54a4f](https://github.com/frontegg/frontegg-react/commit/1e54a4f820cf46526b29a6d0fba52967392b59ed)) +* **connectivity:** FR-2310 - fix test hook form filling ([d54e053](https://github.com/frontegg/frontegg-react/commit/d54e053d3b40059558f91012c3cc82a709da96ff)) +* FR-2312 - fixstyle status button (webhooks); add success for theme' ([1c4ca9d](https://github.com/frontegg/frontegg-react/commit/1c4ca9de4ced5a567740bb2d812c104a51435c3a)) +* **auth:** fix fromik import in FeRecaptcha to prevent build fails ([1f93494](https://github.com/frontegg/frontegg-react/commit/1f934948657dc97663a38760c8c445f50bd77e0f)) +* **auth:** FR-2206 - add error message for sign up form ([fe97e35](https://github.com/frontegg/frontegg-react/commit/fe97e3554e38399678445690f05e4ea2ca434e8d)) +* **auth:** FR-2218 - remove social logins from activate user form ([738ab4f](https://github.com/frontegg/frontegg-react/commit/738ab4f80862203d1be2cc1897d1efb2d634ee86)) +* **elements:** fix onclick event for material menu item ([71193e1](https://github.com/frontegg/frontegg-react/commit/71193e1bbda6c3300bd73fde612f1ba6f5b60ad8)) +* Fix build for rescript ([58c4b3c](https://github.com/frontegg/frontegg-react/commit/58c4b3c09c45bc42b14614e5012e054615d1de4c)) +* FR-2100 - fix expandable table styles ([9586201](https://github.com/frontegg/frontegg-react/commit/9586201e2f95c43648f5c72b3b353c6a3c9766e2)) -## [7.2.0](https://github.com/frontegg/frontegg-react/compare/v7.1.0...v7.2.0) (2024-11-28) -- FR-18838 - Added MFA with email code -- FR-17593 - Fixed SAML enable with non SSO user try to login not throwing an error +### Features +* **auth:** enforce users password config on activate/reset/change password ([#342](https://github.com/frontegg/frontegg-react/issues/342)) ([7aeaeb2](https://github.com/frontegg/frontegg-react/commit/7aeaeb2568608dc9f8d6f0f66caf109fa52a6a66)) +* **auth:** login with facebook account ([5129bc5](https://github.com/frontegg/frontegg-react/commit/5129bc59a09bcc71c4ddddc3a352502a029a7089)) +* **auth:** login with facebook account ([#339](https://github.com/frontegg/frontegg-react/issues/339)) ([f231d75](https://github.com/frontegg/frontegg-react/commit/f231d758a2c2202e037b0caed104d606b9fb3888)) +* **auth:** login with microsoft account ([8fd8590](https://github.com/frontegg/frontegg-react/commit/8fd8590866bf58c6697f2390930c7a05bb2db220)) +* Add Audit logs to frontegg/react-hooks and frontegg/redux-store ([2e46638](https://github.com/frontegg/frontegg-react/commit/2e466385db3242a0547912a8daf3eb6bbd088709)) +* Add auditslogs to @frontegg/react-hooks ([285765a](https://github.com/frontegg/frontegg-react/commit/285765aa3fdbe37d4dbbdb2ad138823afb7e8c64)) +* add redux-store for auth state ([ee807ef](https://github.com/frontegg/frontegg-react/commit/ee807efd45a4a2ef494ce2420a80dc0a458fe4ab)) +* Add Security Policy API and Store Hooks ([e9b7abf](https://github.com/frontegg/frontegg-react/commit/e9b7abfa38e5e958a63f69dd45bd6631f2811e53)) +* Expose onRedirectTo via hooks ([bd38109](https://github.com/frontegg/frontegg-react/commit/bd381097a87e2794d668e3951d9a221f9c9acd51)) +* Extract react hooks to separated sub package ([8ad0333](https://github.com/frontegg/frontegg-react/commit/8ad033332fde18e3f10f7f6f4f5d0d24fc88f0b0)) +* move audits logs state management to @frontegg/redux-store ([08839b6](https://github.com/frontegg/frontegg-react/commit/08839b685dcdc0aaf3b17c0c0baf9bc0ba687536)) +* Split State-Management and hooks from UI components ([20d24cd](https://github.com/frontegg/frontegg-react/commit/20d24cd19f536a7f519d670bd8735feb350e54e9)) +* **redux-store:** Export all actions and interfaces from auth state ([b666ccd](https://github.com/frontegg/frontegg-react/commit/b666ccd9dc508cfffcdf5b1d81f96aab53f167fb)) -## [7.1.0](https://github.com/frontegg/frontegg-react/compare/v7.0.17...v7.1.0) (2024-11-21) +### BREAKING CHANGES -- FR-18699 - Removed entitlements automatic 30 seconds refresh mechanism -- FR-18138 - Added logic to improve login box and admin portal stability and resiliency +* hooks and Entity Types should be imported from @frontegg/react-hooks and @frontegg/redux-store -## [7.0.17](https://github.com/frontegg/frontegg-react/compare/v7.0.16...v7.0.17) (2024-11-19) -- FR-18341 - Fixed social login button size -- FR-12722 - Fixed Incorrect order of social logins display +# [1.28.0](https://github.com/frontegg/frontegg-react/compare/v1.27.0...v1.28.0) (2021-03-22) -## [7.0.16](https://github.com/frontegg/frontegg-react/compare/v7.0.15...v7.0.16) (2024-11-14) -- FR-18646 - Fixed missing permissions with wildcard on custom roles -- FR-18341 - Fixed Google Chrome Translate feature causes a crash -- FR-16902 - Fixed login box scroll on mobile browsers +### Bug Fixes +* FR-2220 - add loader for MenuItem ([4a3e62e](https://github.com/frontegg/frontegg-react/commit/4a3e62e68f7041e0d376ffc411c57198557c20f1)) +* **auth:** FR-2257 - fix t param duplication ([be144f5](https://github.com/frontegg/frontegg-react/commit/be144f5ec204ece9d5e62e0a762400a3b8f284d1)) +* **core:** FR-2126 - removed list dots for error message ([6a233b0](https://github.com/frontegg/frontegg-react/commit/6a233b0dc1f7650f27c1b14548b59539bb7f9966)) -## [7.0.15](https://github.com/frontegg/frontegg-react/compare/v7.0.14...v7.0.15) (2024-11-12) +### Features -- FR-18594 - Fixed blinking bug On IP and domain page -- FR-18499 - Fixed otc page blink -- FR-18005 - Fixed search api tokens with null descriptions -- FR-18499 - Fixed activate with code and password -- FR-18582 - Fixed loader size and wrong massage +* **auth:** request new activation email ([748255f](https://github.com/frontegg/frontegg-react/commit/748255fc924ef5e36764ba264d9a3767a9ea0c59)) -- FR-18561 - Fixed get ip metadata when app name is provided -- FR-17091 - Fixed long name in groups and roles -## [7.0.14](https://github.com/frontegg/frontegg-react/compare/v7.0.13...v7.0.14) (2024-11-10) -- FR-18561 - Fixed get ip metadata when app name is provided -- FR-17091 - Fixed long name in groups and roles +# [1.27.0](https://github.com/frontegg/frontegg-react/compare/v1.26.0...v1.27.0) (2021-03-18) -### React Wrapper 7.0.14: -- FR-18540: support minor versions +### Features -## [7.0.13](https://github.com/frontegg/frontegg-react/compare/v7.0.12...v7.0.13) (2024-11-5) +* **auth:** added option for terms of service in signup page ([f876091](https://github.com/frontegg/frontegg-react/commit/f876091cfde000c7ae003b878bea13ab8271f171)) -- FR-18529 - Fixed empty roles field bug when appName is provided -- FR-18353 - Fixed tooltips mount component -## [7.0.12](https://github.com/frontegg/frontegg-react/compare/v7.0.11...v7.0.12) (2024-10-30) +# [1.26.0](https://github.com/frontegg/frontegg-react/compare/v1.25.0...v1.26.0) (2021-03-17) -- FR-18476 - Added url for beforeRequestInterceptor function +### Bug Fixes -- FR-18476 - Added request interceptor -- FR-18472 - Fixed Google one tap login stuck after unmounting login/signup unmounted +* **auth:** unload captcha after login/sign up ([cb4963c](https://github.com/frontegg/frontegg-react/commit/cb4963c5812586d8a397c7e978911b3e3e3f79e6)) -- FR-18436 - Fixed activate account with empty redirect bug +### Features +* **auth:** allow getting login/signup redirect url via query param ([ce909fd](https://github.com/frontegg/frontegg-react/commit/ce909fd1a5f430ebdeeeb9182837f837c97f720c)) -## [7.0.11](https://github.com/frontegg/frontegg-react/compare/v7.0.10...v7.0.11) (2024-10-29) -- FR-17943 - Added code pages +# [1.25.0](https://github.com/frontegg/frontegg-react/compare/v1.24.0...v1.25.0) (2021-03-07) -## [7.0.10](https://github.com/frontegg/frontegg-react/compare/v7.0.9...v7.0.10) (2024-10-28) -- FR-18427 - Added Support for triggering MFA after native passkeys / iOS apple login -- FR-18211 - Fixed email overlapping roles field +### Bug Fixes +* Set fixed version for i18next in @frontegg/react-core ([20f9879](https://github.com/frontegg/frontegg-react/commit/20f98795e88b08e5e98e71d2d062836f47ce1061)) +* **auth:** FR-1932 - make requested changes; fix ReCaptcha token' ([4903613](https://github.com/frontegg/frontegg-react/commit/490361368e8a7bf1fa8c049eda8f2881bb15a71d)) +* **auth:** FR-1932 - removed unused import ([1e00b41](https://github.com/frontegg/frontegg-react/commit/1e00b41bd1395ba8823519eea395849625ac4e83)) -## [7.0.9](https://github.com/frontegg/frontegg-react/compare/v7.0.8...v7.0.9) (2024-10-22) +### Features -- FR-18356 - Fixed validations localization override type +* FR-1932 - added captcha for login/sign up; removed unused components demosaas ([e5e75c8](https://github.com/frontegg/frontegg-react/commit/e5e75c82524bfffe158924e75128fa84d5224b14)) -# Change Log - -## [7.0.8](https://github.com/frontegg/frontegg-react/compare/v7.0.7...v7.0.8) (2024-10-10) - -- FR-18217 - Fixed impersonation on embedded -- FR-18167 - Fixed back to login on magic link -- FR-8030 - Fixed website dialog cancel button typeography - - - -## [7.0.7](https://github.com/frontegg/frontegg-react/compare/v7.0.6...v7.0.7) (2024-9-5) - -- FR-17760 - Fix empty lastSeen column on expanded user info -- FR-17762 - Fix tree component using mui/x-tree-view -- FR-17517 - Fix login direct action race condition - - - -## [7.0.6](https://github.com/frontegg/frontegg-react/compare/v7.0.5...v7.0.6) (2024-9-3) - - -- FR-17649 - Fix autofill sms code iOS SDK -- FR-17650 - Fix auto capitalize email input in login page - - - - -## [7.0.5](https://github.com/frontegg/frontegg-react/compare/v7.0.4...v7.0.5) (2024-9-1) - -- FR-17443 - Fix infinite loader when using direct login action - - -## [7.0.4](https://github.com/frontegg/frontegg-react/compare/v7.0.3...v7.0.4) (2024-8-28) - -- FR-17626 - Fix clientId on request authorize for multi-apps -- FR-17182 - Fix copy invite user link in hosted admin portal - - -# Change Log - -## [7.0.3](https://github.com/frontegg/frontegg-react/compare/v7.0.2...v7.0.3) (2024-8-26) - - -## [7.0.2](https://github.com/frontegg/frontegg-react/compare/v7.0.1...v7.0.2) (2024-8-21) - -- FR-17531 - Prevent loading embedded views while hosted login is true - - - -## [7.0.1](https://github.com/frontegg/frontegg-react/compare/v7.0.0...v7.0.1) (2024-8-18) - -- FR-17169 - Added support in send unlock account email - - -# Change Log - -## [7.0.0](https://github.com/frontegg/frontegg-react/compare/v6.0.48...v7.0.0) (2024-8-5) - -### 🚀 Major Changes -- **Store Management:** - - Replaced the existing Redux store with Valtio. This change significantly reduces the bundle size, removes the boilerplate code associated with generator functions, and enables actions to be `async/await`. The new approach provides better stack traces and allows actions to be awaited directly, eliminating the need for callback functions. - -### 🔧 Upgrades -- **TypeScript:** - - Upgraded TypeScript from version `v3.9.7` to `v5.5.4`. This update ensures compatibility with the latest TypeScript features and improves overall type-checking and developer experience. - -- **Testing Framework:** - - Upgraded Jest to version `v29`. This upgrade includes performance improvements, new features, and bug fixes, ensuring more reliable and faster tests. - -- **UI Dependencies:** - - Upgraded Material UI, Emotion, and Stylis to their latest versions, enhancing UI component performance, styling flexibility, and overall user experience. - -- **Node.js:** - - Upgraded Node.js from version `16` to `18.18`, ensuring compatibility with the latest features and security updates. - -### 🛠 Enhancements -- **Refactor:** - - Refactored code to use nested imports in Material UI, which helps reduce bundle size. - -- **MFA (Multi-Factor Authentication):** - - Changed the MFA code input to auto-submit upon completion for a smoother user experience. - -- **Dependencies:** - - Incremented various dependencies, Node.js, and TypeScript to align with the latest standards and ensure better performance and security. - -- **New Feature:** - - Added a "Resend Invitation Email" action to the users table on the "All Accounts" page, improving user management capabilities. - -### ⚠️ Breaking Changes -- **Permissions & Privacy Page:** - - Removed permissions from the privacy page. The user privacy page will now be displayed for all users in the admin portal. - - -# Change Log - -## [6.0.48](https://github.com/frontegg/frontegg-react/compare/v6.0.47...v6.0.48) (2024-7-22) - -- FR-16990 - Fixed social logins container styling -- FR-17117 - Added URL validation to direct login action - - - -# Change Log - -## [6.0.47](https://github.com/frontegg/frontegg-react/compare/v6.0.46...v6.0.47) (2024-7-18) - -- FR-17009 - Add the option to disable/enable users from users table - -## [6.0.46](https://github.com/frontegg/frontegg-react/compare/v6.0.45...v6.0.46) (2024-7-15) - -* FR-17055 - Fix SSO redirect race condition - - - -## [6.0.45](https://github.com/frontegg/frontegg-react/compare/v6.0.44...v6.0.45) (2024-7-10) - -- FR-16987 - Add additional params to vanilla js loginWithRedircet -- FR-16960 - Fix typography color in SSO guides dark mode - - - -## [6.0.44](https://github.com/frontegg/frontegg-react/compare/v6.0.43...v6.0.44) (2024-7-9) - -- FR-16737 - Allow terms and conditions checkbox on sign up form being optional - - -# Change Log - -## [6.0.43](https://github.com/frontegg/frontegg-react/compare/v6.0.42...v6.0.43) (2024-7-4) - -- FR-16881 - Fixed hosted login redirect url when using basename -- FR-16812 - Fixed dark theme disabled input palette - - - -## [6.0.42](https://github.com/frontegg/frontegg-react/compare/v6.0.41...v6.0.42) (2024-7-2) - -- FR-16704 - Fixed password meter unsuspended async component - - - -## [6.0.41](https://github.com/frontegg/frontegg-react/compare/v6.0.40...v6.0.41) (2024-6-30) - -- FR-15484 - Fixed update tenant state after account settings change -- FR-16613 - Added automatic sub-account access in MSP - - - -## [6.0.40](https://github.com/frontegg/frontegg-react/compare/v6.0.39...v6.0.40) (2024-6-3) - -- FR-16513 - Show client id on all tokens in API tokens table - - - -## [6.0.39](https://github.com/frontegg/frontegg-react/compare/v6.0.38...v6.0.39) (2024-5-28) - -- FR-16341 - Support hiding columns in admin portal users table - - - -## [6.0.38](https://github.com/frontegg/frontegg-react/compare/v6.0.37...v6.0.38) (2024-5-27) - -- FR-16463 - Removed domain restrictions validation when invite user from All Accounts page -- FR-16421 - Fixed error handling in edit phone number dialog - - -## [6.0.37](https://github.com/frontegg/frontegg-react/compare/v6.0.36...v6.0.37) (2024-5-2) - -- FR-15124 - Store redirectUri before social-login as oauth2 state and restore it after successfully redirect -- FR-16117 - avoid showing apps without name in users table - -- FR-16165 - Added switch tenant functionality for VanillaJS SDK - -# Change Log - -## [6.0.36](https://github.com/frontegg/frontegg-react/compare/v6.0.35...v6.0.36) (2024-4-21) - -- FR-15737 - Added an option for accounts to open sub-account management for their sub-accounts -- FR-15603 - Support embedded SCIM guides - -# Change Log - -## [6.0.35](https://github.com/frontegg/frontegg-react/compare/v6.0.34...v6.0.35) (2024-4-18) - -- FR-16088 - Support hosted admin portal -- FR-FR-15124 - Support multi-apps - -## [6.0.34](https://github.com/frontegg/frontegg-react/compare/v6.0.33...v6.0.34) (2024-4-2) - -- FR-15863 - Improved chip list design in admin portal - -- FR-15367 - Fixed user logo upload for SCIM users -- FR-15111 - Fixed impersonation embedded redirection - - -## [6.0.33](https://github.com/frontegg/frontegg-react/compare/v6.0.32...v6.0.33) (2024-3-20) - -- FR-15233 - Fixed autocomplete popper - - - -## [6.0.32](https://github.com/frontegg/frontegg-react/compare/v6.0.31...v6.0.32) (2024-3-11) - -- FR-13828 - Add option to specify prompt consent from loginDirectAction -- FR-15315 - Added sort for role selections in Admin Portal - -- FR-15270 - Added new roles page to the Admin Portal -- FR-15395 - Fixed tab tenant not cleared after logout - - -# Change Log - -## [6.0.31](https://github.com/frontegg/frontegg-react/compare/v6.0.30...v6.0.31) (2024-3-3) - -- FR-15270 - Added new roles page to the Admin Portal -- FR-15395 - Fixed tab tenant not cleared after logout - -## [6.0.30](https://github.com/frontegg/frontegg-react/compare/v6.0.29...v6.0.30) (2024-2-28) - -- FR-15376 - fixed null group description exception - - -# Change Log - -## [6.0.29](https://github.com/frontegg/frontegg-react/compare/v6.0.28...v6.0.29) (2024-2-28) - -- FR-15305 - Fixed changed fields sent on edit group to support SCIM group update -- FR-15219 - Fixed missing row actions in users table when using MSP - - -# Change Log - -## [6.0.28](https://github.com/frontegg/frontegg-react/compare/v6.0.27...v6.0.28) (2024-2-7) - -- **FR-14855**: Added an option to enable `promptConsent` within `authOptions`. -- **FR-14855**: Fixed the Microsoft social login prompt value for custom configurations. - - -## [6.0.27](https://github.com/frontegg/frontegg-react/compare/v6.0.26...v6.0.27) (2024-2-4) - -- FR-15100 - Fix Apple custom SSO scopes - - - -## [6.0.26](https://github.com/frontegg/frontegg-react/compare/v6.0.25...v6.0.26) (2024-2-1) - -- FR-15087 - Added oidc support for linkedin -- FR-14997 - Fixed invite user dialog UI issues - - -# Change Log - -## [6.0.25](https://github.com/frontegg/frontegg-react/compare/v6.0.24...v6.0.25) (2024-1-28) - -- FR-14910 - Fixed handling entitlements errors -- FR-14740 - Send tenant alias for hosted custom login auth user - - -## [6.0.24](https://github.com/frontegg/frontegg-react/compare/v6.0.23...v6.0.24) (2024-1-17) - -- FR-14197 - Added Canada to MFA sms list of countries -- FR-14859 - fix roles filtering for MSP - - -### React Wrapper 6.0.24: -- FR-13416 - add push trigger for e2e workflow - -## [6.0.23](https://github.com/frontegg/frontegg-react/compare/v6.0.22...v6.0.23) (2024-1-14) - -- FR-14855 - Add support for social login consent, by default it is false. - - -### React Wrapper 6.0.23: -- FR-13416 - add push trigger for e2e workflow - -## [6.0.22](https://github.com/frontegg/frontegg-react/compare/v6.0.21...v6.0.22) (2024-1-10) - -- FR-14813 - Add support for open app page with basename - - - -## [6.0.21](https://github.com/frontegg/frontegg-react/compare/v6.0.20...v6.0.21) (2024-1-10) - -- FR-14813 - Fix texts in open app - -- FR-14813 - Support mobile deep link redirect page - - - -## [6.0.20](https://github.com/frontegg/frontegg-react/compare/v6.0.19...v6.0.20) (2024-1-7) - -- FR-14807 - Fixed step up double call to generate step up session - - -# Change Log - -## [6.0.19](https://github.com/frontegg/frontegg-react/compare/v6.0.18...v6.0.19) (2024-1-3) - -- FR-14753 - Fixed step up key removing when user does not finish step up flow - - - -## [6.0.18](https://github.com/frontegg/frontegg-react/compare/v6.0.17...v6.0.18) (2023-12-31) - -- FR-14228 - Added Step Up feature to allow a second layer of authentication for sensitive actions - - -- FR-14578 - Fixed custom login without tenant alias -- FR-14638 - Missing exp on user interface -- FR-14560 - added temporary users feature - - - -## [6.0.17](https://github.com/frontegg/frontegg-react/compare/v6.0.16...v6.0.17) (2023-12-21) - -- FR-14644 - Fixed enroll authenticator app missing error message when code is wrong - - - -## [6.0.16](https://github.com/frontegg/frontegg-react/compare/v6.0.15...v6.0.16) (2023-12-20) - -- FR-14219 - Step up - Embedded flow - -- FR-14324 - Fix direct login custom social login - - - -## [6.0.15](https://github.com/frontegg/frontegg-react/compare/v6.0.14...v6.0.15) (2023-12-17) - -- FR-10692 - Remove the ability to select a full category on webhooks page - - - -## [6.0.14](https://github.com/frontegg/frontegg-react/compare/v6.0.13...v6.0.14) (2023-12-12) - -- FR-14324 - Fix android native module and add option for direct social login - - -## [6.0.13](https://github.com/frontegg/frontegg-react/compare/v6.0.12...v6.0.13) (2023-12-10) - -- FR-14287 - Fix grammar of 1 day expiry of api tokens - - -# Change Log - -## [6.0.12](https://github.com/frontegg/frontegg-react/compare/v6.0.11...v6.0.12) (2023-11-30) - -- FR-14308 - Fixed alignment issue with the icon in the custom social login button - -# Change Log - -## [6.0.11](https://github.com/frontegg/frontegg-react/compare/v6.0.10...v6.0.11) (2023-11-29) - -- FR-13527 - Added a11y support for admin portal pages: SSO, security, profile, personal tokens, users, groups, provisioning, audit logs, API tokens, webhooks, and account details. - -## [6.0.10](https://github.com/frontegg/frontegg-react/compare/v6.0.9...v6.0.10) (2023-11-23) - -- FR-14237 - Fix direct action with basename -- FR-14324 - Fixed Hosted login race condition in Angular and supported prompt login with silent refresh - -- FR-14201 - Fixed login with SMS resend code action - -- FR-13913 - Let tenants/users set expiry on client credentials API tokens -- FR-14099 - Fix load custom login routes only when necessary -- FR-13605 - Support adding phone number field to signup page and control his required state - - -### React Wrapper 6.0.10: -- Tigger E2E workflows on pre-release pipeline -- FR-13990 - allow creating alpha version manually - -## [6.0.9](https://github.com/frontegg/frontegg-react/compare/v6.0.8...v6.0.9) (2023-11-21) - -- FR-14201 - Fixed login with SMS resend code action - -- FR-13913 - Let tenants/users set expiry on client credentials API tokens -- FR-14099 - Fix load custom login routes only when necessary -- FR-13605 - Support adding phone number field to signup page and control his required state - -- FR-14102 - Fixed entitlements Frontegg user-id attribute usage -- FR-13123 - Added support to provide scopes for social logins - - -### React Wrapper 6.0.9: -- Tigger E2E workflows on pre-release pipeline -- FR-13417 - Fixed include query param after signup -# Change Log - -## [6.0.8](https://github.com/frontegg/frontegg-react/compare/v6.0.7...v6.0.8) (2023-11-6) - -- FR-14102 - Fixed entitlements Frontegg user-id attribute usage -- FR-13123 - Added support to provide scopes for social logins - -# Change Log - -## [6.0.7](https://github.com/frontegg/frontegg-react/compare/v6.0.6...v6.0.7) (2023-11-1) - -- FR-13327 - Update modern theme grey palette -- FR-13834 - Added Rule based entitlements -- FR-13508 - Mobile native module bridge -- FR-13772 - Fixed issue on edit user roles modal - at least 1 role is required -- FR-13808 - Support Login with SMS -- FR-13786 - Support tab per tenant - -# Change Log - -## [6.0.6](https://github.com/frontegg/frontegg-react/compare/v6.0.5...v6.0.6) (2023-10-24) - -- FR-13772 - Fixed issue on edit user roles modal - at least 1 role is required -- FR-13808 - Support Login with SMS -- FR-13786 - Support tab per tenant - - -### React Wrapper 6.0.6: -- Remove duplicated step in trigger e2e test -- trigger e2e pipeline test -# Change Log - -## [6.0.5](https://github.com/frontegg/frontegg-react/compare/v6.0.4...v6.0.5) (2023-10-11) - -- FR-13798 - Added support for login with SMS -- FR-13364 - Improved validations text for sign-in form -- -# Change Log - -## [6.0.4](https://github.com/frontegg/frontegg-react/compare/v6.0.3...v6.0.4) (2023-10-4) - -- FR-13364 - Improved validations text for sign in form -- FR-13737 - Changed breached password page to be shown just for block breached password policy -- FR-13665 - Enhance hasPermission with inEntitled in admin portal in case entitlements is enabled - - -## [6.0.3](https://github.com/frontegg/frontegg-react/compare/v6.0.2...v6.0.3) (2023-10-2) - -- FR-13455 - Fixed impersonation with custom login on embedded initial blank screen -- FR-13649 - Fixed testimonial quote layout for split signup mode - - -# Change Log - -## [6.0.2](https://github.com/frontegg/frontegg-react/compare/v6.0.1...v6.0.2) (2023-9-28) - -### React Wrapper 6.0.2: - -- Revamped the security page in the Admin Portal -#### Note: no migration is needed to upgrade between versions 5 to 6. - -# Change Log - -## [6.0.1](https://github.com/frontegg/frontegg-react/compare/v5.0.50...v6.0.1) (2023-9-27) - -- FR-13509 - Added GTM integration - -# Change Log - -## [5.0.50](https://github.com/frontegg/frontegg-react/compare/v5.0.49...v5.0.50) (2023-9-5) - -- Releasing the new Security Center Page, which will replace the current Security Page. Currently exposed on Early Access with limited availability by a feature flag. - - -# Change Log - -## [5.0.49](https://github.com/frontegg/frontegg-react/compare/v5.0.48...v5.0.49) (2023-8-28) - -- FR-13142 - Support setRootAccountData action for all account feature -- FR-12321 - Added max validations to session management fields - - -- FR-12974 - Fixed the issue with permissions and roles granted from user groups on User context -- FR-12322 - Change redirect to SSO text -- FR-12979 - Fixed MFA options save button to be disabled if the user has no security write permission - -# Change Log - -## [5.0.48](https://github.com/frontegg/frontegg-react/compare/v5.0.47...v5.0.48) (2023-8-14) - -# v5.0.48 -- FR-11857 - Added new support for hosted login to load user on load -- FR-12828 - entitlements API response change -- FR-12224 - support custom login for authenticated users without a tenant alias -- FR-12780 - Entitlements Vanilla JS improvements - -### React Wrapper 5.0.48: -- FR-12986 - Added support of reporting React version header - -# Change Log - -## [5.0.47](https://github.com/frontegg/frontegg-react/compare/v5.0.46...v5.0.47) (2023-7-24) - -- FR-12828 - Entitlements api response change -- FR-12224 - Support custom login for authenticated users without a tenant alias -- FR-12688 - Make Admin box compatible with the updated type of IUserProfile - - -# Change Log - -## [5.0.46](https://github.com/frontegg/frontegg-react/compare/v5.0.45...v5.0.46) (2023-7-13) - -• FR-12550 - Align all auth methods to get the right login response type -• FR-12664 - Rename redux-saga file to prevent loop imports by webpack -• FR-12098 - Updated Admin portal user status to the correct one if email verification is off -• FR-12020 - Fixed blinking workspace title in admin portal vivid theme -• FR-12114 - Fixed custom social login provider shouldn't be shown if not active -• FR-12628 - Fixed custom login with hosted Oauth in URL -• FR-12575 - Changed remember my device value to be true by default - -# Change Log - -## [5.0.45](https://github.com/frontegg/frontegg-react/compare/v5.0.44...v5.0.45) (2023-7-9) - -- FR-12581 - Added support for custom inline html and script -- FR-12343 - Added support for SSO per tenant -- FR-12488 - Backward compatible support for loadUsersV1 -- FR-12164 - Added support for MSP bulk user invitation -- FR-12479 - Fixed MSP warning dialog issue -- FR-12408 - Redesigned Entitlements structure - -# Change Log - -## [5.0.44](https://github.com/frontegg/frontegg-react/compare/v5.0.43...v5.0.44) (2023-6-30) - -- MSP update visibility, bugfix -- add security login flows - -# Change Log - -## [5.0.43](https://github.com/frontegg/frontegg-react/compare/v5.0.42...v5.0.43) (2023-6-28) - -- FR-12277 - Extended tenants state with the active tenant to support MSP sub-accounts -- FR-12405 - MSP bug fixes -- FR-12381 - Migrated users table to load users by users V2 API - -# Change Log - -## [5.0.42](https://github.com/frontegg/frontegg-react/compare/v5.0.41...v5.0.42) (2023-6-22) - -- Update load tenants to new version -- MSP bugfix, improvements -- Add support to load cdn component with the new vite version -- Fix new sso guide dark theme - -# Change Log - -## [5.0.41](https://github.com/frontegg/frontegg-react/compare/v5.0.40...v5.0.41) (2023-6-19) - -- Add support to load cdn component with the new vite version -- Fix for new sso guide dark theme -- Fix for login per tenant embedded with sub domain logout route -- Create MSP all accounts main page - -### React Wrapper 5.0.41: -- add a support to frontegg hooks inside custom components - -# Change Log - -## [5.0.40](https://github.com/frontegg/frontegg-react/compare/v5.0.39...v5.0.40) (2023-6-6) - -- Change iframe login preview for login per tenant self service -- add required to fields in invite user modal -- Improve error handling for login -- Added MSP - all accounts main page and state - -### React Wrapper 5.0.40: -- Added support null for custom component - -# Change Log - -## [5.0.39](https://github.com/frontegg/frontegg-react/compare/v5.0.38...v5.0.39) (2023-5-28) - -- Fixed hosted login with hash -- Support login per tenant self service -- Added Cyprus phone area code 2 fa screen -- Added option to upload metadata file instead of metadata url - -# Change Log - -## [5.0.38](https://github.com/frontegg/frontegg-react/compare/v5.0.37...v5.0.38) (2023-5-22) - -# v5.0.38 - -- Fix the issue with unnecessary white borders on the dark mode theme -- Add metadataHeaders type to contextOptions -- Aadded source header to all admin portal and login box requests -- Added login per tenant per service -- SSO Guides enhancements -- [Snyk] Security upgrade babel-plugin-module-resolver from 4.1.0 to 5.0.0 -- Remove admin provisioning feature flag - -### React Wrapper 5.0.38: -- FR-11599 - add sdkVersion to rollup -- FR-11599 - report framework and version - -# Change Log - -## [5.0.37](https://github.com/frontegg/frontegg-react/compare/v5.0.36...v5.0.37) (2023-5-12) - -- FR-11442 - Removed admin portal provisioning feature flag -- FR-11723 - Fixed refresh token when computer clock is set to a future time -- FR-11735 - Added support for customizing login per tenant in the admin portal -- FR-11442 - Removed legacy SSO tab code -- FR-11718 - Fix users' table UI issues -- FR-11113 - Fixed Frontegg logo overlapping navigation -- FR-11442 - Extract the provisioning tab to a separated page in the admin portal -- FR-11617 - Fixed a11y enter key press issue -- FR-11352 - Added support for nested table -- [Snyk] Security upgrade @azure/storage-blob from 12.11.0 to 12.13.0 - -# Change Log - -## [5.0.36](https://github.com/frontegg/frontegg-react/compare/v5.0.35...v5.0.36) (2023-5-4) - -- FR-11581 - fix a11y login-box onEnter event for links -- FR-11353 - add new tree graph component - -## [5.0.35](https://github.com/frontegg/frontegg-react/compare/v5.0.34...v5.0.35) (2023-4-28) - -- FR-11564 - Social login button shouldn't inherit from secondary color - -# Change Log - -## [5.0.34](https://github.com/frontegg/frontegg-react/compare/v5.0.33...v5.0.34) (2023-4-27) - -- Fixed passkeys issue with reCaptcha -- Removed feature flag from passkeys button -- Enable loading Frontegg helper scripts by providing query params to Frontegg external source -- Security upgrade webpack from 5.74.0 to 5.76.0 - -# Change Log - -## [5.0.33](https://github.com/frontegg/frontegg-react/compare/v5.0.32...v5.0.33) (2023-4-27) - -- Fixed input hover issue on suffix icon -- A11y improvements - -# Change Log - -## [5.0.32](https://github.com/frontegg/frontegg-react/compare/v5.0.31...v5.0.32) (2023-4-25) - -- Fix Passkeys button style -- Support login per tenant with search param - -# Change Log - -## [5.0.31](https://github.com/frontegg/frontegg-react/compare/v5.0.30...v5.0.31) (2023-4-23) - -- Lock reduxjs/toolkit version to be compatible in Vite types plugin -- Fixed password input placeholder text in the login box -- Fixed social login buttons order -- Fix Vite js-sha256 warning -- Fixed company name error in split mode sign up -- Fixed phone number dropdown theming -- Added aria labels to buttons - -# Change Log - -## [5.0.30](https://github.com/frontegg/frontegg-react/compare/v5.0.29...v5.0.30) (2023-4-17) - -- Added support to preserve query params between all auth routes -- Added support for generating a code challenge in non-secure domains [HostedLogin Mode] -- Fixed issue with updating SSO group name in the admin portal -- Added live SSO integration guide - -# Change Log - -## [5.0.29](https://github.com/frontegg/frontegg-react/compare/v5.0.28...v5.0.29) (2023-4-13) - -- Added support to separate first and last name in sign up form by customization option for embedded mode - -# Change Log - -## [5.0.28](https://github.com/frontegg/frontegg-react/compare/v5.0.27...v5.0.28) (2023-4-3) -- Added support for SCIM groups -- Updated texts across login box - grammar and terminology -- Added impersonation indicator to show impersonator that they're in an impersonation session -- Added passkeys feature - -## [5.0.27](https://github.com/frontegg/frontegg-react/compare/v5.0.26...v5.0.27) (2023-3-27) - -- FR-11247 - fix version branch 6.82 -- FR-11065 - add passkeys mock ff -- FR-11189 - mfa authenticator app change input type -- FR-10821 - fix table color -- FR-11204 - add unit testing with jest -- FR-11139 - fix groups -- FR-11039 - fix groups dummy -- FR-11039 - ff groups -- FR-10530 - fix ff store name -- FR-11067 - error handling on profile image upload -- FR-11039 - extend users table with groups column - -- FR-10530 - fix ff -- FR-10654 - Fix OIDC loading screen -- FR-10530 - fix ff store name -- FR-10530 - fix ff store name -- FR-10530 - change ff behavior -- FR-10976 - Remove idle session export from default items -- FR-11120 - fix use permission -- FR-10976 - idle session missing script for local exmaple -- FR-11109 - fix groups design -- FR-10530 - fix passkeys loading mode in login flow -- FR-11065 - fix login flow with prompt for mfa -- FR-10530 - update dependencies between passkeys and mfa -- FR-10976 - Idle session timeout will be reset on a post message from the client iFrame -- FR-10150 - add option to enforce redirect to same site only to avoid security issues - -# Change Log - -## [5.0.26](https://github.com/frontegg/frontegg-react/compare/v5.0.25...v5.0.26) (2023-3-16) -- Fixed use permission regex issue to accept a wild card -- User groups design fixes -- Fixed passkeys loading mode and login flow with MFA -- Update dependencies between passkeys and MFA on the privacy page -- Added support to reset Idle session timeout by post messages from the client iFrame -- Added an option to enforce redirect URLs to the same site only to avoid security issues -- Added support for customized social login providers - -# Change Log - -## [5.0.25](https://github.com/frontegg/frontegg-react/compare/v5.0.24...v5.0.25) (2023-3-10) - -- Fixed resend OTC with reCaptcha -- Added support to let tenants create a manage user groups in the admin portal under a FF -- Added support to login with passkeys and manage passkeys in the admin portal under a FF -- Fixed invite users issue when the vendor is not forcing roles and permissions -- Support auth strategy and social logins for login per tenants -- Refactored feature flag mechanism to be based on rest-api package -- Fixed validation for postcode in admin portal forms -- Fixed SMS code input to have input type number -- Improved auth screens form UX - -# Change Log - -## [5.0.24](https://github.com/frontegg/frontegg-react/compare/v5.0.23...v5.0.24) (2023-2-21) - -- Fixed Admin portal SSO provider's options to be correlated with the vendor choice -- Fixed background for table pivot column -- Fixed impersonation by removing unnecessary redirects and add a refresh call -- Fixed style reorder bug when using @emotion/react and Frontegg Next.JS - -# Change Log - -## [5.0.23](https://github.com/frontegg/frontegg-react/compare/v5.0.22...v5.0.23) (2023-2-8) - -- Updated M2M tokens to reflect the vendor choice - -# Change Log - -## [5.0.22](https://github.com/frontegg/frontegg-react/compare/v5.0.21...v5.0.22) (2023-2-7) - -- Fixed go-to-sign-up message position in speedy login layout -- Added an input component to the library for adding members to a tenant -- Fix filtering SSO providers according to the vendor selection -- Added user groups card header component to the library -- Improved the admin portal and login box performance and bundle size - -### React Wrapper 5.0.22: -- Added TSlib to Frontegg react bundle to prevent TS version conflicts - -## [5.0.21](https://github.com/frontegg/frontegg-react/compare/v5.0.20...v5.0.21) (2023-2-1) - - -### React Wrapper 5.0.21: -- FR-10625 - Export HostedLogin class from @frontegg/js library -# Change Log - -## [5.0.20](https://github.com/frontegg/frontegg-react/compare/v5.0.19...v5.0.20) (2023-1-29) - -- Fixed error message position in login with SMS screen -- Fixed missing client ID after creating API token - -## [5.0.19](https://github.com/frontegg/frontegg-react/compare/v5.0.18...v5.0.19) (2023-1-24) - -- FR-10485 - Update `@frontegg/rest-api` version -- FR-10017 - Add `type="email"` to all email HTML inputs -- FR-10501 - Expand LoginBox width in mobile devices -- FR-10196 - Fix scroll in privacy page -- FR-10489 - UI enhancements in SCIM -- FR-10483 - Added the option to customize forget password button -- FR-10374 - UI enhancements in split mode -- FR-10184 - Add access tokens screen -- FR-9995 - UI enhancements for Invitation text and icon -- FR-10448 - add prettier pre-commit check -- FR-10443 - Fix impersonation -- FR-10282 - fix otc login for mobile -- FR-10410 - fix policies mock -- FR-10371 - sync vendor security policies -- FR-10302 - Add impersonation indication for audit logs -- FR-10281 - Impersonation -- FR-10261 - fix sign up position in dark theme -- FR-10369 - change mfa ff name - -# Change Log - -## [5.0.18](https://github.com/frontegg/frontegg-react/compare/v5.0.17...v5.0.18) (2023-1-16) - -- Fixed sign up position in dark theme -- Added margin to login error -- Added support for built-in authenticators, security keys, and SMS as MFA methods - - -## [5.0.17](https://github.com/frontegg/frontegg-react/compare/v5.0.16...v5.0.17) (2023-1-11) - -- Fixed login with apple redirect URL -- Added impersonation indication in login session table -- Added support for session expired logout on Hosted Login -- Added support for login with Linkedin -- Added support for Google one tap -- Improve insert OTC screen UI -- Improve UX of authentication forms -- Fix apple logo color and match to font color -- Added support for customization of Custom React hooks component - -## [5.0.16](https://github.com/frontegg/frontegg-react/compare/v5.0.15...v5.0.16) (2022-12-22) - -- Few bug fixes - - -## [5.0.15](https://github.com/frontegg/frontegg-react/compare/v5.0.14...v5.0.15) (2022-12-20) - -- Fixed mfa input on mobile -- Enabled scim without roles -- Fixed menu component for dark theme -- Added api navigation icon -- Added tests for mfa -- Added apple social login types -- Added support for Hiding Invoices - - -## [5.0.14](https://github.com/frontegg/frontegg-react/compare/v5.0.13...v5.0.14) (2022-12-13) - -- Fixed MFA flow issues -- Added support for subscriptions billing collection -- Fixed the issue of the OTC screen submit button being disabled on mobile devices -- Added SCIM section in admin portal under FF - - -## [5.0.13](https://github.com/frontegg/frontegg-react/compare/v5.0.12...v5.0.13) (2022-12-8) - -- Fixed ignoring `urlPrefix` issue -- Added the ability to Invite a user by bulk API in the admin portal -- Fixed OTC digits are not visible on mobile devices -- Added MFA devices management section in the admin portal under FF -- Fixed the ability to copy invite link for dynamic base URL as well -- Added new abilities to MFA flows under FF -- Added support for providing an external CDN to load fonts in Frontegg components - - -## [5.0.12](https://github.com/frontegg/frontegg-react/compare/v5.0.11...v5.0.12) (2022-11-28) - -- FR-9750 - change api according to the new names security tabs -- FR-9717 - update rest api to have optional name in add user payload - and make sure to not send name if not exist -- FR-9826 - fix table header in dark theme -- FR-9237 - Max length for secret fields increased to 100 -- FR-9742 - enroll mfa list -- FR-9772 - Send NULL on profilePictureUrl rather than null -- FR-9717 - Invite user customize form API -- FR-9597 - Webhooks - missing validation error on UI when added not allowed URL - - -## [5.0.11](https://github.com/frontegg/frontegg-react/compare/v5.0.10...v5.0.11) (2022-11-23) - -- Added support for admin portal pre-defined theme options (dark, vivid, modern, and classic themes) -- Added support for customizing admin portal navigation hover color -- Fixed typo of Andorra country in countries dropdown -- Fixed select popup alignment issue -- Changed no local authentication feature to also hide the sign-up form when there is no local authentication option (use only social logins and SSO for signing up) -- Added mock for feature flags API for admin portal preview mode -- Fixed resend invitation and activate your account API calls -- Fixed creating custom webhook on the Admin Portal is sent with the event ID and not with the event Key -- Added support for customizing fields and tabs in the admin portal - -### React Wrapper 5.0.11: -- Updated README.md with the current integration guide - -## [5.0.10](https://github.com/frontegg/frontegg-react/compare/v5.0.9...v5.0.10) (2022-11-10) - -- Add support for overriding customzation for admin portal pages and tabs - -### React Wrapper 5.0.9: -- FR-9186 - Add changelog - -## [5.0.8](https://github.com/frontegg/frontegg-react/compare/v5.0.7...v5.0.8) (2022-10-09) - -**Note:** Version bump only for package @fronteg/react - - - - - -## [2.8.14](https://github.com/frontegg/frontegg-react/compare/v2.8.13...v2.8.14) (2021-07-27) - -**Note:** Version bump only for package @fronteg/react - - - - - -## [2.8.13](https://github.com/frontegg/frontegg-react/compare/v2.8.12...v2.8.13) (2021-07-22) - -**Note:** Version bump only for package @fronteg/react - - - - - -## [2.8.12](https://github.com/frontegg/frontegg-react/compare/v2.8.11...v2.8.12) (2021-07-20) - -**Note:** Version bump only for package @fronteg/react - - - - - -## [2.8.11](https://github.com/frontegg/frontegg-react/compare/v2.8.10...v2.8.11) (2021-07-11) - -**Note:** Version bump only for package @fronteg/react - - - - - -## [2.8.10](https://github.com/frontegg/frontegg-react/compare/v2.8.9...v2.8.10) (2021-07-08) - - -### Bug Fixes - -* **audits:** fix store conflict between old audits and new auditlogs state ([5e493fe](https://github.com/frontegg/frontegg-react/commit/5e493fec79dd73198186a6b2a94e8833e4600102)) -* **core:** add destroy store on unmount ([c117bf9](https://github.com/frontegg/frontegg-react/commit/c117bf9c853f0ecfbb3e6c4098dfac8e91dfd9b7)) - - - - - -## [2.8.9](https://github.com/frontegg/frontegg-react/compare/v2.8.8...v2.8.9) (2021-07-08) - - -### Bug Fixes - -* **auth:** add missing callback to sso file config saga ([9c895ab](https://github.com/frontegg/frontegg-react/commit/9c895abf5bb364ef4fabbd8b2961d17722f821c7)) - - - - - -## [2.8.8](https://github.com/frontegg/frontegg-react/compare/v2.8.7...v2.8.8) (2021-07-06) - - -### Bug Fixes - -* **auth:** reload captcha after failed login ([f12be57](https://github.com/frontegg/frontegg-react/commit/f12be57a299de748adb991ad6cdc7d17c226903e)) - - - - - -## [2.8.7](https://github.com/frontegg/frontegg-react/compare/v2.8.6...v2.8.7) (2021-07-01) - -**Note:** Version bump only for package @fronteg/react - - - - - -## [2.8.6](https://github.com/frontegg/frontegg-react/compare/v2.8.5...v2.8.6) (2021-06-30) - - -### Bug Fixes - -* **redux-store:** fix add user saga ([#454](https://github.com/frontegg/frontegg-react/issues/454)) ([3c6aaea](https://github.com/frontegg/frontegg-react/commit/3c6aaea3f4b12805e2c5e1f6116c0f2c77cfba53)) - - - - - -## [2.8.5](https://github.com/frontegg/frontegg-react/compare/v2.8.4...v2.8.5) (2021-06-30) - - -### Bug Fixes - -* **redux-store:** fix addUser saga ([#452](https://github.com/frontegg/frontegg-react/issues/452)) ([f5b19a7](https://github.com/frontegg/frontegg-react/commit/f5b19a75a599aacfc2cd104393272860d4ff7b2e)) - - - - - -## [2.8.4](https://github.com/frontegg/frontegg-react/compare/v2.8.3...v2.8.4) (2021-06-29) - - -### Bug Fixes - -* **redux-store:** [FR-3203] - skip prelogin when saml is disabled ([#448](https://github.com/frontegg/frontegg-react/issues/448)) ([e6febcd](https://github.com/frontegg/frontegg-react/commit/e6febcd6f9305a6fc81a563baba875299eb331e8)) -* FR-3342, round number of total pages to bigger ([7a0fb1f](https://github.com/frontegg/frontegg-react/commit/7a0fb1f8410fb36469d1e3a0a2ad00b725c8bff7)) - - - - - -## [2.8.3](https://github.com/frontegg/frontegg-react/compare/v2.8.1...v2.8.3) (2021-06-23) - - -### Bug Fixes - -* align all dependencies versions ([f1d5c48](https://github.com/frontegg/frontegg-react/commit/f1d5c48aba34827ea06a554cfb61734f1a40c93b)) -* update libraries version ([77da072](https://github.com/frontegg/frontegg-react/commit/77da072c67cb11e6cccb86b044a3a1a22b09625c)) - - - - - -## [2.8.2](https://github.com/frontegg/frontegg-react/compare/v2.8.1...v2.8.2) (2021-06-23) - - -### Bug Fixes - -* update libraries version ([77da072](https://github.com/frontegg/frontegg-react/commit/77da072c67cb11e6cccb86b044a3a1a22b09625c)) - - - - - -## [2.8.1](https://github.com/frontegg/frontegg-react/compare/v2.8.0...v2.8.1) (2021-06-22) - -**Note:** Version bump only for package @fronteg/react - - - - - -# [2.8.0](https://github.com/frontegg/frontegg-react/compare/v2.7.2...v2.8.0) (2021-06-21) - - -### Features - -* add connectivity redux hooks FR-2889 ([b8a5f98](https://github.com/frontegg/frontegg-react/commit/b8a5f98a6e173b239d8bfc9111c9f4794a322259)) -* move connectivity state to redux-store package FR-2889 ([55b80f8](https://github.com/frontegg/frontegg-react/commit/55b80f87181fa0283f187f7b36286cb0575586c9)) - - - - - -## [2.7.2](https://github.com/frontegg/frontegg-react/compare/v2.7.1...v2.7.2) (2021-06-14) - -**Note:** Version bump only for package @fronteg/react - - - - - -## [2.7.1](https://github.com/frontegg/frontegg-react/compare/v2.7.0...v2.7.1) (2021-06-10) - -**Note:** Version bump only for package @fronteg/react - - - - - -# [2.7.0](https://github.com/frontegg/frontegg-react/compare/v2.6.0...v2.7.0) (2021-06-07) - - -### Bug Fixes - -* **audits:** fix ip cell crash ([#425](https://github.com/frontegg/frontegg-react/issues/425)) ([169c4b6](https://github.com/frontegg/frontegg-react/commit/169c4b67f038d53f4eee600f0c9973cc56855779)) -* fix owasp type error FR-3131 ([ad57394](https://github.com/frontegg/frontegg-react/commit/ad57394fe6ec8483f450be4b492490cf73f655e5)) -* **connectivity:** fix the documentation ([02946a9](https://github.com/frontegg/frontegg-react/commit/02946a928360e12045ad5f23ba4717d8cbbf499b)) -* **connectivity:** remove the fitContent property. fix scrolling of the container ([b2a7c8f](https://github.com/frontegg/frontegg-react/commit/b2a7c8f13425c90d51b22cce613c71819a9c9f64)) -* **core:** increase domain suffix length ([02af3e9](https://github.com/frontegg/frontegg-react/commit/02af3e9d379831e833adfae85003506146013da3)) - - -### Features - -* **auth:** display error from strategy on activation form ([ad6a5c4](https://github.com/frontegg/frontegg-react/commit/ad6a5c43d18564cbb91d9ebb4901c33741c5a8ae)) - - - - - -# [2.6.0](https://github.com/frontegg/frontegg-react/compare/v2.5.2...v2.6.0) (2021-05-27) - - -### Features - -* **auth:** force terms on social sign up FR-2869 ([#406](https://github.com/frontegg/frontegg-react/issues/406)) ([4462402](https://github.com/frontegg/frontegg-react/commit/4462402c8648a023eb7595c4153a9943c039f995)) -* **auth:** space for release ([#420](https://github.com/frontegg/frontegg-react/issues/420)) ([fd18c60](https://github.com/frontegg/frontegg-react/commit/fd18c60e41dcc76f713b32ed84b5bfd7e2f8c355)) - - - - - -## [2.5.2](https://github.com/frontegg/frontegg-react/compare/v2.5.1...v2.5.2) (2021-05-24) - -**Note:** Version bump only for package @fronteg/react - - - - - -## [2.5.1](https://github.com/frontegg/frontegg-react/compare/v2.5.0...v2.5.1) (2021-05-23) - -**Note:** Version bump only for package @fronteg/react - - - - - -# [2.5.0](https://github.com/frontegg/frontegg-react/compare/v2.4.0...v2.5.0) (2021-05-21) - - -### Bug Fixes - -* **connectivity:** add overflow auto to connectivity page FR-3005 ([fd4b341](https://github.com/frontegg/frontegg-react/commit/fd4b34177d0da2b386325b4144bba2bad4a235d8)) - - -### Features - -* **connectivity:** add new paraneter fitConntent ([d31c281](https://github.com/frontegg/frontegg-react/commit/d31c28122b09b2b41ebcab1cc89d5a5f0bc93d17)) - - - - - -# [2.4.0](https://github.com/frontegg/frontegg-react/compare/v2.3.2...v2.4.0) (2021-05-19) - - -### Features - -* **auth:** [FR-2731] remember MFA devices ([#404](https://github.com/frontegg/frontegg-react/issues/404)) ([7f135d2](https://github.com/frontegg/frontegg-react/commit/7f135d200657ffd19ab54bcf9fd2049c07db43b4)) - - - - - -## [2.3.2](https://github.com/frontegg/frontegg-react/compare/v2.3.1...v2.3.2) (2021-05-10) - - -### Bug Fixes - -* **connectivity:** fix UI glitches ([a78c4f0](https://github.com/frontegg/frontegg-react/commit/a78c4f0587a606cc529909d35a24d98ab3e66f01)) - - - - - -## [2.3.1](https://github.com/frontegg/frontegg-react/compare/v2.3.0...v2.3.1) (2021-05-10) - - -### Bug Fixes - -* **connectivity:** fix changes not saved on swithcing connecticity context ([04758b5](https://github.com/frontegg/frontegg-react/commit/04758b5070da15f20f93010ddd24d9bd9b4f27ab)) -* **connectivity:** fix connectivity slack UI ([b214466](https://github.com/frontegg/frontegg-react/commit/b2144661bad8a6d827f4e6fc652fae1b9eae7dde)) - - - - - -# [2.3.0](https://github.com/frontegg/frontegg-react/compare/v2.2.2...v2.3.0) (2021-05-07) - - -### Bug Fixes - -* **audits:** fix position and behaviors of the User Agent logo ([3e067ef](https://github.com/frontegg/frontegg-react/commit/3e067efe8253c460174e6ea103774a5fc64016e9)) -* **auth:** remove duplicated useField destructure ([4efd3f1](https://github.com/frontegg/frontegg-react/commit/4efd3f1e477c29c7198d46fd3400ad5c7ec5f21f)) -* **connectivity:** fix save data in the slack configurattionn ([23ff145](https://github.com/frontegg/frontegg-react/commit/23ff1452cafc91debd4ee99ee473798e37e5d739)) - - -### Features - -* add random user for auditLogsDataDemo ([b7aaa9d](https://github.com/frontegg/frontegg-react/commit/b7aaa9d3c1367aa547ca62b7e878127ebd9cbfbd)) - - - - - -## [2.2.2](https://github.com/frontegg/frontegg-react/compare/v2.2.1...v2.2.2) (2021-04-29) - - -### Bug Fixes - -* **auth:** FR-2591 call account strategy after logout ([#380](https://github.com/frontegg/frontegg-react/issues/380)) ([09fe728](https://github.com/frontegg/frontegg-react/commit/09fe728203009d23f53dc4a51deb36b392c86de8)) - - - - - -## [2.2.1](https://github.com/frontegg/frontegg-react/compare/v2.2.0...v2.2.1) (2021-04-28) - -**Note:** Version bump only for package @fronteg/react - - - - - -# [2.2.0](https://github.com/frontegg/frontegg-react/compare/v2.1.0...v2.2.0) (2021-04-28) - - -### Bug Fixes - -* **connectivity:** FR-2311 validate webhook secret key length and format error message ([#358](https://github.com/frontegg/frontegg-react/issues/358)) ([589fccb](https://github.com/frontegg/frontegg-react/commit/589fccbf8c34704cbf16f246cda96cdcd5b85f92)) -* Add react-redux to react-hooks dist file ([a1244e7](https://github.com/frontegg/frontegg-react/commit/a1244e7fe49fbad6c53e02331493a7a3aaef7d64)), closes [#FR-2682](https://github.com/frontegg/frontegg-react/issues/FR-2682) -* disable refresh token after activate user ([84db237](https://github.com/frontegg/frontegg-react/commit/84db2376901b34f53d7079108d989b7d1c5d57b7)), closes [#FR-2761](https://github.com/frontegg/frontegg-react/issues/FR-2761) -* fix rollup config for react-hooks ([7338e52](https://github.com/frontegg/frontegg-react/commit/7338e5229636754342e8cf4efa817095fd681b48)), closes [#FR-2682](https://github.com/frontegg/frontegg-react/issues/FR-2682) -* fix rollup config for react-hooks ([a13f343](https://github.com/frontegg/frontegg-react/commit/a13f3433cf44057a6479c3b4267d9116271a2a1a)), closes [#FR-2682](https://github.com/frontegg/frontegg-react/issues/FR-2682) -* remove encoding variable in audits ip filtering ([8f77b39](https://github.com/frontegg/frontegg-react/commit/8f77b39d6f059efc2031f58d0464af3b407c2013)) - - -### Features - -* **auth:** add action to get public vendor config ([#367](https://github.com/frontegg/frontegg-react/issues/367)) ([48eb6ec](https://github.com/frontegg/frontegg-react/commit/48eb6ecf523def554fd9622609fffc5e2bef9692)) -* **auth:** get activate account config in order to determine if user should set password ([#370](https://github.com/frontegg/frontegg-react/issues/370)) ([b04d42a](https://github.com/frontegg/frontegg-react/commit/b04d42a8d84778bfdadcc3a7a872f9b24eb18028)) -* **connectivity:** FR-2586 format dates on webhook page ([#357](https://github.com/frontegg/frontegg-react/issues/357)) ([80a6832](https://github.com/frontegg/frontegg-react/commit/80a683273d49517583967ed04fc589da74e8d020)) -* add frontegg react library to support routing and sharing store ([2fed55f](https://github.com/frontegg/frontegg-react/commit/2fed55f61832c785d4ec99d7193226b9cf4f3a16)), closes [#FR-2761](https://github.com/frontegg/frontegg-react/issues/FR-2761) - - - - - -# [2.1.0](https://github.com/frontegg/frontegg-react/compare/v2.0.0...v2.1.0) (2021-04-13) - - -### Bug Fixes - -* remove live sagas from mock generator function ([ae3a366](https://github.com/frontegg/frontegg-react/commit/ae3a366633d1ab7f502437bec9413942e902104b)) - - -### Features - -* add option to consume switch tenant callback ([74fd8c6](https://github.com/frontegg/frontegg-react/commit/74fd8c65a3e4dd624d0144d7a330b98ad8a09d43)) - - - - - -# [2.0.0](https://github.com/frontegg/frontegg-react/compare/v1.28.0...v2.0.0) (2021-04-11) - - -### Bug Fixes - -* Wait for refresh token after switch tenant ([defadc1](https://github.com/frontegg/frontegg-react/commit/defadc1354b345bf7a07526b80e2d10eb16f0aaf)) -* **core:** fix server side rendering issue with loading animation ([d9bb7a3](https://github.com/frontegg/frontegg-react/commit/d9bb7a349052eb6fd2a6ab1d76a6e6a4bc225cb3)) -* FR-2312 - add success variant support for all elements libraries ([58b85b7](https://github.com/frontegg/frontegg-react/commit/58b85b7fe2f07a954a95ba87a17d44567efd946f)) -* **audits:** FR-2162 - remove expandable in case of nothing to show(audits) ([25df9ed](https://github.com/frontegg/frontegg-react/commit/25df9edb782ba00a49fcd87ebbd2cae05824f10f)) -* **auth:** duplicate profile picture timestamps ([1e54a4f](https://github.com/frontegg/frontegg-react/commit/1e54a4f820cf46526b29a6d0fba52967392b59ed)) -* **connectivity:** FR-2310 - fix test hook form filling ([d54e053](https://github.com/frontegg/frontegg-react/commit/d54e053d3b40059558f91012c3cc82a709da96ff)) -* FR-2312 - fixstyle status button (webhooks); add success for theme' ([1c4ca9d](https://github.com/frontegg/frontegg-react/commit/1c4ca9de4ced5a567740bb2d812c104a51435c3a)) -* **auth:** fix fromik import in FeRecaptcha to prevent build fails ([1f93494](https://github.com/frontegg/frontegg-react/commit/1f934948657dc97663a38760c8c445f50bd77e0f)) -* **auth:** FR-2206 - add error message for sign up form ([fe97e35](https://github.com/frontegg/frontegg-react/commit/fe97e3554e38399678445690f05e4ea2ca434e8d)) -* **auth:** FR-2218 - remove social logins from activate user form ([738ab4f](https://github.com/frontegg/frontegg-react/commit/738ab4f80862203d1be2cc1897d1efb2d634ee86)) -* **elements:** fix onclick event for material menu item ([71193e1](https://github.com/frontegg/frontegg-react/commit/71193e1bbda6c3300bd73fde612f1ba6f5b60ad8)) -* Fix build for rescript ([58c4b3c](https://github.com/frontegg/frontegg-react/commit/58c4b3c09c45bc42b14614e5012e054615d1de4c)) -* FR-2100 - fix expandable table styles ([9586201](https://github.com/frontegg/frontegg-react/commit/9586201e2f95c43648f5c72b3b353c6a3c9766e2)) - - -### Features - -* **auth:** enforce users password config on activate/reset/change password ([#342](https://github.com/frontegg/frontegg-react/issues/342)) ([7aeaeb2](https://github.com/frontegg/frontegg-react/commit/7aeaeb2568608dc9f8d6f0f66caf109fa52a6a66)) -* **auth:** login with facebook account ([5129bc5](https://github.com/frontegg/frontegg-react/commit/5129bc59a09bcc71c4ddddc3a352502a029a7089)) -* **auth:** login with facebook account ([#339](https://github.com/frontegg/frontegg-react/issues/339)) ([f231d75](https://github.com/frontegg/frontegg-react/commit/f231d758a2c2202e037b0caed104d606b9fb3888)) -* **auth:** login with microsoft account ([8fd8590](https://github.com/frontegg/frontegg-react/commit/8fd8590866bf58c6697f2390930c7a05bb2db220)) -* Add Audit logs to frontegg/react-hooks and frontegg/redux-store ([2e46638](https://github.com/frontegg/frontegg-react/commit/2e466385db3242a0547912a8daf3eb6bbd088709)) -* Add auditslogs to @frontegg/react-hooks ([285765a](https://github.com/frontegg/frontegg-react/commit/285765aa3fdbe37d4dbbdb2ad138823afb7e8c64)) -* add redux-store for auth state ([ee807ef](https://github.com/frontegg/frontegg-react/commit/ee807efd45a4a2ef494ce2420a80dc0a458fe4ab)) -* Add Security Policy API and Store Hooks ([e9b7abf](https://github.com/frontegg/frontegg-react/commit/e9b7abfa38e5e958a63f69dd45bd6631f2811e53)) -* Expose onRedirectTo via hooks ([bd38109](https://github.com/frontegg/frontegg-react/commit/bd381097a87e2794d668e3951d9a221f9c9acd51)) -* Extract react hooks to separated sub package ([8ad0333](https://github.com/frontegg/frontegg-react/commit/8ad033332fde18e3f10f7f6f4f5d0d24fc88f0b0)) -* move audits logs state management to @frontegg/redux-store ([08839b6](https://github.com/frontegg/frontegg-react/commit/08839b685dcdc0aaf3b17c0c0baf9bc0ba687536)) -* Split State-Management and hooks from UI components ([20d24cd](https://github.com/frontegg/frontegg-react/commit/20d24cd19f536a7f519d670bd8735feb350e54e9)) -* **redux-store:** Export all actions and interfaces from auth state ([b666ccd](https://github.com/frontegg/frontegg-react/commit/b666ccd9dc508cfffcdf5b1d81f96aab53f167fb)) - - -### BREAKING CHANGES - -* hooks and Entity Types should be imported from @frontegg/react-hooks and @frontegg/redux-store - - - - - -# [1.28.0](https://github.com/frontegg/frontegg-react/compare/v1.27.0...v1.28.0) (2021-03-22) - - -### Bug Fixes - -* FR-2220 - add loader for MenuItem ([4a3e62e](https://github.com/frontegg/frontegg-react/commit/4a3e62e68f7041e0d376ffc411c57198557c20f1)) -* **auth:** FR-2257 - fix t param duplication ([be144f5](https://github.com/frontegg/frontegg-react/commit/be144f5ec204ece9d5e62e0a762400a3b8f284d1)) -* **core:** FR-2126 - removed list dots for error message ([6a233b0](https://github.com/frontegg/frontegg-react/commit/6a233b0dc1f7650f27c1b14548b59539bb7f9966)) - - -### Features - -* **auth:** request new activation email ([748255f](https://github.com/frontegg/frontegg-react/commit/748255fc924ef5e36764ba264d9a3767a9ea0c59)) - - - - - -# [1.27.0](https://github.com/frontegg/frontegg-react/compare/v1.26.0...v1.27.0) (2021-03-18) - - -### Features - -* **auth:** added option for terms of service in signup page ([f876091](https://github.com/frontegg/frontegg-react/commit/f876091cfde000c7ae003b878bea13ab8271f171)) - - - - - -# [1.26.0](https://github.com/frontegg/frontegg-react/compare/v1.25.0...v1.26.0) (2021-03-17) - - -### Bug Fixes - -* **auth:** unload captcha after login/sign up ([cb4963c](https://github.com/frontegg/frontegg-react/commit/cb4963c5812586d8a397c7e978911b3e3e3f79e6)) - - -### Features - -* **auth:** allow getting login/signup redirect url via query param ([ce909fd](https://github.com/frontegg/frontegg-react/commit/ce909fd1a5f430ebdeeeb9182837f837c97f720c)) - - - - - -# [1.25.0](https://github.com/frontegg/frontegg-react/compare/v1.24.0...v1.25.0) (2021-03-07) - - -### Bug Fixes - -* Set fixed version for i18next in @frontegg/react-core ([20f9879](https://github.com/frontegg/frontegg-react/commit/20f98795e88b08e5e98e71d2d062836f47ce1061)) -* **auth:** FR-1932 - make requested changes; fix ReCaptcha token' ([4903613](https://github.com/frontegg/frontegg-react/commit/490361368e8a7bf1fa8c049eda8f2881bb15a71d)) -* **auth:** FR-1932 - removed unused import ([1e00b41](https://github.com/frontegg/frontegg-react/commit/1e00b41bd1395ba8823519eea395849625ac4e83)) - - -### Features - -* FR-1932 - added captcha for login/sign up; removed unused components demosaas ([e5e75c8](https://github.com/frontegg/frontegg-react/commit/e5e75c82524bfffe158924e75128fa84d5224b14)) - - - - - -# [1.24.0](https://github.com/frontegg/frontegg-react/compare/v1.23.1...v1.24.0) (2021-03-03) - - -### Bug Fixes - -* **audits:** align icon and add cursor pointer on icons ([c6977b0](https://github.com/frontegg/frontegg-react/commit/c6977b006c2086600ed42c36aee29116871a457f)) - - -### Features - -* account-settings ([134b7b0](https://github.com/frontegg/frontegg-react/commit/134b7b0a8f2b33630ded395cbef90eb6929d7754)) -* **auth:** change social login redriect url behavior ([7199487](https://github.com/frontegg/frontegg-react/commit/7199487a71b524b9de7843048ac6f92836f8b592)) - - - - - -## [1.23.1](https://github.com/frontegg/frontegg-react/compare/v1.23.0...v1.23.1) (2021-02-21) - -**Note:** Version bump only for package @fronteg/react - - - - - -# [1.23.0](https://github.com/frontegg/frontegg-react/compare/v1.22.1...v1.23.0) (2021-02-18) - - -### Features - -* **audits:** add several custom browser icons to the User Agent field ([8bba084](https://github.com/frontegg/frontegg-react/commit/8bba0841ec39bc6b9c04100abc437964464907b1)) -* **auth:** support render prop ([f96ca8b](https://github.com/frontegg/frontegg-react/commit/f96ca8b2fe0ff90abaa502ec7ad639e1380af254)) - - - - - -## [1.22.1](https://github.com/frontegg/frontegg-react/compare/v1.22.0...v1.22.1) (2021-02-16) - - -### Bug Fixes - -* Fix ActivateAccount test ([bbf3781](https://github.com/frontegg/frontegg-react/commit/bbf37817feccdc1331e92b829b39d33d0461052e)) -* **auth:** fix after activation refirect to look on the last user requested route ([fcb53ef](https://github.com/frontegg/frontegg-react/commit/fcb53ef8f3a1b1397f71eb751c33ed221caa0064)) - - - - - -# [1.22.0](https://github.com/frontegg/frontegg-react/compare/v1.21.1...v1.22.0) (2021-02-13) - - -### Bug Fixes - -* **audits:** fix nullable conversion case in audits table ([0acacdf](https://github.com/frontegg/frontegg-react/commit/0acacdf2e1b14062cb557a96e5b6d9c7cb966087)) -* **auth:** small fixes ([1463b88](https://github.com/frontegg/frontegg-react/commit/1463b88eb51b048abb7a57a53f58b492b6cc1adf)) -* **connectivity:** fix scroll for the event table ([7871686](https://github.com/frontegg/frontegg-react/commit/78716867a077d30d9cf8698424e25f08f8e4591d)) -* **core:** remove unused imported components in the FileInput component ([54a7f29](https://github.com/frontegg/frontegg-react/commit/54a7f29a778eeb0a2f609c3239c85e8244562c90)) - - -### Features - -* **auth:** autosave profile photo ([77a3ec4](https://github.com/frontegg/frontegg-react/commit/77a3ec4311ca959b9ae8224f2a56993402a3b7b4)) -* **auth:** instead of text in the user agent field, now show an icon of a browser ([8267338](https://github.com/frontegg/frontegg-react/commit/82673384710144597e484676c30f6569628399a3)) -* **auth:** split process update inforamtion between photo and inforamtion ([0140836](https://github.com/frontegg/frontegg-react/commit/0140836d691e87a8235ca1d3612f7b9881311747)) -* **core:** add support ref for the input component ([f47fc79](https://github.com/frontegg/frontegg-react/commit/f47fc79e57738c867d7eb5574caa259f5598633a)) -* **core:** add suppurt ref for the FileInput component ([86b3790](https://github.com/frontegg/frontegg-react/commit/86b3790c35c44cfddb9ceee76b19cd2428b5cccd)) -* **core:** Added tab disabling for FeTabs component; disabled pwd tab in Profile FR-789 ([2354f47](https://github.com/frontegg/frontegg-react/commit/2354f47a5d0fe22e05b3e869b7e963192cd86b45)) -* **elements:** add support ref for Input elements in UI libraries ([39c1ebc](https://github.com/frontegg/frontegg-react/commit/39c1ebc05262aa0f1ee47dbae8c23bb37d0a0a0d)) - - - - - -## [1.21.1](https://github.com/frontegg/frontegg-react/compare/v1.21.0...v1.21.1) (2021-02-04) - -**Note:** Version bump only for package @fronteg/react - - - - - -# [1.21.0](https://github.com/frontegg/frontegg-react/compare/v1.20.1...v1.21.0) (2021-02-04) - - -### Bug Fixes - -* **audits:** fix show filter icon if the filterable value is desabled ([00f0afd](https://github.com/frontegg/frontegg-react/commit/00f0afd19f3d87deff66c198fe115d2f1e7d6708)) -* **audits:** FR-1875 - changed time filter format ([24e58a2](https://github.com/frontegg/frontegg-react/commit/24e58a2c742ac660a92add0035db0e2a4adac12e)) -* **audits:** leave the empty values in the cell instead of text ([360c09c](https://github.com/frontegg/frontegg-react/commit/360c09c6b9d47cd0a840851dec265ddd96463d96)) -* **auth:** fix caching photo in the profile page ([2acb7e7](https://github.com/frontegg/frontegg-react/commit/2acb7e70186cda2607c16223c9499c4abab92a17)) -* **auth:** Fix redirect after reset password succeeded ([e20d9f4](https://github.com/frontegg/frontegg-react/commit/e20d9f46d345c06f2426e91448835d23e42dc239)) -* **auth:** Remove go to login from activate account succeeded ([e1a8746](https://github.com/frontegg/frontegg-react/commit/e1a8746ad7c1a246038818232967052641d0a040)) -* **core:** leave the empty values in the cell instead of text in the FeTable ([8997a53](https://github.com/frontegg/frontegg-react/commit/8997a53a48e53e6a220bc8d709e94674a49b1519)) -* Remove deprecated @frontegg/react support ([48f493c](https://github.com/frontegg/frontegg-react/commit/48f493cafb98dfcf66096c6f2a577c067c5c8bdf)) - - -### Features - -* Add option to inject SSO components without routes ([79dd172](https://github.com/frontegg/frontegg-react/commit/79dd17267da92c1d8bb651fe6210d3c6b5b42519)) -* **auth:** Add silent logout saga action ([f5781f7](https://github.com/frontegg/frontegg-react/commit/f5781f720d8944ef23e2b57653c7f816f3cce4d8)) -* **auth:** Auto login after activate account succeeded ([eebdd71](https://github.com/frontegg/frontegg-react/commit/eebdd710199c505f927d8446079f70c22f25909b)) - - - - - -## [1.20.1](https://github.com/frontegg/frontegg-react/compare/v1.20.0...v1.20.1) (2021-02-02) - - -### Bug Fixes - -* **connectivity:** fix the delete dialog message disappers ([f40e50a](https://github.com/frontegg/frontegg-react/commit/f40e50a36ee9f184f0d8b5bddffa6842ed827605)) - - - - - -# [1.20.0](https://github.com/frontegg/frontegg-react/compare/v1.19.1...v1.20.0) (2021-02-01) - - -### Bug Fixes - -* **connectivity:** fix icons on the list of platform ([8de499c](https://github.com/frontegg/frontegg-react/commit/8de499c6e50faf29ad2198f9d0661d38d7201394)) -* **connectivity:** fix send the security parameter for the webhook configuration ([cbc8bbf](https://github.com/frontegg/frontegg-react/commit/cbc8bbf9e347d5561169f0cfbf724b3f6d1c6e29)) -* **core:** fix close tolltip popup ([f78cc43](https://github.com/frontegg/frontegg-react/commit/f78cc437d812be6d04e0f65c1664da9ce609fae6)) -* **core:** fiz a problem with caching filters data in the FeTable componet ([319d7be](https://github.com/frontegg/frontegg-react/commit/319d7bec60e88c146f98d5825770ed8721ce0637)) -* **core:** remove required field for the secret field in the webhook configuration ([fe97d7e](https://github.com/frontegg/frontegg-react/commit/fe97d7e23e6148a381153daa44fae08484733184)) - - -### Features - -* **audits:** changes component for posibility use in the deshboard project ([a6365b8](https://github.com/frontegg/frontegg-react/commit/a6365b8aa65702bf0299a3f08d1147b8788a1890)) -* **auth:** add option to keep sessions alive via the AuthPlugin props ([8fd2427](https://github.com/frontegg/frontegg-react/commit/8fd2427cf1d562b12f0657300c526f19d286ccc4)) -* **core:** add TExportAudits type as separet type in interfaces ([fcc1992](https://github.com/frontegg/frontegg-react/commit/fcc199294ae10abc85102b1c2345a4bdcad6c78b)) -* **core:** implement horizontal scrolling in the table component ([1315b75](https://github.com/frontegg/frontegg-react/commit/1315b75aa92abeace8b2ea811f62efbb6e8db6c7)) - - - - - -## [1.19.1](https://github.com/frontegg/frontegg-react/compare/v1.19.0...v1.19.1) (2021-01-20) - -**Note:** Version bump only for package @fronteg/react - - - - - -# [1.19.0](https://github.com/frontegg/frontegg-react/compare/v1.18.6...v1.19.0) (2021-01-20) - - -### Bug Fixes - -* Disable MFA input auto complete ([#254](https://github.com/frontegg/frontegg-react/issues/254)) ([b7420c6](https://github.com/frontegg/frontegg-react/commit/b7420c627850887d17bf5b24a2e4bf5a75ded798)) -* **connectivity:** fix sorting the Status column in the webhooks list ([c7d1428](https://github.com/frontegg/frontegg-react/commit/c7d14282d349c7cd7a952a431361faee5d53f893)) -* **connectivity:** remove old dead code on the webhooks list ([609bfe0](https://github.com/frontegg/frontegg-react/commit/609bfe0116c6b8d64cabc14a9aaa3cca9ff5b8da)) -* **connectivity:** remove warnings in the devTools for some svg elements ([dde9731](https://github.com/frontegg/frontegg-react/commit/dde973150f9dce8043b6d99d9b7c8339fb014df6)) -* **core:** add support the data-* attribute for the CheckBox component ([bbcc4d5](https://github.com/frontegg/frontegg-react/commit/bbcc4d59d972254483b83329839e2c29251b8aed)) -* **core:** fix align for the label in the CheckBox ([d094a09](https://github.com/frontegg/frontegg-react/commit/d094a090a95bbf0b3fa61661ea84c1359b10db17)) -* **core:** fix click by form button if process loading is active ([5a9971c](https://github.com/frontegg/frontegg-react/commit/5a9971ce1a4741ac2b73f6fa01c52a9e84fd9905)) - - -### Features - -* **connectivity:** add error message from the server to the webhook component ([7b53d9f](https://github.com/frontegg/frontegg-react/commit/7b53d9f66eed8e32127abbb4ca95a3d60c743a43)) -* **connectivity:** add loading when changes status in the webhooks list ([b834ac6](https://github.com/frontegg/frontegg-react/commit/b834ac6570eff62d5629445fac4417baee9d5ea0)) -* **connectivity:** add sorting data by columns in the webhooks list ([6fc81b6](https://github.com/frontegg/frontegg-react/commit/6fc81b654a164654cde7821f430621c0344d5974)) -* **connectivity:** change behaviors of select catagory and envents ([368849e](https://github.com/frontegg/frontegg-react/commit/368849e427b384b8203fa6ab28cf6dc25be522fd)) -* **connectivity:** Disabled the V mark if no one of events is active. ([7cea078](https://github.com/frontegg/frontegg-react/commit/7cea078dc70939dc9c3b2fa28af495d1e2d988d9)) -* **core:** add support the sortType param to the Column values for the FeTable component ([9702cee](https://github.com/frontegg/frontegg-react/commit/9702cee6b9c80c7e2bb9db126a4e93d9935a25d0)) - - -### Performance Improvements - -* **connectivity:** move handlers to the useCallback hook in the AccordionCategories ([b89826b](https://github.com/frontegg/frontegg-react/commit/b89826bbd009d05c9f7a52c8d2ccaadae3a2b896)) -* **core:** move onChange handler to the useCallback hook ([25c78da](https://github.com/frontegg/frontegg-react/commit/25c78dafe667accee048acbb9a916fd4ad91b0fc)) - - - - - -## [1.18.6](https://github.com/frontegg/frontegg-react/compare/v1.18.5...v1.18.6) (2021-01-19) - - -### Bug Fixes - -* **core:** Fix toggled expandable button svg color FR-1173 ([bcf486d](https://github.com/frontegg/frontegg-react/commit/bcf486d5334a0fade4305bac70fb37b8ec63a53f)) -* Add more space to first column in Table components FR-1171 ([c0b6b38](https://github.com/frontegg/frontegg-react/commit/c0b6b38479b52b3fce66439a640be8e7b4a59809)) -* **auth:** Ellipses user name in AccountDropdown component ([d3b5ecf](https://github.com/frontegg/frontegg-react/commit/d3b5ecf05e4f9dcf42a44fdebc6caa14cd7b43c5)) -* **auth:** stop loading if error api-tokens FR-1366 ([d245449](https://github.com/frontegg/frontegg-react/commit/d245449e2ee622a49abd34cbef071078a687aa9f)) -* **core:** Break user full name when no free space available FR-1528 ([da508f4](https://github.com/frontegg/frontegg-react/commit/da508f4bf7e59b8fbb2a232a3f0aec97ff8b2e0c)) - - - - - -## [1.18.5](https://github.com/frontegg/frontegg-react/compare/v1.18.4...v1.18.5) (2021-01-18) - - -### Bug Fixes - -* **core:** fix perfomance for the INputChip component ([cea5e19](https://github.com/frontegg/frontegg-react/commit/cea5e19aef64a6cf42f75d3cbb77cd74fb01f560)) -* Reset state on session expiration ([713310a](https://github.com/frontegg/frontegg-react/commit/713310aa183829c46f536b90f871d92496a5621a)) - - - - - -## [1.18.4](https://github.com/frontegg/frontegg-react/compare/v1.18.3...v1.18.4) (2021-01-18) - -**Note:** Version bump only for package @fronteg/react - - - - - -## [1.18.3](https://github.com/frontegg/frontegg-react/compare/v1.18.2...v1.18.3) (2021-01-17) - - -### Bug Fixes - -* **auth:** fix activate button ([#240](https://github.com/frontegg/frontegg-react/issues/240)) ([dfc369c](https://github.com/frontegg/frontegg-react/commit/dfc369c2ade168ca2b4af30c0eb34fac59b4da35)) - - - - - -## [1.18.2](https://github.com/frontegg/frontegg-react/compare/v1.18.1...v1.18.2) (2021-01-15) - -**Note:** Version bump only for package @fronteg/react - - - - - -## [1.18.1](https://github.com/frontegg/frontegg-react/compare/v1.18.0...v1.18.1) (2021-01-15) - - -### Bug Fixes - -* **auth:** change oidc icon to correct ([#235](https://github.com/frontegg/frontegg-react/issues/235)) ([95bf5dc](https://github.com/frontegg/frontegg-react/commit/95bf5dc83225c0710e9ec1927d4f4b536d621c1a)) - - - - - -# [1.18.0](https://github.com/frontegg/frontegg-react/compare/v1.17.3...v1.18.0) (2021-01-14) - - -### Bug Fixes - -* Reload profile data on profile component mount ([0791ffa](https://github.com/frontegg/frontegg-react/commit/0791ffaf867ad339b8bf24820e15dc3ca107aa6d)) -* Reset Frontegg store after logout ([37d1de8](https://github.com/frontegg/frontegg-react/commit/37d1de8e816fec9e671ec1eff5e139b99e857941)) -* **core:** fix close property for the FePopup component ([e9f9d85](https://github.com/frontegg/frontegg-react/commit/e9f9d85fbc51c3e83e789fe326acf5117f0d47ca)) - - -### Features - -* **core:** support enter data on blur event in the InputChip element ([6f43239](https://github.com/frontegg/frontegg-react/commit/6f43239fe2ab03f794d8eedc8742eb811f4567de)) - - - - - -## [1.17.3](https://github.com/frontegg/frontegg-react/compare/v1.17.2...v1.17.3) (2021-01-13) - - -### Bug Fixes - -* **auth:** fix scopes for github login ([#230](https://github.com/frontegg/frontegg-react/issues/230)) ([c73f0c3](https://github.com/frontegg/frontegg-react/commit/c73f0c32d58ce3727db9f1782fe60d4d946932b1)) - - - - - -## [1.17.2](https://github.com/frontegg/frontegg-react/compare/v1.17.1...v1.17.2) (2021-01-12) - -**Note:** Version bump only for package @fronteg/react - - - - - -## [1.17.1](https://github.com/frontegg/frontegg-react/compare/v1.17.0...v1.17.1) (2021-01-05) - -**Note:** Version bump only for package @fronteg/react - - - - - -# [1.17.0](https://github.com/frontegg/frontegg-react/compare/v1.16.2...v1.17.0) (2021-01-03) - - -### Bug Fixes - -* **auth:** fix comments ([d1a826f](https://github.com/frontegg/frontegg-react/commit/d1a826f78211514b6853f4a444bb202737f4053b)) -* **auth:** fix FR-1304 ([#220](https://github.com/frontegg/frontegg-react/issues/220)) ([c540512](https://github.com/frontegg/frontegg-react/commit/c540512b62e4eafc85f546da952360297c56410b)) - - -### Features - -* **auth:** Add manage authorization step to sso FR-1148 ([f961e74](https://github.com/frontegg/frontegg-react/commit/f961e74ef163fa5893afc999ed360763b3172297)) -* **core:** set company name as optional on singup form ([#214](https://github.com/frontegg/frontegg-react/issues/214)) ([de83d17](https://github.com/frontegg/frontegg-react/commit/de83d170cb0bf35288e5c69891f902e754ad5ff3)) - - - - - -## [1.16.2](https://github.com/frontegg/frontegg-react/compare/v1.16.0...v1.16.2) (2020-12-27) - - -### Bug Fixes - -* **auth:** fix force mfa recovery code not showing up ([#204](https://github.com/frontegg/frontegg-react/issues/204)) ([f6ce5ac](https://github.com/frontegg/frontegg-react/commit/f6ce5ac2be84c931454051760ae3fcca232b6c12)) -* **auth:** fix some minot texts and css issues on MFA ([#203](https://github.com/frontegg/frontegg-react/issues/203)) ([688cbc7](https://github.com/frontegg/frontegg-react/commit/688cbc75fb1a74730d433d0026841856f666018d)) -* Fix force MFA screen bugs ([#196](https://github.com/frontegg/frontegg-react/issues/196)) ([8d51fb9](https://github.com/frontegg/frontegg-react/commit/8d51fb9794d0d0728bd04a742ce6a3f77845d1fe)) -* Fix pre-release action ([6b7f616](https://github.com/frontegg/frontegg-react/commit/6b7f6164660323982bc290f7bc6d853b8b4a075d)) -* **auth:** add support in dynamic component in login and signup for socail logins ([#192](https://github.com/frontegg/frontegg-react/issues/192)) ([195b977](https://github.com/frontegg/frontegg-react/commit/195b97704618135cb17edc7569019f236dda7fd6)) - - - - - -## [1.16.1](https://github.com/frontegg/frontegg-react/compare/v1.16.0...v1.16.1) (2020-12-27) - - -### Bug Fixes - -* **auth:** fix force mfa recovery code not showing up ([#204](https://github.com/frontegg/frontegg-react/issues/204)) ([f6ce5ac](https://github.com/frontegg/frontegg-react/commit/f6ce5ac2be84c931454051760ae3fcca232b6c12)) -* Fix force MFA screen bugs ([#196](https://github.com/frontegg/frontegg-react/issues/196)) ([8d51fb9](https://github.com/frontegg/frontegg-react/commit/8d51fb9794d0d0728bd04a742ce6a3f77845d1fe)) -* Fix pre-release action ([6b7f616](https://github.com/frontegg/frontegg-react/commit/6b7f6164660323982bc290f7bc6d853b8b4a075d)) -* **auth:** add support in dynamic component in login and signup for socail logins ([#192](https://github.com/frontegg/frontegg-react/issues/192)) ([195b977](https://github.com/frontegg/frontegg-react/commit/195b97704618135cb17edc7569019f236dda7fd6)) - - - - - -# [1.16.0](https://github.com/frontegg/frontegg-react/compare/v1.15.2...v1.16.0) (2020-12-24) - - -### Bug Fixes - -* wront state with logged in user ([#198](https://github.com/frontegg/frontegg-react/issues/198)) ([970c99b](https://github.com/frontegg/frontegg-react/commit/970c99b8fd20147d00e30558d3a7c20726136579)) -* **connectivity:** fix some design and behavioral issues ([fd5d16d](https://github.com/frontegg/frontegg-react/commit/fd5d16d1e114fa8445ea5a0548d7b4cc00a530f0)) -* **core:** fix clean data in the FeInput component ([6d900c8](https://github.com/frontegg/frontegg-react/commit/6d900c8e04986f21744d69ee741408fd9d66ffb3)) -* **core:** fix style the FeChip component ([1f76479](https://github.com/frontegg/frontegg-react/commit/1f76479817129653b08bac047531d68d2f132fab)) -* **localize:** fix text for secure tooltip ([8761141](https://github.com/frontegg/frontegg-react/commit/87611410fc23b881a11bcc80e9ba489bcf3ccf32)) - - -### Features - -* **core:** add anew property dontDisableSaving to the FInput compoennt ([6ff5648](https://github.com/frontegg/frontegg-react/commit/6ff56488a76816f5b501b616656e0bc97afe03ee)) - - - - - -## [1.15.2](https://github.com/frontegg/frontegg-react/compare/v1.15.1...v1.15.2) (2020-12-24) - - -### Bug Fixes - -* **tests:** Fix two factor authentication tests ([27b3539](https://github.com/frontegg/frontegg-react/commit/27b3539db64d42d86de7630e85340fd5cf3b48ba)) -* Fix crash state mutation was detected between dispatches ([6f8d8e6](https://github.com/frontegg/frontegg-react/commit/6f8d8e6083742d01ba8bd167436f3c7bf850e146)) -* Fix UI css bugs ([b94b49c](https://github.com/frontegg/frontegg-react/commit/b94b49c020f7a26059ab19b0345d5f266043c8ca)) -* **core:** fix validate the InputChip component ([e363849](https://github.com/frontegg/frontegg-react/commit/e36384952d95edf22541b5a648d6cd09b42b4c95)) - - - - - -## [1.15.1](https://github.com/frontegg/frontegg-react/compare/v1.15.0...v1.15.1) (2020-12-21) - - -### Bug Fixes - -* **auth:** fix authorized content data validation ([2728cd9](https://github.com/frontegg/frontegg-react/commit/2728cd9f51b8c3404a09870ff256ba07dbcc1d6c)) -* **connectivity:** fix bug of edit the email and sms events ([e34788c](https://github.com/frontegg/frontegg-react/commit/e34788c0b58dc07d20509b4addb39d33a57e1dea)) - - - - - -# [1.15.0](https://github.com/frontegg/frontegg-react/compare/v1.14.1...v1.15.0) (2020-12-20) - - -### Bug Fixes - -* Fix owsp validation exception on undefined value ([54ffaee](https://github.com/frontegg/frontegg-react/commit/54ffaeed42564481bc1f1b9592e02ba4e266f9e6)) - - -### Features - -* Add new component `AuthorizedContent` to strict content visibility by permission ([e4be8dc](https://github.com/frontegg/frontegg-react/commit/e4be8dc758b185a88f7b42960c733e7c6763d748)) - - - - - -## [1.14.1](https://github.com/frontegg/frontegg-react/compare/v1.14.0...v1.14.1) (2020-12-17) - - -### Bug Fixes - -* change primary color to darker blue ([04f94bd](https://github.com/frontegg/frontegg-react/commit/04f94bd5caa1135560e89cf0c886a4b81665956a)) -* Fix AccountDropdown style bugs ([dde2680](https://github.com/frontegg/frontegg-react/commit/dde26808277b12695c9dcf9daa7dd79f04d1ef88)) -* Fix create new webhooks button typo ([0be76c3](https://github.com/frontegg/frontegg-react/commit/0be76c38a771996ad849a027b752fb7107b9d3db)) -* Fix infinite loading in switch tenant popup ([78a63c1](https://github.com/frontegg/frontegg-react/commit/78a63c1affa2cd05b4a556d63d692f3ed0380788)) -* Fix input style issues when autocomplete enables ([790e8b4](https://github.com/frontegg/frontegg-react/commit/790e8b41e1e36855baa04b730025ac3246bc6b89)) -* Fix search bar alignments and ui bug fixes ([dd51197](https://github.com/frontegg/frontegg-react/commit/dd5119705cad6e379459171e34a5a3abe4d891ff)) -* move branch changelog to pull request body while create release ([fc676e1](https://github.com/frontegg/frontegg-react/commit/fc676e13ce7eb31f5f410201f617b19317bf469d)) -* prevent dialog from closing if clicking on other portal element ([c6a3b5b](https://github.com/frontegg/frontegg-react/commit/c6a3b5bec8e0f362f9fa816f6dff719d6db23f1a)) -* Re-enable fields in SSO claim domain in validation failed ([4fb385c](https://github.com/frontegg/frontegg-react/commit/4fb385c544d03658964b40285b9ec8041250d269)) -* Update add new webhook api url ([65533c1](https://github.com/frontegg/frontegg-react/commit/65533c1922c43f4331664d14f70c3876e7ada40a)) -* **auth:** [FR-1080] fix social login wrapper ([f825fc7](https://github.com/frontegg/frontegg-react/commit/f825fc7378780944f9edee94e4f920a99912c5a5)) -* remove switch tenant button user only have on tenant ([c3b5df5](https://github.com/frontegg/frontegg-react/commit/c3b5df52cce439537ab7a483abaf4645d6fa7d1a)) -* **auth:** fix loader api tokens table loader, some improvements ([8db076f](https://github.com/frontegg/frontegg-react/commit/8db076f91de61358577b304cc955151850ff4cfa)) - - -### Features - -* add support to publish prerelease version ([3611311](https://github.com/frontegg/frontegg-react/commit/361131133c73d86ad13bff7b4a38390d36bc6cd7)) - - - - - -# [1.14.0](https://github.com/frontegg/frontegg-react/compare/v1.13.2...v1.14.0) (2020-12-16) - - -### Bug Fixes - -* disable profile image uploader limitation ([07e082e](https://github.com/frontegg/frontegg-react/commit/07e082e8e4ff551b84fa277a8476e676f503ff2e)) -* Fix MFA cancel button size ([be9a44b](https://github.com/frontegg/frontegg-react/commit/be9a44b2df12a9beac07198a2aaa82c3bd940717)) -* Fix profile tabs color ([3ab5742](https://github.com/frontegg/frontegg-react/commit/3ab57426355700c05930075c014faa2c10456b9c)) -* UI enhancements for SSO components ([6be3aea](https://github.com/frontegg/frontegg-react/commit/6be3aea9e54aa56e4f28da3d63a81df500435fab)) -* Update components primary color ([ee6d08e](https://github.com/frontegg/frontegg-react/commit/ee6d08ec880fc7ae9427d993a544385db8d3da5b)) -* **connectivity:** fix actions for saving data ([c92d051](https://github.com/frontegg/frontegg-react/commit/c92d051f309e3cbb5862bec73c6e12dd5b96da67)) -* **connectivity:** FR-1005 fix the alignment of the line data in the webhook table ([bccfa8b](https://github.com/frontegg/frontegg-react/commit/bccfa8b96b372f1762e6b681fcccfc9d556fe9d3)) -* **core:** fix mistakes in URLs for the connectivity component ([2fcafc8](https://github.com/frontegg/frontegg-react/commit/2fcafc8d639bb634d4acdf34dbce841c9feb0954)) -* **elements:** fix console error for the InputChip component ([0f25e0f](https://github.com/frontegg/frontegg-react/commit/0f25e0f12673301d8f3cbfb4ccbf7a532e573840)) - - -### Features - -* **auth:** Api tokens component for users and tenants ([c8b1e17](https://github.com/frontegg/frontegg-react/commit/c8b1e176bee4f4402afbd9625841312428c14b75)) - - - - - -## [1.13.2](https://github.com/frontegg/frontegg-react/compare/v1.13.1...v1.13.2) (2020-12-13) - - -### Bug Fixes - -* **audits:** FR-1001 add 'unknown' when cell value is undefined ([889bc83](https://github.com/frontegg/frontegg-react/commit/889bc83ae9105228b88c32826a81eb7b8de4d0d4)) -* **build:** remove hoist-non-react-statics from internal deps ([3eccdd2](https://github.com/frontegg/frontegg-react/commit/3eccdd297b8b79cccae11200a62ca69e81f6856f)) -* **connectivity:** fix the enabled edit data form the SMS and Email webhooks ([6314a4e](https://github.com/frontegg/frontegg-react/commit/6314a4ea2eb1be8b1a0043077de3ceb4f410bd68)) -* **elements:** fix the fullWidth style for the InputChip component in the material library ([2cac48f](https://github.com/frontegg/frontegg-react/commit/2cac48f2fb55a3810a1d2fe41f2bdacc25f56c01)) - - - - - -## [1.13.1](https://github.com/frontegg/frontegg-react/compare/v1.13.0...v1.13.1) (2020-12-10) - - -### Bug Fixes - -* **audits:** FR-996 add closing popup when reference hidden to prevent ui bugs ([b046f95](https://github.com/frontegg/frontegg-react/commit/b046f9503f983401ff26eb2e16edc6954cb101d5)) -* **audits:** FR-999 add x btn for ip popup ([6efc8ea](https://github.com/frontegg/frontegg-react/commit/6efc8ea6e229b6434d75f9af59c0b6f0993ec9cd)) -* **auth:** fix google social login scopes. closes [#159](https://github.com/frontegg/frontegg-react/issues/159) ([0ddb2ca](https://github.com/frontegg/frontegg-react/commit/0ddb2ca54f250900f79a1f52382469922d371c12)) - - - - - -# [1.13.0](https://github.com/frontegg/frontegg-react/compare/v1.12.0...v1.13.0) (2020-12-09) - - -### Bug Fixes - -* **connectivity:** fix show platform if the it dosen't have any events FR-984 ([6319b02](https://github.com/frontegg/frontegg-react/commit/6319b0201888b7226d600d43cb2bfc65f1d24b20)) -* fix testId error in material components ([0e3d2a6](https://github.com/frontegg/frontegg-react/commit/0e3d2a610f762d9065eee261dd996ecea77e1c8d)), closes [#119](https://github.com/frontegg/frontegg-react/issues/119) -* **auth:** loadUsers on TeamTable did mount ([9b8ff6b](https://github.com/frontegg/frontegg-react/commit/9b8ff6b033e2f9973109e618b5ce9da91eec7ec3)) - - -### Features - -* [FR-808] add support in users sign ups ([1a6f7c3](https://github.com/frontegg/frontegg-react/commit/1a6f7c3639ab4c351593d540296e67f65293bbf9)) - - - - - -# [1.12.0](https://github.com/frontegg/frontegg-react/compare/v1.11.1...v1.12.0) (2020-12-09) - - -### Bug Fixes - -* **audits:** display none cross btns in filters selectors to prevent errors ([6afb046](https://github.com/frontegg/frontegg-react/commit/6afb046e69ac7fb10be28bda9bd44eb3d9524cc6)) -* prevent after login redirect if it is in auth routes ([ae0371d](https://github.com/frontegg/frontegg-react/commit/ae0371d82e5fa3b99349d74f9cd0c7d6754cd710)), closes [#108](https://github.com/frontegg/frontegg-react/issues/108) -* split AuditsPage to separated components ([9aa109a](https://github.com/frontegg/frontegg-react/commit/9aa109a09a357333788abab845f9ff906e636cc3)) -* **audits:** fix font-weights for ip popup titels ([5b23d08](https://github.com/frontegg/frontegg-react/commit/5b23d0877faf836193c401b4a2afabcbd3e65d89)) -* **audits:** fix scroll to top on page change for material lib ([1878dd4](https://github.com/frontegg/frontegg-react/commit/1878dd491bc3d86b182f88469841392215d589e8)) -* **audits:** FR-1000 fix updating filter value ([00f84d4](https://github.com/frontegg/frontegg-react/commit/00f84d427db5cf1faaedb69d7025debe0513debf)) -* **audits:** FR-1002 add globe icon when location is unknown ([53265fd](https://github.com/frontegg/frontegg-react/commit/53265fd5cdc56d3fc141ad77469d452e2f6dc430)) -* **audits:** FR-1003 Change severity filters to match the actual filters ([ce9d834](https://github.com/frontegg/frontegg-react/commit/ce9d834e204f7bc75e57b27ba82042ba45fd971a)) -* **audits:** FR-1004 add startRefresh action to prevent double fetch after init render ([eddb763](https://github.com/frontegg/frontegg-react/commit/eddb763711863f3a153679455053a963719b876a)) -* **audits:** FR-1004 prevent call action onPageChange after init render ([bca3bc1](https://github.com/frontegg/frontegg-react/commit/bca3bc14818c01589a5c33ce9f8a1f2c6923a585)) -* **audits:** FR-1004 prevent setFilterData action call after init render ([9656648](https://github.com/frontegg/frontegg-react/commit/9656648fdd5ba7c37fa9474f3f088b24f855b420)) -* **audits:** FR-1004 remove debounce filter, prevent onFilterChange call after init render ([b842cd6](https://github.com/frontegg/frontegg-react/commit/b842cd6186f032750fb6daed3e01e01d5b135498)) -* **audits:** FR-997 reduce margins, change tags to divs in ip popup ([66f0005](https://github.com/frontegg/frontegg-react/commit/66f000585ef74da4d4056d3dd72e6df981955b4c)) -* **audits:** FR-998 change severity attention letters coloring ([efdb0c8](https://github.com/frontegg/frontegg-react/commit/efdb0c8388af715bcdd9657632d5e13c5ca94eb2)) -* **auth:** add missing css variables for authentication pages ([4bb2c66](https://github.com/frontegg/frontegg-react/commit/4bb2c66f292aa1794e456516eb928c156186a0f5)) -* **build:** fix npmrc in github action ([d8ee0ad](https://github.com/frontegg/frontegg-react/commit/d8ee0ad24f6c6f410b04e9b5a71db7d8761e9017)) -* **connectivity:** fix color of the Install button FR-975 ([ac9d61e](https://github.com/frontegg/frontegg-react/commit/ac9d61e7cc329410954f4830a476602a3ab73e49)) -* **connectivity:** fix styles for the material UI ([ca1258d](https://github.com/frontegg/frontegg-react/commit/ca1258d85a29d40b9de59407c41f7e755f1e4206)) -* **core:** fix the z-index value for the popup component ([6201205](https://github.com/frontegg/frontegg-react/commit/620120501945c9e0a8e89add87e466f397bb7421)) -* **elements:** fix className property for the semantic Button element ([8646833](https://github.com/frontegg/frontegg-react/commit/864683387e221ab350f7f3f439918a6b411254d9)) -* **elements:** fix styles and behavior for the semantic InputChip component ([29671d2](https://github.com/frontegg/frontegg-react/commit/29671d2b3ea070e712c1352fbb7356d236d710d9)) -* **elements:** fix styles for the InputChip component in the material library ([8f6404a](https://github.com/frontegg/frontegg-react/commit/8f6404aa9cb659512c17ce3ec7b03e48b6f0f2e4)) - - -### Features - -* **connectivity:** add UI design for the semantic library ([986c86c](https://github.com/frontegg/frontegg-react/commit/986c86cc1d3bc5f35b8a409cbdc9fba7737bb522)) -* **elements:** add the TextArea component to the semantic library ([bacc6c3](https://github.com/frontegg/frontegg-react/commit/bacc6c35eb52cf0bf644503b9ca654f4e3073e50)) - - - - - -## [1.11.1](https://github.com/frontegg/frontegg-react/compare/v1.11.0...v1.11.1) (2020-12-02) - - -### Bug Fixes - -* **auth:** add exports for socialLogins components ([018b5ea](https://github.com/frontegg/frontegg-react/commit/018b5eaa8b4758c38103e7052944ce8047a275b3)) -* **connectivity:** fix styles for separate components ([3b4b8d3](https://github.com/frontegg/frontegg-react/commit/3b4b8d3909942716889f72830796294e846c45d0)) - - - - - -# [1.11.0](https://github.com/frontegg/frontegg-react/compare/v1.10.0...v1.11.0) (2020-11-30) - - -### Bug Fixes - -* **audits:** fix cancel button on audits filter ([007fec7](https://github.com/frontegg/frontegg-react/commit/007fec7f0f826a9cc9b671bfb49af043adb47b00)) -* **auth:** add missing query and hash after login redirect ([#135](https://github.com/frontegg/frontegg-react/issues/135)) ([87f36aa](https://github.com/frontegg/frontegg-react/commit/87f36aa21ddeb3aadc3153411b6679360879d02a)), closes [#134](https://github.com/frontegg/frontegg-react/issues/134) -* [FR-815] change password policy to be aligned with backend ([fbc9abf](https://github.com/frontegg/frontegg-react/commit/fbc9abfa776b9f7ac0a8f3c89eaa5c6a39b320b6)) -* Remove debugger line ([58643c1](https://github.com/frontegg/frontegg-react/commit/58643c19e05fea9b9fbabf507eeccb3595bc4903)) -* **auth:** fix team management roles dropdown ui ([#126](https://github.com/frontegg/frontegg-react/issues/126)) ([c74949b](https://github.com/frontegg/frontegg-react/commit/c74949b2dc409c5af7f00d5d1c0b985c74d3da56)) - - -### Features - -* **ci:** add option to publish prerelease version ([0ff0c67](https://github.com/frontegg/frontegg-react/commit/0ff0c672f86eacf175790b89173f1c6e34789b7e)) - - - - - -# [1.10.0](https://github.com/frontegg/frontegg-react/compare/v1.9.0...v1.10.0) (2020-11-27) - - -### Bug Fixes - -* **audits:** move initData action to Audits.tsx, fix reducer storeName ([28a69b6](https://github.com/frontegg/frontegg-react/commit/28a69b64375652720e2bb9589471b35045d6295c)) -* **audits:** remove comment ([e9fdc6f](https://github.com/frontegg/frontegg-react/commit/e9fdc6f1adfec1c5d6d689b0bf6b006d819418b9)) - - -### Features - -* **connectivity:** add listener ([e463fae](https://github.com/frontegg/frontegg-react/commit/e463faeb097a07959b88cbf4a535bb1871b9e6b3)) - - - - - -# [1.9.0](https://github.com/frontegg/frontegg-react/compare/v1.8.0...v1.9.0) (2020-11-25) - - -### Bug Fixes - -* fix console errors ([47a0679](https://github.com/frontegg/frontegg-react/commit/47a0679cb426eeb09bd5d97e0b28fe697c24e3b2)), closes [#119](https://github.com/frontegg/frontegg-react/issues/119) -* **auth:** fix social logins loader ([#116](https://github.com/frontegg/frontegg-react/issues/116)) ([e965d3d](https://github.com/frontegg/frontegg-react/commit/e965d3db6a423a0457dc89471418f70c57f2e856)) -* **core:** fix external finterFunction ([72419cb](https://github.com/frontegg/frontegg-react/commit/72419cb98c279356c30aa3a736f9a0ccd6f285ce)) -* **integrations:** add styles to the accordion ([0b74561](https://github.com/frontegg/frontegg-react/commit/0b74561dae589ba603acef322114c3ca51b8e0a2)) -* **integrations:** fix problem with search ([eb2bf94](https://github.com/frontegg/frontegg-react/commit/eb2bf9425db0d2a7290acd2ad2ce7a6531ec96a9)) -* **integrations:** fix styles for webhooks ([436c68d](https://github.com/frontegg/frontegg-react/commit/436c68d35cdf8e017b71a701c8c23cd8c4577626)) -* **integrations:** fix the email icon and some styles ([20665eb](https://github.com/frontegg/frontegg-react/commit/20665ebc611b488a1b70739395d53d8c5bb86593)) -* **integrations:** fix validations ([f9c6c15](https://github.com/frontegg/frontegg-react/commit/f9c6c15b541abe38351f745182f81f772c3e3b40)) - - -### Features - -* **auth:** New AccountDropdown component added to AuthPlugin ([018f2f8](https://github.com/frontegg/frontegg-react/commit/018f2f8db3ad22981f9270bd2e166b56cf6eb7ff)) -* **core:** add dupport the ClassName property to the Table component ([71a582d](https://github.com/frontegg/frontegg-react/commit/71a582d69e44130cb164a5951e6f1225406cb7e7)) -* **core:** add support the fullWidth property to the InputChip component ([f1c6586](https://github.com/frontegg/frontegg-react/commit/f1c65869939d176aceb9eb8dcd9d8c4d42592caa)) -* **elements:** add dupport the ClassName property to the Table component ([610ba5a](https://github.com/frontegg/frontegg-react/commit/610ba5a6cd8e432da5ad15c631621b27eb329563)) -* **integrations:** add an accordion to the catagory data ([0bfa866](https://github.com/frontegg/frontegg-react/commit/0bfa866651744af85b4474f4f95a51123391edc6)) -* **integrations:** add icons to the list of platforms ([28dc591](https://github.com/frontegg/frontegg-react/commit/28dc5917cdbcbf839c10db0e811d0518ee285398)) -* **integrations:** add search by events and categories ([6785a81](https://github.com/frontegg/frontegg-react/commit/6785a81fcaca327043f56b077c16a95878305152)) -* New plugin for Audits ([#106](https://github.com/frontegg/frontegg-react/issues/106)) ([921b37a](https://github.com/frontegg/frontegg-react/commit/921b37aca98f23c800c8b5d094ed595d52617679)) - - - - - -# [1.8.0](https://github.com/frontegg/frontegg-react/compare/v1.7.0...v1.8.0) (2020-11-23) - - -### Features - -* Add Connectivity Plugin ([#98](https://github.com/frontegg/frontegg-react/issues/98)) ([db77431](https://github.com/frontegg/frontegg-react/commit/db77431b6d744b93431430543019fedd1c0dbae2)) -* **auth:** Add support in google and github social logins ([#111](https://github.com/frontegg/frontegg-react/issues/111)) ([938b04c](https://github.com/frontegg/frontegg-react/commit/938b04cba618e2029b55ff4c39d5c0fc0d884e6b)) - - - - - -# [1.7.0](https://github.com/frontegg/frontegg-react/compare/v1.6.1...v1.7.0) (2020-11-22) - - -### Bug Fixes - -* remove auth deps from core lib ([1686abd](https://github.com/frontegg/frontegg-react/commit/1686abd9f2dd7a92b997d8a6e649be70aff5b29b)) - - -### Features - -* resolve saga actions outside fronteggprovider ([7878beb](https://github.com/frontegg/frontegg-react/commit/7878bebf49b5131fcdf16bbd21c1bcab03c2d1ae)) - - - - - -## [1.6.2](https://github.com/frontegg/frontegg-react/compare/v1.6.1...v1.6.2) (2020-11-19) - -**Note:** Version bump only for package @fronteg/react - - - - - -## [1.6.1](https://github.com/frontegg/frontegg-react/compare/v1.6.0...v1.6.1) (2020-11-15) - - -### Bug Fixes - -* **auth:** fix multiple call to loadUsers while mounting TeamTable ([4a51547](https://github.com/frontegg/frontegg-react/commit/4a51547cf5cd86905d3c760af13016a3751bab0b)) -* **auth:** fix remove last role in TeamTable ([22d2ff6](https://github.com/frontegg/frontegg-react/commit/22d2ff60715249c13c7a454f255a371170504887)) -* **auth:** redirect user to login with two-factor after saml if required ([e6abd6a](https://github.com/frontegg/frontegg-react/commit/e6abd6a04ff6b7e62eb335cd71c19382cdd9472a)) -* **auth:** remain user data after editing roles in TeamTable ([186aaa4](https://github.com/frontegg/frontegg-react/commit/186aaa4827f87dd27d35375d1c35fd8a1818c6d6)), closes [#99](https://github.com/frontegg/frontegg-react/issues/99) -* fix multiple store initialization in strict-mode ([a569f86](https://github.com/frontegg/frontegg-react/commit/a569f86b37292e71b985c3a2e54610121ab419ce)) -* restore test-id to forgot password button ([b8a4ab4](https://github.com/frontegg/frontegg-react/commit/b8a4ab448c5c3fd45e7ad4a1189242d27d3f5822)) - - - - - -# [1.6.0](https://github.com/frontegg/frontegg-react/compare/v1.5.0...v1.6.0) (2020-11-12) - - -### Bug Fixes - -* fix pagination bug in TeamTable ([8ba1c3d](https://github.com/frontegg/frontegg-react/commit/8ba1c3d861257231b1890766c5042cba58998965)) - - -### Features - -* add option to logout from FronteggContext object ([e35b4f0](https://github.com/frontegg/frontegg-react/commit/e35b4f0e8d79660641676257aa5440d2f2bf84ef)) -* add option to upload profile image ([#96](https://github.com/frontegg/frontegg-react/issues/96)) ([0e4c45c](https://github.com/frontegg/frontegg-react/commit/0e4c45cb08a84519e1f2ebb06295af26cdc05ff7)) - - - - - -# [1.5.0](https://github.com/frontegg/frontegg-react/compare/v1.4.0...v1.5.0) (2020-11-10) - - -### Bug Fixes - -* export missing interface AcceptInvitationState ([#92](https://github.com/frontegg/frontegg-react/issues/92)) ([9981fe1](https://github.com/frontegg/frontegg-react/commit/9981fe1921d1517aea1a4aa4c484a4974bbc464a)) - - -### Features - -* sync session between tabs on Auth Listener ([f2bfa04](https://github.com/frontegg/frontegg-react/commit/f2bfa04bb452f8a5ad165b4f9c382ce3fb07e105)) - - - - - -# [1.4.0](https://github.com/frontegg/frontegg-react/compare/v1.3.0...v1.4.0) (2020-11-09) - - -### Bug Fixes - -* **build:** fix prerelese versioning ([#83](https://github.com/frontegg/frontegg-react/issues/83)) ([07d1544](https://github.com/frontegg/frontegg-react/commit/07d1544562ba83aa5dbffa931117a0bd9286026c)) -* add option to add user without roles ([4d17333](https://github.com/frontegg/frontegg-react/commit/4d17333fc0f157d3c5d4462f20d8f2269b579a65)) -* css enhancements ([317e875](https://github.com/frontegg/frontegg-react/commit/317e8756e7c56deaa0d4c16ce699a7b9ebe5f2e5)) -* disable angular render children ([658dbbf](https://github.com/frontegg/frontegg-react/commit/658dbbf05319224caf326adca2b90da23eedefe0)) -* make redux internal deps ([#88](https://github.com/frontegg/frontegg-react/issues/88)) ([331e8c5](https://github.com/frontegg/frontegg-react/commit/331e8c5e4ee518ffd4918b41df0e3fb3cdd3809e)) -* remove default font-size from root css ([ad774e9](https://github.com/frontegg/frontegg-react/commit/ad774e95b1199efaf53951f3b2a0df52c1bd2900)), closes [#90](https://github.com/frontegg/frontegg-react/issues/90) -* **teams:** remove roles columns if no roles configured ([7078eba](https://github.com/frontegg/frontegg-react/commit/7078ebaa57cfd46ed9f644e7802a354853c28fb9)) -* remove logs ([16b0976](https://github.com/frontegg/frontegg-react/commit/16b09762f77e8c4491e1570b954a1c04511ba53f)) -* remove memorized store ([b4d2b25](https://github.com/frontegg/frontegg-react/commit/b4d2b2550c3c54220866fbc7540014b279aa12f9)) - - -### Features - -* notifications plugin ([#78](https://github.com/frontegg/frontegg-react/issues/78)) ([0439d17](https://github.com/frontegg/frontegg-react/commit/0439d179ed5c0abae510b7d132dbf03ae907f7f6)) - - - - - -# [1.3.0](https://github.com/frontegg/frontegg-react/compare/v1.2.0...v1.3.0) (2020-11-04) - - -### Bug Fixes - -* **auth:** bug fixes ([#76](https://github.com/frontegg/frontegg-react/issues/76)) ([a758642](https://github.com/frontegg/frontegg-react/commit/a758642458751e930dc4ea6de6acc50b3ede0b77)) -* **auth:** fix ui bugs ([#79](https://github.com/frontegg/frontegg-react/issues/79)) ([0e75d8d](https://github.com/frontegg/frontegg-react/commit/0e75d8dff80937dc2e4308a0ffdd527a12a84a39)) -* calling `response.text()` after `response.json()` fails ([#80](https://github.com/frontegg/frontegg-react/issues/80)) ([3cde90d](https://github.com/frontegg/frontegg-react/commit/3cde90db8a5e9f1850dbf51db492f94e77cce93e)) -* fix material button console errors ([5558c52](https://github.com/frontegg/frontegg-react/commit/5558c52c61276847109d137b698fd857ffbfcf2e)) -* fix material table head position sticky ([99a9423](https://github.com/frontegg/frontegg-react/commit/99a9423e43596d932f1e1f234e2dc569c5e166eb)) - - -### Features - -* add elements page ([9eb19a8](https://github.com/frontegg/frontegg-react/commit/9eb19a886a4cbc788ad236ce9f597c33da7f68ef)) -* **auth:** add options to update user roles ([3ec734a](https://github.com/frontegg/frontegg-react/commit/3ec734a79dce6df707562a4555e9d7bf124f85a1)) -* **elements:** add FeInput element ([f23e439](https://github.com/frontegg/frontegg-react/commit/f23e4392ab849d32c5e0ba67e055f793ecfd5ceb)) -* add support for nextjs and angular ([#82](https://github.com/frontegg/frontegg-react/issues/82)) ([5fa36eb](https://github.com/frontegg/frontegg-react/commit/5fa36ebe7bfa6866c78455a746727ba8b1cafbbc)) - - - - - -# [1.2.0](https://github.com/frontegg/frontegg-react/compare/v1.1.0...v1.2.0) (2020-10-25) - - -### Bug Fixes - -* **ci:** add conventional-commits to prereleases ([#58](https://github.com/frontegg/frontegg-react/issues/58)) ([d0941cc](https://github.com/frontegg/frontegg-react/commit/d0941ccaa279dbb4901563f9f6391ffcbc44dc06)) -* **ci:** add pre-release action ([07bd99d](https://github.com/frontegg/frontegg-react/commit/07bd99db45c76d43a71b9e0aaf316e6720e9ad67)) -* **elements:** UI elements small fixes ([effb9bd](https://github.com/frontegg/frontegg-react/commit/effb9bd54186133184010c34874af212c040ef90)) -* **packaging:** downgrade typescript to 3.7.5 ([10294fc](https://github.com/frontegg/frontegg-react/commit/10294fc3f6c2f5ade727d2e05070a978fe1c1cc7)) -* **packaging:** remove tesrser from build steps ([a4ff453](https://github.com/frontegg/frontegg-react/commit/a4ff453ca63f5a0b0c4ae2399e0b63779f17d825)) -* **packaging:** revert typescript to 3.9.7 ([#56](https://github.com/frontegg/frontegg-react/issues/56)) ([cb1a4dd](https://github.com/frontegg/frontegg-react/commit/cb1a4ddf8c47f3a3ceb7a1bea6d81a800c8b4a84)) -* restore old react and checkout from release branch ([adbff2e](https://github.com/frontegg/frontegg-react/commit/adbff2e9b28248ae9d292b633fde4233b853a29c)) - - -### Features - -* **auth:** add SSO components ([#64](https://github.com/frontegg/frontegg-react/issues/64)) ([f083762](https://github.com/frontegg/frontegg-react/commit/f0837623073c1f9a636480c6a88d5969fb020a09)) -* **auth:** support all auth functionalities ([#70](https://github.com/frontegg/frontegg-react/issues/70)) ([26d725e](https://github.com/frontegg/frontegg-react/commit/26d725e2f386c6b4a703e4371fac8efef29676d1)) -* **core:** add dynamic elements for Frontegg Components ([#10](https://github.com/frontegg/frontegg-react/issues/10)) ([2bddbb2](https://github.com/frontegg/frontegg-react/commit/2bddbb2794ac0dc4af90c8df0f33b69a312e063a)) -* **core:** add FeSwitchToggle component ([#66](https://github.com/frontegg/frontegg-react/issues/66)) ([5b6c603](https://github.com/frontegg/frontegg-react/commit/5b6c603d912be45b1982c06b7f026badd8e82f1c)) -* **core:** Add FeTabs component ([3699299](https://github.com/frontegg/frontegg-react/commit/36992997e64907345a254a4b44580759705186bb)), closes [#67](https://github.com/frontegg/frontegg-react/issues/67) - - - - - -# [1.1.0](https://github.com/frontegg/frontegg-react/compare/v1.0.89...v1.1.0) (2020-10-14) - - -### Bug Fixes - -* **auth:** fix loading splitted sagas in AuthPLugin ([d0fba43](https://github.com/frontegg/frontegg-react/commit/d0fba436bd442ff047849397d8bedcc897e16b1c)) -* **auth:** fix saga initializing bug ([80727a3](https://github.com/frontegg/frontegg-react/commit/80727a3e65b3d34ff455a8d0c252495ab3731c48)) -* **packaging:** add missing immer dependency ([#52](https://github.com/frontegg/frontegg-react/issues/52)) ([36c6c15](https://github.com/frontegg/frontegg-react/commit/36c6c1583809a532885e65a8c2c375151ad8b9dc)), closes [#51](https://github.com/frontegg/frontegg-react/issues/51) - - -### Features - -* **auth:** add accept invitation component by url ([#50](https://github.com/frontegg/frontegg-react/issues/50)) ([c3a43d6](https://github.com/frontegg/frontegg-react/commit/c3a43d60dad3fc8da9cffc6a81f468b5671d3af9)) -* **auth:** add Team (reducer/saga) to Auth Plugin ([7bed273](https://github.com/frontegg/frontegg-react/commit/7bed27378efe32c9e9091495d0ac4a3f268b206c)) -* **auth:** add TeamAPI to frontegg/react-core api.team collection ([600a8f8](https://github.com/frontegg/frontegg-react/commit/600a8f81a0322702d22dc2abede93d271d1c81f7)) - - - - - -## [1.0.89](https://github.com/frontegg/frontegg-react/compare/v1.0.88...v1.0.89) (2020-10-13) - - -### Bug Fixes - -* **cli:** fix missing property in frontegg/react-cli ([2d45c3f](https://github.com/frontegg/frontegg-react/commit/2d45c3f2c44c4531e72434cb7935a42c28012992)), closes [#44](https://github.com/frontegg/frontegg-react/issues/44) -* **cli:** fix missing property in frontegg/react-cli ([b468a08](https://github.com/frontegg/frontegg-react/commit/b468a0845b070d28c4f6c60dbb1cc982e6af1f72)), closes [#43](https://github.com/frontegg/frontegg-react/issues/43) -* **cli:** upload cypress failure artifacts ([b23aa60](https://github.com/frontegg/frontegg-react/commit/b23aa60016dee1b8089bafd417664263241d8c84)) - - - - - -## [1.0.88](https://github.com/frontegg/frontegg-react/compare/v1.0.87...v1.0.88) (2020-10-11) - - -### Bug Fixes - -* **ci:** fix npmrc file ([97df62e](https://github.com/frontegg/frontegg-react/commit/97df62e26446ec42c655b03b4dc7c41d25f13d9e)) -* **cli:** add space between commit scope and summary text ([ef6fec0](https://github.com/frontegg/frontegg-react/commit/ef6fec04c4e84e1ae7b4f77d4fccef62694bd1c7)) -* **cli:** fix create pull request action ([daf216d](https://github.com/frontegg/frontegg-react/commit/daf216d03c7423408c1fa534b8af65e81a7eaccb)) -* **cli:** increase max size of the summary ([f7243c1](https://github.com/frontegg/frontegg-react/commit/f7243c1c865c46530b05df7b94e42a52a7050892)) - - - - - -## [1.0.87](https://github.com/frontegg/frontegg-react/compare/v1.0.86...v1.0.87) (2020-10-09) - - -### Bug Fixes - -* throw error if prettier check failed ([446e86c](https://github.com/frontegg/frontegg-react/commit/446e86c9b73dbdcbec925f3930580efca7b1effa)) -* **ci:** add changelog content to release pull request ([b196013](https://github.com/frontegg/frontegg-react/commit/b196013779aa620652d9ab43d537c85f1e989502)) -* **ci:** checkout with full hisotry for lerna conventional-commits ([4846fe2](https://github.com/frontegg/frontegg-react/commit/4846fe239496ced6a2cabe7d809fdcea410c4e9d)) -* **ci:** checkout with history to generate changelog ([307057c](https://github.com/frontegg/frontegg-react/commit/307057c8ea89aa863449392623db1372039881a7)) -* **cli:** add missing tslib to cli package json ([de0bc6e](https://github.com/frontegg/frontegg-react/commit/de0bc6e2f7558077eef8c7c1aeb815ff561f000e)) -* **packaging:** move cjs.js to index.js for main entry point ([0f8f700](https://github.com/frontegg/frontegg-react/commit/0f8f70016566a8f1940d16441a5afa1707dc02a2)) -* **security:** remove option for members to create new releases ([d9f8607](https://github.com/frontegg/frontegg-react/commit/d9f8607adaaeb5933ffc27021b798ac731cefcc0)) - - - - - -## 1.0.85 (2020-10-08) - -**Note:** Version bump only for package @fronteg/react - - - - - -## 1.0.84 (2020-10-08) - -**Note:** Version bump only for package @fronteg/react + + + +# [1.24.0](https://github.com/frontegg/frontegg-react/compare/v1.23.1...v1.24.0) (2021-03-03) + + +### Bug Fixes + +* **audits:** align icon and add cursor pointer on icons ([c6977b0](https://github.com/frontegg/frontegg-react/commit/c6977b006c2086600ed42c36aee29116871a457f)) + + +### Features + +* account-settings ([134b7b0](https://github.com/frontegg/frontegg-react/commit/134b7b0a8f2b33630ded395cbef90eb6929d7754)) +* **auth:** change social login redriect url behavior ([7199487](https://github.com/frontegg/frontegg-react/commit/7199487a71b524b9de7843048ac6f92836f8b592)) + + + + + +## [1.23.1](https://github.com/frontegg/frontegg-react/compare/v1.23.0...v1.23.1) (2021-02-21) + +**Note:** Version bump only for package @fronteg/react + + + + + +# [1.23.0](https://github.com/frontegg/frontegg-react/compare/v1.22.1...v1.23.0) (2021-02-18) + + +### Features + +* **audits:** add several custom browser icons to the User Agent field ([8bba084](https://github.com/frontegg/frontegg-react/commit/8bba0841ec39bc6b9c04100abc437964464907b1)) +* **auth:** support render prop ([f96ca8b](https://github.com/frontegg/frontegg-react/commit/f96ca8b2fe0ff90abaa502ec7ad639e1380af254)) + + + + + +## [1.22.1](https://github.com/frontegg/frontegg-react/compare/v1.22.0...v1.22.1) (2021-02-16) + + +### Bug Fixes + +* Fix ActivateAccount test ([bbf3781](https://github.com/frontegg/frontegg-react/commit/bbf37817feccdc1331e92b829b39d33d0461052e)) +* **auth:** fix after activation refirect to look on the last user requested route ([fcb53ef](https://github.com/frontegg/frontegg-react/commit/fcb53ef8f3a1b1397f71eb751c33ed221caa0064)) + + + + + +# [1.22.0](https://github.com/frontegg/frontegg-react/compare/v1.21.1...v1.22.0) (2021-02-13) + + +### Bug Fixes + +* **audits:** fix nullable conversion case in audits table ([0acacdf](https://github.com/frontegg/frontegg-react/commit/0acacdf2e1b14062cb557a96e5b6d9c7cb966087)) +* **auth:** small fixes ([1463b88](https://github.com/frontegg/frontegg-react/commit/1463b88eb51b048abb7a57a53f58b492b6cc1adf)) +* **connectivity:** fix scroll for the event table ([7871686](https://github.com/frontegg/frontegg-react/commit/78716867a077d30d9cf8698424e25f08f8e4591d)) +* **core:** remove unused imported components in the FileInput component ([54a7f29](https://github.com/frontegg/frontegg-react/commit/54a7f29a778eeb0a2f609c3239c85e8244562c90)) + + +### Features + +* **auth:** autosave profile photo ([77a3ec4](https://github.com/frontegg/frontegg-react/commit/77a3ec4311ca959b9ae8224f2a56993402a3b7b4)) +* **auth:** instead of text in the user agent field, now show an icon of a browser ([8267338](https://github.com/frontegg/frontegg-react/commit/82673384710144597e484676c30f6569628399a3)) +* **auth:** split process update inforamtion between photo and inforamtion ([0140836](https://github.com/frontegg/frontegg-react/commit/0140836d691e87a8235ca1d3612f7b9881311747)) +* **core:** add support ref for the input component ([f47fc79](https://github.com/frontegg/frontegg-react/commit/f47fc79e57738c867d7eb5574caa259f5598633a)) +* **core:** add suppurt ref for the FileInput component ([86b3790](https://github.com/frontegg/frontegg-react/commit/86b3790c35c44cfddb9ceee76b19cd2428b5cccd)) +* **core:** Added tab disabling for FeTabs component; disabled pwd tab in Profile FR-789 ([2354f47](https://github.com/frontegg/frontegg-react/commit/2354f47a5d0fe22e05b3e869b7e963192cd86b45)) +* **elements:** add support ref for Input elements in UI libraries ([39c1ebc](https://github.com/frontegg/frontegg-react/commit/39c1ebc05262aa0f1ee47dbae8c23bb37d0a0a0d)) + + + + + +## [1.21.1](https://github.com/frontegg/frontegg-react/compare/v1.21.0...v1.21.1) (2021-02-04) + +**Note:** Version bump only for package @fronteg/react + + + + + +# [1.21.0](https://github.com/frontegg/frontegg-react/compare/v1.20.1...v1.21.0) (2021-02-04) + + +### Bug Fixes + +* **audits:** fix show filter icon if the filterable value is desabled ([00f0afd](https://github.com/frontegg/frontegg-react/commit/00f0afd19f3d87deff66c198fe115d2f1e7d6708)) +* **audits:** FR-1875 - changed time filter format ([24e58a2](https://github.com/frontegg/frontegg-react/commit/24e58a2c742ac660a92add0035db0e2a4adac12e)) +* **audits:** leave the empty values in the cell instead of text ([360c09c](https://github.com/frontegg/frontegg-react/commit/360c09c6b9d47cd0a840851dec265ddd96463d96)) +* **auth:** fix caching photo in the profile page ([2acb7e7](https://github.com/frontegg/frontegg-react/commit/2acb7e70186cda2607c16223c9499c4abab92a17)) +* **auth:** Fix redirect after reset password succeeded ([e20d9f4](https://github.com/frontegg/frontegg-react/commit/e20d9f46d345c06f2426e91448835d23e42dc239)) +* **auth:** Remove go to login from activate account succeeded ([e1a8746](https://github.com/frontegg/frontegg-react/commit/e1a8746ad7c1a246038818232967052641d0a040)) +* **core:** leave the empty values in the cell instead of text in the FeTable ([8997a53](https://github.com/frontegg/frontegg-react/commit/8997a53a48e53e6a220bc8d709e94674a49b1519)) +* Remove deprecated @frontegg/react support ([48f493c](https://github.com/frontegg/frontegg-react/commit/48f493cafb98dfcf66096c6f2a577c067c5c8bdf)) + + +### Features + +* Add option to inject SSO components without routes ([79dd172](https://github.com/frontegg/frontegg-react/commit/79dd17267da92c1d8bb651fe6210d3c6b5b42519)) +* **auth:** Add silent logout saga action ([f5781f7](https://github.com/frontegg/frontegg-react/commit/f5781f720d8944ef23e2b57653c7f816f3cce4d8)) +* **auth:** Auto login after activate account succeeded ([eebdd71](https://github.com/frontegg/frontegg-react/commit/eebdd710199c505f927d8446079f70c22f25909b)) + + + + + +## [1.20.1](https://github.com/frontegg/frontegg-react/compare/v1.20.0...v1.20.1) (2021-02-02) + + +### Bug Fixes + +* **connectivity:** fix the delete dialog message disappers ([f40e50a](https://github.com/frontegg/frontegg-react/commit/f40e50a36ee9f184f0d8b5bddffa6842ed827605)) + + + + + +# [1.20.0](https://github.com/frontegg/frontegg-react/compare/v1.19.1...v1.20.0) (2021-02-01) + + +### Bug Fixes + +* **connectivity:** fix icons on the list of platform ([8de499c](https://github.com/frontegg/frontegg-react/commit/8de499c6e50faf29ad2198f9d0661d38d7201394)) +* **connectivity:** fix send the security parameter for the webhook configuration ([cbc8bbf](https://github.com/frontegg/frontegg-react/commit/cbc8bbf9e347d5561169f0cfbf724b3f6d1c6e29)) +* **core:** fix close tolltip popup ([f78cc43](https://github.com/frontegg/frontegg-react/commit/f78cc437d812be6d04e0f65c1664da9ce609fae6)) +* **core:** fiz a problem with caching filters data in the FeTable componet ([319d7be](https://github.com/frontegg/frontegg-react/commit/319d7bec60e88c146f98d5825770ed8721ce0637)) +* **core:** remove required field for the secret field in the webhook configuration ([fe97d7e](https://github.com/frontegg/frontegg-react/commit/fe97d7e23e6148a381153daa44fae08484733184)) + + +### Features + +* **audits:** changes component for posibility use in the deshboard project ([a6365b8](https://github.com/frontegg/frontegg-react/commit/a6365b8aa65702bf0299a3f08d1147b8788a1890)) +* **auth:** add option to keep sessions alive via the AuthPlugin props ([8fd2427](https://github.com/frontegg/frontegg-react/commit/8fd2427cf1d562b12f0657300c526f19d286ccc4)) +* **core:** add TExportAudits type as separet type in interfaces ([fcc1992](https://github.com/frontegg/frontegg-react/commit/fcc199294ae10abc85102b1c2345a4bdcad6c78b)) +* **core:** implement horizontal scrolling in the table component ([1315b75](https://github.com/frontegg/frontegg-react/commit/1315b75aa92abeace8b2ea811f62efbb6e8db6c7)) + + + + + +## [1.19.1](https://github.com/frontegg/frontegg-react/compare/v1.19.0...v1.19.1) (2021-01-20) + +**Note:** Version bump only for package @fronteg/react + + + + + +# [1.19.0](https://github.com/frontegg/frontegg-react/compare/v1.18.6...v1.19.0) (2021-01-20) + + +### Bug Fixes + +* Disable MFA input auto complete ([#254](https://github.com/frontegg/frontegg-react/issues/254)) ([b7420c6](https://github.com/frontegg/frontegg-react/commit/b7420c627850887d17bf5b24a2e4bf5a75ded798)) +* **connectivity:** fix sorting the Status column in the webhooks list ([c7d1428](https://github.com/frontegg/frontegg-react/commit/c7d14282d349c7cd7a952a431361faee5d53f893)) +* **connectivity:** remove old dead code on the webhooks list ([609bfe0](https://github.com/frontegg/frontegg-react/commit/609bfe0116c6b8d64cabc14a9aaa3cca9ff5b8da)) +* **connectivity:** remove warnings in the devTools for some svg elements ([dde9731](https://github.com/frontegg/frontegg-react/commit/dde973150f9dce8043b6d99d9b7c8339fb014df6)) +* **core:** add support the data-* attribute for the CheckBox component ([bbcc4d5](https://github.com/frontegg/frontegg-react/commit/bbcc4d59d972254483b83329839e2c29251b8aed)) +* **core:** fix align for the label in the CheckBox ([d094a09](https://github.com/frontegg/frontegg-react/commit/d094a090a95bbf0b3fa61661ea84c1359b10db17)) +* **core:** fix click by form button if process loading is active ([5a9971c](https://github.com/frontegg/frontegg-react/commit/5a9971ce1a4741ac2b73f6fa01c52a9e84fd9905)) + + +### Features + +* **connectivity:** add error message from the server to the webhook component ([7b53d9f](https://github.com/frontegg/frontegg-react/commit/7b53d9f66eed8e32127abbb4ca95a3d60c743a43)) +* **connectivity:** add loading when changes status in the webhooks list ([b834ac6](https://github.com/frontegg/frontegg-react/commit/b834ac6570eff62d5629445fac4417baee9d5ea0)) +* **connectivity:** add sorting data by columns in the webhooks list ([6fc81b6](https://github.com/frontegg/frontegg-react/commit/6fc81b654a164654cde7821f430621c0344d5974)) +* **connectivity:** change behaviors of select catagory and envents ([368849e](https://github.com/frontegg/frontegg-react/commit/368849e427b384b8203fa6ab28cf6dc25be522fd)) +* **connectivity:** Disabled the V mark if no one of events is active. ([7cea078](https://github.com/frontegg/frontegg-react/commit/7cea078dc70939dc9c3b2fa28af495d1e2d988d9)) +* **core:** add support the sortType param to the Column values for the FeTable component ([9702cee](https://github.com/frontegg/frontegg-react/commit/9702cee6b9c80c7e2bb9db126a4e93d9935a25d0)) + + +### Performance Improvements + +* **connectivity:** move handlers to the useCallback hook in the AccordionCategories ([b89826b](https://github.com/frontegg/frontegg-react/commit/b89826bbd009d05c9f7a52c8d2ccaadae3a2b896)) +* **core:** move onChange handler to the useCallback hook ([25c78da](https://github.com/frontegg/frontegg-react/commit/25c78dafe667accee048acbb9a916fd4ad91b0fc)) + + + + + +## [1.18.6](https://github.com/frontegg/frontegg-react/compare/v1.18.5...v1.18.6) (2021-01-19) + + +### Bug Fixes + +* **core:** Fix toggled expandable button svg color FR-1173 ([bcf486d](https://github.com/frontegg/frontegg-react/commit/bcf486d5334a0fade4305bac70fb37b8ec63a53f)) +* Add more space to first column in Table components FR-1171 ([c0b6b38](https://github.com/frontegg/frontegg-react/commit/c0b6b38479b52b3fce66439a640be8e7b4a59809)) +* **auth:** Ellipses user name in AccountDropdown component ([d3b5ecf](https://github.com/frontegg/frontegg-react/commit/d3b5ecf05e4f9dcf42a44fdebc6caa14cd7b43c5)) +* **auth:** stop loading if error api-tokens FR-1366 ([d245449](https://github.com/frontegg/frontegg-react/commit/d245449e2ee622a49abd34cbef071078a687aa9f)) +* **core:** Break user full name when no free space available FR-1528 ([da508f4](https://github.com/frontegg/frontegg-react/commit/da508f4bf7e59b8fbb2a232a3f0aec97ff8b2e0c)) + + + + + +## [1.18.5](https://github.com/frontegg/frontegg-react/compare/v1.18.4...v1.18.5) (2021-01-18) + + +### Bug Fixes + +* **core:** fix perfomance for the INputChip component ([cea5e19](https://github.com/frontegg/frontegg-react/commit/cea5e19aef64a6cf42f75d3cbb77cd74fb01f560)) +* Reset state on session expiration ([713310a](https://github.com/frontegg/frontegg-react/commit/713310aa183829c46f536b90f871d92496a5621a)) + + + + + +## [1.18.4](https://github.com/frontegg/frontegg-react/compare/v1.18.3...v1.18.4) (2021-01-18) + +**Note:** Version bump only for package @fronteg/react + + + + + +## [1.18.3](https://github.com/frontegg/frontegg-react/compare/v1.18.2...v1.18.3) (2021-01-17) + + +### Bug Fixes + +* **auth:** fix activate button ([#240](https://github.com/frontegg/frontegg-react/issues/240)) ([dfc369c](https://github.com/frontegg/frontegg-react/commit/dfc369c2ade168ca2b4af30c0eb34fac59b4da35)) + + + + + +## [1.18.2](https://github.com/frontegg/frontegg-react/compare/v1.18.1...v1.18.2) (2021-01-15) + +**Note:** Version bump only for package @fronteg/react + + + + + +## [1.18.1](https://github.com/frontegg/frontegg-react/compare/v1.18.0...v1.18.1) (2021-01-15) + + +### Bug Fixes + +* **auth:** change oidc icon to correct ([#235](https://github.com/frontegg/frontegg-react/issues/235)) ([95bf5dc](https://github.com/frontegg/frontegg-react/commit/95bf5dc83225c0710e9ec1927d4f4b536d621c1a)) + + + + + +# [1.18.0](https://github.com/frontegg/frontegg-react/compare/v1.17.3...v1.18.0) (2021-01-14) + + +### Bug Fixes + +* Reload profile data on profile component mount ([0791ffa](https://github.com/frontegg/frontegg-react/commit/0791ffaf867ad339b8bf24820e15dc3ca107aa6d)) +* Reset Frontegg store after logout ([37d1de8](https://github.com/frontegg/frontegg-react/commit/37d1de8e816fec9e671ec1eff5e139b99e857941)) +* **core:** fix close property for the FePopup component ([e9f9d85](https://github.com/frontegg/frontegg-react/commit/e9f9d85fbc51c3e83e789fe326acf5117f0d47ca)) + + +### Features + +* **core:** support enter data on blur event in the InputChip element ([6f43239](https://github.com/frontegg/frontegg-react/commit/6f43239fe2ab03f794d8eedc8742eb811f4567de)) + + + + + +## [1.17.3](https://github.com/frontegg/frontegg-react/compare/v1.17.2...v1.17.3) (2021-01-13) + + +### Bug Fixes + +* **auth:** fix scopes for github login ([#230](https://github.com/frontegg/frontegg-react/issues/230)) ([c73f0c3](https://github.com/frontegg/frontegg-react/commit/c73f0c32d58ce3727db9f1782fe60d4d946932b1)) + + + + + +## [1.17.2](https://github.com/frontegg/frontegg-react/compare/v1.17.1...v1.17.2) (2021-01-12) + +**Note:** Version bump only for package @fronteg/react + + + + + +## [1.17.1](https://github.com/frontegg/frontegg-react/compare/v1.17.0...v1.17.1) (2021-01-05) + +**Note:** Version bump only for package @fronteg/react + + + + + +# [1.17.0](https://github.com/frontegg/frontegg-react/compare/v1.16.2...v1.17.0) (2021-01-03) + + +### Bug Fixes + +* **auth:** fix comments ([d1a826f](https://github.com/frontegg/frontegg-react/commit/d1a826f78211514b6853f4a444bb202737f4053b)) +* **auth:** fix FR-1304 ([#220](https://github.com/frontegg/frontegg-react/issues/220)) ([c540512](https://github.com/frontegg/frontegg-react/commit/c540512b62e4eafc85f546da952360297c56410b)) + + +### Features + +* **auth:** Add manage authorization step to sso FR-1148 ([f961e74](https://github.com/frontegg/frontegg-react/commit/f961e74ef163fa5893afc999ed360763b3172297)) +* **core:** set company name as optional on singup form ([#214](https://github.com/frontegg/frontegg-react/issues/214)) ([de83d17](https://github.com/frontegg/frontegg-react/commit/de83d170cb0bf35288e5c69891f902e754ad5ff3)) + + + + + +## [1.16.2](https://github.com/frontegg/frontegg-react/compare/v1.16.0...v1.16.2) (2020-12-27) + + +### Bug Fixes + +* **auth:** fix force mfa recovery code not showing up ([#204](https://github.com/frontegg/frontegg-react/issues/204)) ([f6ce5ac](https://github.com/frontegg/frontegg-react/commit/f6ce5ac2be84c931454051760ae3fcca232b6c12)) +* **auth:** fix some minot texts and css issues on MFA ([#203](https://github.com/frontegg/frontegg-react/issues/203)) ([688cbc7](https://github.com/frontegg/frontegg-react/commit/688cbc75fb1a74730d433d0026841856f666018d)) +* Fix force MFA screen bugs ([#196](https://github.com/frontegg/frontegg-react/issues/196)) ([8d51fb9](https://github.com/frontegg/frontegg-react/commit/8d51fb9794d0d0728bd04a742ce6a3f77845d1fe)) +* Fix pre-release action ([6b7f616](https://github.com/frontegg/frontegg-react/commit/6b7f6164660323982bc290f7bc6d853b8b4a075d)) +* **auth:** add support in dynamic component in login and signup for socail logins ([#192](https://github.com/frontegg/frontegg-react/issues/192)) ([195b977](https://github.com/frontegg/frontegg-react/commit/195b97704618135cb17edc7569019f236dda7fd6)) + + + + + +## [1.16.1](https://github.com/frontegg/frontegg-react/compare/v1.16.0...v1.16.1) (2020-12-27) + + +### Bug Fixes + +* **auth:** fix force mfa recovery code not showing up ([#204](https://github.com/frontegg/frontegg-react/issues/204)) ([f6ce5ac](https://github.com/frontegg/frontegg-react/commit/f6ce5ac2be84c931454051760ae3fcca232b6c12)) +* Fix force MFA screen bugs ([#196](https://github.com/frontegg/frontegg-react/issues/196)) ([8d51fb9](https://github.com/frontegg/frontegg-react/commit/8d51fb9794d0d0728bd04a742ce6a3f77845d1fe)) +* Fix pre-release action ([6b7f616](https://github.com/frontegg/frontegg-react/commit/6b7f6164660323982bc290f7bc6d853b8b4a075d)) +* **auth:** add support in dynamic component in login and signup for socail logins ([#192](https://github.com/frontegg/frontegg-react/issues/192)) ([195b977](https://github.com/frontegg/frontegg-react/commit/195b97704618135cb17edc7569019f236dda7fd6)) + + + + + +# [1.16.0](https://github.com/frontegg/frontegg-react/compare/v1.15.2...v1.16.0) (2020-12-24) + + +### Bug Fixes + +* wront state with logged in user ([#198](https://github.com/frontegg/frontegg-react/issues/198)) ([970c99b](https://github.com/frontegg/frontegg-react/commit/970c99b8fd20147d00e30558d3a7c20726136579)) +* **connectivity:** fix some design and behavioral issues ([fd5d16d](https://github.com/frontegg/frontegg-react/commit/fd5d16d1e114fa8445ea5a0548d7b4cc00a530f0)) +* **core:** fix clean data in the FeInput component ([6d900c8](https://github.com/frontegg/frontegg-react/commit/6d900c8e04986f21744d69ee741408fd9d66ffb3)) +* **core:** fix style the FeChip component ([1f76479](https://github.com/frontegg/frontegg-react/commit/1f76479817129653b08bac047531d68d2f132fab)) +* **localize:** fix text for secure tooltip ([8761141](https://github.com/frontegg/frontegg-react/commit/87611410fc23b881a11bcc80e9ba489bcf3ccf32)) + + +### Features + +* **core:** add anew property dontDisableSaving to the FInput compoennt ([6ff5648](https://github.com/frontegg/frontegg-react/commit/6ff56488a76816f5b501b616656e0bc97afe03ee)) + + + + + +## [1.15.2](https://github.com/frontegg/frontegg-react/compare/v1.15.1...v1.15.2) (2020-12-24) + + +### Bug Fixes + +* **tests:** Fix two factor authentication tests ([27b3539](https://github.com/frontegg/frontegg-react/commit/27b3539db64d42d86de7630e85340fd5cf3b48ba)) +* Fix crash state mutation was detected between dispatches ([6f8d8e6](https://github.com/frontegg/frontegg-react/commit/6f8d8e6083742d01ba8bd167436f3c7bf850e146)) +* Fix UI css bugs ([b94b49c](https://github.com/frontegg/frontegg-react/commit/b94b49c020f7a26059ab19b0345d5f266043c8ca)) +* **core:** fix validate the InputChip component ([e363849](https://github.com/frontegg/frontegg-react/commit/e36384952d95edf22541b5a648d6cd09b42b4c95)) + + + + + +## [1.15.1](https://github.com/frontegg/frontegg-react/compare/v1.15.0...v1.15.1) (2020-12-21) + + +### Bug Fixes + +* **auth:** fix authorized content data validation ([2728cd9](https://github.com/frontegg/frontegg-react/commit/2728cd9f51b8c3404a09870ff256ba07dbcc1d6c)) +* **connectivity:** fix bug of edit the email and sms events ([e34788c](https://github.com/frontegg/frontegg-react/commit/e34788c0b58dc07d20509b4addb39d33a57e1dea)) + + + + + +# [1.15.0](https://github.com/frontegg/frontegg-react/compare/v1.14.1...v1.15.0) (2020-12-20) + + +### Bug Fixes + +* Fix owsp validation exception on undefined value ([54ffaee](https://github.com/frontegg/frontegg-react/commit/54ffaeed42564481bc1f1b9592e02ba4e266f9e6)) + + +### Features + +* Add new component `AuthorizedContent` to strict content visibility by permission ([e4be8dc](https://github.com/frontegg/frontegg-react/commit/e4be8dc758b185a88f7b42960c733e7c6763d748)) + + + + + +## [1.14.1](https://github.com/frontegg/frontegg-react/compare/v1.14.0...v1.14.1) (2020-12-17) + + +### Bug Fixes + +* change primary color to darker blue ([04f94bd](https://github.com/frontegg/frontegg-react/commit/04f94bd5caa1135560e89cf0c886a4b81665956a)) +* Fix AccountDropdown style bugs ([dde2680](https://github.com/frontegg/frontegg-react/commit/dde26808277b12695c9dcf9daa7dd79f04d1ef88)) +* Fix create new webhooks button typo ([0be76c3](https://github.com/frontegg/frontegg-react/commit/0be76c38a771996ad849a027b752fb7107b9d3db)) +* Fix infinite loading in switch tenant popup ([78a63c1](https://github.com/frontegg/frontegg-react/commit/78a63c1affa2cd05b4a556d63d692f3ed0380788)) +* Fix input style issues when autocomplete enables ([790e8b4](https://github.com/frontegg/frontegg-react/commit/790e8b41e1e36855baa04b730025ac3246bc6b89)) +* Fix search bar alignments and ui bug fixes ([dd51197](https://github.com/frontegg/frontegg-react/commit/dd5119705cad6e379459171e34a5a3abe4d891ff)) +* move branch changelog to pull request body while create release ([fc676e1](https://github.com/frontegg/frontegg-react/commit/fc676e13ce7eb31f5f410201f617b19317bf469d)) +* prevent dialog from closing if clicking on other portal element ([c6a3b5b](https://github.com/frontegg/frontegg-react/commit/c6a3b5bec8e0f362f9fa816f6dff719d6db23f1a)) +* Re-enable fields in SSO claim domain in validation failed ([4fb385c](https://github.com/frontegg/frontegg-react/commit/4fb385c544d03658964b40285b9ec8041250d269)) +* Update add new webhook api url ([65533c1](https://github.com/frontegg/frontegg-react/commit/65533c1922c43f4331664d14f70c3876e7ada40a)) +* **auth:** [FR-1080] fix social login wrapper ([f825fc7](https://github.com/frontegg/frontegg-react/commit/f825fc7378780944f9edee94e4f920a99912c5a5)) +* remove switch tenant button user only have on tenant ([c3b5df5](https://github.com/frontegg/frontegg-react/commit/c3b5df52cce439537ab7a483abaf4645d6fa7d1a)) +* **auth:** fix loader api tokens table loader, some improvements ([8db076f](https://github.com/frontegg/frontegg-react/commit/8db076f91de61358577b304cc955151850ff4cfa)) + + +### Features + +* add support to publish prerelease version ([3611311](https://github.com/frontegg/frontegg-react/commit/361131133c73d86ad13bff7b4a38390d36bc6cd7)) + + + + + +# [1.14.0](https://github.com/frontegg/frontegg-react/compare/v1.13.2...v1.14.0) (2020-12-16) + + +### Bug Fixes + +* disable profile image uploader limitation ([07e082e](https://github.com/frontegg/frontegg-react/commit/07e082e8e4ff551b84fa277a8476e676f503ff2e)) +* Fix MFA cancel button size ([be9a44b](https://github.com/frontegg/frontegg-react/commit/be9a44b2df12a9beac07198a2aaa82c3bd940717)) +* Fix profile tabs color ([3ab5742](https://github.com/frontegg/frontegg-react/commit/3ab57426355700c05930075c014faa2c10456b9c)) +* UI enhancements for SSO components ([6be3aea](https://github.com/frontegg/frontegg-react/commit/6be3aea9e54aa56e4f28da3d63a81df500435fab)) +* Update components primary color ([ee6d08e](https://github.com/frontegg/frontegg-react/commit/ee6d08ec880fc7ae9427d993a544385db8d3da5b)) +* **connectivity:** fix actions for saving data ([c92d051](https://github.com/frontegg/frontegg-react/commit/c92d051f309e3cbb5862bec73c6e12dd5b96da67)) +* **connectivity:** FR-1005 fix the alignment of the line data in the webhook table ([bccfa8b](https://github.com/frontegg/frontegg-react/commit/bccfa8b96b372f1762e6b681fcccfc9d556fe9d3)) +* **core:** fix mistakes in URLs for the connectivity component ([2fcafc8](https://github.com/frontegg/frontegg-react/commit/2fcafc8d639bb634d4acdf34dbce841c9feb0954)) +* **elements:** fix console error for the InputChip component ([0f25e0f](https://github.com/frontegg/frontegg-react/commit/0f25e0f12673301d8f3cbfb4ccbf7a532e573840)) + + +### Features + +* **auth:** Api tokens component for users and tenants ([c8b1e17](https://github.com/frontegg/frontegg-react/commit/c8b1e176bee4f4402afbd9625841312428c14b75)) + + + + + +## [1.13.2](https://github.com/frontegg/frontegg-react/compare/v1.13.1...v1.13.2) (2020-12-13) + + +### Bug Fixes + +* **audits:** FR-1001 add 'unknown' when cell value is undefined ([889bc83](https://github.com/frontegg/frontegg-react/commit/889bc83ae9105228b88c32826a81eb7b8de4d0d4)) +* **build:** remove hoist-non-react-statics from internal deps ([3eccdd2](https://github.com/frontegg/frontegg-react/commit/3eccdd297b8b79cccae11200a62ca69e81f6856f)) +* **connectivity:** fix the enabled edit data form the SMS and Email webhooks ([6314a4e](https://github.com/frontegg/frontegg-react/commit/6314a4ea2eb1be8b1a0043077de3ceb4f410bd68)) +* **elements:** fix the fullWidth style for the InputChip component in the material library ([2cac48f](https://github.com/frontegg/frontegg-react/commit/2cac48f2fb55a3810a1d2fe41f2bdacc25f56c01)) + + + + + +## [1.13.1](https://github.com/frontegg/frontegg-react/compare/v1.13.0...v1.13.1) (2020-12-10) + + +### Bug Fixes + +* **audits:** FR-996 add closing popup when reference hidden to prevent ui bugs ([b046f95](https://github.com/frontegg/frontegg-react/commit/b046f9503f983401ff26eb2e16edc6954cb101d5)) +* **audits:** FR-999 add x btn for ip popup ([6efc8ea](https://github.com/frontegg/frontegg-react/commit/6efc8ea6e229b6434d75f9af59c0b6f0993ec9cd)) +* **auth:** fix google social login scopes. closes [#159](https://github.com/frontegg/frontegg-react/issues/159) ([0ddb2ca](https://github.com/frontegg/frontegg-react/commit/0ddb2ca54f250900f79a1f52382469922d371c12)) + + + + + +# [1.13.0](https://github.com/frontegg/frontegg-react/compare/v1.12.0...v1.13.0) (2020-12-09) + + +### Bug Fixes + +* **connectivity:** fix show platform if the it dosen't have any events FR-984 ([6319b02](https://github.com/frontegg/frontegg-react/commit/6319b0201888b7226d600d43cb2bfc65f1d24b20)) +* fix testId error in material components ([0e3d2a6](https://github.com/frontegg/frontegg-react/commit/0e3d2a610f762d9065eee261dd996ecea77e1c8d)), closes [#119](https://github.com/frontegg/frontegg-react/issues/119) +* **auth:** loadUsers on TeamTable did mount ([9b8ff6b](https://github.com/frontegg/frontegg-react/commit/9b8ff6b033e2f9973109e618b5ce9da91eec7ec3)) + + +### Features + +* [FR-808] add support in users sign ups ([1a6f7c3](https://github.com/frontegg/frontegg-react/commit/1a6f7c3639ab4c351593d540296e67f65293bbf9)) + + + + + +# [1.12.0](https://github.com/frontegg/frontegg-react/compare/v1.11.1...v1.12.0) (2020-12-09) + + +### Bug Fixes + +* **audits:** display none cross btns in filters selectors to prevent errors ([6afb046](https://github.com/frontegg/frontegg-react/commit/6afb046e69ac7fb10be28bda9bd44eb3d9524cc6)) +* prevent after login redirect if it is in auth routes ([ae0371d](https://github.com/frontegg/frontegg-react/commit/ae0371d82e5fa3b99349d74f9cd0c7d6754cd710)), closes [#108](https://github.com/frontegg/frontegg-react/issues/108) +* split AuditsPage to separated components ([9aa109a](https://github.com/frontegg/frontegg-react/commit/9aa109a09a357333788abab845f9ff906e636cc3)) +* **audits:** fix font-weights for ip popup titels ([5b23d08](https://github.com/frontegg/frontegg-react/commit/5b23d0877faf836193c401b4a2afabcbd3e65d89)) +* **audits:** fix scroll to top on page change for material lib ([1878dd4](https://github.com/frontegg/frontegg-react/commit/1878dd491bc3d86b182f88469841392215d589e8)) +* **audits:** FR-1000 fix updating filter value ([00f84d4](https://github.com/frontegg/frontegg-react/commit/00f84d427db5cf1faaedb69d7025debe0513debf)) +* **audits:** FR-1002 add globe icon when location is unknown ([53265fd](https://github.com/frontegg/frontegg-react/commit/53265fd5cdc56d3fc141ad77469d452e2f6dc430)) +* **audits:** FR-1003 Change severity filters to match the actual filters ([ce9d834](https://github.com/frontegg/frontegg-react/commit/ce9d834e204f7bc75e57b27ba82042ba45fd971a)) +* **audits:** FR-1004 add startRefresh action to prevent double fetch after init render ([eddb763](https://github.com/frontegg/frontegg-react/commit/eddb763711863f3a153679455053a963719b876a)) +* **audits:** FR-1004 prevent call action onPageChange after init render ([bca3bc1](https://github.com/frontegg/frontegg-react/commit/bca3bc14818c01589a5c33ce9f8a1f2c6923a585)) +* **audits:** FR-1004 prevent setFilterData action call after init render ([9656648](https://github.com/frontegg/frontegg-react/commit/9656648fdd5ba7c37fa9474f3f088b24f855b420)) +* **audits:** FR-1004 remove debounce filter, prevent onFilterChange call after init render ([b842cd6](https://github.com/frontegg/frontegg-react/commit/b842cd6186f032750fb6daed3e01e01d5b135498)) +* **audits:** FR-997 reduce margins, change tags to divs in ip popup ([66f0005](https://github.com/frontegg/frontegg-react/commit/66f000585ef74da4d4056d3dd72e6df981955b4c)) +* **audits:** FR-998 change severity attention letters coloring ([efdb0c8](https://github.com/frontegg/frontegg-react/commit/efdb0c8388af715bcdd9657632d5e13c5ca94eb2)) +* **auth:** add missing css variables for authentication pages ([4bb2c66](https://github.com/frontegg/frontegg-react/commit/4bb2c66f292aa1794e456516eb928c156186a0f5)) +* **build:** fix npmrc in github action ([d8ee0ad](https://github.com/frontegg/frontegg-react/commit/d8ee0ad24f6c6f410b04e9b5a71db7d8761e9017)) +* **connectivity:** fix color of the Install button FR-975 ([ac9d61e](https://github.com/frontegg/frontegg-react/commit/ac9d61e7cc329410954f4830a476602a3ab73e49)) +* **connectivity:** fix styles for the material UI ([ca1258d](https://github.com/frontegg/frontegg-react/commit/ca1258d85a29d40b9de59407c41f7e755f1e4206)) +* **core:** fix the z-index value for the popup component ([6201205](https://github.com/frontegg/frontegg-react/commit/620120501945c9e0a8e89add87e466f397bb7421)) +* **elements:** fix className property for the semantic Button element ([8646833](https://github.com/frontegg/frontegg-react/commit/864683387e221ab350f7f3f439918a6b411254d9)) +* **elements:** fix styles and behavior for the semantic InputChip component ([29671d2](https://github.com/frontegg/frontegg-react/commit/29671d2b3ea070e712c1352fbb7356d236d710d9)) +* **elements:** fix styles for the InputChip component in the material library ([8f6404a](https://github.com/frontegg/frontegg-react/commit/8f6404aa9cb659512c17ce3ec7b03e48b6f0f2e4)) + + +### Features + +* **connectivity:** add UI design for the semantic library ([986c86c](https://github.com/frontegg/frontegg-react/commit/986c86cc1d3bc5f35b8a409cbdc9fba7737bb522)) +* **elements:** add the TextArea component to the semantic library ([bacc6c3](https://github.com/frontegg/frontegg-react/commit/bacc6c35eb52cf0bf644503b9ca654f4e3073e50)) + + + + + +## [1.11.1](https://github.com/frontegg/frontegg-react/compare/v1.11.0...v1.11.1) (2020-12-02) + + +### Bug Fixes + +* **auth:** add exports for socialLogins components ([018b5ea](https://github.com/frontegg/frontegg-react/commit/018b5eaa8b4758c38103e7052944ce8047a275b3)) +* **connectivity:** fix styles for separate components ([3b4b8d3](https://github.com/frontegg/frontegg-react/commit/3b4b8d3909942716889f72830796294e846c45d0)) + + + + + +# [1.11.0](https://github.com/frontegg/frontegg-react/compare/v1.10.0...v1.11.0) (2020-11-30) + + +### Bug Fixes + +* **audits:** fix cancel button on audits filter ([007fec7](https://github.com/frontegg/frontegg-react/commit/007fec7f0f826a9cc9b671bfb49af043adb47b00)) +* **auth:** add missing query and hash after login redirect ([#135](https://github.com/frontegg/frontegg-react/issues/135)) ([87f36aa](https://github.com/frontegg/frontegg-react/commit/87f36aa21ddeb3aadc3153411b6679360879d02a)), closes [#134](https://github.com/frontegg/frontegg-react/issues/134) +* [FR-815] change password policy to be aligned with backend ([fbc9abf](https://github.com/frontegg/frontegg-react/commit/fbc9abfa776b9f7ac0a8f3c89eaa5c6a39b320b6)) +* Remove debugger line ([58643c1](https://github.com/frontegg/frontegg-react/commit/58643c19e05fea9b9fbabf507eeccb3595bc4903)) +* **auth:** fix team management roles dropdown ui ([#126](https://github.com/frontegg/frontegg-react/issues/126)) ([c74949b](https://github.com/frontegg/frontegg-react/commit/c74949b2dc409c5af7f00d5d1c0b985c74d3da56)) + + +### Features + +* **ci:** add option to publish prerelease version ([0ff0c67](https://github.com/frontegg/frontegg-react/commit/0ff0c672f86eacf175790b89173f1c6e34789b7e)) + + + + + +# [1.10.0](https://github.com/frontegg/frontegg-react/compare/v1.9.0...v1.10.0) (2020-11-27) + + +### Bug Fixes + +* **audits:** move initData action to Audits.tsx, fix reducer storeName ([28a69b6](https://github.com/frontegg/frontegg-react/commit/28a69b64375652720e2bb9589471b35045d6295c)) +* **audits:** remove comment ([e9fdc6f](https://github.com/frontegg/frontegg-react/commit/e9fdc6f1adfec1c5d6d689b0bf6b006d819418b9)) + + +### Features + +* **connectivity:** add listener ([e463fae](https://github.com/frontegg/frontegg-react/commit/e463faeb097a07959b88cbf4a535bb1871b9e6b3)) + + + + + +# [1.9.0](https://github.com/frontegg/frontegg-react/compare/v1.8.0...v1.9.0) (2020-11-25) + + +### Bug Fixes + +* fix console errors ([47a0679](https://github.com/frontegg/frontegg-react/commit/47a0679cb426eeb09bd5d97e0b28fe697c24e3b2)), closes [#119](https://github.com/frontegg/frontegg-react/issues/119) +* **auth:** fix social logins loader ([#116](https://github.com/frontegg/frontegg-react/issues/116)) ([e965d3d](https://github.com/frontegg/frontegg-react/commit/e965d3db6a423a0457dc89471418f70c57f2e856)) +* **core:** fix external finterFunction ([72419cb](https://github.com/frontegg/frontegg-react/commit/72419cb98c279356c30aa3a736f9a0ccd6f285ce)) +* **integrations:** add styles to the accordion ([0b74561](https://github.com/frontegg/frontegg-react/commit/0b74561dae589ba603acef322114c3ca51b8e0a2)) +* **integrations:** fix problem with search ([eb2bf94](https://github.com/frontegg/frontegg-react/commit/eb2bf9425db0d2a7290acd2ad2ce7a6531ec96a9)) +* **integrations:** fix styles for webhooks ([436c68d](https://github.com/frontegg/frontegg-react/commit/436c68d35cdf8e017b71a701c8c23cd8c4577626)) +* **integrations:** fix the email icon and some styles ([20665eb](https://github.com/frontegg/frontegg-react/commit/20665ebc611b488a1b70739395d53d8c5bb86593)) +* **integrations:** fix validations ([f9c6c15](https://github.com/frontegg/frontegg-react/commit/f9c6c15b541abe38351f745182f81f772c3e3b40)) + + +### Features + +* **auth:** New AccountDropdown component added to AuthPlugin ([018f2f8](https://github.com/frontegg/frontegg-react/commit/018f2f8db3ad22981f9270bd2e166b56cf6eb7ff)) +* **core:** add dupport the ClassName property to the Table component ([71a582d](https://github.com/frontegg/frontegg-react/commit/71a582d69e44130cb164a5951e6f1225406cb7e7)) +* **core:** add support the fullWidth property to the InputChip component ([f1c6586](https://github.com/frontegg/frontegg-react/commit/f1c65869939d176aceb9eb8dcd9d8c4d42592caa)) +* **elements:** add dupport the ClassName property to the Table component ([610ba5a](https://github.com/frontegg/frontegg-react/commit/610ba5a6cd8e432da5ad15c631621b27eb329563)) +* **integrations:** add an accordion to the catagory data ([0bfa866](https://github.com/frontegg/frontegg-react/commit/0bfa866651744af85b4474f4f95a51123391edc6)) +* **integrations:** add icons to the list of platforms ([28dc591](https://github.com/frontegg/frontegg-react/commit/28dc5917cdbcbf839c10db0e811d0518ee285398)) +* **integrations:** add search by events and categories ([6785a81](https://github.com/frontegg/frontegg-react/commit/6785a81fcaca327043f56b077c16a95878305152)) +* New plugin for Audits ([#106](https://github.com/frontegg/frontegg-react/issues/106)) ([921b37a](https://github.com/frontegg/frontegg-react/commit/921b37aca98f23c800c8b5d094ed595d52617679)) + + + + + +# [1.8.0](https://github.com/frontegg/frontegg-react/compare/v1.7.0...v1.8.0) (2020-11-23) + + +### Features + +* Add Connectivity Plugin ([#98](https://github.com/frontegg/frontegg-react/issues/98)) ([db77431](https://github.com/frontegg/frontegg-react/commit/db77431b6d744b93431430543019fedd1c0dbae2)) +* **auth:** Add support in google and github social logins ([#111](https://github.com/frontegg/frontegg-react/issues/111)) ([938b04c](https://github.com/frontegg/frontegg-react/commit/938b04cba618e2029b55ff4c39d5c0fc0d884e6b)) + + + + + +# [1.7.0](https://github.com/frontegg/frontegg-react/compare/v1.6.1...v1.7.0) (2020-11-22) + + +### Bug Fixes + +* remove auth deps from core lib ([1686abd](https://github.com/frontegg/frontegg-react/commit/1686abd9f2dd7a92b997d8a6e649be70aff5b29b)) + + +### Features + +* resolve saga actions outside fronteggprovider ([7878beb](https://github.com/frontegg/frontegg-react/commit/7878bebf49b5131fcdf16bbd21c1bcab03c2d1ae)) + + + + + +## [1.6.2](https://github.com/frontegg/frontegg-react/compare/v1.6.1...v1.6.2) (2020-11-19) + +**Note:** Version bump only for package @fronteg/react + + + + + +## [1.6.1](https://github.com/frontegg/frontegg-react/compare/v1.6.0...v1.6.1) (2020-11-15) + + +### Bug Fixes + +* **auth:** fix multiple call to loadUsers while mounting TeamTable ([4a51547](https://github.com/frontegg/frontegg-react/commit/4a51547cf5cd86905d3c760af13016a3751bab0b)) +* **auth:** fix remove last role in TeamTable ([22d2ff6](https://github.com/frontegg/frontegg-react/commit/22d2ff60715249c13c7a454f255a371170504887)) +* **auth:** redirect user to login with two-factor after saml if required ([e6abd6a](https://github.com/frontegg/frontegg-react/commit/e6abd6a04ff6b7e62eb335cd71c19382cdd9472a)) +* **auth:** remain user data after editing roles in TeamTable ([186aaa4](https://github.com/frontegg/frontegg-react/commit/186aaa4827f87dd27d35375d1c35fd8a1818c6d6)), closes [#99](https://github.com/frontegg/frontegg-react/issues/99) +* disable store caching in cypress tests ([4f5e5f5](https://github.com/frontegg/frontegg-react/commit/4f5e5f5b0cbbd74794fc8ae36770223a1356bf2a)) +* fix multiple store initialization in strict-mode ([a569f86](https://github.com/frontegg/frontegg-react/commit/a569f86b37292e71b985c3a2e54610121ab419ce)) +* restore test-id to forgot password button ([b8a4ab4](https://github.com/frontegg/frontegg-react/commit/b8a4ab448c5c3fd45e7ad4a1189242d27d3f5822)) + + + + + +# [1.6.0](https://github.com/frontegg/frontegg-react/compare/v1.5.0...v1.6.0) (2020-11-12) + + +### Bug Fixes + +* fix pagination bug in TeamTable ([8ba1c3d](https://github.com/frontegg/frontegg-react/commit/8ba1c3d861257231b1890766c5042cba58998965)) + + +### Features + +* add option to logout from FronteggContext object ([e35b4f0](https://github.com/frontegg/frontegg-react/commit/e35b4f0e8d79660641676257aa5440d2f2bf84ef)) +* add option to upload profile image ([#96](https://github.com/frontegg/frontegg-react/issues/96)) ([0e4c45c](https://github.com/frontegg/frontegg-react/commit/0e4c45cb08a84519e1f2ebb06295af26cdc05ff7)) + + + + + +# [1.5.0](https://github.com/frontegg/frontegg-react/compare/v1.4.0...v1.5.0) (2020-11-10) + + +### Bug Fixes + +* export missing interface AcceptInvitationState ([#92](https://github.com/frontegg/frontegg-react/issues/92)) ([9981fe1](https://github.com/frontegg/frontegg-react/commit/9981fe1921d1517aea1a4aa4c484a4974bbc464a)) + + +### Features + +* sync session between tabs on Auth Listener ([f2bfa04](https://github.com/frontegg/frontegg-react/commit/f2bfa04bb452f8a5ad165b4f9c382ce3fb07e105)) + + + + + +# [1.4.0](https://github.com/frontegg/frontegg-react/compare/v1.3.0...v1.4.0) (2020-11-09) + + +### Bug Fixes + +* **build:** fix prerelese versioning ([#83](https://github.com/frontegg/frontegg-react/issues/83)) ([07d1544](https://github.com/frontegg/frontegg-react/commit/07d1544562ba83aa5dbffa931117a0bd9286026c)) +* add option to add user without roles ([4d17333](https://github.com/frontegg/frontegg-react/commit/4d17333fc0f157d3c5d4462f20d8f2269b579a65)) +* css enhancements ([317e875](https://github.com/frontegg/frontegg-react/commit/317e8756e7c56deaa0d4c16ce699a7b9ebe5f2e5)) +* disable angular render children ([658dbbf](https://github.com/frontegg/frontegg-react/commit/658dbbf05319224caf326adca2b90da23eedefe0)) +* make redux internal deps ([#88](https://github.com/frontegg/frontegg-react/issues/88)) ([331e8c5](https://github.com/frontegg/frontegg-react/commit/331e8c5e4ee518ffd4918b41df0e3fb3cdd3809e)) +* remove default font-size from root css ([ad774e9](https://github.com/frontegg/frontegg-react/commit/ad774e95b1199efaf53951f3b2a0df52c1bd2900)), closes [#90](https://github.com/frontegg/frontegg-react/issues/90) +* **teams:** remove roles columns if no roles configured ([7078eba](https://github.com/frontegg/frontegg-react/commit/7078ebaa57cfd46ed9f644e7802a354853c28fb9)) +* remove logs ([16b0976](https://github.com/frontegg/frontegg-react/commit/16b09762f77e8c4491e1570b954a1c04511ba53f)) +* remove memorized store ([b4d2b25](https://github.com/frontegg/frontegg-react/commit/b4d2b2550c3c54220866fbc7540014b279aa12f9)) + + +### Features + +* notifications plugin ([#78](https://github.com/frontegg/frontegg-react/issues/78)) ([0439d17](https://github.com/frontegg/frontegg-react/commit/0439d179ed5c0abae510b7d132dbf03ae907f7f6)) + + + + + +# [1.3.0](https://github.com/frontegg/frontegg-react/compare/v1.2.0...v1.3.0) (2020-11-04) + + +### Bug Fixes + +* **auth:** bug fixes ([#76](https://github.com/frontegg/frontegg-react/issues/76)) ([a758642](https://github.com/frontegg/frontegg-react/commit/a758642458751e930dc4ea6de6acc50b3ede0b77)) +* **auth:** fix ui bugs ([#79](https://github.com/frontegg/frontegg-react/issues/79)) ([0e75d8d](https://github.com/frontegg/frontegg-react/commit/0e75d8dff80937dc2e4308a0ffdd527a12a84a39)) +* calling `response.text()` after `response.json()` fails ([#80](https://github.com/frontegg/frontegg-react/issues/80)) ([3cde90d](https://github.com/frontegg/frontegg-react/commit/3cde90db8a5e9f1850dbf51db492f94e77cce93e)) +* fix material button console errors ([5558c52](https://github.com/frontegg/frontegg-react/commit/5558c52c61276847109d137b698fd857ffbfcf2e)) +* fix material table head position sticky ([99a9423](https://github.com/frontegg/frontegg-react/commit/99a9423e43596d932f1e1f234e2dc569c5e166eb)) + + +### Features + +* add elements page ([9eb19a8](https://github.com/frontegg/frontegg-react/commit/9eb19a886a4cbc788ad236ce9f597c33da7f68ef)) +* **auth:** add options to update user roles ([3ec734a](https://github.com/frontegg/frontegg-react/commit/3ec734a79dce6df707562a4555e9d7bf124f85a1)) +* **elements:** add FeInput element ([f23e439](https://github.com/frontegg/frontegg-react/commit/f23e4392ab849d32c5e0ba67e055f793ecfd5ceb)) +* add support for nextjs and angular ([#82](https://github.com/frontegg/frontegg-react/issues/82)) ([5fa36eb](https://github.com/frontegg/frontegg-react/commit/5fa36ebe7bfa6866c78455a746727ba8b1cafbbc)) + + + + + +# [1.2.0](https://github.com/frontegg/frontegg-react/compare/v1.1.0...v1.2.0) (2020-10-25) + + +### Bug Fixes + +* **ci:** add conventional-commits to prereleases ([#58](https://github.com/frontegg/frontegg-react/issues/58)) ([d0941cc](https://github.com/frontegg/frontegg-react/commit/d0941ccaa279dbb4901563f9f6391ffcbc44dc06)) +* **ci:** add pre-release action ([07bd99d](https://github.com/frontegg/frontegg-react/commit/07bd99db45c76d43a71b9e0aaf316e6720e9ad67)) +* **elements:** UI elements small fixes ([effb9bd](https://github.com/frontegg/frontegg-react/commit/effb9bd54186133184010c34874af212c040ef90)) +* **packaging:** downgrade typescript to 3.7.5 ([10294fc](https://github.com/frontegg/frontegg-react/commit/10294fc3f6c2f5ade727d2e05070a978fe1c1cc7)) +* **packaging:** remove tesrser from build steps ([a4ff453](https://github.com/frontegg/frontegg-react/commit/a4ff453ca63f5a0b0c4ae2399e0b63779f17d825)) +* **packaging:** revert typescript to 3.9.7 ([#56](https://github.com/frontegg/frontegg-react/issues/56)) ([cb1a4dd](https://github.com/frontegg/frontegg-react/commit/cb1a4ddf8c47f3a3ceb7a1bea6d81a800c8b4a84)) +* restore old react and checkout from release branch ([adbff2e](https://github.com/frontegg/frontegg-react/commit/adbff2e9b28248ae9d292b633fde4233b853a29c)) + + +### Features + +* **auth:** add SSO components ([#64](https://github.com/frontegg/frontegg-react/issues/64)) ([f083762](https://github.com/frontegg/frontegg-react/commit/f0837623073c1f9a636480c6a88d5969fb020a09)) +* **auth:** support all auth functionalities ([#70](https://github.com/frontegg/frontegg-react/issues/70)) ([26d725e](https://github.com/frontegg/frontegg-react/commit/26d725e2f386c6b4a703e4371fac8efef29676d1)) +* **core:** add dynamic elements for Frontegg Components ([#10](https://github.com/frontegg/frontegg-react/issues/10)) ([2bddbb2](https://github.com/frontegg/frontegg-react/commit/2bddbb2794ac0dc4af90c8df0f33b69a312e063a)) +* **core:** add FeSwitchToggle component ([#66](https://github.com/frontegg/frontegg-react/issues/66)) ([5b6c603](https://github.com/frontegg/frontegg-react/commit/5b6c603d912be45b1982c06b7f026badd8e82f1c)) +* **core:** Add FeTabs component ([3699299](https://github.com/frontegg/frontegg-react/commit/36992997e64907345a254a4b44580759705186bb)), closes [#67](https://github.com/frontegg/frontegg-react/issues/67) + + + + + +# [1.1.0](https://github.com/frontegg/frontegg-react/compare/v1.0.89...v1.1.0) (2020-10-14) + + +### Bug Fixes + +* **auth:** fix loading splitted sagas in AuthPLugin ([d0fba43](https://github.com/frontegg/frontegg-react/commit/d0fba436bd442ff047849397d8bedcc897e16b1c)) +* **auth:** fix saga initializing bug ([80727a3](https://github.com/frontegg/frontegg-react/commit/80727a3e65b3d34ff455a8d0c252495ab3731c48)) +* **packaging:** add missing immer dependency ([#52](https://github.com/frontegg/frontegg-react/issues/52)) ([36c6c15](https://github.com/frontegg/frontegg-react/commit/36c6c1583809a532885e65a8c2c375151ad8b9dc)), closes [#51](https://github.com/frontegg/frontegg-react/issues/51) + + +### Features + +* **auth:** add accept invitation component by url ([#50](https://github.com/frontegg/frontegg-react/issues/50)) ([c3a43d6](https://github.com/frontegg/frontegg-react/commit/c3a43d60dad3fc8da9cffc6a81f468b5671d3af9)) +* **auth:** add Team (reducer/saga) to Auth Plugin ([7bed273](https://github.com/frontegg/frontegg-react/commit/7bed27378efe32c9e9091495d0ac4a3f268b206c)) +* **auth:** add TeamAPI to frontegg/react-core api.team collection ([600a8f8](https://github.com/frontegg/frontegg-react/commit/600a8f81a0322702d22dc2abede93d271d1c81f7)) + + + + + +## [1.0.89](https://github.com/frontegg/frontegg-react/compare/v1.0.88...v1.0.89) (2020-10-13) + + +### Bug Fixes + +* **cli:** fix missing property in frontegg/react-cli ([2d45c3f](https://github.com/frontegg/frontegg-react/commit/2d45c3f2c44c4531e72434cb7935a42c28012992)), closes [#44](https://github.com/frontegg/frontegg-react/issues/44) +* **cli:** fix missing property in frontegg/react-cli ([b468a08](https://github.com/frontegg/frontegg-react/commit/b468a0845b070d28c4f6c60dbb1cc982e6af1f72)), closes [#43](https://github.com/frontegg/frontegg-react/issues/43) +* **cli:** upload cypress failure artifacts ([b23aa60](https://github.com/frontegg/frontegg-react/commit/b23aa60016dee1b8089bafd417664263241d8c84)) + + + + + +## [1.0.88](https://github.com/frontegg/frontegg-react/compare/v1.0.87...v1.0.88) (2020-10-11) + + +### Bug Fixes + +* **ci:** fix npmrc file ([97df62e](https://github.com/frontegg/frontegg-react/commit/97df62e26446ec42c655b03b4dc7c41d25f13d9e)) +* **cli:** add space between commit scope and summary text ([ef6fec0](https://github.com/frontegg/frontegg-react/commit/ef6fec04c4e84e1ae7b4f77d4fccef62694bd1c7)) +* **cli:** fix create pull request action ([daf216d](https://github.com/frontegg/frontegg-react/commit/daf216d03c7423408c1fa534b8af65e81a7eaccb)) +* **cli:** increase max size of the summary ([f7243c1](https://github.com/frontegg/frontegg-react/commit/f7243c1c865c46530b05df7b94e42a52a7050892)) + + + + + +## [1.0.87](https://github.com/frontegg/frontegg-react/compare/v1.0.86...v1.0.87) (2020-10-09) + + +### Bug Fixes + +* throw error if prettier check failed ([446e86c](https://github.com/frontegg/frontegg-react/commit/446e86c9b73dbdcbec925f3930580efca7b1effa)) +* **ci:** add changelog content to release pull request ([b196013](https://github.com/frontegg/frontegg-react/commit/b196013779aa620652d9ab43d537c85f1e989502)) +* **ci:** checkout with full hisotry for lerna conventional-commits ([4846fe2](https://github.com/frontegg/frontegg-react/commit/4846fe239496ced6a2cabe7d809fdcea410c4e9d)) +* **ci:** checkout with history to generate changelog ([307057c](https://github.com/frontegg/frontegg-react/commit/307057c8ea89aa863449392623db1372039881a7)) +* **cli:** add missing tslib to cli package json ([de0bc6e](https://github.com/frontegg/frontegg-react/commit/de0bc6e2f7558077eef8c7c1aeb815ff561f000e)) +* **packaging:** move cjs.js to index.js for main entry point ([0f8f700](https://github.com/frontegg/frontegg-react/commit/0f8f70016566a8f1940d16441a5afa1707dc02a2)) +* **security:** remove option for members to create new releases ([d9f8607](https://github.com/frontegg/frontegg-react/commit/d9f8607adaaeb5933ffc27021b798ac731cefcc0)) + + + + + +## 1.0.85 (2020-10-08) + +**Note:** Version bump only for package @fronteg/react + + + + + +## 1.0.84 (2020-10-08) + +**Note:** Version bump only for package @fronteg/react diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 39c05bcd8..1330a5f97 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,6 +27,7 @@ After cloning FronteggReact, run `make init` to initialize your. Then, you can r **Tests:** - `make test-integration` to run cypress integration tests +- `make test-component` to run cypress component tests - `make test-unit` to run cypress unit tests @@ -44,6 +45,7 @@ Make sure your changes does not break build flow, make sure to run these command ``` make install make build + make test-component ``` **The commit message should be structured as follows:** diff --git a/Makefile b/Makefile index 170a83bba..7dba7d0fd 100755 --- a/Makefile +++ b/Makefile @@ -71,6 +71,48 @@ install: ##@1 Global yarn install all packages add-dist-folders-%: @mkdir -p ./packages/${*}/dist +######################################################################################################################## +# +# PACKAGES +# +######################################################################################################################## + +lint: ##@2 Linting run lint on all packages + @echo "${YELLOW}Running tslint on all packages${RESET}" + @./node_modules/.bin/tslint "./packages/*/{src,tests}/**/*.{ts,tsx}" + +lint-%: ##@2 Linting run lint on specific packages + @echo "${YELLOW}Running tslint on package ${WHITE}${SERVICE_NAME}-${*}${RESET}" + @./node_modules/.bin/tslint ./packages/${*}/{src}/**/*.ts + +######################################################################################################################## +# +# TEST Operations +# +######################################################################################################################## +# + +test-integration: ##@3 Tests integration test with cypress + @echo "${YELLOW}Integration Test Cypress${RESET}" + @echo "Building DemoSaaS project" + @cd ./packages/demo-saas && yarn build + @echo "Start Cypress tests on port 3000" + @start-server-and-test 'cd ./packages/demo-saas && serve -l 3000 -s build' 3000 'cypress run --headless --config baseUrl=http://localhost:3000' + +test-component: ##@3 Tests component test with cypress + @echo "${YELLOW}Component Test Cypress${RESET}" + ${MAKE} test-component-auth + ${MAKE} test-component-audits + +test-component-%: + @echo "${YELLOW}Component Test Cypress [${*}]${RESET}" + @./node_modules/.bin/cypress run --headed --spec "packages/${*}/**/*" + +test-unit: ##@3 Tests unit test with jest + @echo "${YELLOW}Unit Test Jest${RESET}" + @./node_modules/.bin/lerna run test --parallel + + ######################################################################################################################## # # BUILD Operations @@ -78,7 +120,15 @@ add-dist-folders-%: ######################################################################################################################## build: ##@4 Build build all packages + ${MAKE} build-cli ${MAKE} build-react + ${MAKE} build-nextjs + ${MAKE} build-core + ${MAKE} build-elements-semantic + ${MAKE} build-elements-material-ui + ${MAKE} build-auth + ${MAKE} build-connectivity + ${MAKE} build-audits build-%: ##@4 Build build a specific package @echo "${YELLOW}Building package ${WHITE}${*}${RESET}" @@ -97,16 +147,17 @@ bw-%: ##@2 Build build:watch specific package # ######################################################################################################################## +commit-changes: + @git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" + @git config user.name "${GITHUB_ACTOR}" + @git add . + @git commit -m "Add generated files" || true + move-package-json-to-dist: @find ./packages -type d -maxdepth 1 ! -path ./packages \ | sed 's|^./packages/||' \ | xargs -I '{}' sh -c 'node scripts/move-package-json-to-dist.js ./packages/{}' -update-entry: - @find ./packages -type d -maxdepth 1 ! -path ./packages \ - | sed 's|^./packages/||' \ - | xargs -I '{}' sh -c 'node scripts/update-entry.js ./packages/{}' - prerelease-version-upgrade-%: @find ./packages -type d -maxdepth 1 ! -path ./packages \ | sed 's|^./packages/||' \ @@ -118,6 +169,9 @@ prerelease-version-upgrade-%: # ######################################################################################################################## +commit: + @node ./scripts/commit.js + pretty: @yarn prettier-hook @@ -125,16 +179,27 @@ demo: @cd ./packages/demo-saas && yarn start publish-packages-next: - @cp ./.npmrc "./packages/react/dist/.npmrc" - @cp ./.npmignore "./packages/react/dist/.npmignore" - @cd "./packages/react/dist" && npm publish --tag next - -publish-packages-alpha: - @cp ./.npmrc "./packages/react/dist/.npmrc" - @cp ./.npmignore "./packages/react/dist/.npmignore" - @cd "./packages/react/dist" && npm publish --tag alpha - -publish-packages-latest: - @cp ./.npmrc "./packages/react/dist/.npmrc" - @cp ./.npmignore "./packages/react/dist/.npmignore" - @cd "./packages/react/dist" && npm publish --tag latest + @make publish-package-next-react + @make publish-package-next-nextjs + @make publish-package-next-core + @make publish-package-next-auth + @make publish-package-next-audits + @make publish-package-next-connectivity + +publish-package-next-%: + @cp ./.npmrc "./packages/${*}/dist/.npmrc" + @cp ./.npmignore "./packages/${*}/dist/.npmignore" + @cd "./packages/${*}/dist" && npm publish --tag next + +publish-packages: + @make publish-package-latest-react + @make publish-package-latest-nextjs + @make publish-package-latest-core + @make publish-package-latest-auth + @make publish-package-latest-audits + @make publish-package-latest-connectivity + +publish-package-latest-%: + @cp ./.npmrc "./packages/${*}/dist/.npmrc" + @cp ./.npmignore "./packages/${*}/dist/.npmignore" + @cd "./packages/${*}/dist" && npm publish --tag latest diff --git a/README.md b/README.md index aeccf3963..18d455d9e 100644 --- a/README.md +++ b/README.md @@ -1,118 +1,70 @@ -
-
-Frontegg Logo +# Frontegg React -

Frontegg React.js

+![alt text](https://fronteggstuff.blob.core.windows.net/frongegg-logos/logo-transparent.png) -

- Frontegg is a web platform where SaaS companies can set up their fully managed, scalable and brand aware - SaaS features and integrate them into their SaaS portals in up to 5 lines of code. -
-

+Frontegg is a web platform where SaaS companies can set up their fully managed, scalable and brand aware - SaaS features +and integrate them into their SaaS portals in up to 5 lines of code. + +## BREAKING CHANGES SINCE VERSION 3.0.0 + +### The new @frontegg/react uses AdminPortal and LoginBox instead of multiple components. ## Installation Use the package manager [npm](https://www.npmjs.com/) to install frontegg React.JS library. ```bash -npm install @frontegg/react react-router-dom +npm install @frontegg/react ``` ## Configuration -Wrap your root component with `Frontegg Provider`: +Wrap your application with `Frontegg Provider`: ```js -import React from 'react'; -import ReactDOM from 'react-dom'; // For react 17 -// For react 18: import ReactDOM from 'react-dom/client'; -import App from './App'; -import './index.css'; - -import { FronteggProvider } from '@frontegg/react'; +import { FronteggProvider } from '@frontegg/react' const contextOptions = { - baseUrl: 'https://[YOUR_SUBDOMAIN].frontegg.com', - clientId: '[YOUR-CLIENT-ID]' -}; - -// For react 18: -// const root = ReactDOM.createRoot(document.getElementById('root')); -// root.render( -ReactDOM.render( - - - , - document.getElementById('root') -); + baseUrl: 'https://{HOST}.frontegg.com', // Your backend base URL (frontegg will direct the requests to it) +} + +export const App = () => { + return + {/*...*/} + +} + ``` -In order to get your subDomain and clientId, visit our portal. -### Redirect to login +### Usage -Using the Frontegg useAuth hook, you can determine whether a user is authenticated or not. -If the user is not authenticated, you can redirect the user to login via the useLoginWithRedirect hook as shown below. +You can use React Hooks to access Frontegg store. ```js -import './App.css'; -// import { useEffect } from 'react'; -import { ContextHolder } from '@frontegg/rest-api'; -import { useAuth, useLoginWithRedirect } from '@frontegg/react'; - -function App() { - const { user, isAuthenticated } = useAuth(); - const loginWithRedirect = useLoginWithRedirect(); - - // Uncomment this to redirect to login automatically - // useEffect(() => { - // if (!isAuthenticated) { - // loginWithRedirect(); - // } - // }, [isAuthenticated, loginWithRedirect]); - - const logout = () => { - const baseUrl = ContextHolder.getContext().baseUrl; - window.location.href = `${baseUrl}/oauth/logout?post_logout_redirect_uri=${window.location}`; - }; - - return ( -
- {isAuthenticated ? ( -
-
- {user?.name} -
-
- Logged in as: {user?.name} -
-
- -
-
- -
-
- ) : ( -
- -
- )} -
- ); -} +import { useAuthUser } from '@frontegg/react' -export default App; -``` +const HomePage = () => { + const user = useAuthUser(); -## Integrate Admin Portal + return
+ Logged In user: {user.email} +
+} +``` Opening the Admin Portal is available via the following code snippet. ```js import { AdminPortal } from '@frontegg/react' -const handleClick = () => { - AdminPortal.show(); -}; +const Toolbar = () => { + + return +} ``` diff --git a/babel.config.js b/babel.config.js deleted file mode 100644 index 3df52407e..000000000 --- a/babel.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - env: { - test: { - presets: ['@babel/preset-env', '@babel/preset-typescript', ['@babel/preset-react', { runtime: 'automatic' }]], - }, - }, -}; diff --git a/cypress.json b/cypress.json index 0537205af..4be3e8d0f 100644 --- a/cypress.json +++ b/cypress.json @@ -4,6 +4,7 @@ "experimentalSourceRewriting": true, "componentFolder": "packages", "testFiles": "**/*.cy-spec.*", + "projectId": "odcj7u", "viewportHeight": 800, "viewportWidth": 1440 } diff --git a/cypress/consts.ts b/cypress/consts.ts new file mode 100644 index 000000000..4ed7f401c --- /dev/null +++ b/cypress/consts.ts @@ -0,0 +1,305 @@ +export const auditsStats = { + totalToday: 124, + severeThisWeek: 0, +}; + +export const auditsMetadata = [ + { + _id: '5fb18c38955a8f002bff7afc', + entityName: 'audits', + vendorId: '5989f34a-f047-4cd9-b51d-b9d925e30572', + __v: 0, + createdAt: '2020-11-15T20:14:48.088Z', + hybridMode: false, + id: 'a9a1b174-2b52-4f29-9308-b761ad699a3a', + properties: [ + { + showInTable: true, + _id: '5e625def2b93b800370c5d4d', + name: 'user', + displayName: 'User', + type: 'AlphaNumeric', + sortable: true, + filterable: true, + showInMoreInfo: 'Always', + chosen: false, + selected: false, + }, + { + showInTable: true, + _id: '5e625def2b93b800370c5d4e', + name: 'createdAt', + displayName: 'Time', + type: 'Timestamp', + sortable: true, + filterable: true, + showInMoreInfo: 'Always', + chosen: false, + selected: false, + }, + { + showInTable: true, + _id: '5e625def2b93b800370c5d4c', + name: 'resource', + displayName: 'Resource', + type: 'AlphaNumeric', + sortable: true, + filterable: true, + showInMoreInfo: 'Always', + chosen: false, + selected: false, + }, + { + showInTable: true, + _id: '5e625def2b93b800370c5d4b', + name: 'action', + displayName: 'Action', + type: 'AlphaNumeric', + sortable: true, + filterable: true, + showInMoreInfo: 'Always', + chosen: false, + selected: false, + }, + { + showInTable: true, + _id: '5e625def2b93b800370c5d4a', + name: 'severity', + displayName: 'Severity', + type: 'Severity', + sortable: true, + filterable: true, + showInMoreInfo: 'Always', + chosen: false, + selected: false, + }, + { + showInTable: true, + _id: '5e625def2b93b800370c5d49', + name: 'ip', + displayName: 'IP Address', + type: 'IpAddress', + sortable: true, + filterable: true, + showInMoreInfo: 'Always', + chosen: false, + selected: false, + }, + { + showInTable: false, + _id: '5fbcf1d9f5295f002aecbcc0', + name: 'json', + type: 'Json', + sortable: false, + filterable: false, + displayName: 'Approvers', + showInMoreInfo: 'Always', + chosen: false, + }, + ], + updatedAt: '2020-11-24T11:43:21.986Z', + }, +]; + +export const auditsData = [ + { + ip: '25.42.49.21', + user: 'Naida Rinker', + action: 'Sanity Check Finished', + scanId: 'd5ae96fe-4df4-4ac3-937b-b16f9fba5eb4', + service: 'Users', + infoLink: 'https://iapp.org/news/a/understanding-data-processors-iso-and-soc-2-credentials-for-gdpr-compliance/', + resource: 'Cluster', + severity: 'Info', + standard: 'SOC2', + tenantId: 'my-tenant-id', + vendorId: '5989f34a-f047-4cd9-b51d-b9d925e30572', + createdAt: '2020-12-09 14:00:10.227', + frontegg_id: '9639a4bd-7004-460c-90be-77efbf73a719', + }, + { + ip: '23.92.49.21', + user: 'Wendi Burghardt', + action: 'Periodic Scan Finished', + result: '9 issues found', + scanId: '4274b8c8-1379-43c7-9a88-ed7d985ed13f', + service: 'Users', + resource: 'Service', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '5989f34a-f047-4cd9-b51d-b9d925e30572', + createdAt: '2020-12-09 14:00:05.362', + frontegg_id: 'fb664bbf-1db0-4f79-9d21-00b2e1551401', + }, + { + ip: '35.92.49.21', + user: 'Iris Basso', + cause: 'Service Response Lag', + action: 'Remediated', + scanId: '6d13f058-8930-404b-867d-8e2c78851ba9', + lagTime: '2500ms', + service: 'Payments', + resource: 'Service', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '5989f34a-f047-4cd9-b51d-b9d925e30572', + createdAt: '2020-12-09 14:00:00.425', + restartTime: '2000ms', + frontegg_id: '856bc536-75a4-4226-87f6-cbf80a8c006b', + }, + { + ip: '72.28.55.231', + user: 'Iris Basso', + action: 'Lag Detected', + result: 'Service Response Lag', + scanId: '60d534de-e49f-473f-8fa2-5fa54ecd9ed4', + lagTime: '11140ms', + service: 'Cars', + resource: 'Service', + severity: 'Error', + tenantId: 'my-tenant-id', + vendorId: '5989f34a-f047-4cd9-b51d-b9d925e30572', + createdAt: '2020-12-09 14:00:00.424', + frontegg_id: '382af9f2-5679-4956-a895-37bc0b1390de', + }, + { + ip: '161.185.160.93', + user: 'Normand Menz', + action: 'Cache Purged', + resource: 'API Gateway', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '5989f34a-f047-4cd9-b51d-b9d925e30572', + createdAt: '2020-12-09 14:00:00.423', + cachedItems: 972, + frontegg_id: 'af4b0f43-3cd9-4770-951d-de6ba87ca1f2', + }, + { + ip: '72.28.55.231', + user: 'Rhoda Blaylock', + action: 'Settings Modified', + resource: 'Microservice', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '5989f34a-f047-4cd9-b51d-b9d925e30572', + createdAt: '2020-12-09 14:00:00.422', + microservice: 'Cars', + settingChanged: 'Amount of Pods', + frontegg_id: 'cecff511-de43-41fb-98af-14e1b27f2697', + }, + { + ip: '72.28.101.231', + user: 'Deanna Post', + action: 'Remap', + changed: 'Security Level', + cluster: 'Main', + resource: 'Cluster', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '5989f34a-f047-4cd9-b51d-b9d925e30572', + createdAt: '2020-12-09 14:00:00.420', + frontegg_id: '3269541f-8a1c-4c26-a3ed-df0f98c7f575', + }, + { + ip: '72.28.55.231', + api: 'GET /insights/343', + user: 'Wendi Burghardt', + action: 'Liveness Check Perfomed', + result: 'Total Failure', + scanId: '277480cb-2c87-4dab-846d-f0da49a3a2c3', + resource: 'APIs', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '5989f34a-f047-4cd9-b51d-b9d925e30572', + createdAt: '2020-12-09 14:00:00.419', + frontegg_id: '829a3e61-e91b-415d-82c4-c16c6007d285', + }, + { + ip: '72.28.101.231', + user: 'Kieth Mason', + action: 'Settings Modified', + changed: 'Security Level', + cluster: 'Main', + resource: 'Cluster', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '5989f34a-f047-4cd9-b51d-b9d925e30572', + createdAt: '2020-12-09 14:00:00.293', + frontegg_id: '2a697fab-2623-4fdc-8355-088fb60b3a3d', + }, + { + ip: '25.42.29.21', + user: 'Marg Lovelace', + action: 'Sanity Check Finished', + scanId: 'c3df0a04-45e1-44e8-9259-b4b5fcc2adc2', + service: 'Cars', + infoLink: 'https://iapp.org/news/a/understanding-data-processors-iso-and-soc-2-credentials-for-gdpr-compliance/', + resource: 'Cluster', + severity: 'Info', + standard: 'GDPR', + tenantId: 'my-tenant-id', + vendorId: '5989f34a-f047-4cd9-b51d-b9d925e30572', + createdAt: '2020-12-09 13:00:10.179', + frontegg_id: '3697dd41-2554-4bdd-95ea-613b7e4d7400', + }, + { + ip: '25.42.29.21', + user: 'Rena Flanders', + action: 'Periodic Scan Finished', + result: '9 issues found', + scanId: 'f0c0cc43-5e1e-4f78-81b2-f07862bc405b', + service: 'Users', + resource: 'Service', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '5989f34a-f047-4cd9-b51d-b9d925e30572', + createdAt: '2020-12-09 13:00:05.239', + frontegg_id: '18749431-4c12-462b-927b-3ec196c0f87f', + }, +]; + +export const auditsDataDescName = [ + { + ip: '23.92.49.21', + user: 'Wendi Burghardt', + action: 'Periodic Scan Finished', + result: '9 issues found', + scanId: '4274b8c8-1379-43c7-9a88-ed7d985ed13f', + service: 'Users', + resource: 'Service', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '5989f34a-f047-4cd9-b51d-b9d925e30572', + createdAt: '2020-12-09 14:00:05.362', + frontegg_id: 'fb664bbf-1db0-4f79-9d21-00b2e1551401', + }, + { + ip: '23.92.49.21', + user: 'Wendi Mendi', + action: 'Sanity Check Finished', + result: '3 issues found', + scanId: '4274b8c8-1379-43c7-9a88-ed7d985ed13f', + service: 'Users', + resource: 'Service', + severity: 'info', + tenantId: 'my-tenant-id', + vendorId: '5989f34a-f047-4cd9-b51d-b9d925e30572', + createdAt: '2020-12-10 14:00:05.362', + frontegg_id: 'fb664bbf-1db0-4f73-9d21-00b2e1551401', + }, + { + ip: '25.42.49.21', + user: 'Naida Rinker', + action: 'Sanity Check Finished', + scanId: 'd5ae96fe-4df4-4ac3-937b-b16f9fba5eb4', + service: 'Users', + infoLink: 'https://iapp.org/news/a/understanding-data-processors-iso-and-soc-2-credentials-for-gdpr-compliance/', + resource: 'Cluster', + severity: 'Info', + standard: 'SOC2', + tenantId: 'my-tenant-id', + vendorId: '5989f34a-f047-4cd9-b51d-b9d925e30572', + createdAt: '2020-12-09 14:00:10.227', + frontegg_id: '9639a4bd-7004-460c-90be-77efbf73a719', + }, +]; diff --git a/cypress/helpers.tsx b/cypress/helpers.tsx new file mode 100644 index 000000000..a1c874d48 --- /dev/null +++ b/cypress/helpers.tsx @@ -0,0 +1,223 @@ +/* istanbul ignore file */ + +import React, { FC } from 'react'; +import { FronteggProvider, PluginConfig, ContextOptions } from '@frontegg/react-core'; +import { uiLibrary } from '@frontegg/react-elements-semantic'; +import { auditsData, auditsDataDescName, auditsMetadata, auditsStats } from './consts'; + +export const METADATA_SERVICE = 'http://localhost:8080/frontegg/metadata'; +export const IDENTITY_SERVICE = 'http://localhost:8080/frontegg/identity'; +export const AUDITS_SERVICE = 'http://localhost:8080/frontegg/audits'; +export const TEAM_SERVICE = 'http://localhost:8080/frontegg/team'; + +const contextOptions: ContextOptions = { + baseUrl: `http://localhost:8080`, + requestCredentials: 'include', +}; + +export type TestFronteggWrapperProps = { + plugins: PluginConfig[]; +}; +export const TestFronteggWrapper: FC = (props) => ( + + {props.children} + +); + +export const mountOptions = { + stylesheets: 'https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css', +}; + +declare global { + interface Window { + cypressHistory: any; + } +} + +export const navigateTo = (path: string) => { + cy.window().then((win) => { + win.cypressHistory.push(path); + }); +}; + +export const mockAuthMe = () => { + cy.route({ + method: 'GET', + url: `${IDENTITY_SERVICE}/resources/users/v2/me`, + status: 200, + delay: 200, + response: { + activatedForTenant: true, + email: EMAIL_1, + id: USER_ID_1, + metadata: null, + mfaEnrolled: false, + name: 'Test Test', + permissions: [], + phoneNumber: null, + profilePictureUrl: null, + provider: 'local', + roles: [], + tenantId: 'my-tenant-id', + tenantIds: ['my-tenant-id'], + verified: true, + }, + }).as('me'); + cy.route({ + method: 'GET', + url: `${IDENTITY_SERVICE}/resources/users/v2/me/tenants`, + status: 200, + delay: 200, + response: [], + }).as('meTenants'); +}; + +export const mockAuthApi = ( + authenticated: boolean, + saml: boolean, + socialLogin: boolean = false, + publicConfigurations = { + allowOverrideEnforcePasswordHistory: false, + allowOverridePasswordComplexity: false, + allowOverridePasswordExpiration: false, + allowSignups: false, + }, + publicAuthStrategyConfigurations = { + secondaryAuthStrategies: [], + } +) => { + if (authenticated) { + cy.route({ + method: 'POST', + url: `${IDENTITY_SERVICE}/resources/auth/v1/user/token/refresh`, + status: 200, + response: { + accessToken: '', + refreshToken: '', + verified: true, + }, + }).as('refreshToken'); + } else { + cy.route({ + method: 'POST', + url: `${IDENTITY_SERVICE}/resources/auth/v1/user/token/refresh`, + status: 401, + response: 'Unauthorized', + }).as('refreshToken'); + } + if (saml) { + cy.route({ + method: 'GET', + url: `${TEAM_SERVICE}/resources/sso/v2/configurations/public`, + status: 200, + delay: 200, + response: { + isActive: true, + }, + }).as('metadata'); + } else { + cy.route({ + method: 'GET', + url: `${TEAM_SERVICE}/resources/sso/v2/configurations/public`, + status: 200, + delay: 200, + response: { + isActive: false, + }, + }).as('metadata'); + } + + if (socialLogin) { + cy.route({ + method: 'GET', + url: `${IDENTITY_SERVICE}/resources/sso/v1`, + status: 200, + delay: 200, + response: [ + { + active: true, + clientId: 'google_client_id', + redirectUrl: 'http://localhost:3000/account/social/success', + type: 'google', + }, + ], + }).as('socialLogin'); + } else { + cy.route({ + method: 'GET', + url: `${IDENTITY_SERVICE}/resources/sso/v1`, + status: 200, + delay: 200, + response: [], + }).as('socialLogin'); + } + cy.route({ + method: 'GET', + url: `${IDENTITY_SERVICE}/resources/configurations/v1/public`, + status: 200, + delay: 200, + response: publicConfigurations, + }).as('publicConfigurations'); + cy.route({ + method: 'GET', + url: `${IDENTITY_SERVICE}/resources/configurations/v1/auth/strategies/public`, + status: 200, + delay: 200, + response: publicAuthStrategyConfigurations, + }).as('publicAuthStrategyConfigurations'); +}; + +export const mockAuditsApi = () => { + cy.route({ + method: 'GET', + url: `${AUDITS_SERVICE}?sortDirection=desc&sortBy=createdAt&filter=&offset=0&count=20`, + status: 200, + delay: 200, + response: { + data: auditsData, + total: auditsData.length, + }, + }).as('auditsData'); + cy.route({ + method: 'GET', + url: `${AUDITS_SERVICE}?sortDirection=desc&sortBy=user&filter=&offset=0&count=20`, + status: 200, + delay: 200, + response: { + data: auditsDataDescName, + total: auditsDataDescName.length, + }, + }).as('auditsDataNameDesc'); + cy.route({ + method: 'GET', + url: `${METADATA_SERVICE}?entityName=audits`, + status: 200, + response: { + rows: auditsMetadata, + }, + }).as('auditsMetadata'); + cy.route({ + method: 'GET', + url: `${AUDITS_SERVICE}/stats?sortBy=createdAt&sortDirection=desc&count=20`, + status: 200, + delay: 200, + response: auditsStats, + }).as('auditsStats'); +}; + +export const EMAIL_1 = 'test1@frontegg.com'; +export const USER_ID_1 = '3065bce5-a3ff-42bd-a519-97bbace20e8b'; +export const PASSWORD = 'ValidPassword123!'; +export const ACCESS_TOKEN = + 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI4YTIyYjQyNy01MjA0LTQ2NzYtOWNhMC03ZTVjMWJkMDhiZjYiLCJuYW1lIjoiRGF2aWQiLCJlbWFpbCI6ImRhdmlkQGZyb250ZWdnLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJyb2xlcyI6WyJ3cml0ZSJdLCJwZXJtaXNzaW9ucyI6WyJjb25maWd1cmUtc3NvIiwiYWRkLXNsYWNrIiwiYWRkLXdlYmhvb2tzIl0sInRlbmFudElkIjoibXktdGVuYW50LWlkIiwidGVuYW50SWRzIjpbIm15LXRlbmFudC1pZCJdLCJpYXQiOjE1OTk2MTUyOTMsImV4cCI6MTU5OTYxNTU5MywiaXNzIjoiZnJvbnRlZ2cifQ.CdNSM-0I6cU9cEpBE5dj7jZyRfgBK3ozZ0hNxFFhM_jv9NdQp2BBkUkHTdKpvwFdub4LCUwd1h2kdvdTuGHaQDNVVoetCpzJsXMUejBdCPu6MiShNLstBdzAjnypCuwy3Mfv7tIEB3njuKeNDWJZY32EDXawdepnugRjsDIqQsQ'; +export const checkEmailValidation = (emailSelector: string = '[name="email"]') => { + cy.get(emailSelector).focus().clear().type('invalid email').blur(); + cy.contains('Must be a valid email').should('be.visible'); + cy.get(emailSelector).focus().clear().blur(); + cy.contains('The Email is required').should('be.visible'); + cy.get(emailSelector).focus().clear().type(EMAIL_1).blur(); + cy.get(emailSelector).parents('.field').should('not.have.class', 'error'); +}; + +export const submitButtonSelector = 'button[type="submit"]'; +export const emailInputSelector = 'input[name="email"]'; diff --git a/cypress/integration/auth.spec.ts b/cypress/integration/auth.spec.ts new file mode 100644 index 000000000..a06d74227 --- /dev/null +++ b/cypress/integration/auth.spec.ts @@ -0,0 +1,73 @@ +import { IDENTITY_SERVICE, METADATA_SERVICE } from './constants'; + +describe('Auth Test', () => { + // it('[login page], no saml, test input validation', () => { + // cy.server(); + // cy.route({ method: 'POST', url: `${IDENTITY_SERVICE}/resources/auth/v1/user/token/refresh`, status: 401, response: 'Unauthorized' }); + // cy.route({ method: 'GET', url: `${METADATA_SERVICE}?entityName=saml`, status: 200, response: { 'rows': [] } }); + // cy.visit('/account/login'); + // + // const emailSelector = '[name="email"]'; + // const passwordSelector = '[name="password"]'; + // const submitSelector = 'button[type=submit]'; + // + // cy.get(submitSelector).contains('Login').should('be.disabled'); + // cy.get(emailSelector).focus().clear().type('invalid email').blur(); + // cy.get(emailSelector).parent().should('have.class', 'error'); + // cy.get(emailSelector).focus().clear().type('test1@frontegg.com').blur(); + // cy.get(emailSelector).parent().should('not.have.class', 'error'); + // cy.get(submitSelector).contains('Login').should('be.disabled'); + // cy.get(passwordSelector).focus().clear().type('not').blur(); + // cy.get(passwordSelector).parent().should('have.class', 'error'); + // cy.get(submitSelector).contains('Login').should('be.disabled'); + // cy.get(passwordSelector).focus().clear().type('valid_password').blur(); + // cy.get(passwordSelector).parent().should('not.have.class', 'error'); + // cy.get(submitSelector).contains('Login').should('not.be.disabled'); + // cy.get(emailSelector).focus().clear().type('invalid email').blur(); + // cy.get(emailSelector).parent().should('have.class', 'error'); + // cy.get(submitSelector).contains('Login').should('be.disabled'); + // cy.get(emailSelector).focus().clear().type('test1@frontegg.com').blur(); + // cy.get(emailSelector).parent().should('not.have.class', 'error'); + // cy.get(submitSelector).contains('Login').should('not.be.disabled'); + // }); + + it('[login page], with saml, test input validation', () => { + cy.server(); + cy.route({ + method: 'POST', + url: `${IDENTITY_SERVICE}/resources/auth/v1/user/token/refresh`, + status: 401, + response: 'Unauthorized', + }); + cy.route({ method: 'GET', url: `${METADATA_SERVICE}?entityName=saml`, status: 200, response: { rows: [{}] } }); + cy.visit('/account/login'); + + const emailSelector = '[name="email"]'; + const passwordSelector = '[name="password"]'; + const submitSelector = 'button[type=submit]'; + + cy.get(submitSelector).contains('Continue').should('be.disabled'); + cy.get(emailSelector).focus().clear().type('invalid email').blur(); + cy.get(emailSelector).parent().should('have.class', 'error'); + cy.get(emailSelector).focus().clear().type('test1@frontegg.com').blur(); + cy.get(emailSelector).parent().should('not.have.class', 'error'); + cy.get(submitSelector).contains('Continue').should('not.be.disabled'); + + cy.get(submitSelector).contains('Continue').click(); + + // cy.get(passwordSelector).focus().clear().type('not').blur(); + // cy.get(passwordSelector).parent().should('have.class', 'error'); + // cy.get(submitSelector).contains('Login').should('be.disabled'); + // cy.get(passwordSelector).focus().clear().type('valid_password').blur(); + // cy.get(passwordSelector).parent().should('not.have.class', 'error'); + // cy.get(submitSelector).contains('Login').should('not.be.disabled'); + // cy.get(emailSelector).focus().clear().type('invalid email').blur(); + // cy.get(emailSelector).parent().should('have.class', 'error'); + // cy.get(submitSelector).contains('Login').should('be.disabled'); + // cy.get(emailSelector).focus().clear().type('test1@frontegg.com').blur(); + // cy.get(emailSelector).parent().should('not.have.class', 'error'); + // cy.get(submitSelector).contains('Login').should('not.be.disabled'); + }); +}); + +export {}; diff --git a/cypress/integration/constants.d.ts b/cypress/integration/constants.d.ts new file mode 100644 index 000000000..90ebeebb9 --- /dev/null +++ b/cypress/integration/constants.d.ts @@ -0,0 +1,2 @@ +export declare const METADATA_SERVICE = "http://localhost:8080/frontegg/metadata"; +export declare const IDENTITY_SERVICE = "http://localhost:8080/frontegg/identity"; diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js new file mode 100644 index 000000000..ef87b6b8a --- /dev/null +++ b/cypress/plugins/index.js @@ -0,0 +1,68 @@ +/// +const webpack = require('@cypress/webpack-preprocessor'); +const webpackOptions = { + mode: 'development', + node: { + fs: 'empty', + }, + resolve: { + extensions: ['.jsx', '.tsx', '.js', '.ts'], + }, + module: { + rules: [ + { + test: /\.(js|jsx|mjs)$/, + loader: 'babel-loader', + exclude: /node_modules/, + }, + { + test: /\.(ts|tsx)$/, + loader: 'babel-loader', + exclude: /node_modules/, + options: { + presets: ['@babel/preset-env', '@babel/preset-typescript', '@babel/preset-react'], + plugins: [ + '@babel/plugin-proposal-class-properties', + '@babel/plugin-proposal-object-rest-spread', + [ + '@babel/plugin-transform-runtime', + { + regenerator: true, + }, + ], + 'istanbul', + ], + }, + }, + { + test: /\.css$/, + use: ['style-loader', 'css-loader'], + }, + { + test: /\.scss$/, + exclude: [/node_modules/], + use: ['style-loader', 'css-loader', 'sass-loader'], + }, + ], + }, + optimization: { + removeAvailableModules: false, + removeEmptyChunks: false, + splitChunks: false, + }, +}; + +/** + * @type {Cypress.PluginConfig} + */ +module.exports = (on, config) => { + require('@cypress/code-coverage/task')(on, config); + on( + 'file:preprocessor', + webpack({ + webpackOptions, + watchOptions: {}, + }) + ); + return config; +}; diff --git a/cypress/support/commands.js b/cypress/support/commands.js new file mode 100644 index 000000000..ca4d256f3 --- /dev/null +++ b/cypress/support/commands.js @@ -0,0 +1,25 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add("login", (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) diff --git a/cypress/support/index.js b/cypress/support/index.js new file mode 100644 index 000000000..f3155a4cd --- /dev/null +++ b/cypress/support/index.js @@ -0,0 +1,20 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +// import './commands' + +import 'cypress-react-unit-test/support'; +import '@cypress/code-coverage/support'; diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json new file mode 100644 index 000000000..b9cb36a45 --- /dev/null +++ b/cypress/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "sourceMap": false, + "inlineSourceMap": true, + "declaration": false, + "types": [ + "cypress" + ] + } +} diff --git a/imgs/sso-basic-example.png b/imgs/sso-basic-example.png new file mode 100644 index 000000000..9709c5079 Binary files /dev/null and b/imgs/sso-basic-example.png differ diff --git a/lerna.json b/lerna.json index 37d687bf5..f28b6e833 100755 --- a/lerna.json +++ b/lerna.json @@ -2,7 +2,7 @@ "packages": [ "packages/*" ], - "version": "7.15.2", + "version": "4.0.23", "npmClient": "yarn", "useWorkspaces": true, "publishConfig": { diff --git a/package.json b/package.json index 00d430597..b3f624a3d 100644 --- a/package.json +++ b/package.json @@ -4,46 +4,109 @@ "private": true, "author": "Frontegg LTD", "scripts": { - "prettier-hook": "pretty-quick", - "prettier-check-hook": "pretty-quick --check", - "prepublishOnly": "make move-package-json-to-dist", - "update-version": "make update-entry", - "test": "jest --env=jsdom", - "dev": "NODE_ENV=development lerna run build:watch --parallel", - "demo": "cd ./packages/demo-saas && yarn start", - "prettier": "pretty-quick" + "prettier-hook": "prettier --config ./.prettierrc.json --write .", + "prettier-check-hook": "prettier --config ./.prettierrc.json --check .", + "prepublishOnly": "make move-package-json-to-dist" }, "devDependencies": { - "@babel/preset-env": "^7.23.3", - "@babel/preset-react": "^7.23.3", - "@babel/preset-typescript": "^7.23.3", + "@babel/core": "^7.12.3", + "@babel/plugin-proposal-class-properties": "^7.10.4", + "@babel/plugin-proposal-object-rest-spread": "^7.11.0", + "@babel/plugin-transform-modules-commonjs": "^7.10.4", + "@babel/plugin-transform-runtime": "^7.12.1", + "@babel/preset-env": "^7.12.1", + "@babel/preset-react": "^7.12.1", + "@babel/preset-typescript": "^7.12.1", + "@babel/runtime": "^7.12.1", + "@cypress/code-coverage": "^3.8.1", + "@cypress/instrument-cra": "^1.3.1", + "@cypress/webpack-preprocessor": "^5.4.5", + "@istanbuljs/nyc-config-typescript": "^1.0.1", + "@rollup/plugin-alias": "^3.1.1", + "@rollup/plugin-commonjs": "^13.0.0", + "@rollup/plugin-json": "^4.1.0", "@rollup/plugin-node-resolve": "^8.0.1", - "@testing-library/react": "12.1.2", + "@rollup/plugin-replace": "^2.3.3", + "@testing-library/cypress": "^6.0.1", + "@types/clear": "^0.1.0", + "@types/enzyme": "^3.10.3", + "@types/enzyme-adapter-react-16": "^1.0.5", + "@types/figlet": "^1.2.0", "@types/history": "^4.7.7", - "@types/jest": "^29.5.8 ", - "@types/node": "^18.18.0", - "@types/react-is": "^17.0.3", - "babel-jest": "^29.7.0", - "eslint": "^7.0.0", + "@types/jest": "^25.1.4", + "@types/node": "^13.9.1", + "@types/prompts": "^2.0.8", + "@types/react": "^16.9.16", + "@types/react-is": "^16.7.1", + "@types/rollup-plugin-postcss": "^2.0.0", + "@types/testing-library__cypress": "^5.0.6", + "@types/yargs": "^15.0.5", + "@types/yup": "^0.29.4", + "@zerollup/ts-transform-paths": "^1.7.18", + "awesome-typescript-loader": "^5.2.1", + "babel-eslint": "^10.0.3", + "babel-jest": "^24.9.0", + "babel-loader": "^8.1.0", + "babel-plugin-istanbul": "^6.0.0", + "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2", + "chalk": "^4.1.0", + "class-transformer": "^0.3.1", + "class-validator": "^0.11.0", + "clear": "^0.1.0", + "concurrently": "^5.3.0", + "css-loader": "^3.5.3", + "cypress-react-unit-test": "^4.12.0", + "enzyme": "^3.11.0", + "enzyme-adapter-react-16": "^1.15.1", + "eslint": "^6.6.0", "eslint-plugin-react": "^7.17.0", + "figlet": "^1.5.0", + "git-format-staged": "^2.1.0", + "handlebars": "^4.7.6", "history": "^4.9.0", - "jest": "^29.7.0", - "jest-environment-jsdom": "^29.7.0", - "lerna": "5.1.8", + "jest": "26.0.1", + "jest-css-modules-transform": "^4.0.0", + "lerna": "3.20.2", + "node-sass": "^4.14.1", "path": "^0.12.7", "pre-commit": "^1.2.2", - "prettier": "2.7.1", - "pretty-quick": "^3.1.3", - "react": "^17.0.1", - "react-dom": "^17.0.1", - "@types/react": "^17.0.1", - "@types/react-dom": "^17.0.1", + "prettier": "2.1.1", + "prompts": "^2.3.2", + "react": ">16.8.6", + "react-dom": ">16.8.6", + "react-is": ">16.8.6", "rollup": "^2.15.0", + "rollup-plugin-analyzer": "^3.2.3", + "rollup-plugin-cleanup": "^3.1.1", + "rollup-plugin-clear": "^2.0.7", + "rollup-plugin-commonjs": "^10.1.0", + "rollup-plugin-copy": "^3.3.0", + "rollup-plugin-filesize": "^9.0.0", "rollup-plugin-node-resolve": "^5.2.0", + "rollup-plugin-peer-deps-external": "^2.2.2", + "rollup-plugin-postcss": "^3.1.2", + "rollup-plugin-prettier": "^2.1.0", + "rollup-plugin-progress": "^1.1.2", + "rollup-plugin-sass": "^1.2.2", + "rollup-plugin-strip-banner": "^2.0.0", + "rollup-plugin-terser": "^6.1.0", "rollup-plugin-typescript2": "^0.27.2", + "rollup-plugin-uglify": "^6.0.4", + "rollup-plugin-visualizer": "^4.0.4", + "sass-loader": "^10.0.1", + "serve": "^11.3.2", + "start-server-and-test": "^1.11.3", + "style-inject": "^0.3.0", + "style-loader": "^1.2.1", + "ts-jest": "^25.2.1", + "ts-loader": "^7.0.5", + "ts-node": "^8.6.2", + "tsconfig-paths": "^3.9.0", "tslib": "^2.0.1", "tslint": "^6.1.0", - "typescript": "^3.9.7" + "typescript": "^3.9.7", + "webpack": "^4.44.1", + "yargs": "^15.4.1" }, "nyc": { "reporter": [ @@ -55,10 +118,5 @@ }, "workspaces": [ "packages/*" - ], - "jest": { - "transformIgnorePatterns": [ - "!node_modules/" - ] - } + ] } diff --git a/packages/audits/CHANGELOG.md b/packages/audits/CHANGELOG.md new file mode 100644 index 000000000..32f86ed88 --- /dev/null +++ b/packages/audits/CHANGELOG.md @@ -0,0 +1,616 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [2.8.14](https://github.com/frontegg/frontegg-react/compare/v2.8.13...v2.8.14) (2021-07-27) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +## [2.8.12](https://github.com/frontegg/frontegg-react/compare/v2.8.11...v2.8.12) (2021-07-20) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +## [2.8.10](https://github.com/frontegg/frontegg-react/compare/v2.8.9...v2.8.10) (2021-07-08) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +## [2.8.9](https://github.com/frontegg/frontegg-react/compare/v2.8.8...v2.8.9) (2021-07-08) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +## [2.8.8](https://github.com/frontegg/frontegg-react/compare/v2.8.7...v2.8.8) (2021-07-06) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +## [2.8.7](https://github.com/frontegg/frontegg-react/compare/v2.8.6...v2.8.7) (2021-07-01) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +## [2.8.6](https://github.com/frontegg/frontegg-react/compare/v2.8.5...v2.8.6) (2021-06-30) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +## [2.8.5](https://github.com/frontegg/frontegg-react/compare/v2.8.4...v2.8.5) (2021-06-30) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +## [2.8.4](https://github.com/frontegg/frontegg-react/compare/v2.8.3...v2.8.4) (2021-06-29) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +## [2.8.3](https://github.com/frontegg/frontegg-react/compare/v2.8.1...v2.8.3) (2021-06-23) + + +### Bug Fixes + +* align all dependencies versions ([f1d5c48](https://github.com/frontegg/frontegg-react/commit/f1d5c48aba34827ea06a554cfb61734f1a40c93b)) + + + + + +## [2.8.2](https://github.com/frontegg/frontegg-react/compare/v2.8.1...v2.8.2) (2021-06-23) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +## [2.8.1](https://github.com/frontegg/frontegg-react/compare/v2.8.0...v2.8.1) (2021-06-22) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +# [2.8.0](https://github.com/frontegg/frontegg-react/compare/v2.7.2...v2.8.0) (2021-06-21) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +## [2.7.2](https://github.com/frontegg/frontegg-react/compare/v2.7.1...v2.7.2) (2021-06-14) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +# [2.7.0](https://github.com/frontegg/frontegg-react/compare/v2.6.0...v2.7.0) (2021-06-07) + + +### Bug Fixes + +* **audits:** fix ip cell crash ([#425](https://github.com/frontegg/frontegg-react/issues/425)) ([169c4b6](https://github.com/frontegg/frontegg-react/commit/169c4b67f038d53f4eee600f0c9973cc56855779)) + + + + + +# [2.6.0](https://github.com/frontegg/frontegg-react/compare/v2.5.2...v2.6.0) (2021-05-27) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +## [2.5.2](https://github.com/frontegg/frontegg-react/compare/v2.5.1...v2.5.2) (2021-05-24) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +## [2.5.1](https://github.com/frontegg/frontegg-react/compare/v2.5.0...v2.5.1) (2021-05-23) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +# [2.4.0](https://github.com/frontegg/frontegg-react/compare/v2.3.2...v2.4.0) (2021-05-19) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +## [2.3.2](https://github.com/frontegg/frontegg-react/compare/v2.3.1...v2.3.2) (2021-05-10) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +## [2.3.1](https://github.com/frontegg/frontegg-react/compare/v2.3.0...v2.3.1) (2021-05-10) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +# [2.3.0](https://github.com/frontegg/frontegg-react/compare/v2.2.2...v2.3.0) (2021-05-07) + + +### Bug Fixes + +* **audits:** fix position and behaviors of the User Agent logo ([3e067ef](https://github.com/frontegg/frontegg-react/commit/3e067efe8253c460174e6ea103774a5fc64016e9)) + + + + + +## [2.2.2](https://github.com/frontegg/frontegg-react/compare/v2.2.1...v2.2.2) (2021-04-29) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +## [2.2.1](https://github.com/frontegg/frontegg-react/compare/v2.2.0...v2.2.1) (2021-04-28) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +# [2.2.0](https://github.com/frontegg/frontegg-react/compare/v2.1.0...v2.2.0) (2021-04-28) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +# [2.1.0](https://github.com/frontegg/frontegg-react/compare/v2.0.0...v2.1.0) (2021-04-13) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +# [2.0.0](https://github.com/frontegg/frontegg-react/compare/v1.28.0...v2.0.0) (2021-04-11) + + +### Bug Fixes + +* **audits:** FR-2162 - remove expandable in case of nothing to show(audits) ([25df9ed](https://github.com/frontegg/frontegg-react/commit/25df9edb782ba00a49fcd87ebbd2cae05824f10f)) + + + + + +# [1.28.0](https://github.com/frontegg/frontegg-react/compare/v1.27.0...v1.28.0) (2021-03-22) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +# [1.27.0](https://github.com/frontegg/frontegg-react/compare/v1.26.0...v1.27.0) (2021-03-18) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +# [1.26.0](https://github.com/frontegg/frontegg-react/compare/v1.25.0...v1.26.0) (2021-03-17) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +# [1.25.0](https://github.com/frontegg/frontegg-react/compare/v1.24.0...v1.25.0) (2021-03-07) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +# [1.24.0](https://github.com/frontegg/frontegg-react/compare/v1.23.1...v1.24.0) (2021-03-03) + + +### Bug Fixes + +* **audits:** align icon and add cursor pointer on icons ([c6977b0](https://github.com/frontegg/frontegg-react/commit/c6977b006c2086600ed42c36aee29116871a457f)) + + + + + +## [1.23.1](https://github.com/frontegg/frontegg-react/compare/v1.23.0...v1.23.1) (2021-02-21) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +# [1.23.0](https://github.com/frontegg/frontegg-react/compare/v1.22.1...v1.23.0) (2021-02-18) + + +### Features + +* **audits:** add several custom browser icons to the User Agent field ([8bba084](https://github.com/frontegg/frontegg-react/commit/8bba0841ec39bc6b9c04100abc437964464907b1)) + + + + + +## [1.22.1](https://github.com/frontegg/frontegg-react/compare/v1.22.0...v1.22.1) (2021-02-16) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +# [1.22.0](https://github.com/frontegg/frontegg-react/compare/v1.21.1...v1.22.0) (2021-02-13) + + +### Bug Fixes + +* **audits:** fix nullable conversion case in audits table ([0acacdf](https://github.com/frontegg/frontegg-react/commit/0acacdf2e1b14062cb557a96e5b6d9c7cb966087)) +* **auth:** small fixes ([1463b88](https://github.com/frontegg/frontegg-react/commit/1463b88eb51b048abb7a57a53f58b492b6cc1adf)) + + +### Features + +* **auth:** instead of text in the user agent field, now show an icon of a browser ([8267338](https://github.com/frontegg/frontegg-react/commit/82673384710144597e484676c30f6569628399a3)) + + + + + +## [1.21.1](https://github.com/frontegg/frontegg-react/compare/v1.21.0...v1.21.1) (2021-02-04) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +# [1.21.0](https://github.com/frontegg/frontegg-react/compare/v1.20.1...v1.21.0) (2021-02-04) + + +### Bug Fixes + +* **audits:** fix show filter icon if the filterable value is desabled ([00f0afd](https://github.com/frontegg/frontegg-react/commit/00f0afd19f3d87deff66c198fe115d2f1e7d6708)) +* **audits:** FR-1875 - changed time filter format ([24e58a2](https://github.com/frontegg/frontegg-react/commit/24e58a2c742ac660a92add0035db0e2a4adac12e)) +* **audits:** leave the empty values in the cell instead of text ([360c09c](https://github.com/frontegg/frontegg-react/commit/360c09c6b9d47cd0a840851dec265ddd96463d96)) + + + + + +## [1.20.1](https://github.com/frontegg/frontegg-react/compare/v1.20.0...v1.20.1) (2021-02-02) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +# [1.20.0](https://github.com/frontegg/frontegg-react/compare/v1.19.1...v1.20.0) (2021-02-01) + + +### Features + +* **audits:** changes component for posibility use in the deshboard project ([a6365b8](https://github.com/frontegg/frontegg-react/commit/a6365b8aa65702bf0299a3f08d1147b8788a1890)) + + + + + +## [1.19.1](https://github.com/frontegg/frontegg-react/compare/v1.19.0...v1.19.1) (2021-01-20) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +# [1.19.0](https://github.com/frontegg/frontegg-react/compare/v1.18.6...v1.19.0) (2021-01-20) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +## [1.18.6](https://github.com/frontegg/frontegg-react/compare/v1.18.5...v1.18.6) (2021-01-19) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +## [1.18.5](https://github.com/frontegg/frontegg-react/compare/v1.18.4...v1.18.5) (2021-01-18) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +## [1.18.4](https://github.com/frontegg/frontegg-react/compare/v1.18.3...v1.18.4) (2021-01-18) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +## [1.18.3](https://github.com/frontegg/frontegg-react/compare/v1.18.2...v1.18.3) (2021-01-17) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +## [1.18.2](https://github.com/frontegg/frontegg-react/compare/v1.18.1...v1.18.2) (2021-01-15) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +## [1.18.1](https://github.com/frontegg/frontegg-react/compare/v1.18.0...v1.18.1) (2021-01-15) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +# [1.18.0](https://github.com/frontegg/frontegg-react/compare/v1.17.3...v1.18.0) (2021-01-14) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +## [1.17.3](https://github.com/frontegg/frontegg-react/compare/v1.17.2...v1.17.3) (2021-01-13) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +## [1.17.2](https://github.com/frontegg/frontegg-react/compare/v1.17.1...v1.17.2) (2021-01-12) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +## [1.17.1](https://github.com/frontegg/frontegg-react/compare/v1.17.0...v1.17.1) (2021-01-05) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +# [1.17.0](https://github.com/frontegg/frontegg-react/compare/v1.16.2...v1.17.0) (2021-01-03) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +## [1.16.2](https://github.com/frontegg/frontegg-react/compare/v1.16.0...v1.16.2) (2020-12-27) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +## [1.16.1](https://github.com/frontegg/frontegg-react/compare/v1.16.0...v1.16.1) (2020-12-27) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +# [1.16.0](https://github.com/frontegg/frontegg-react/compare/v1.15.2...v1.16.0) (2020-12-24) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +## [1.15.2](https://github.com/frontegg/frontegg-react/compare/v1.15.1...v1.15.2) (2020-12-24) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +## [1.15.1](https://github.com/frontegg/frontegg-react/compare/v1.15.0...v1.15.1) (2020-12-21) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +# [1.15.0](https://github.com/frontegg/frontegg-react/compare/v1.14.1...v1.15.0) (2020-12-20) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +## [1.14.1](https://github.com/frontegg/frontegg-react/compare/v1.14.0...v1.14.1) (2020-12-17) + + +### Bug Fixes + +* Fix search bar alignments and ui bug fixes ([dd51197](https://github.com/frontegg/frontegg-react/commit/dd5119705cad6e379459171e34a5a3abe4d891ff)) + + + + + +# [1.14.0](https://github.com/frontegg/frontegg-react/compare/v1.13.2...v1.14.0) (2020-12-16) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +## [1.13.2](https://github.com/frontegg/frontegg-react/compare/v1.13.1...v1.13.2) (2020-12-13) + + +### Bug Fixes + +* **audits:** FR-1001 add 'unknown' when cell value is undefined ([889bc83](https://github.com/frontegg/frontegg-react/commit/889bc83ae9105228b88c32826a81eb7b8de4d0d4)) + + + + + +## [1.13.1](https://github.com/frontegg/frontegg-react/compare/v1.13.0...v1.13.1) (2020-12-10) + + +### Bug Fixes + +* **audits:** FR-999 add x btn for ip popup ([6efc8ea](https://github.com/frontegg/frontegg-react/commit/6efc8ea6e229b6434d75f9af59c0b6f0993ec9cd)) + + + + + +# [1.13.0](https://github.com/frontegg/frontegg-react/compare/v1.12.0...v1.13.0) (2020-12-09) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +# [1.12.0](https://github.com/frontegg/frontegg-react/compare/v1.11.1...v1.12.0) (2020-12-09) + + +### Bug Fixes + +* **audits:** display none cross btns in filters selectors to prevent errors ([6afb046](https://github.com/frontegg/frontegg-react/commit/6afb046e69ac7fb10be28bda9bd44eb3d9524cc6)) +* split AuditsPage to separated components ([9aa109a](https://github.com/frontegg/frontegg-react/commit/9aa109a09a357333788abab845f9ff906e636cc3)) +* **audits:** fix font-weights for ip popup titels ([5b23d08](https://github.com/frontegg/frontegg-react/commit/5b23d0877faf836193c401b4a2afabcbd3e65d89)) +* **audits:** FR-1002 add globe icon when location is unknown ([53265fd](https://github.com/frontegg/frontegg-react/commit/53265fd5cdc56d3fc141ad77469d452e2f6dc430)) +* **audits:** FR-1003 Change severity filters to match the actual filters ([ce9d834](https://github.com/frontegg/frontegg-react/commit/ce9d834e204f7bc75e57b27ba82042ba45fd971a)) +* **audits:** FR-1004 add startRefresh action to prevent double fetch after init render ([eddb763](https://github.com/frontegg/frontegg-react/commit/eddb763711863f3a153679455053a963719b876a)) +* **audits:** FR-1004 prevent setFilterData action call after init render ([9656648](https://github.com/frontegg/frontegg-react/commit/9656648fdd5ba7c37fa9474f3f088b24f855b420)) +* **audits:** FR-997 reduce margins, change tags to divs in ip popup ([66f0005](https://github.com/frontegg/frontegg-react/commit/66f000585ef74da4d4056d3dd72e6df981955b4c)) + + + + + +## [1.11.1](https://github.com/frontegg/frontegg-react/compare/v1.11.0...v1.11.1) (2020-12-02) + +**Note:** Version bump only for package @frontegg/react-audits + + + + + +# [1.11.0](https://github.com/frontegg/frontegg-react/compare/v1.10.0...v1.11.0) (2020-11-30) + + +### Bug Fixes + +* **audits:** fix cancel button on audits filter ([007fec7](https://github.com/frontegg/frontegg-react/commit/007fec7f0f826a9cc9b671bfb49af043adb47b00)) + + + + + +# [1.10.0](https://github.com/frontegg/frontegg-react/compare/v1.9.0...v1.10.0) (2020-11-27) + + +### Bug Fixes + +* **audits:** move initData action to Audits.tsx, fix reducer storeName ([28a69b6](https://github.com/frontegg/frontegg-react/commit/28a69b64375652720e2bb9589471b35045d6295c)) +* **audits:** remove comment ([e9fdc6f](https://github.com/frontegg/frontegg-react/commit/e9fdc6f1adfec1c5d6d689b0bf6b006d819418b9)) + + + + + +# [1.9.0](https://github.com/frontegg/frontegg-react/compare/v1.8.0...v1.9.0) (2020-11-25) + + +### Features + +* New plugin for Audits ([#106](https://github.com/frontegg/frontegg-react/issues/106)) ([921b37a](https://github.com/frontegg/frontegg-react/commit/921b37aca98f23c800c8b5d094ed595d52617679)) diff --git a/packages/audits/README.md b/packages/audits/README.md new file mode 100644 index 000000000..201d251a6 --- /dev/null +++ b/packages/audits/README.md @@ -0,0 +1,65 @@ + +

+ + Frontegg logo + +

+

Audits Plugin

+
+ +Pre-built Table to easily integrate Audit logs into your [React](https://reactjs.org/) App. +
+ +## Installation + +Frontegg-React-Audits is available as an [npm package](https://www.npmjs.com/package/@frontegg/react-audits). + +```sh +// using npm +npm install @frontegg/react-audits + +// using yarn +yarn add @frontegg/react-audits + +// NOTE: to get the latest stable use @latest. +``` + +## Usage + +All you need is to pass AuditsPlugin to the ``FronteggProvider``: + +```jsx +/* imports */ +import { FronteggProvider } from '@frontegg/react-core'; +import { AuditsPlugin } from '@frontegg/react-audits'; + +ReactDOM.render( + + + + +, document.querySelector('#app')); +``` + + Then add `Audits` component to your route: + + ```jsx + import { Audits } from '@frontegg/react-audits'; + + + ``` + +## Contributing + +The main purpose of this repository is to continue developing Frontegg React to making it faster and easier to use. +Read our [contributing guide](/CONTRIBUTING.md) to learn about our development process. + +**Notice** that contributions go far beyond pull requests and commits. + +## License + +This project is licensed under the terms of the [MIT license](/LICENSE). diff --git a/packages/audits/package.json b/packages/audits/package.json new file mode 100644 index 000000000..17cf732a5 --- /dev/null +++ b/packages/audits/package.json @@ -0,0 +1,70 @@ +{ + "name": "@frontegg/react-audits", + "libName": "FronteggAudits", + "version": "4.0.23", + "author": "Frontegg LTD", + "main": "dist/index.js", + "module": "dist/index.esm.js", + "es2015": "dist/index.es.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "rollup -c ../../scripts/rollup.config.js && echo DONE", + "build:watch": "rollup -w -c ../../scripts/rollup.config.js", + "test": "jest --runInBand --passWithNoTests -c ../../scripts/jest.config.json --rootDir . && echo DONE" + }, + "dependencies": { + "google-map-react": "^2.1.9", + "react-datepicker": "^4.7.0", + "ua-parser-js": "^0.7.23" + }, + "devDependencies": { + "@frontegg/react-core": "^4.0.23", + "@types/google-map-react": "^2.1.0", + "@types/react-datepicker": "^4.4.0", + "@types/ua-parser-js": "^0.7.35" + }, + "jest": { + "preset": "ts-jest", + "testEnvironment": "jsdom", + "testPathIgnorePatterns": [ + "dist/" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "prettier": { + "printWidth": 120, + "semi": true, + "singleQuote": true, + "jsxSingleQuote": true, + "trailingComma": "all", + "jsxBracketSameLine": true, + "endOfLine": "lf", + "tabWidth": 2 + }, + "standard": { + "ignore": [ + "node_modules/", + "dist/" + ], + "globals": [ + "describe", + "it", + "test", + "expect", + "afterAll", + "jest" + ] + }, + "gitHead": "00c0bd425a211916eec1170b8359cf302727b338" +} diff --git a/packages/audits/src/Api/selectors.ts b/packages/audits/src/Api/selectors.ts new file mode 100644 index 000000000..ffdb42cfb --- /dev/null +++ b/packages/audits/src/Api/selectors.ts @@ -0,0 +1,16 @@ +import { createSelector } from 'reselect'; +import { OldAuditsState as AuditsState } from '@frontegg/redux-store'; + +const filters = (state: AuditsState) => state.filters; +const headerProps = (state: AuditsState) => state.headerProps; +const predefinedFilters = (state: AuditsState) => state.predefinedFilters; + +export const filtersWithoutPredefined = createSelector(predefinedFilters, filters, (predefinedFilters, filters) => + filters.filter((f: any) => !Object.keys(predefinedFilters).includes(f.key)) +); + +export const headerPropsWithoutPredefinedFilters = createSelector( + headerProps, + predefinedFilters, + (headerProps, predefinedFilters) => headerProps.filter((i: any) => !Object.keys(predefinedFilters).includes(i.name)) +); diff --git a/packages/audits/src/components/Audits.tsx b/packages/audits/src/components/Audits.tsx new file mode 100644 index 000000000..47cc5f08e --- /dev/null +++ b/packages/audits/src/components/Audits.tsx @@ -0,0 +1,26 @@ +import React, { FC, useEffect } from 'react'; +import { Grid } from '@frontegg/react-core'; +import { AuditsHeader } from './AuditsHeader'; +import './styles.scss'; +import { AuditsSubHeader } from './AuditsSubHeader'; +import { prefixCls } from './constants'; +import { AuditsTable } from './AuditsTable'; +import { ContextHolder } from '@frontegg/rest-api'; +import { useAuditsActions } from '../helpers/hooks'; + +export const AuditsPage: FC = () => { + const { setVirtualScroll } = useAuditsActions(); + + useEffect(() => { + const context: any = ContextHolder.getContext(); + setVirtualScroll(!!context?.auditsOptions?.virtualScroll); + }, [setVirtualScroll]); + + return ( + + + + + + ); +}; diff --git a/packages/audits/src/components/AuditsHeader.tsx b/packages/audits/src/components/AuditsHeader.tsx new file mode 100644 index 000000000..3e51dc864 --- /dev/null +++ b/packages/audits/src/components/AuditsHeader.tsx @@ -0,0 +1,47 @@ +import React, { FC, useEffect, useState } from 'react'; +import { Icon, PageHeader } from '@frontegg/react-core'; +import { useAuditsState, useAuditsActions } from '../helpers/hooks'; +import { getLastUpdatedTime } from '../helpers/getLastUpdatedTime'; +import { Stat } from './Stat'; +import classNames from 'classnames'; +import { prefixCls } from './constants'; + +export const AuditsHeader: FC = () => { + const { lastUpdated, isLoading, totalToday, severeThisWeek } = useAuditsState( + ({ lastUpdated, isLoading, totalToday, severeThisWeek }) => ({ + lastUpdated, + isLoading, + totalToday, + severeThisWeek, + }) + ); + const { startRefresh } = useAuditsActions(); + const [_, forceUpdate] = useState(); + useEffect(() => { + const intervalId = setInterval(() => forceUpdate(undefined), 30000); + return () => clearInterval(intervalId); + }, []); + + return ( + + Last updated {getLastUpdatedTime(lastUpdated)} + + + } + > +
+ + +
+
+ ); +}; diff --git a/packages/audits/src/components/AuditsListener.tsx b/packages/audits/src/components/AuditsListener.tsx new file mode 100644 index 000000000..ce5550f08 --- /dev/null +++ b/packages/audits/src/components/AuditsListener.tsx @@ -0,0 +1,13 @@ +import React, { FC, useEffect } from 'react'; +import { useAuditsActions } from '../helpers/hooks'; +import { OldAuditsActions, storeName } from '@frontegg/redux-store'; +import { ListenerProps } from '@frontegg/react-core'; + +export const AuditsListener: FC> = (props) => { + const actions = useAuditsActions(); + + useEffect(() => { + props.resolveActions?.(storeName, actions); + }, [props.resolveActions, actions]); + return null; +}; diff --git a/packages/audits/src/components/AuditsRawTable.tsx b/packages/audits/src/components/AuditsRawTable.tsx new file mode 100644 index 000000000..2e8d71bcc --- /dev/null +++ b/packages/audits/src/components/AuditsRawTable.tsx @@ -0,0 +1,82 @@ +import { Loader, Table, TableColumnProps, TableProps } from '@frontegg/react-core'; +import { AuditRowData } from '@frontegg/rest-api'; +import React, { FC, useCallback, useMemo } from 'react'; +import { defaultItemsPerPage, HeaderProps } from '@frontegg/redux-store'; +import { getAuditsTableCells, getMinWidthTableCell } from './AuditsTableCell'; +import { Filter } from './Filter'; +import { renderExpandedComponent } from './renderExpandedComponent'; +import { str2bool } from '../helpers/str2bool'; + +export interface IAuditsRawTable + extends Pick< + TableProps, + 'data' | 'totalData' | 'loading' | 'onPageChange' | 'onSortChange' | 'filters' | 'onFilterChange' + > { + headerProps: HeaderProps[]; + virtualScroll?: boolean; +} + +export const AuditsRawTable: FC = React.memo(({ headerProps, data, virtualScroll, ...tableProps }) => { + const headersToShow = useMemo(() => headerProps.filter((_) => str2bool(_.showInMoreInfo)), [headerProps]); + + const columns = useMemo(() => { + return headerProps + .filter((_) => _.showInTable) + .map( + (header): TableColumnProps => ({ + accessor: header.name, + Header: header.displayName, + sortable: header.sortable, + Cell: getAuditsTableCells(header.name), + minWidth: getMinWidthTableCell(header.name), + Filter: header.filterable + ? ({ value, setFilterValue, closePopup }) => ( + + ) + : undefined, + }) + ); + }, [headerProps]); + + const getTableData = useCallback(() => { + const columnsName = headerProps.map(({ name }) => name); + let tableData = [...data]; + + for (const columnName of columnsName) { + tableData = tableData.map((item) => { + const value = item[columnName]; + return { + ...item, + [columnName]: value && typeof value === 'object' ? JSON.stringify(value) : value, + }; + }); + } + + return tableData; + }, [data, headerProps]); + + if (!headerProps.length) { + return ; + } + + return ( + + ); +}); diff --git a/packages/audits/src/components/AuditsSubHeader.tsx b/packages/audits/src/components/AuditsSubHeader.tsx new file mode 100644 index 000000000..ace091fbf --- /dev/null +++ b/packages/audits/src/components/AuditsSubHeader.tsx @@ -0,0 +1,111 @@ +import React, { FC, useState, useEffect, useMemo, useRef, useCallback, ChangeEvent } from 'react'; +import { prefixCls } from './constants'; +import { useAuditsState, useAuditsActions } from '../helpers/hooks'; +import { getFilterName, getFilterValue } from '../helpers/filterHelper'; +import { Icon, Input, Button, Tag, useDebounce, Menu, MenuItemProps, Loader } from '@frontegg/react-core'; +import { Filter } from '..'; + +export interface IAuditsSubHeader { + filters?: Filter[]; + onSetFilter: (filters: Filter[]) => void; + onDownloadPDF: () => void; + onDownloadCSV: () => void; + isDownloadingCsv: boolean; +} + +export const AuditsSubHeader: FC = (props) => { + const prevSearch = useRef<{ search: string | null }>({ search: null }); + const { onSetFilter, onDownloadCSV, onDownloadPDF } = props as IAuditsSubHeader; + + const state = useAuditsState(({ filters, isDownloadingCsv }) => ({ + filters, + isDownloadingCsv, + })); + + const { filters, isDownloadingCsv } = useMemo( + () => (props.hasOwnProperty('filters') ? (props as IAuditsSubHeader) : state), + [state, props] + ); + + const { setFilterData, exportCSV } = useAuditsActions(); + + const [search, setSearch] = useState(''); + const searchValue = useDebounce(search, 500); + + const filterOnly = useMemo(() => filters?.filter((f: any) => f.key !== 'filter') ?? [], [filters]); + const handlerOnSetFilter = useCallback( + (values) => { + (onSetFilter ?? setFilterData)(values); + }, + [onSetFilter, setFilterData] + ); + + const downloadItems: MenuItemProps[] = useMemo( + () => [ + { + icon: isDownloadingCsv ? : , + iconClassName: 'fe-audits__subHeader-menuIcon', + text:
Download Csv
, + }, + ], + [exportCSV, onDownloadCSV, onDownloadPDF, isDownloadingCsv] + ); + + useEffect(() => { + prevSearch.current.search !== null && + prevSearch.current.search !== searchValue && + handlerOnSetFilter([...filterOnly, { key: 'filter', value: searchValue.toLowerCase() }]); + prevSearch.current.search = searchValue; + }, [searchValue, handlerOnSetFilter, filterOnly, prevSearch]); + + const handlerSearch = useCallback( + (e: ChangeEvent) => { + setSearch(e.target.value); + }, + [setSearch] + ); + + const handlerCleanAll = useCallback(() => { + handlerOnSetFilter(filters?.filter((f: any) => f.key === 'filter') ?? []); + }, [handlerOnSetFilter, filters]); + + return ( +
+
+ + Download} items={downloadItems} /> +
+ {filterOnly && !!filterOnly.length && ( +
+ {filterOnly.map((f: any, idx: number) => ( + + {getFilterName(f)}: {getFilterValue(f)} + handlerOnSetFilter(filterOnly.filter((filter: any) => f.key !== filter.key))} + /> + + ))} + {filterOnly && filterOnly.length >= 2 && ( + + )} +
+ )} +
+ ); +}; diff --git a/packages/audits/src/components/AuditsTable.tsx b/packages/audits/src/components/AuditsTable.tsx new file mode 100644 index 000000000..49b478ad8 --- /dev/null +++ b/packages/audits/src/components/AuditsTable.tsx @@ -0,0 +1,68 @@ +import React, { FC, useCallback, useEffect, useMemo } from 'react'; +import { TableFilter, TableSort } from '@frontegg/react-core'; +import { useAuditsState, useAuditsActions } from '../helpers/hooks'; +import { AuditsRawTable } from './AuditsRawTable'; +import { ContextHolder } from '@frontegg/rest-api'; + +export const AuditsTable: FC = () => { + const { isLoading, headerProps, rowsData, filters, total, virtualScroll } = useAuditsState( + ({ isLoading, headerProps, rowsData, filters, total, virtualScroll }) => ({ + isLoading, + headerProps, + rowsData, + filters, + total, + virtualScroll, + }) + ); + const { onPageChange, setDataSorting, setFilterData, initData, setVirtualScroll } = useAuditsActions(); + + useEffect(() => { + const context = ContextHolder.getContext(); + setVirtualScroll(!!context?.auditsOptions?.virtualScroll); + initData(); + }, [initData]); + + const dataFilters = useMemo(() => (filters ? filters.map((f: any) => ({ id: f.key, value: f.value })) : []), [ + filters, + ]); + const handlerPageChange = useCallback( + (pageSize: number, pageIndex: number) => { + onPageChange(pageIndex + 1); + }, + [onPageChange] + ); + + const handlerSortChange = useCallback( + (tableSorts: TableSort[]) => { + if (!tableSorts.length) return; + + setDataSorting({ + sortBy: tableSorts[0].id, + sortDirection: tableSorts[0].desc ? 'desc' : 'asc', + }); + }, + [setDataSorting] + ); + + const handlerFilterChange = useCallback( + (filters: TableFilter[]) => { + setFilterData(filters.map(({ id, value }) => ({ key: id, value }))); + }, + [setFilterData] + ); + + return ( + + ); +}; diff --git a/packages/audits/src/components/AuditsTableCell.tsx b/packages/audits/src/components/AuditsTableCell.tsx new file mode 100644 index 000000000..8df97b746 --- /dev/null +++ b/packages/audits/src/components/AuditsTableCell.tsx @@ -0,0 +1,107 @@ +import UAParser from 'ua-parser-js'; +import classNames from 'classnames'; +import React, { useMemo } from 'react'; +import { CellComponent, Popup, TableCells } from '@frontegg/react-core'; + +import { AuditsTableJson } from './AuditsTableJson'; +import { AuditsTableIpCell } from './AuditsTableIpCell'; +import { browsers, prefixCls, sizeOfIcon } from './constants'; +import { browserIcons, TBrowserIcons } from './BrowserIcons'; + +const AuditsTag: CellComponent = (props) => { + return ( +
+ + {props.value} +
+ ); +}; + +// Logos take from the cdnjs site. The repository with the src is here https://github.com/alrra/browser-logos +const ownRegexp = [ + [/^(axios).*\/([\d\.]+)/i], + [[UAParser.BROWSER.NAME, 'Axios'], UAParser.BROWSER.VERSION], + [/^(postman).*\/([\d\.]+)/i], + [[UAParser.BROWSER.NAME, 'Postman'], UAParser.BROWSER.VERSION], + [/^(python-requests).*\/([\d\.]+)/i], + [[UAParser.BROWSER.NAME, 'Python'], UAParser.BROWSER.VERSION], +]; +const Parser = new UAParser(undefined, { browser: ownRegexp }); + +const UserAgent: CellComponent = ({ value }: { value: string }) => { + const browser = useMemo(() => { + try { + const browser = value ? Parser.setUA(value).getBrowser().name : ''; + return browser && browsers.includes(browser?.toLowerCase() ?? '') ? browser : 'Web'; + } catch (e) { + return 'Web'; + } + }, [value]); + + const triggerElement = useMemo(() => { + const br = browser.toLowerCase(); + const ComponentIcon = browserIcons[br as TBrowserIcons]; + if (ComponentIcon) { + return ; + } + + const imgSrc = `//cdnjs.cloudflare.com/ajax/libs/browser-logos/69.0.4/${br}/${br}_${sizeOfIcon}x${sizeOfIcon}.png`; + return React.createElement('img', { alt: browser, src: imgSrc }); + }, [browser]); + + if (!value) { + return null; + } + + return ( +
+ {value}
} + trigger={triggerElement} + /> + + ); +}; + +export const getAuditsTableCells = (column: string): CellComponent => { + switch (column) { + case 'user': + return TableCells.Title; + case 'createdAt': + return TableCells.DateAgo; + case 'severity': + return AuditsTag; + case 'ip': + return AuditsTableIpCell; + case 'json': + return AuditsTableJson; + case 'userAgent': + return UserAgent; + default: + return TableCells.Description; + } +}; + +export const getMinWidthTableCell = (column: string): number | undefined => { + switch (column) { + case 'user': + return 240; + case 'createdAt': + return 330; + case 'severity': + return 160; + case 'ip': + return 280; + case 'json': + return 240; + case 'userAgent': + return 200; + default: + return 240; + } +}; diff --git a/packages/audits/src/components/AuditsTableIpCell.tsx b/packages/audits/src/components/AuditsTableIpCell.tsx new file mode 100644 index 000000000..16a8cfdf0 --- /dev/null +++ b/packages/audits/src/components/AuditsTableIpCell.tsx @@ -0,0 +1,171 @@ +import React, { useState, useEffect, FC, useRef } from 'react'; +import { prefixCls } from './constants'; +import { api } from '@frontegg/rest-api'; +import GoogleMapReact from 'google-map-react'; +import { CellComponent, Popup, Loader, Icon } from '@frontegg/react-core'; + +export interface AuditsTableIpAdressState { + loading: boolean; + data: { + latitude: number; + longitude: number; + city: string | null; + country_name: string | null; + country_code: string | null; + zip: string | null; + location: { + country_flag?: string | null; + country_flag_emoji?: string | null; + }; + }; +} + +const defaultCenter = { + lat: 40.73061, + lng: -73.935242, +}; + +const MapMark = () =>
; + +export const AuditsTableIpCell: FC = (props) => { + const popupRef = useRef(null); + const [state, setState] = useState({ + loading: false, + data: { + latitude: 0, + longitude: 0, + city: null, + country_name: null, + country_code: null, + zip: null, + location: { country_flag: undefined, country_flag_emoji: undefined }, + }, + }); + + const loadIpAddressMetadata = async () => { + try { + setState({ ...state, loading: true }); + // @ts-ignore + if (!window.cacheIps) { + // @ts-ignore + window.cacheIps = {}; + } + // @ts-ignore + let data: any = window.cacheIps?.[props.value]; + if (data) { + setState({ data, loading: false }); + return; + } + const ipData = await api.metadata.getIpAdressMetadata(props.value); + // @ts-ignore + window.cacheIps[props.value] = ipData; + setState({ data: ipData, loading: false }); + } catch (e) { + console.log('failed to load metadata for ip address - ', e); + setState({ + data: { + latitude: 0, + longitude: 0, + city: null, + country_name: null, + country_code: null, + zip: null, + location: { country_flag: undefined, country_flag_emoji: undefined }, + }, + loading: false, + }); + } + }; + + useEffect(() => { + loadIpAddressMetadata(); + }, []); + const { data, loading } = state; + + const renderItems = (key: string) => { + const { data } = state; + switch (key) { + case 'city': + return ( +
+
+
City
+
{data[key] ?? ''}
+
+
+
Zip
+
{data.zip ?? ''}
+
+
+ ); + case 'latitude': + return ( +
+
Latitude, Longitude
+
+ {data.latitude ? `${data.latitude.toFixed(4)},` : ''} + {data.longitude ? data.longitude.toFixed(4) : ''} +
+
+ ); + case 'country_name': + return ( +
+
Country
+
{data[key] ?? ''}
+
+ ); + default: + return; + } + }; + + return ( + +
+ {loading ? : data?.location?.country_flag_emoji ?? } +
+
{props.value}
+
+ } + content={ +
+
+ IP ADDRESS {props.value} popupRef?.current?.closePopup?.()} /> +
+
+
+ {Object.keys(data).map((key) => { + return renderItems(key); + })} +
+
+ {data && ( + + {data.longitude && ( + + )} + + )} +
+
+
+ } + /> + ); +}; diff --git a/packages/audits/src/components/AuditsTableJson.tsx b/packages/audits/src/components/AuditsTableJson.tsx new file mode 100644 index 000000000..a4d65df49 --- /dev/null +++ b/packages/audits/src/components/AuditsTableJson.tsx @@ -0,0 +1,91 @@ +import React, { FC, useState } from 'react'; +import { Table, Popup, Icon, Pagination, CellComponent } from '@frontegg/react-core'; +import classNames from 'classnames'; +import { prefixCls } from './constants'; + +const pageSize = 2; + +export const AuditsTableJson: FC = (props) => { + const { value } = props; + const [state, setState] = useState({ + items: value.slice(0, pageSize), + page: 1, + }); + + const { items, page } = state; + const showPagination = value.length > pageSize; + const totalPages = Math.ceil(value.length / pageSize); + const showingItems = items.length >= 2 ? page * pageSize : page * pageSize - 1; + + const onPageChange = (pageNumber: number) => { + setState({ + items: value.slice((pageNumber - 1) * pageSize, pageNumber * pageSize), + page: pageNumber, + }); + }; + + return ( + value && ( + + {value.length} total + + } + content={ +
+
+ {value.length} total (showing {showingItems} out of {value.length}) +
+ {items.map((object: any, idx: number) => + typeof object === 'string' ? ( + +
+ +
+
{(page - 1) * pageSize + (idx + 1)}
+
{object}
+
+
+
+ ) : ( + 1, + })} + > + {Object.keys(object).map((key) => { + return ( +
+ +
+
{key}
+
+ {typeof object[key] === 'object' ? JSON.stringify(object[key]) : object[key]} +
+
+
+ ); + })} +
+ ) + )} + {showPagination && ( +
+ { + onPageChange(v); + }} + /> +
+ )} +
+ } + /> + ) + ); +}; diff --git a/packages/audits/src/components/BrowserIcons.tsx b/packages/audits/src/components/BrowserIcons.tsx new file mode 100644 index 000000000..e2b94eb52 --- /dev/null +++ b/packages/audits/src/components/BrowserIcons.tsx @@ -0,0 +1,129 @@ +import React, { FC } from 'react'; + +interface IBrowserIcon { + width?: number; + height?: number; +} + +const Postman: FC = ({ width = 32, height = 32 }) => ( + + + + + + + + + + + + + + + + + + + + + + + +); + +const Axios: FC = ({ width = 32, height = 32 }) => ( + + + + + +); + +const Python: FC = ({ width = 32, height = 32 }) => ( + + + + + + + + + + + + + + + + + + + +); + +export type TBrowserIcons = 'axios' | 'python' | 'postman'; + +export const browserIcons: Record> = { + axios: Axios, + python: Python, + postman: Postman, +}; diff --git a/packages/audits/src/components/Filter.tsx b/packages/audits/src/components/Filter.tsx new file mode 100644 index 000000000..433677078 --- /dev/null +++ b/packages/audits/src/components/Filter.tsx @@ -0,0 +1,279 @@ +import React, { FC, useCallback, useState, useEffect, useMemo } from 'react'; +import { Input, Button, useT, Grid, Select } from '@frontegg/react-core'; +import './datepicker.scss'; +import { + getFilterType, + getFilterTime, + timeOptions, + severityOptions, + getTimeDiff, + TimeOptions, + SeverityOptions, + FProps, +} from '../helpers/filterHelper'; +import DatePicker from 'react-datepicker'; +import { prefixCls } from './constants'; +import moment from 'moment'; + +export interface FilterProps { + name: string; + type: string; + value: any; + closePopup: (() => void) | undefined; + setFilterValue: (value: any) => void; +} + +const AlphaNumericFilter: FC = ({ value, onChange }) => { + const [inputState, setInputState] = useState(`${value ?? ''}`); + + return ( + { + setInputState(e.target.value); + onChange(e.target.value); + }} + value={`${inputState ?? ''}`} + /> + ); +}; + +const SeverityFilter: FC = ({ value, onChange }) => { + const [selectState, setSelectState] = useState(severityOptions[0]); + + useEffect(() => { + onChange(selectState.value); + if (value) { + setSelectState(severityOptions.filter((o) => o.value === value)[0]); + } + }, []); + + const handleSelect = useCallback( + (value) => { + setSelectState(value); + onChange(value.value); + }, + [setSelectState, onChange] + ); + + return ( + { + const timeValue = e.target.value; + let [hours, minutes] = timeValue.split(':'); + if (!hours) { + hours = '0'; + } + if (!minutes) { + minutes = '0'; + } + const newDate = new Date(startDate ?? new Date()); + newDate.setHours(Number(hours), Number(minutes)); + setStartTime(moment(newDate).format('HH:mm')); + onChange(getFilterTime({ from: newDate, to: endDate ?? new Date() })); + }} + onChange={(e) => { + const timeValue = e.target.value; + if (/^\d{1,2}:\d{2}$/.test(timeValue)) { + const [hours = '0', minutes = '0'] = timeValue.split(':'); + const newDate = new Date(startDate ?? new Date()); + newDate.setHours(Number(hours), Number(minutes)); + setStartTime(moment(newDate).format('HH:mm')); + onChange(getFilterTime({ from: newDate, to: endDate ?? new Date() })); + } else if (/^[\d:]+$/.test(timeValue) || timeValue === '') { + const date = new Date(startDate ?? new Date()); + if (date?.setHours(11, 20)) { + setStartTime(timeValue); + } + } + }} + /> + { + const timeValue = e.target.value; + let [hours, minutes] = timeValue.split(':'); + if (!hours) { + hours = '23'; + } + if (!minutes) { + minutes = '59'; + } + const newDate = new Date(endDate ?? new Date()); + newDate.setHours(Number(hours), Number(minutes)); + setEndTime(moment(newDate).format('HH:mm')); + onChange(getFilterTime({ from: startDate ?? new Date(), to: newDate })); + }} + onChange={(e) => { + const timeValue = e.target.value; + if (/^\d{1,2}:\d{2}$/.test(timeValue)) { + const [hours = '0', minutes = '0'] = timeValue.split(':'); + const newDate = new Date(endDate ?? new Date()); + newDate.setHours(Number(hours), Number(minutes)); + setEndTime(moment(newDate).format('HH:mm')); + onChange(getFilterTime({ from: startDate ?? new Date(), to: newDate })); + } else if (/^[\d:]+$/.test(timeValue) || timeValue === '') { + const date = new Date(endDate ?? new Date()); + if (date?.setHours(11, 20)) { + setEndTime(timeValue); + } + } + }} + /> + + ) : null} + + ); +}; + +export const Filter: FC = ({ closePopup, value, setFilterValue, type, name }) => { + const { t } = useT(); + const [state, setState] = useState(''); + + const FilterComponent = useMemo(() => { + switch (type) { + case 'Timestamp': + return TimeStampFilter; + case 'Severity': + return SeverityFilter; + default: + return AlphaNumericFilter; + } + }, [type, value, setState]); + + const handleFilter = useCallback(() => { + if (getFilterType(type) === 'input') { + return !!state.trim() && setFilterValue(state); + } else { + setFilterValue(state); + } + }, [type, state]); + + const onSubmit = useCallback( + (e) => { + e?.preventDefault?.(); + handleFilter(); + closePopup?.(); + }, + [handleFilter, closePopup] + ); + + return ( +
+
+ {type !== 'Timestamp' &&
Filter by {name}
} + +
+ setState(value)} + onSubmit={onSubmit} + closePopup={closePopup} + setFilterValue={setFilterValue} + /> +
+
+ + + + + + + + + +
+ ); +}; diff --git a/packages/audits/src/components/Stat.tsx b/packages/audits/src/components/Stat.tsx new file mode 100644 index 000000000..5b29c454a --- /dev/null +++ b/packages/audits/src/components/Stat.tsx @@ -0,0 +1,24 @@ +import React, { FC } from 'react'; +import { Grid, Icon, IconNames } from '@frontegg/react-core'; +import { prefixCls } from './constants'; +import classNames from 'classnames'; + +interface StatProps { + stat: number; + statName: string; + iconName: IconNames; + severity: 'primary' | 'danger'; +} + +export const Stat: FC = ({ stat, statName, iconName, severity }) => ( +
+ + + {stat} + {statName} + +
+ +
+
+); diff --git a/packages/audits/src/components/constants.ts b/packages/audits/src/components/constants.ts new file mode 100644 index 000000000..c9ddb0870 --- /dev/null +++ b/packages/audits/src/components/constants.ts @@ -0,0 +1,92 @@ +import { browserIcons } from './BrowserIcons'; + +export const prefixCls = 'fe-audits'; + +export const sizeOfIcon = 32; + +export const browsers = [ + ...Object.keys(browserIcons), + 'android-webview-beta', + 'android-webview-canary', + 'android-webview-dev', + 'android-webview', + 'archive', + 'avant', + 'basilisk', + 'brave-beta', + 'brave-dev', + 'brave-nightly', + 'brave', + 'browsh', + 'chrome-beta', + 'chrome-canary', + 'chrome-dev', + 'chrome-devtools', + 'chrome', + 'chromium', + 'cốc-cốc', + 'dolphin', + 'edge-beta', + 'edge-canary', + 'edge-dev', + 'edge', + 'electron', + 'epic', + 'epiphany-technology-preview', + 'falkon', + 'firefox-beta', + 'firefox-developer-edition', + 'firefox-lite', + 'firefox-nightly', + 'firefox-reality', + 'firefox', + 'geckoview', + 'hermes', + 'icecat', + 'jsdom', + 'konqueror', + 'maxthon', + 'midori', + 'netsurf', + 'nw.js', + 'opera-beta', + 'opera-developer', + 'opera-gx', + 'opera-mini-beta', + 'opera-mini', + 'opera-neon', + 'opera-touch', + 'opera', + 'otter', + 'pale-moon', + 'puffin', + 'qutebrowser', + 'safari-ios', + 'safari-technology-preview', + 'safari', + 'samsung-internet-beta', + 'samsung-internet', + 'seamonkey', + 'servo', + 'silk', + 'sogou-mobile', + 'tor-alpha', + 'tor-nightly', + 'tor', + 'uc-mini', + 'uc', + 'v8-ignition', + 'v8-liftoff', + 'v8-orinoco', + 'v8-turbofan', + 'v8', + 'vivaldi-snapshot', + 'vivaldi', + 'web', + 'webkit-nightly', + 'webkit', + 'yandex-alpha', + 'yandex-beta', + 'yandex-lite', + 'yandex', +]; diff --git a/packages/audits/src/components/datepicker.scss b/packages/audits/src/components/datepicker.scss new file mode 100644 index 000000000..d5f416341 --- /dev/null +++ b/packages/audits/src/components/datepicker.scss @@ -0,0 +1,788 @@ +$datepicker__background-color: var(--color-gray-0) !default; +$datepicker__border-color: #aeaeae !default; +$datepicker__highlighted-color: #3dcc4a !default; +$datepicker__muted-color: #ccc !default; +$datepicker__selected-color: #245dacff !default; +$datepicker__text-color: #000 !default; +$datepicker__header-color: #000 !default; +$datepicker__navigation-disabled-color: lighten($datepicker__muted-color, 10%) !default; + +$datepicker__border-radius: 0.3rem !default; +$datepicker__day-margin: 0.166rem !default; +$datepicker__font-size: 0.8rem !default; +$datepicker__font-family: 'Helvetica Neue', helvetica, arial, sans-serif !default; +$datepicker__item-size: 2rem !default; +$datepicker__margin: 0.25rem !default; +$datepicker__navigation-button-size: 32px !default; +$datepicker__triangle-size: 8px !default; + +%navigation-chevron { + border-color: $datepicker__muted-color; + border-style: solid; + border-width: 3px 3px 0 0; + content: ''; + display: block; + height: 9px; + position: absolute; + top: 6px; + width: 9px; + + &--disabled, + &--disabled:hover { + border-color: $datepicker__navigation-disabled-color; + cursor: default; + } +} + +%triangle-arrow { + margin-left: -$datepicker__triangle-size * 0.5; + position: absolute; + width: 0; + + &::before, + &::after { + box-sizing: content-box; + position: absolute; + border: $datepicker__triangle-size solid transparent; + height: 0; + width: 1px; + content: ''; + z-index: -1; + border-width: $datepicker__triangle-size; + left: -$datepicker__triangle-size; + } + + &::before { + border-bottom-color: $datepicker__border-color; + } +} + +%triangle-arrow-up { + @extend %triangle-arrow; + + top: 0; + margin-top: -$datepicker__triangle-size; + + &::before, + &::after { + border-top: none; + border-bottom-color: $datepicker__background-color; + } + + &::after { + top: 0; + } + + &::before { + top: -1px; + border-bottom-color: $datepicker__border-color; + } +} + +%triangle-arrow-down { + @extend %triangle-arrow; + + bottom: 0; + margin-bottom: -$datepicker__triangle-size; + + &::before, + &::after { + border-bottom: none; + border-top-color: #fff; + } + + &::after { + bottom: 0; + } + + &::before { + bottom: -1px; + border-top-color: $datepicker__border-color; + } +} + +.fe-audits__filter { + .react-datepicker-wrapper { + display: inline-block; + padding: 0; + border: 0; + width: 100%; + } + + .react-datepicker { + font-family: $datepicker__font-family; + font-size: $datepicker__font-size; + background-color: #fff; + color: $datepicker__text-color; + //border: 1px solid $datepicker__border-color; + //border-radius: $datepicker__border-radius; + display: inline-block; + position: relative; + margin: calc(-1 * var(--fe-popup-padding, var(--element-padding-lg))); + width: calc(100% + 2 * var(--fe-popup-padding, var(--element-padding-lg))); + } + + .react-datepicker--time-only { + .react-datepicker__triangle { + left: 35px; + } + + .react-datepicker__time-container { + border-left: 0; + } + + .react-datepicker__time, + .react-datepicker__time-box { + border-bottom-left-radius: 0.3rem; + border-bottom-right-radius: 0.3rem; + } + } + + .react-datepicker__triangle { + position: absolute; + left: 50px; + } + + .react-datepicker-popper { + z-index: 1; + + &[data-placement^='bottom'] { + padding-top: $datepicker__triangle-size + 2px; + + .react-datepicker__triangle { + @extend %triangle-arrow-up; + } + } + + &[data-placement='bottom-end'], + &[data-placement='top-end'] { + .react-datepicker__triangle { + left: auto; + right: 50px; + } + } + + &[data-placement^='top'] { + padding-bottom: $datepicker__triangle-size + 2px; + + .react-datepicker__triangle { + @extend %triangle-arrow-down; + } + } + + &[data-placement^='right'] { + padding-left: $datepicker__triangle-size; + + .react-datepicker__triangle { + left: auto; + right: 42px; + } + } + + &[data-placement^='left'] { + padding-right: $datepicker__triangle-size; + + .react-datepicker__triangle { + left: 42px; + right: auto; + } + } + } + + .react-datepicker__header { + text-align: center; + background-color: $datepicker__background-color; + border-bottom: 1px solid #ece7e7; + border-top-left-radius: $datepicker__border-radius; + padding: 8px 0; + position: relative; + + &--time { + padding-bottom: 8px; + padding-left: 5px; + padding-right: 5px; + + &:not(&--only) { + border-top-left-radius: 0; + } + } + + &:not(&--has-time-select) { + border-top-right-radius: $datepicker__border-radius; + } + } + + .react-datepicker__year-dropdown-container--select, + .react-datepicker__month-dropdown-container--select, + .react-datepicker__month-year-dropdown-container--select, + .react-datepicker__year-dropdown-container--scroll, + .react-datepicker__month-dropdown-container--scroll, + .react-datepicker__month-year-dropdown-container--scroll { + display: inline-block; + margin: 0 2px; + } + + .react-datepicker__current-month, + .react-datepicker-time__header, + .react-datepicker-year-header { + margin-top: 0; + color: $datepicker__header-color; + font-weight: bold; + font-size: $datepicker__font-size * 1.18; + } + + .react-datepicker-time__header { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + + .react-datepicker__navigation { + align-items: center; + background: none; + display: flex; + justify-content: center; + text-align: center; + cursor: pointer; + position: absolute; + top: 2px; + padding: 0; + border: none; + z-index: 1; + height: $datepicker__navigation-button-size; + width: $datepicker__navigation-button-size; + text-indent: -999em; + overflow: hidden; + + &--previous { + left: 2px; + } + + &--next { + right: 2px; + + &--with-time:not(&--with-today-button) { + right: 85px; + } + } + + &--years { + position: relative; + top: 0; + display: block; + margin-left: auto; + margin-right: auto; + + &-previous { + top: 4px; + } + + &-upcoming { + top: -4px; + } + } + + &:hover { + *::before { + border-color: darken($datepicker__muted-color, 15%); + } + } + } + + .react-datepicker__navigation-icon { + position: relative; + top: -1px; + font-size: 20px; + width: 0; + + &::before { + @extend %navigation-chevron; + } + + &--next { + left: -2px; + + &::before { + transform: rotate(45deg); + left: -7px; + } + } + + &--previous { + right: -2px; + + &::before { + transform: rotate(225deg); + right: -7px; + } + } + } + + .react-datepicker__month-container { + float: left; + width: 100%; + } + + .react-datepicker__year { + margin: $datepicker__margin; + text-align: center; + + &-wrapper { + display: flex; + flex-wrap: wrap; + max-width: 180px; + } + + .react-datepicker__year-text { + display: inline-block; + width: 4rem; + margin: 2px; + } + } + + .react-datepicker__month { + margin: $datepicker__margin; + text-align: center; + + .react-datepicker__month-text, + .react-datepicker__quarter-text { + display: inline-block; + width: 4rem; + margin: 2px; + } + } + + .react-datepicker__input-time-container { + clear: both; + width: 100%; + float: left; + margin: 5px 0 10px 15px; + text-align: left; + + .react-datepicker-time__caption { + display: inline-block; + } + + .react-datepicker-time__input-container { + display: inline-block; + + .react-datepicker-time__input { + display: inline-block; + margin-left: 10px; + + input { + width: auto; + } + + input[type='time']::-webkit-inner-spin-button, + input[type='time']::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; + } + + input[type='time'] { + -moz-appearance: textfield; + } + } + + .react-datepicker-time__delimiter { + margin-left: 5px; + display: inline-block; + } + } + } + + .react-datepicker__time-container { + float: right; + border-left: 1px solid $datepicker__border-color; + width: 85px; + + &--with-today-button { + display: inline; + border: 1px solid #aeaeae; + border-radius: 0.3rem; + position: absolute; + right: -72px; + top: 0; + } + + .react-datepicker__time { + position: relative; + background: white; + border-bottom-right-radius: 0.3rem; + + .react-datepicker__time-box { + width: 85px; + overflow-x: hidden; + margin: 0 auto; + text-align: center; + border-bottom-right-radius: 0.3rem; + + ul.react-datepicker__time-list { + list-style: none; + margin: 0; + height: calc(195px + (#{$datepicker__item-size} / 2)); + overflow-y: scroll; + padding-right: 0; + padding-left: 0; + width: 100%; + box-sizing: content-box; + + li.react-datepicker__time-list-item { + height: 30px; + padding: 5px 10px; + white-space: nowrap; + + &:hover { + cursor: pointer; + background-color: $datepicker__background-color; + } + + &--selected { + background-color: var(--color-primary); + color: white; + font-weight: bold; + + &:hover { + background-color: var(--color-primary); + } + } + + &--disabled { + color: $datepicker__muted-color; + + &:hover { + cursor: default; + background-color: transparent; + } + } + } + } + } + } + } + + .react-datepicker__week-number { + color: $datepicker__muted-color; + display: inline-block; + width: $datepicker__item-size; + line-height: $datepicker__item-size; + text-align: center; + margin: $datepicker__day-margin; + + &.react-datepicker__week-number--clickable { + cursor: pointer; + + &:hover { + border-radius: $datepicker__border-radius; + background-color: $datepicker__background-color; + } + } + } + + .react-datepicker__day-names, + .react-datepicker__week { + white-space: nowrap; + } + + .react-datepicker__day-names { + margin-bottom: -8px; + } + + .react-datepicker__day-name, + .react-datepicker__day, + .react-datepicker__time-name { + color: $datepicker__text-color; + display: inline-block; + width: $datepicker__item-size; + line-height: $datepicker__item-size; + text-align: center; + margin: $datepicker__day-margin; + } + + .react-datepicker__month, + .react-datepicker__quarter { + &--selected, + &--in-selecting-range, + &--in-range { + border-radius: $datepicker__border-radius; + background-color: var(--color-primary); + color: #fff; + + &:hover { + background-color: darken($datepicker__selected-color, 5%); + } + } + + &--disabled { + color: $datepicker__muted-color; + pointer-events: none; + + &:hover { + cursor: default; + background-color: transparent; + } + } + } + + .react-datepicker__day, + .react-datepicker__month-text, + .react-datepicker__quarter-text, + .react-datepicker__year-text { + cursor: pointer; + + &:hover { + border-radius: $datepicker__border-radius; + background-color: $datepicker__background-color; + } + + &--today { + font-weight: bold; + } + + &--highlighted { + border-radius: $datepicker__border-radius; + background-color: $datepicker__highlighted-color; + color: #fff; + + &:hover { + background-color: darken($datepicker__highlighted-color, 5%); + } + + &-custom-1 { + color: magenta; + } + + &-custom-2 { + color: green; + } + } + + &--selected, + &--in-selecting-range, + &--in-range { + border-radius: $datepicker__border-radius; + background-color: var(--color-primary); + color: #fff; + + &:hover { + background-color: darken($datepicker__selected-color, 5%); + } + } + + &--keyboard-selected { + border-radius: $datepicker__border-radius; + background-color: lighten($datepicker__selected-color, 5%); + color: #fff; + + &:hover { + background-color: darken($datepicker__selected-color, 5%); + } + } + + &--in-selecting-range:not(&--in-range) { + background-color: rgba($datepicker__selected-color, 0.5); + } + + &--in-range:not(&--in-selecting-range) { + .react-datepicker__month--selecting-range & { + background-color: $datepicker__background-color; + color: $datepicker__text-color; + } + } + + &--disabled { + cursor: default; + color: $datepicker__muted-color; + + &:hover { + background-color: transparent; + } + } + } + + .react-datepicker__month-text, + .react-datepicker__quarter-text { + &.react-datepicker__month--selected, + &.react-datepicker__month--in-range, + &.react-datepicker__quarter--selected, + &.react-datepicker__quarter--in-range { + &:hover { + background-color: var(--color-primary); + } + } + + &:hover { + background-color: $datepicker__background-color; + } + } + + .react-datepicker__input-container { + position: relative; + display: inline-block; + width: 100%; + } + + .react-datepicker__year-read-view, + .react-datepicker__month-read-view, + .react-datepicker__month-year-read-view { + border: 1px solid transparent; + border-radius: $datepicker__border-radius; + position: relative; + + &:hover { + cursor: pointer; + + .react-datepicker__year-read-view--down-arrow, + .react-datepicker__month-read-view--down-arrow { + border-top-color: darken($datepicker__muted-color, 10%); + } + } + + &--down-arrow { + @extend %navigation-chevron; + + transform: rotate(135deg); + right: -16px; + top: 0; + } + } + + .react-datepicker__year-dropdown, + .react-datepicker__month-dropdown, + .react-datepicker__month-year-dropdown { + background-color: $datepicker__background-color; + position: absolute; + width: 50%; + left: 25%; + top: 30px; + z-index: 1; + text-align: center; + border-radius: $datepicker__border-radius; + border: 1px solid $datepicker__border-color; + + &:hover { + cursor: pointer; + } + + &--scrollable { + height: 150px; + overflow-y: scroll; + } + } + + .react-datepicker__year-option, + .react-datepicker__month-option, + .react-datepicker__month-year-option { + line-height: 20px; + width: 100%; + display: block; + margin-left: auto; + margin-right: auto; + + &:first-of-type { + border-top-left-radius: $datepicker__border-radius; + border-top-right-radius: $datepicker__border-radius; + } + + &:last-of-type { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + border-bottom-left-radius: $datepicker__border-radius; + border-bottom-right-radius: $datepicker__border-radius; + } + + &:hover { + background-color: $datepicker__muted-color; + + .react-datepicker__navigation--years-upcoming { + border-bottom-color: darken($datepicker__muted-color, 10%); + } + + .react-datepicker__navigation--years-previous { + border-top-color: darken($datepicker__muted-color, 10%); + } + } + + &--selected { + position: absolute; + left: 15px; + } + } + + .react-datepicker__close-icon { + cursor: pointer; + background-color: transparent; + border: 0; + outline: 0; + padding: 0 6px 0 0; + position: absolute; + top: 0; + right: 0; + height: 100%; + display: table-cell; + vertical-align: middle; + + &::after { + cursor: pointer; + background-color: var(--color-primary); + color: #fff; + border-radius: 50%; + height: 16px; + width: 16px; + padding: 2px; + font-size: 12px; + line-height: 1; + text-align: center; + display: table-cell; + vertical-align: middle; + content: '\00d7'; + } + } + + .react-datepicker__today-button { + background: $datepicker__background-color; + border-top: 1px solid $datepicker__border-color; + cursor: pointer; + text-align: center; + font-weight: bold; + padding: 5px 0; + clear: left; + } + + .react-datepicker__portal { + position: fixed; + width: 100vw; + height: 100vh; + background-color: rgba(0, 0, 0, 0.8); + left: 0; + top: 0; + justify-content: center; + align-items: center; + display: flex; + z-index: 2147483647; + + .react-datepicker__day-name, + .react-datepicker__day, + .react-datepicker__time-name { + width: 3rem; + line-height: 3rem; + } + + @media (max-width: 400px), (max-height: 550px) { + .react-datepicker__day-name, + .react-datepicker__day, + .react-datepicker__time-name { + width: 2rem; + line-height: 2rem; + } + } + + .react-datepicker__current-month, + .react-datepicker-time__header { + font-size: $datepicker__font-size * 1.8; + } + } +} diff --git a/packages/audits/src/components/renderExpandedComponent.tsx b/packages/audits/src/components/renderExpandedComponent.tsx new file mode 100644 index 000000000..b03b02ad7 --- /dev/null +++ b/packages/audits/src/components/renderExpandedComponent.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import moment from 'moment'; +import { Grid } from '@frontegg/react-core'; +import { HeaderProps } from '@frontegg/redux-store'; +import { AuditRowData } from '@frontegg/rest-api'; +import classNames from 'classnames'; +import { AuditsTableIpCell } from './AuditsTableIpCell'; +import { AuditsTableJson } from './AuditsTableJson'; +import { prefixCls } from './constants'; + +export const renderExpandedComponent = (headersToShow: HeaderProps[]) => (data: AuditRowData) => { + const getValue = (type: string, data: any) => { + if (!data) return 'N/A'; + + switch (type) { + case 'Json': + return ; + case 'IpAddress': + return ; + case 'Timestamp': + return moment(data).format('dddd, LL H:mm'); + default: + return data; + } + }; + + return ( + + {headersToShow + .filter((_) => data.hasOwnProperty(_.name) && data[_.name] !== undefined) + .map((header) => ( + + + + {header.displayName}: + + + {getValue(header.type, data[header.name])} + + + + ))} + + ); +}; diff --git a/packages/audits/src/components/styles.scss b/packages/audits/src/components/styles.scss new file mode 100644 index 000000000..1332cebac --- /dev/null +++ b/packages/audits/src/components/styles.scss @@ -0,0 +1,494 @@ +.fe-audits { + $class: &; + overflow: hidden; + max-height: 100vh; + + .fe-page-header { + min-height: 8rem; + margin-bottom: 0; + } + + &__vertical-dash { + height: 100%; + vertical-align: middle; + background-color: var(--color-gray-3); + width: 6px; + margin-right: 1rem; + border-radius: 0.5rem; + } + + &__refresh { + color: var(--color-info); + cursor: pointer; + transition: all 0.2s; + height: 1.5rem !important; + width: 1.5rem !important; + margin: -0.5rem 0.5rem; + } + + &__spin { + animation: spin 0.8s infinite linear; + pointer-events: none; + color: var(--color-disabled); + } + + #{$class}__stats-container { + display: flex; + justify-content: flex-end; + + #{$class}__stat { + border: 1px solid var(--color-gray-2); + padding: var(--element-spacing); + display: inline-flex; + border-radius: var(--element-border-radius-sm); + margin-left: 1rem; + min-width: 14rem; + + #{$class}__stat-value { + font-size: 1.25rem; + font-weight: bold; + } + + #{$class}__stat-name { + color: var(--color-gray-5); + font-size: var(--element-font-size-sm); + } + + #{$class}__stat-icon { + padding: var(--element-padding); + border-radius: var(--element-border-radius); + position: relative; + margin: auto; + + svg { + width: var(--element-height-lg); + vertical-align: middle; + } + + &__primary { + background-color: var(--color-green-0); + + svg { + color: var(--color-green-5); + } + } + + &__danger { + background-color: var(--color-red-0); + + svg { + color: var(--color-red-5); + } + } + } + } + } + + &__header { + padding: var(--element-padding); + + #{$class}__title-container { + font-size: 2rem; + font-weight: 900; + width: auto; + + #{$class}__last-updated { + color: var(--color-gray-5); + font-size: var(--element-font-size); + display: inline-flex; + } + } + } + + &__subHeader { + padding: 1rem 2rem; + + &-menuIcon { + fill: var(--color-primary); + + .fe-loader__inner { + left: 0; + } + } + + &-top { + display: flex; + justify-content: space-between; + + input { + min-width: 31rem; + } + } + + &-filters { + margin-top: 2rem; + text-align: left; + } + + &-tag { + b { + margin-right: 0.25rem; + } + + .MuiChip-label { + display: flex; + } + + svg { + width: 15px; + height: 15px; + margin-left: 0.75rem; + margin-right: 0 !important; + cursor: pointer; + } + + margin-right: 0.875rem; + } + } + + &__filter { + min-width: 16rem; + + &-buttons { + display: flex; + justify-content: space-between; + } + + &-title { + color: #3c4a5a; + background-color: var(--color-gray-0); + font-size: 0.75rem; + font-weight: bold; + padding: 0.85rem 1rem; + text-transform: uppercase; + border-radius: 4px; + margin: -1rem -1rem 1rem -1rem; + } + + &-main { + margin-bottom: 1rem; + + .MuiAutocomplete-clearIndicator { + display: none; + } + } + + &-border { + border: none; + border-top: 1px solid #ebedf2; + margin: 1rem -1rem; + } + } + + &__time-filter-container { + display: flex; + flex-direction: row; + width: 300px; + overflow: hidden; + flex-wrap: wrap; + + > .fe-button-primary { + color: var(--color-primary); + display: block; + flex: 1; + min-width: 50%; + text-align: left; + height: initial; + padding: 4px 8px; + } + } + + &__ipCell { + display: flex; + + &-countryFlag, + &-ipAddress { + display: inline-block; + } + + &-countryFlag { + color: #3c4a5a; + min-width: 1.2rem; + margin-right: 0.5rem; + display: inline-flex; + align-items: center; + + .fe-icon { + width: 1rem; + height: 1rem; + } + } + + &-ipAddress { + cursor: pointer; + color: #9699a3; + text-decoration: underline; + } + + &-window { + &-container { + display: flex; + min-width: 25rem; + } + + &-title { + display: flex; + justify-content: space-between; + align-items: center; + color: #3c4a5a; + background-color: var(--color-gray-0); + font-size: 0.75rem; + padding: 0.85rem 1rem; + text-transform: uppercase; + border-radius: 4px; + margin-bottom: 1rem; + font-weight: 500; + + .fe-icon { + padding-left: 0.5rem; + height: 1rem; + width: 1.5rem; + cursor: pointer; + } + } + + &-map { + width: 15.75rem; + height: 10.4375rem; + } + + &-info { + padding-left: 1rem; + } + } + + &-item { + &-name { + font-weight: 500; + color: #3c4a5a; + font-size: 0.65rem; + text-transform: uppercase; + margin-top: 0; + margin-bottom: 0.4rem; + } + + &-desc { + margin-bottom: 1rem; + font-size: 0.875rem; + color: #9699a3; + margin-right: 2rem; + } + } + + &-item:last-child { + .fe-audits__ipCell-item-desc { + margin-bottom: 0; + } + } + + &-mapMark { + background-color: rgb(156, 0, 39); + width: 15px; + height: 15px; + border-radius: 50%; + } + } + + &__jsonMenu { + min-width: 16.5rem; + + &-icon { + width: 1rem; + height: 1rem; + cursor: pointer; + margin-bottom: -5px; + } + + &-dash { + height: 100%; + vertical-align: middle; + background-color: var(--color-gray-3); + min-width: 6px; + margin-right: 1rem; + border-radius: 0.5rem; + } + + &-border:not(:last-of-type) { + border-bottom: solid 1px rgba(112, 112, 112, 0.28); + } + + &-itemGroup { + display: grid; + grid-template-columns: 1fr 1fr; + grid-gap: 0.5rem 1rem; + margin: 0 -1rem; + padding: 0 1rem; + } + + &-itemGroup:nth-of-type(1) { + padding-bottom: 1rem; + padding-top: 0; + } + + &-itemGroup:not(:last-of-type) { + padding-bottom: 1rem; + padding-top: 1rem; + } + + &-itemGroup:last-of-type { + padding-top: 1rem; + } + + &-item { + display: flex; + height: 100%; + border-radius: 0.5rem; + max-width: 12rem; + } + + &-desc { + color: rgba(112, 112, 112, 0.5); + text-overflow: ellipsis; + overflow: hidden; + + &:hover { + white-space: normal; + word-break: break-word; + } + } + + &-pagination { + margin-top: 1rem; + margin-bottom: -0.5rem; + } + } + + &__severity { + padding: 0.25rem 1rem; + border-radius: 12px; + display: flex; + align-items: center; + + &-dot { + width: 6px; + height: 6px; + display: block; + border-radius: 50%; + margin-right: 0.5rem; + margin-top: -1px; + } + + &-info { + color: var(--color-info); + background-color: var(--background-info); + + span { + background-color: var(--color-info); + } + } + + &-useragent { + margin-left: auto; + margin-right: auto; + + > span { + cursor: pointer; + } + } + + &-high, + &-error { + color: var(--color-danger); + background-color: var(--color-danger-light); + + span { + background-color: var(--color-danger); + } + } + + &-medium, + &-attention { + color: var(--color-warning); + background-color: var(--background-warning); + + span { + background-color: var(--color-warning); + } + } + } + + &__useragent { + height: 100%; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + } + + &__expand-content { + gap: var(--element-height) 0; + padding: var(--element-height) var(--element-spacing); + + #{$class}__property { + display: flex; + gap: 0 var(--element-spacing); + flex-wrap: nowrap; + padding: 0 var(--element-spacing); + + #{$class}__property-name { + color: var(--color-gray-7); + font-size: var(--element-font-size); + line-height: 1.24; + margin-bottom: var(--element-spacing); + } + + #{$class}__property-value { + color: var(--color-gray-5); + font-size: var(--element-font-size); + word-break: break-word; + } + + #{$class}__severity-text { + &-info { + color: var(--color-info); + } + + &-high, + &-error { + color: var(--color-danger); + } + + &-medium, + &-attention { + color: var(--color-warning); + } + } + + #{$class}__severity-background { + &-info { + background-color: var(--color-info); + } + + &-high, + &-error { + background-color: var(--color-danger); + } + + &-medium, + &-attention { + background-color: var(--color-warning); + } + } + } + } + + &__useragent-hover { + max-width: 26rem; + } +} + +@keyframes spin { + 100% { + transform: rotate(-360deg); + } +} diff --git a/packages/audits/src/helpers/filterHelper.ts b/packages/audits/src/helpers/filterHelper.ts new file mode 100644 index 000000000..a099d884e --- /dev/null +++ b/packages/audits/src/helpers/filterHelper.ts @@ -0,0 +1,131 @@ +import moment from 'moment'; +import { Filter } from '@frontegg/redux-store/audits/backward-compatibility'; + +export const getFilterType = (type: string): 'input' | 'select' => { + switch (type) { + case 'Timestamp': + return 'select'; + case 'Severity': + return 'select'; + default: + return 'input'; + } +}; + +export const getFilterTime = (time: TimeValues) => { + let value; + + if (typeof time === 'object' && new Date(time.from).toString() !== 'Invalid Date') { + if (new Date(time.to).toString() !== 'Invalid Date') { + return { $gt: time.from.toISOString(), $lt: time.to.toISOString() }; + } + return { $gt: time.from.toISOString() }; + } + switch (time) { + case 'last_hour': + value = moment().utc().subtract(1, 'hours').toISOString(); + break; + case 'last_4_hours': + value = moment().utc().subtract(4, 'hours').toISOString(); + break; + case 'last_12_hours': + value = moment().utc().subtract(12, 'hours').toISOString(); + break; + case 'last_day': + value = moment().utc().subtract(1, 'days').toISOString(); + break; + case 'last_week': + value = moment().utc().subtract(7, 'days').toISOString(); + break; + case 'last_month': + value = moment().utc().subtract(1, 'months').toISOString(); + break; + case 'last_year': + value = moment().utc().subtract(1, 'years').toISOString(); + break; + default: + value = moment().utc().toISOString(); + } + return { $gt: value }; +}; + +const capitalize = (s: string) => { + // noinspection SuspiciousTypeOfGuard + if (typeof s !== 'string') return `${s}`; + return s.charAt(0).toUpperCase() + s.slice(1); +}; + +export const getFilterName = (filter: Filter) => { + switch (filter.key) { + case 'createdAt': + return 'Time'; + + default: + return capitalize(filter.key); + } +}; + +export const getTimeDiff = (time: any) => { + const currentTime = moment(); + const diff = currentTime.diff(time.$gt, 'day'); + return diff <= 1 ? 'last_day' : diff <= 7 ? 'last_week' : diff <= 30 ? 'last_month' : 'last_year'; +}; + +export const getFilterValue = (filter: any) => { + if (filter.key === 'createdAt') { + let value = moment(filter.value.$gt).format('DD/MM/YYYY H:mm'); + if (filter.value.$lt) { + value += ` - ${moment(filter.value.$lt).format('DD/MM/YYYY H:mm')}`; + } + return value; + } + return filter.value; +}; + +export const timeOptions: TimeOptions[] = [ + { label: 'Last hour', value: 'last_hour' }, + { label: 'Last 4 hours', value: 'last_4_hours' }, + { label: 'Last 12 hours', value: 'last_12_hours' }, + { label: 'Last day', value: 'last_day' }, + { label: 'Last week', value: 'last_week' }, + { label: 'Last month', value: 'last_month' }, + { label: 'Last year', value: 'last_year' }, +]; + +export const severityOptions: SeverityOptions[] = [ + { label: 'Info', value: 'Info' }, + { label: 'Attention', value: 'Attention' }, + { label: 'Error', value: 'Error' }, +]; + +export type TimeValues = + | 'last_hour' + | 'last_4_hours' + | 'last_12_hours' + | 'last_day' + | 'last_week' + | 'last_month' + | 'last_year' + | { + from: Date; + to: Date; + }; +type SeverityValues = 'Info' | 'Error' | 'Attention'; + +export interface TimeOptions { + label: string; + value: TimeValues; +} + +export interface SeverityOptions { + label: string; + value: SeverityValues; +} + +export interface FProps { + value: any; + onChange: (val: any) => void; + setFilterValue: (val: any) => void; + onSubmit: (e?: any) => void; + closePopup?: () => void; +} diff --git a/packages/audits/src/helpers/getLastUpdatedTime.ts b/packages/audits/src/helpers/getLastUpdatedTime.ts new file mode 100644 index 000000000..d9c0b9705 --- /dev/null +++ b/packages/audits/src/helpers/getLastUpdatedTime.ts @@ -0,0 +1,22 @@ +import moment from 'moment'; + +export const getLastUpdatedTime = (date: moment.MomentInput, currentDate = moment()) => { + const seconds = currentDate.diff(date, 'seconds'); + const minutes = currentDate.diff(date, 'minutes'); + const hours = currentDate.diff(date, 'hours'); + const days = currentDate.diff(date, 'days'); + const months = currentDate.diff(date, 'months'); + if (minutes < 1) { + return `${seconds} seconds ago`; + } + if (minutes < 60) { + return `${minutes} ${minutes < 2 ? 'minute' : 'minutes'} ago`; + } + if (minutes > 60 && hours < 24) { + return `${hours} ${hours < 2 ? 'hour' : 'hours'} ago`; + } + if (hours > 24 && days < 30) { + return `${days} ${days < 2 ? 'day' : 'days'} ago`; + } + return `${months} ${months < 2 ? 'month' : 'months'} ago`; +}; diff --git a/packages/audits/src/helpers/hooks.ts b/packages/audits/src/helpers/hooks.ts new file mode 100644 index 000000000..02bf8470d --- /dev/null +++ b/packages/audits/src/helpers/hooks.ts @@ -0,0 +1,24 @@ +import { useMemo } from 'react'; +import { useDispatch, useSelector, memoEqual } from '@frontegg/react-core'; +import { bindActionCreators } from '@reduxjs/toolkit'; +import { + actions, + OldAuditsState as AuditsState, + OldAuditsActions as AuditsActions, + storeName, +} from '@frontegg/redux-store'; + +export type AuditsStateMapper = (state: AuditsState) => S; +const defaultAuditsStateMapper: any = (state: AuditsState) => ({ ...state }); + +export const useAuditsState = (stateMapper: AuditsStateMapper = defaultAuditsStateMapper): S => { + const auditsState = useSelector((state: any) => stateMapper(state[storeName]), memoEqual); + return { + ...auditsState, + }; +}; + +export const useAuditsActions = (): AuditsActions => { + const dispatch = useDispatch(); + return useMemo(() => bindActionCreators(actions, dispatch), [bindActionCreators, dispatch, actions]); +}; diff --git a/packages/audits/src/helpers/str2bool.ts b/packages/audits/src/helpers/str2bool.ts new file mode 100644 index 000000000..3e282f816 --- /dev/null +++ b/packages/audits/src/helpers/str2bool.ts @@ -0,0 +1,13 @@ +export const str2bool = (str?: string) => { + const value = str?.toLowerCase?.(); + if (!value) { + return false; + } + switch (value) { + case 'yes': + case 'always': + return true; + default: + return false; + } +}; diff --git a/packages/audits/src/index.ts b/packages/audits/src/index.ts new file mode 100644 index 000000000..18ab9d4e6 --- /dev/null +++ b/packages/audits/src/index.ts @@ -0,0 +1,30 @@ +import { PluginConfig } from '@frontegg/react-core'; +import { AuditsListener } from './components/AuditsListener'; + +import { AuditsPage } from './components/Audits'; +import { AuditsHeader } from './components/AuditsHeader'; +import { AuditsSubHeader } from './components/AuditsSubHeader'; +import { AuditsTable } from './components/AuditsTable'; +import { AuditsRawTable } from './components/AuditsRawTable'; +import { OldAuditsActions as AuditsActions, initialState, reducer, sagas, storeName } from '@frontegg/redux-store/'; + +export type { AuditsActions }; +export * from '@frontegg/redux-store/'; + +export const AuditsPlugin = (): PluginConfig => ({ + storeName, + preloadedState: { + ...initialState, + }, + reducer, + sagas, + Listener: AuditsListener, +}); + +export const Audits = { + Page: AuditsPage, + Header: AuditsHeader, + TableHeader: AuditsSubHeader, + Table: AuditsTable, + TableRaw: AuditsRawTable, +}; diff --git a/packages/audits/src/tests/expandable-row.cy-spec.tsx b/packages/audits/src/tests/expandable-row.cy-spec.tsx new file mode 100644 index 000000000..6c8f2417e --- /dev/null +++ b/packages/audits/src/tests/expandable-row.cy-spec.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { mount } from 'cypress-react-unit-test'; +import { AuditsPlugin, Audits } from '../index'; +import { mockAuditsApi, TestFronteggWrapper } from '../../../../cypress/helpers'; + +describe('Expandable Rows', () => { + it('Rows should expand', () => { + cy.server(); + mockAuditsApi(); + mount( + + + + ); + cy.wait('@auditsData'); + cy.wait('@auditsMetadata'); + cy.wait('@auditsStats'); + cy.get('.fe-table').should('be.visible'); + cy.get('.fe-table__expand-button').first().click(); + cy.get('.fe-table__tr-expanded-content').first().should('have.class', 'is-expanded'); + cy.get('.fe-audits__expand-content').first().should('be.visible'); + }); +}); diff --git a/packages/audits/src/tests/first-load-audits-page.cy-spec.tsx b/packages/audits/src/tests/first-load-audits-page.cy-spec.tsx new file mode 100644 index 000000000..4b644a634 --- /dev/null +++ b/packages/audits/src/tests/first-load-audits-page.cy-spec.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { mount } from 'cypress-react-unit-test'; +import { AuditsPlugin, Audits } from '../index'; +import { mockAuditsApi, TestFronteggWrapper } from '../../../../cypress/helpers'; + +describe('Load audits page', () => { + it('Audits page should be rendered', () => { + cy.server(); + mockAuditsApi(); + mount( + + + + ); + cy.wait('@auditsData'); + cy.wait('@auditsMetadata'); + cy.wait('@auditsStats'); + cy.get('.fe-page-header').should('be.visible'); + cy.get('.fe-audits__subHeader').should('be.visible'); + cy.get('.fe-table').should('be.visible'); + }); + + it('Audits rows should be rendered', () => { + cy.server(); + mockAuditsApi(); + mount( + + + + ); + cy.wait('@auditsData'); + cy.wait('@auditsMetadata'); + cy.wait('@auditsStats'); + cy.get('.fe-table').should('be.visible'); + cy.get('.fe-table__tr-td').should('be.visible'); + }); +}); diff --git a/packages/audits/src/tests/open-popups.cy-spec.tsx b/packages/audits/src/tests/open-popups.cy-spec.tsx new file mode 100644 index 000000000..3cd21a82b --- /dev/null +++ b/packages/audits/src/tests/open-popups.cy-spec.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { mount } from 'cypress-react-unit-test'; +import { AuditsPlugin, Audits } from '../index'; +import { mockAuditsApi, TestFronteggWrapper } from '../../../../cypress/helpers'; + +describe('Open Ip Popup', () => { + it('Ip popup should open', () => { + cy.server(); + mockAuditsApi(); + mount( + + + + ); + cy.wait('@auditsData'); + cy.wait('@auditsMetadata'); + cy.wait('@auditsStats'); + cy.get('.fe-table').should('be.visible'); + cy.get('.fe-audits__ipCell').first().click(); + cy.get('.fe-audits__ipCell-window').should('be.visible'); + }); + it('Ip popup in expandable row should open', () => { + cy.server(); + mockAuditsApi(); + mount( + + + + ); + cy.wait('@auditsData'); + cy.wait('@auditsMetadata'); + cy.wait('@auditsStats'); + cy.get('.fe-table').should('be.visible'); + cy.get('.fe-table__expand-button').first().click(); + cy.get('.fe-audits__expand-content').find('.fe-audits__ipCell').first().click(); + cy.get('.fe-audits__ipCell-window').should('be.visible'); + }); + it('Filter popup should open', () => { + cy.server(); + mockAuditsApi(); + mount( + + + + ); + cy.wait('@auditsData'); + cy.wait('@auditsMetadata'); + cy.wait('@auditsStats'); + cy.get('.fe-table').should('be.visible'); + cy.get('.fe-table__filter-button').first().click(); + cy.get('.fe-audits__filter').should('be.visible'); + }); +}); diff --git a/packages/audits/src/tests/table-sorting.cy-spec.tsx b/packages/audits/src/tests/table-sorting.cy-spec.tsx new file mode 100644 index 000000000..f1a0ebd71 --- /dev/null +++ b/packages/audits/src/tests/table-sorting.cy-spec.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { mount } from 'cypress-react-unit-test'; +import { AuditsPlugin, Audits } from '../index'; +import { mockAuditsApi, TestFronteggWrapper } from '../../../../cypress/helpers'; + +describe('Audits page table sorting', () => { + it('Sort by name Z-A', () => { + cy.server(); + mockAuditsApi(); + mount( + + + + ); + cy.wait('@auditsData'); + cy.wait('@auditsMetadata'); + cy.wait('@auditsStats'); + cy.get('.fe-table').should('be.visible'); + cy.get('.fe-table__thead-tr-th').eq(1).click().should('have.class', 'fe-table__thead-sortable-asc'); + cy.wait('@auditsDataNameDesc'); + cy.get('.fe-table__tr').first().contains('Wendi Burghardt').should('be.visible'); + }); +}); diff --git a/packages/audits/tsconfig.json b/packages/audits/tsconfig.json new file mode 100644 index 000000000..150a85181 --- /dev/null +++ b/packages/audits/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "declarationDir": "./dist" + }, + "include": [ + "./src/**/*.tsx", + "./src/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist", + "src/**/*.stories.tsx", + "src/**/*.test.tsx", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.spec.tsx", + "src/**/*.cy-spec.ts", + "src/**/*.cy-spec.tsx" + ] +} + diff --git a/packages/auth/CHANGELOG.md b/packages/auth/CHANGELOG.md new file mode 100644 index 000000000..e09a4b317 --- /dev/null +++ b/packages/auth/CHANGELOG.md @@ -0,0 +1,971 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [2.8.13](https://github.com/frontegg/frontegg-react/compare/v2.8.12...v2.8.13) (2021-07-22) + +**Note:** Version bump only for package @frontegg/react-auth + + + + + +## [2.8.12](https://github.com/frontegg/frontegg-react/compare/v2.8.11...v2.8.12) (2021-07-20) + +**Note:** Version bump only for package @frontegg/react-auth + + + + + +## [2.8.10](https://github.com/frontegg/frontegg-react/compare/v2.8.9...v2.8.10) (2021-07-08) + +**Note:** Version bump only for package @frontegg/react-auth + + + + + +## [2.8.9](https://github.com/frontegg/frontegg-react/compare/v2.8.8...v2.8.9) (2021-07-08) + +**Note:** Version bump only for package @frontegg/react-auth + + + + + +## [2.8.8](https://github.com/frontegg/frontegg-react/compare/v2.8.7...v2.8.8) (2021-07-06) + + +### Bug Fixes + +* **auth:** reload captcha after failed login ([f12be57](https://github.com/frontegg/frontegg-react/commit/f12be57a299de748adb991ad6cdc7d17c226903e)) + + + + + +## [2.8.7](https://github.com/frontegg/frontegg-react/compare/v2.8.6...v2.8.7) (2021-07-01) + +**Note:** Version bump only for package @frontegg/react-auth + + + + + +## [2.8.6](https://github.com/frontegg/frontegg-react/compare/v2.8.5...v2.8.6) (2021-06-30) + +**Note:** Version bump only for package @frontegg/react-auth + + + + + +## [2.8.5](https://github.com/frontegg/frontegg-react/compare/v2.8.4...v2.8.5) (2021-06-30) + +**Note:** Version bump only for package @frontegg/react-auth + + + + + +## [2.8.4](https://github.com/frontegg/frontegg-react/compare/v2.8.3...v2.8.4) (2021-06-29) + +**Note:** Version bump only for package @frontegg/react-auth + + + + + +## [2.8.3](https://github.com/frontegg/frontegg-react/compare/v2.8.1...v2.8.3) (2021-06-23) + + +### Bug Fixes + +* align all dependencies versions ([f1d5c48](https://github.com/frontegg/frontegg-react/commit/f1d5c48aba34827ea06a554cfb61734f1a40c93b)) +* update libraries version ([77da072](https://github.com/frontegg/frontegg-react/commit/77da072c67cb11e6cccb86b044a3a1a22b09625c)) + + + + + +## [2.8.2](https://github.com/frontegg/frontegg-react/compare/v2.8.1...v2.8.2) (2021-06-23) + + +### Bug Fixes + +* update libraries version ([77da072](https://github.com/frontegg/frontegg-react/commit/77da072c67cb11e6cccb86b044a3a1a22b09625c)) + + + + + +## [2.8.1](https://github.com/frontegg/frontegg-react/compare/v2.8.0...v2.8.1) (2021-06-22) + +**Note:** Version bump only for package @frontegg/react-auth + + + + + +# [2.8.0](https://github.com/frontegg/frontegg-react/compare/v2.7.2...v2.8.0) (2021-06-21) + +**Note:** Version bump only for package @frontegg/react-auth + + + + + +## [2.7.2](https://github.com/frontegg/frontegg-react/compare/v2.7.1...v2.7.2) (2021-06-14) + +**Note:** Version bump only for package @frontegg/react-auth + + + + + +# [2.7.0](https://github.com/frontegg/frontegg-react/compare/v2.6.0...v2.7.0) (2021-06-07) + + +### Features + +* **auth:** display error from strategy on activation form ([ad6a5c4](https://github.com/frontegg/frontegg-react/commit/ad6a5c43d18564cbb91d9ebb4901c33741c5a8ae)) + + + + + +# [2.6.0](https://github.com/frontegg/frontegg-react/compare/v2.5.2...v2.6.0) (2021-05-27) + + +### Features + +* **auth:** force terms on social sign up FR-2869 ([#406](https://github.com/frontegg/frontegg-react/issues/406)) ([4462402](https://github.com/frontegg/frontegg-react/commit/4462402c8648a023eb7595c4153a9943c039f995)) +* **auth:** space for release ([#420](https://github.com/frontegg/frontegg-react/issues/420)) ([fd18c60](https://github.com/frontegg/frontegg-react/commit/fd18c60e41dcc76f713b32ed84b5bfd7e2f8c355)) + + + + + +## [2.5.2](https://github.com/frontegg/frontegg-react/compare/v2.5.1...v2.5.2) (2021-05-24) + +**Note:** Version bump only for package @frontegg/react-auth + + + + + +## [2.5.1](https://github.com/frontegg/frontegg-react/compare/v2.5.0...v2.5.1) (2021-05-23) + +**Note:** Version bump only for package @frontegg/react-auth + + + + + +# [2.4.0](https://github.com/frontegg/frontegg-react/compare/v2.3.2...v2.4.0) (2021-05-19) + + +### Features + +* **auth:** [FR-2731] remember MFA devices ([#404](https://github.com/frontegg/frontegg-react/issues/404)) ([7f135d2](https://github.com/frontegg/frontegg-react/commit/7f135d200657ffd19ab54bcf9fd2049c07db43b4)) + + + + + +## [2.3.2](https://github.com/frontegg/frontegg-react/compare/v2.3.1...v2.3.2) (2021-05-10) + +**Note:** Version bump only for package @frontegg/react-auth + + + + + +## [2.3.1](https://github.com/frontegg/frontegg-react/compare/v2.3.0...v2.3.1) (2021-05-10) + +**Note:** Version bump only for package @frontegg/react-auth + + + + + +# [2.3.0](https://github.com/frontegg/frontegg-react/compare/v2.2.2...v2.3.0) (2021-05-07) + + +### Bug Fixes + +* **auth:** remove duplicated useField destructure ([4efd3f1](https://github.com/frontegg/frontegg-react/commit/4efd3f1e477c29c7198d46fd3400ad5c7ec5f21f)) + + + + + +## [2.2.2](https://github.com/frontegg/frontegg-react/compare/v2.2.1...v2.2.2) (2021-04-29) + + +### Bug Fixes + +* **auth:** FR-2591 call account strategy after logout ([#380](https://github.com/frontegg/frontegg-react/issues/380)) ([09fe728](https://github.com/frontegg/frontegg-react/commit/09fe728203009d23f53dc4a51deb36b392c86de8)) + + + + + +## [2.2.1](https://github.com/frontegg/frontegg-react/compare/v2.2.0...v2.2.1) (2021-04-28) + +**Note:** Version bump only for package @frontegg/react-auth + + + + + +# [2.2.0](https://github.com/frontegg/frontegg-react/compare/v2.1.0...v2.2.0) (2021-04-28) + + +### Features + +* **auth:** get activate account config in order to determine if user should set password ([#370](https://github.com/frontegg/frontegg-react/issues/370)) ([b04d42a](https://github.com/frontegg/frontegg-react/commit/b04d42a8d84778bfdadcc3a7a872f9b24eb18028)) + + + + + +# [2.1.0](https://github.com/frontegg/frontegg-react/compare/v2.0.0...v2.1.0) (2021-04-13) + +**Note:** Version bump only for package @frontegg/react-auth + + + + + +# [2.0.0](https://github.com/frontegg/frontegg-react/compare/v1.28.0...v2.0.0) (2021-04-11) + + +### Bug Fixes + +* **auth:** duplicate profile picture timestamps ([1e54a4f](https://github.com/frontegg/frontegg-react/commit/1e54a4f820cf46526b29a6d0fba52967392b59ed)) +* **auth:** fix fromik import in FeRecaptcha to prevent build fails ([1f93494](https://github.com/frontegg/frontegg-react/commit/1f934948657dc97663a38760c8c445f50bd77e0f)) +* **auth:** FR-2218 - remove social logins from activate user form ([738ab4f](https://github.com/frontegg/frontegg-react/commit/738ab4f80862203d1be2cc1897d1efb2d634ee86)) +* Fix build for rescript ([58c4b3c](https://github.com/frontegg/frontegg-react/commit/58c4b3c09c45bc42b14614e5012e054615d1de4c)) +* **auth:** FR-2206 - add error message for sign up form ([fe97e35](https://github.com/frontegg/frontegg-react/commit/fe97e3554e38399678445690f05e4ea2ca434e8d)) + + +### Features + +* **auth:** enforce users password config on activate/reset/change password ([#342](https://github.com/frontegg/frontegg-react/issues/342)) ([7aeaeb2](https://github.com/frontegg/frontegg-react/commit/7aeaeb2568608dc9f8d6f0f66caf109fa52a6a66)) +* **auth:** login with facebook account ([#339](https://github.com/frontegg/frontegg-react/issues/339)) ([f231d75](https://github.com/frontegg/frontegg-react/commit/f231d758a2c2202e037b0caed104d606b9fb3888)) +* **auth:** login with microsoft account ([8fd8590](https://github.com/frontegg/frontegg-react/commit/8fd8590866bf58c6697f2390930c7a05bb2db220)) +* Add Audit logs to frontegg/react-hooks and frontegg/redux-store ([2e46638](https://github.com/frontegg/frontegg-react/commit/2e466385db3242a0547912a8daf3eb6bbd088709)) +* add redux-store for auth state ([ee807ef](https://github.com/frontegg/frontegg-react/commit/ee807efd45a4a2ef494ce2420a80dc0a458fe4ab)) +* Add Security Policy API and Store Hooks ([e9b7abf](https://github.com/frontegg/frontegg-react/commit/e9b7abfa38e5e958a63f69dd45bd6631f2811e53)) +* Expose onRedirectTo via hooks ([bd38109](https://github.com/frontegg/frontegg-react/commit/bd381097a87e2794d668e3951d9a221f9c9acd51)) +* Split State-Management and hooks from UI components ([20d24cd](https://github.com/frontegg/frontegg-react/commit/20d24cd19f536a7f519d670bd8735feb350e54e9)) + + +### BREAKING CHANGES + +* hooks and Entity Types should be imported from @frontegg/react-hooks and @frontegg/redux-store + + + + + +# [1.28.0](https://github.com/frontegg/frontegg-react/compare/v1.27.0...v1.28.0) (2021-03-22) + + +### Bug Fixes + +* FR-2220 - add loader for MenuItem ([4a3e62e](https://github.com/frontegg/frontegg-react/commit/4a3e62e68f7041e0d376ffc411c57198557c20f1)) +* **auth:** FR-2257 - fix t param duplication ([be144f5](https://github.com/frontegg/frontegg-react/commit/be144f5ec204ece9d5e62e0a762400a3b8f284d1)) + + +### Features + +* **auth:** request new activation email ([748255f](https://github.com/frontegg/frontegg-react/commit/748255fc924ef5e36764ba264d9a3767a9ea0c59)) + + + + + +# [1.27.0](https://github.com/frontegg/frontegg-react/compare/v1.26.0...v1.27.0) (2021-03-18) + + +### Features + +* **auth:** added option for terms of service in signup page ([f876091](https://github.com/frontegg/frontegg-react/commit/f876091cfde000c7ae003b878bea13ab8271f171)) + + + + + +# [1.26.0](https://github.com/frontegg/frontegg-react/compare/v1.25.0...v1.26.0) (2021-03-17) + + +### Bug Fixes + +* **auth:** unload captcha after login/sign up ([cb4963c](https://github.com/frontegg/frontegg-react/commit/cb4963c5812586d8a397c7e978911b3e3e3f79e6)) + + +### Features + +* **auth:** allow getting login/signup redirect url via query param ([ce909fd](https://github.com/frontegg/frontegg-react/commit/ce909fd1a5f430ebdeeeb9182837f837c97f720c)) + + + + + +# [1.25.0](https://github.com/frontegg/frontegg-react/compare/v1.24.0...v1.25.0) (2021-03-07) + + +### Bug Fixes + +* **auth:** FR-1932 - removed unused import ([1e00b41](https://github.com/frontegg/frontegg-react/commit/1e00b41bd1395ba8823519eea395849625ac4e83)) +* FR-1932 - merge with master; removed unused Captcha ref; ([a088a21](https://github.com/frontegg/frontegg-react/commit/a088a21f673a843d35511cfdaa4fb5b2283bfb09)) +* **auth:** FR-1932 - make requested changes; fix ReCaptcha token' ([4903613](https://github.com/frontegg/frontegg-react/commit/490361368e8a7bf1fa8c049eda8f2881bb15a71d)) + + +### Features + +* FR-1932 - added captcha for login/sign up; removed unused components demosaas ([e5e75c8](https://github.com/frontegg/frontegg-react/commit/e5e75c82524bfffe158924e75128fa84d5224b14)) + + + + + +# [1.24.0](https://github.com/frontegg/frontegg-react/compare/v1.23.1...v1.24.0) (2021-03-03) + + +### Features + +* account-settings ([134b7b0](https://github.com/frontegg/frontegg-react/commit/134b7b0a8f2b33630ded395cbef90eb6929d7754)) +* **auth:** change social login redriect url behavior ([7199487](https://github.com/frontegg/frontegg-react/commit/7199487a71b524b9de7843048ac6f92836f8b592)) + + + + + +## [1.23.1](https://github.com/frontegg/frontegg-react/compare/v1.23.0...v1.23.1) (2021-02-21) + +**Note:** Version bump only for package @frontegg/react-auth + + + + + +# [1.23.0](https://github.com/frontegg/frontegg-react/compare/v1.22.1...v1.23.0) (2021-02-18) + + +### Features + +* **auth:** support render prop ([f96ca8b](https://github.com/frontegg/frontegg-react/commit/f96ca8b2fe0ff90abaa502ec7ad639e1380af254)) + + + + + +## [1.22.1](https://github.com/frontegg/frontegg-react/compare/v1.22.0...v1.22.1) (2021-02-16) + + +### Bug Fixes + +* Fix ActivateAccount test ([bbf3781](https://github.com/frontegg/frontegg-react/commit/bbf37817feccdc1331e92b829b39d33d0461052e)) +* **auth:** fix after activation refirect to look on the last user requested route ([fcb53ef](https://github.com/frontegg/frontegg-react/commit/fcb53ef8f3a1b1397f71eb751c33ed221caa0064)) + + + + + +# [1.22.0](https://github.com/frontegg/frontegg-react/compare/v1.21.1...v1.22.0) (2021-02-13) + + +### Features + +* **auth:** autosave profile photo ([77a3ec4](https://github.com/frontegg/frontegg-react/commit/77a3ec4311ca959b9ae8224f2a56993402a3b7b4)) +* **auth:** split process update inforamtion between photo and inforamtion ([0140836](https://github.com/frontegg/frontegg-react/commit/0140836d691e87a8235ca1d3612f7b9881311747)) +* **core:** Added tab disabling for FeTabs component; disabled pwd tab in Profile FR-789 ([2354f47](https://github.com/frontegg/frontegg-react/commit/2354f47a5d0fe22e05b3e869b7e963192cd86b45)) + + + + + +## [1.21.1](https://github.com/frontegg/frontegg-react/compare/v1.21.0...v1.21.1) (2021-02-04) + +**Note:** Version bump only for package @frontegg/react-auth + + + + + +# [1.21.0](https://github.com/frontegg/frontegg-react/compare/v1.20.1...v1.21.0) (2021-02-04) + + +### Bug Fixes + +* **auth:** fix caching photo in the profile page ([2acb7e7](https://github.com/frontegg/frontegg-react/commit/2acb7e70186cda2607c16223c9499c4abab92a17)) +* **auth:** Fix redirect after reset password succeeded ([e20d9f4](https://github.com/frontegg/frontegg-react/commit/e20d9f46d345c06f2426e91448835d23e42dc239)) +* **auth:** Remove go to login from activate account succeeded ([e1a8746](https://github.com/frontegg/frontegg-react/commit/e1a8746ad7c1a246038818232967052641d0a040)) + + +### Features + +* Add option to inject SSO components without routes ([79dd172](https://github.com/frontegg/frontegg-react/commit/79dd17267da92c1d8bb651fe6210d3c6b5b42519)) +* **auth:** Add silent logout saga action ([f5781f7](https://github.com/frontegg/frontegg-react/commit/f5781f720d8944ef23e2b57653c7f816f3cce4d8)) +* **auth:** Auto login after activate account succeeded ([eebdd71](https://github.com/frontegg/frontegg-react/commit/eebdd710199c505f927d8446079f70c22f25909b)) + + + + + +## [1.20.1](https://github.com/frontegg/frontegg-react/compare/v1.20.0...v1.20.1) (2021-02-02) + +**Note:** Version bump only for package @frontegg/react-auth + + + + + +# [1.20.0](https://github.com/frontegg/frontegg-react/compare/v1.19.1...v1.20.0) (2021-02-01) + + +### Features + +* **auth:** add option to keep sessions alive via the AuthPlugin props ([8fd2427](https://github.com/frontegg/frontegg-react/commit/8fd2427cf1d562b12f0657300c526f19d286ccc4)) + + + + + +## [1.19.1](https://github.com/frontegg/frontegg-react/compare/v1.19.0...v1.19.1) (2021-01-20) + +**Note:** Version bump only for package @frontegg/react-auth + + + + + +# [1.19.0](https://github.com/frontegg/frontegg-react/compare/v1.18.6...v1.19.0) (2021-01-20) + + +### Bug Fixes + +* Disable MFA input auto complete ([#254](https://github.com/frontegg/frontegg-react/issues/254)) ([b7420c6](https://github.com/frontegg/frontegg-react/commit/b7420c627850887d17bf5b24a2e4bf5a75ded798)) + + + + + +## [1.18.6](https://github.com/frontegg/frontegg-react/compare/v1.18.5...v1.18.6) (2021-01-19) + + +### Bug Fixes + +* **auth:** Ellipses user name in AccountDropdown component ([d3b5ecf](https://github.com/frontegg/frontegg-react/commit/d3b5ecf05e4f9dcf42a44fdebc6caa14cd7b43c5)) +* **auth:** stop loading if error api-tokens FR-1366 ([d245449](https://github.com/frontegg/frontegg-react/commit/d245449e2ee622a49abd34cbef071078a687aa9f)) + + + + + +## [1.18.5](https://github.com/frontegg/frontegg-react/compare/v1.18.4...v1.18.5) (2021-01-18) + + +### Bug Fixes + +* Reset state on session expiration ([713310a](https://github.com/frontegg/frontegg-react/commit/713310aa183829c46f536b90f871d92496a5621a)) + + + + + +## [1.18.4](https://github.com/frontegg/frontegg-react/compare/v1.18.3...v1.18.4) (2021-01-18) + +**Note:** Version bump only for package @frontegg/react-auth + + + + + +## [1.18.3](https://github.com/frontegg/frontegg-react/compare/v1.18.2...v1.18.3) (2021-01-17) + + +### Bug Fixes + +* **auth:** fix activate button ([#240](https://github.com/frontegg/frontegg-react/issues/240)) ([dfc369c](https://github.com/frontegg/frontegg-react/commit/dfc369c2ade168ca2b4af30c0eb34fac59b4da35)) + + + + + +## [1.18.2](https://github.com/frontegg/frontegg-react/compare/v1.18.1...v1.18.2) (2021-01-15) + +**Note:** Version bump only for package @frontegg/react-auth + + + + + +## [1.18.1](https://github.com/frontegg/frontegg-react/compare/v1.18.0...v1.18.1) (2021-01-15) + + +### Bug Fixes + +* **auth:** change oidc icon to correct ([#235](https://github.com/frontegg/frontegg-react/issues/235)) ([95bf5dc](https://github.com/frontegg/frontegg-react/commit/95bf5dc83225c0710e9ec1927d4f4b536d621c1a)) + + + + + +# [1.18.0](https://github.com/frontegg/frontegg-react/compare/v1.17.3...v1.18.0) (2021-01-14) + + +### Bug Fixes + +* Reload profile data on profile component mount ([0791ffa](https://github.com/frontegg/frontegg-react/commit/0791ffaf867ad339b8bf24820e15dc3ca107aa6d)) +* Reset Frontegg store after logout ([37d1de8](https://github.com/frontegg/frontegg-react/commit/37d1de8e816fec9e671ec1eff5e139b99e857941)) + + + + + +## [1.17.3](https://github.com/frontegg/frontegg-react/compare/v1.17.2...v1.17.3) (2021-01-13) + + +### Bug Fixes + +* **auth:** fix scopes for github login ([#230](https://github.com/frontegg/frontegg-react/issues/230)) ([c73f0c3](https://github.com/frontegg/frontegg-react/commit/c73f0c32d58ce3727db9f1782fe60d4d946932b1)) + + + + + +## [1.17.2](https://github.com/frontegg/frontegg-react/compare/v1.17.1...v1.17.2) (2021-01-12) + +**Note:** Version bump only for package @frontegg/react-auth + + + + + +## [1.17.1](https://github.com/frontegg/frontegg-react/compare/v1.17.0...v1.17.1) (2021-01-05) + +**Note:** Version bump only for package @frontegg/react-auth + + + + + +# [1.17.0](https://github.com/frontegg/frontegg-react/compare/v1.16.2...v1.17.0) (2021-01-03) + + +### Bug Fixes + +* **auth:** fix comments ([d1a826f](https://github.com/frontegg/frontegg-react/commit/d1a826f78211514b6853f4a444bb202737f4053b)) +* **auth:** fix FR-1304 ([#220](https://github.com/frontegg/frontegg-react/issues/220)) ([c540512](https://github.com/frontegg/frontegg-react/commit/c540512b62e4eafc85f546da952360297c56410b)) + + +### Features + +* **core:** set company name as optional on singup form ([#214](https://github.com/frontegg/frontegg-react/issues/214)) ([de83d17](https://github.com/frontegg/frontegg-react/commit/de83d170cb0bf35288e5c69891f902e754ad5ff3)) + + + + + +## [1.16.2](https://github.com/frontegg/frontegg-react/compare/v1.16.0...v1.16.2) (2020-12-27) + + +### Bug Fixes + +* **auth:** add support in dynamic component in login and signup for socail logins ([#192](https://github.com/frontegg/frontegg-react/issues/192)) ([195b977](https://github.com/frontegg/frontegg-react/commit/195b97704618135cb17edc7569019f236dda7fd6)) +* **auth:** fix force mfa recovery code not showing up ([#204](https://github.com/frontegg/frontegg-react/issues/204)) ([f6ce5ac](https://github.com/frontegg/frontegg-react/commit/f6ce5ac2be84c931454051760ae3fcca232b6c12)) +* **auth:** fix some minot texts and css issues on MFA ([#203](https://github.com/frontegg/frontegg-react/issues/203)) ([688cbc7](https://github.com/frontegg/frontegg-react/commit/688cbc75fb1a74730d433d0026841856f666018d)) +* Fix force MFA screen bugs ([#196](https://github.com/frontegg/frontegg-react/issues/196)) ([8d51fb9](https://github.com/frontegg/frontegg-react/commit/8d51fb9794d0d0728bd04a742ce6a3f77845d1fe)) + + + + + +## [1.16.1](https://github.com/frontegg/frontegg-react/compare/v1.16.0...v1.16.1) (2020-12-27) + + +### Bug Fixes + +* **auth:** fix force mfa recovery code not showing up ([#204](https://github.com/frontegg/frontegg-react/issues/204)) ([f6ce5ac](https://github.com/frontegg/frontegg-react/commit/f6ce5ac2be84c931454051760ae3fcca232b6c12)) +* Fix force MFA screen bugs ([#196](https://github.com/frontegg/frontegg-react/issues/196)) ([8d51fb9](https://github.com/frontegg/frontegg-react/commit/8d51fb9794d0d0728bd04a742ce6a3f77845d1fe)) +* **auth:** add support in dynamic component in login and signup for socail logins ([#192](https://github.com/frontegg/frontegg-react/issues/192)) ([195b977](https://github.com/frontegg/frontegg-react/commit/195b97704618135cb17edc7569019f236dda7fd6)) + + + + + +# [1.16.0](https://github.com/frontegg/frontegg-react/compare/v1.15.2...v1.16.0) (2020-12-24) + + +### Bug Fixes + +* wront state with logged in user ([#198](https://github.com/frontegg/frontegg-react/issues/198)) ([970c99b](https://github.com/frontegg/frontegg-react/commit/970c99b8fd20147d00e30558d3a7c20726136579)) + + + + + +## [1.15.2](https://github.com/frontegg/frontegg-react/compare/v1.15.1...v1.15.2) (2020-12-24) + + +### Bug Fixes + +* **tests:** Fix two factor authentication tests ([27b3539](https://github.com/frontegg/frontegg-react/commit/27b3539db64d42d86de7630e85340fd5cf3b48ba)) +* Fix crash state mutation was detected between dispatches ([6f8d8e6](https://github.com/frontegg/frontegg-react/commit/6f8d8e6083742d01ba8bd167436f3c7bf850e146)) +* Fix UI css bugs ([b94b49c](https://github.com/frontegg/frontegg-react/commit/b94b49c020f7a26059ab19b0345d5f266043c8ca)) + + + + + +## [1.15.1](https://github.com/frontegg/frontegg-react/compare/v1.15.0...v1.15.1) (2020-12-21) + + +### Bug Fixes + +* **auth:** fix authorized content data validation ([2728cd9](https://github.com/frontegg/frontegg-react/commit/2728cd9f51b8c3404a09870ff256ba07dbcc1d6c)) + + + + + +# [1.15.0](https://github.com/frontegg/frontegg-react/compare/v1.14.1...v1.15.0) (2020-12-20) + + +### Features + +* Add new component `AuthorizedContent` to strict content visibility by permission ([e4be8dc](https://github.com/frontegg/frontegg-react/commit/e4be8dc758b185a88f7b42960c733e7c6763d748)) + + + + + +## [1.14.1](https://github.com/frontegg/frontegg-react/compare/v1.14.0...v1.14.1) (2020-12-17) + + +### Bug Fixes + +* Fix AccountDropdown style bugs ([dde2680](https://github.com/frontegg/frontegg-react/commit/dde26808277b12695c9dcf9daa7dd79f04d1ef88)) +* **auth:** [FR-1080] fix social login wrapper ([f825fc7](https://github.com/frontegg/frontegg-react/commit/f825fc7378780944f9edee94e4f920a99912c5a5)) +* change primary color to darker blue ([04f94bd](https://github.com/frontegg/frontegg-react/commit/04f94bd5caa1135560e89cf0c886a4b81665956a)) +* Fix infinite loading in switch tenant popup ([78a63c1](https://github.com/frontegg/frontegg-react/commit/78a63c1affa2cd05b4a556d63d692f3ed0380788)) +* Fix search bar alignments and ui bug fixes ([dd51197](https://github.com/frontegg/frontegg-react/commit/dd5119705cad6e379459171e34a5a3abe4d891ff)) +* Re-enable fields in SSO claim domain in validation failed ([4fb385c](https://github.com/frontegg/frontegg-react/commit/4fb385c544d03658964b40285b9ec8041250d269)) +* remove switch tenant button user only have on tenant ([c3b5df5](https://github.com/frontegg/frontegg-react/commit/c3b5df52cce439537ab7a483abaf4645d6fa7d1a)) +* **auth:** fix loader api tokens table loader, some improvements ([8db076f](https://github.com/frontegg/frontegg-react/commit/8db076f91de61358577b304cc955151850ff4cfa)) + + + + + +# [1.14.0](https://github.com/frontegg/frontegg-react/compare/v1.13.2...v1.14.0) (2020-12-16) + + +### Bug Fixes + +* disable profile image uploader limitation ([07e082e](https://github.com/frontegg/frontegg-react/commit/07e082e8e4ff551b84fa277a8476e676f503ff2e)) +* Fix MFA cancel button size ([be9a44b](https://github.com/frontegg/frontegg-react/commit/be9a44b2df12a9beac07198a2aaa82c3bd940717)) +* Fix profile tabs color ([3ab5742](https://github.com/frontegg/frontegg-react/commit/3ab57426355700c05930075c014faa2c10456b9c)) +* UI enhancements for SSO components ([6be3aea](https://github.com/frontegg/frontegg-react/commit/6be3aea9e54aa56e4f28da3d63a81df500435fab)) +* Update components primary color ([ee6d08e](https://github.com/frontegg/frontegg-react/commit/ee6d08ec880fc7ae9427d993a544385db8d3da5b)) + + +### Features + +* **auth:** Api tokens component for users and tenants ([c8b1e17](https://github.com/frontegg/frontegg-react/commit/c8b1e176bee4f4402afbd9625841312428c14b75)) + + + + + +## [1.13.2](https://github.com/frontegg/frontegg-react/compare/v1.13.1...v1.13.2) (2020-12-13) + +**Note:** Version bump only for package @frontegg/react-auth + + + + + +## [1.13.1](https://github.com/frontegg/frontegg-react/compare/v1.13.0...v1.13.1) (2020-12-10) + + +### Bug Fixes + +* **auth:** fix google social login scopes. closes [#159](https://github.com/frontegg/frontegg-react/issues/159) ([0ddb2ca](https://github.com/frontegg/frontegg-react/commit/0ddb2ca54f250900f79a1f52382469922d371c12)) + + + + + +# [1.13.0](https://github.com/frontegg/frontegg-react/compare/v1.12.0...v1.13.0) (2020-12-09) + + +### Bug Fixes + +* fix testId error in material components ([0e3d2a6](https://github.com/frontegg/frontegg-react/commit/0e3d2a610f762d9065eee261dd996ecea77e1c8d)), closes [#119](https://github.com/frontegg/frontegg-react/issues/119) +* **auth:** loadUsers on TeamTable did mount ([9b8ff6b](https://github.com/frontegg/frontegg-react/commit/9b8ff6b033e2f9973109e618b5ce9da91eec7ec3)) + + +### Features + +* [FR-808] add support in users sign ups ([1a6f7c3](https://github.com/frontegg/frontegg-react/commit/1a6f7c3639ab4c351593d540296e67f65293bbf9)) + + + + + +# [1.12.0](https://github.com/frontegg/frontegg-react/compare/v1.11.1...v1.12.0) (2020-12-09) + + +### Bug Fixes + +* prevent after login redirect if it is in auth routes ([ae0371d](https://github.com/frontegg/frontegg-react/commit/ae0371d82e5fa3b99349d74f9cd0c7d6754cd710)), closes [#108](https://github.com/frontegg/frontegg-react/issues/108) +* split AuditsPage to separated components ([9aa109a](https://github.com/frontegg/frontegg-react/commit/9aa109a09a357333788abab845f9ff906e636cc3)) +* **auth:** add missing css variables for authentication pages ([4bb2c66](https://github.com/frontegg/frontegg-react/commit/4bb2c66f292aa1794e456516eb928c156186a0f5)) + + + + + +## [1.11.1](https://github.com/frontegg/frontegg-react/compare/v1.11.0...v1.11.1) (2020-12-02) + + +### Bug Fixes + +* **auth:** add exports for socialLogins components ([018b5ea](https://github.com/frontegg/frontegg-react/commit/018b5eaa8b4758c38103e7052944ce8047a275b3)) + + + + + +# [1.11.0](https://github.com/frontegg/frontegg-react/compare/v1.10.0...v1.11.0) (2020-11-30) + + +### Bug Fixes + +* **auth:** add missing query and hash after login redirect ([#135](https://github.com/frontegg/frontegg-react/issues/135)) ([87f36aa](https://github.com/frontegg/frontegg-react/commit/87f36aa21ddeb3aadc3153411b6679360879d02a)), closes [#134](https://github.com/frontegg/frontegg-react/issues/134) +* [FR-815] change password policy to be aligned with backend ([fbc9abf](https://github.com/frontegg/frontegg-react/commit/fbc9abfa776b9f7ac0a8f3c89eaa5c6a39b320b6)) +* **auth:** fix team management roles dropdown ui ([#126](https://github.com/frontegg/frontegg-react/issues/126)) ([c74949b](https://github.com/frontegg/frontegg-react/commit/c74949b2dc409c5af7f00d5d1c0b985c74d3da56)) + + + + + +# [1.10.0](https://github.com/frontegg/frontegg-react/compare/v1.9.0...v1.10.0) (2020-11-27) + +**Note:** Version bump only for package @frontegg/react-auth + + + + + +# [1.9.0](https://github.com/frontegg/frontegg-react/compare/v1.8.0...v1.9.0) (2020-11-25) + + +### Bug Fixes + +* **auth:** fix social logins loader ([#116](https://github.com/frontegg/frontegg-react/issues/116)) ([e965d3d](https://github.com/frontegg/frontegg-react/commit/e965d3db6a423a0457dc89471418f70c57f2e856)) + + +### Features + +* **auth:** New AccountDropdown component added to AuthPlugin ([018f2f8](https://github.com/frontegg/frontegg-react/commit/018f2f8db3ad22981f9270bd2e166b56cf6eb7ff)) + + + + + +# [1.8.0](https://github.com/frontegg/frontegg-react/compare/v1.7.0...v1.8.0) (2020-11-23) + + +### Features + +* **auth:** Add support in google and github social logins ([#111](https://github.com/frontegg/frontegg-react/issues/111)) ([938b04c](https://github.com/frontegg/frontegg-react/commit/938b04cba618e2029b55ff4c39d5c0fc0d884e6b)) + + + + + +# [1.7.0](https://github.com/frontegg/frontegg-react/compare/v1.6.1...v1.7.0) (2020-11-22) + + +### Features + +* resolve saga actions outside fronteggprovider ([7878beb](https://github.com/frontegg/frontegg-react/commit/7878bebf49b5131fcdf16bbd21c1bcab03c2d1ae)) + + + + + +## [1.6.2](https://github.com/frontegg/frontegg-react/compare/v1.6.1...v1.6.2) (2020-11-19) + +**Note:** Version bump only for package @frontegg/react-auth + + + + + +## [1.6.1](https://github.com/frontegg/frontegg-react/compare/v1.6.0...v1.6.1) (2020-11-15) + + +### Bug Fixes + +* **auth:** fix multiple call to loadUsers while mounting TeamTable ([4a51547](https://github.com/frontegg/frontegg-react/commit/4a51547cf5cd86905d3c760af13016a3751bab0b)) +* **auth:** fix remove last role in TeamTable ([22d2ff6](https://github.com/frontegg/frontegg-react/commit/22d2ff60715249c13c7a454f255a371170504887)) +* **auth:** redirect user to login with two-factor after saml if required ([e6abd6a](https://github.com/frontegg/frontegg-react/commit/e6abd6a04ff6b7e62eb335cd71c19382cdd9472a)) +* **auth:** remain user data after editing roles in TeamTable ([186aaa4](https://github.com/frontegg/frontegg-react/commit/186aaa4827f87dd27d35375d1c35fd8a1818c6d6)), closes [#99](https://github.com/frontegg/frontegg-react/issues/99) +* restore test-id to forgot password button ([b8a4ab4](https://github.com/frontegg/frontegg-react/commit/b8a4ab448c5c3fd45e7ad4a1189242d27d3f5822)) + + + + + +# [1.6.0](https://github.com/frontegg/frontegg-react/compare/v1.5.0...v1.6.0) (2020-11-12) + + +### Bug Fixes + +* fix pagination bug in TeamTable ([8ba1c3d](https://github.com/frontegg/frontegg-react/commit/8ba1c3d861257231b1890766c5042cba58998965)) + + +### Features + +* add option to logout from FronteggContext object ([e35b4f0](https://github.com/frontegg/frontegg-react/commit/e35b4f0e8d79660641676257aa5440d2f2bf84ef)) +* add option to upload profile image ([#96](https://github.com/frontegg/frontegg-react/issues/96)) ([0e4c45c](https://github.com/frontegg/frontegg-react/commit/0e4c45cb08a84519e1f2ebb06295af26cdc05ff7)) + + + + + +# [1.5.0](https://github.com/frontegg/frontegg-react/compare/v1.4.0...v1.5.0) (2020-11-10) + + +### Bug Fixes + +* export missing interface AcceptInvitationState ([#92](https://github.com/frontegg/frontegg-react/issues/92)) ([9981fe1](https://github.com/frontegg/frontegg-react/commit/9981fe1921d1517aea1a4aa4c484a4974bbc464a)) + + +### Features + +* sync session between tabs on Auth Listener ([f2bfa04](https://github.com/frontegg/frontegg-react/commit/f2bfa04bb452f8a5ad165b4f9c382ce3fb07e105)) + + + + + +# [1.4.0](https://github.com/frontegg/frontegg-react/compare/v1.3.0...v1.4.0) (2020-11-09) + + +### Bug Fixes + +* add option to add user without roles ([4d17333](https://github.com/frontegg/frontegg-react/commit/4d17333fc0f157d3c5d4462f20d8f2269b579a65)) +* css enhancements ([317e875](https://github.com/frontegg/frontegg-react/commit/317e8756e7c56deaa0d4c16ce699a7b9ebe5f2e5)) +* disable angular render children ([658dbbf](https://github.com/frontegg/frontegg-react/commit/658dbbf05319224caf326adca2b90da23eedefe0)) +* **teams:** remove roles columns if no roles configured ([7078eba](https://github.com/frontegg/frontegg-react/commit/7078ebaa57cfd46ed9f644e7802a354853c28fb9)) +* remove logs ([16b0976](https://github.com/frontegg/frontegg-react/commit/16b09762f77e8c4491e1570b954a1c04511ba53f)) + + + + + +# [1.3.0](https://github.com/frontegg/frontegg-react/compare/v1.2.0...v1.3.0) (2020-11-04) + + +### Bug Fixes + +* **auth:** bug fixes ([#76](https://github.com/frontegg/frontegg-react/issues/76)) ([a758642](https://github.com/frontegg/frontegg-react/commit/a758642458751e930dc4ea6de6acc50b3ede0b77)) +* **auth:** fix ui bugs ([#79](https://github.com/frontegg/frontegg-react/issues/79)) ([0e75d8d](https://github.com/frontegg/frontegg-react/commit/0e75d8dff80937dc2e4308a0ffdd527a12a84a39)) + + +### Features + +* **auth:** add options to update user roles ([3ec734a](https://github.com/frontegg/frontegg-react/commit/3ec734a79dce6df707562a4555e9d7bf124f85a1)) +* add support for nextjs and angular ([#82](https://github.com/frontegg/frontegg-react/issues/82)) ([5fa36eb](https://github.com/frontegg/frontegg-react/commit/5fa36ebe7bfa6866c78455a746727ba8b1cafbbc)) + + + + + +# [1.2.0](https://github.com/frontegg/frontegg-react/compare/v1.1.0...v1.2.0) (2020-10-25) + + +### Bug Fixes + +* **elements:** UI elements small fixes ([effb9bd](https://github.com/frontegg/frontegg-react/commit/effb9bd54186133184010c34874af212c040ef90)) + + +### Features + +* **auth:** add SSO components ([#64](https://github.com/frontegg/frontegg-react/issues/64)) ([f083762](https://github.com/frontegg/frontegg-react/commit/f0837623073c1f9a636480c6a88d5969fb020a09)) +* **auth:** support all auth functionalities ([#70](https://github.com/frontegg/frontegg-react/issues/70)) ([26d725e](https://github.com/frontegg/frontegg-react/commit/26d725e2f386c6b4a703e4371fac8efef29676d1)) +* **core:** add dynamic elements for Frontegg Components ([#10](https://github.com/frontegg/frontegg-react/issues/10)) ([2bddbb2](https://github.com/frontegg/frontegg-react/commit/2bddbb2794ac0dc4af90c8df0f33b69a312e063a)) +* **core:** Add FeTabs component ([3699299](https://github.com/frontegg/frontegg-react/commit/36992997e64907345a254a4b44580759705186bb)), closes [#67](https://github.com/frontegg/frontegg-react/issues/67) + + + + + +# [1.1.0](https://github.com/frontegg/frontegg-react/compare/v1.0.89...v1.1.0) (2020-10-14) + + +### Bug Fixes + +* **auth:** fix loading splitted sagas in AuthPLugin ([d0fba43](https://github.com/frontegg/frontegg-react/commit/d0fba436bd442ff047849397d8bedcc897e16b1c)) +* **auth:** fix saga initializing bug ([80727a3](https://github.com/frontegg/frontegg-react/commit/80727a3e65b3d34ff455a8d0c252495ab3731c48)) +* **packaging:** add missing immer dependency ([#52](https://github.com/frontegg/frontegg-react/issues/52)) ([36c6c15](https://github.com/frontegg/frontegg-react/commit/36c6c1583809a532885e65a8c2c375151ad8b9dc)), closes [#51](https://github.com/frontegg/frontegg-react/issues/51) + + +### Features + +* **auth:** add accept invitation component by url ([#50](https://github.com/frontegg/frontegg-react/issues/50)) ([c3a43d6](https://github.com/frontegg/frontegg-react/commit/c3a43d60dad3fc8da9cffc6a81f468b5671d3af9)) +* **auth:** add Team (reducer/saga) to Auth Plugin ([7bed273](https://github.com/frontegg/frontegg-react/commit/7bed27378efe32c9e9091495d0ac4a3f268b206c)) +* **auth:** add TeamAPI to frontegg/react-core api.team collection ([600a8f8](https://github.com/frontegg/frontegg-react/commit/600a8f81a0322702d22dc2abede93d271d1c81f7)) + + + + + +## [1.0.89](https://github.com/frontegg/frontegg-react/compare/v1.0.88...v1.0.89) (2020-10-13) + +**Note:** Version bump only for package @frontegg/react-auth + + + + + +## [1.0.88](https://github.com/frontegg/frontegg-react/compare/v1.0.87...v1.0.88) (2020-10-11) + +**Note:** Version bump only for package @frontegg/react-auth + + + + + +## [1.0.87](https://github.com/frontegg/frontegg-react/compare/v1.0.86...v1.0.87) (2020-10-09) + + +### Bug Fixes + +* **packaging:** move cjs.js to index.js for main entry point ([0f8f700](https://github.com/frontegg/frontegg-react/commit/0f8f70016566a8f1940d16441a5afa1707dc02a2)) + + + + + +## 1.0.85 (2020-10-08) + +**Note:** Version bump only for package @frontegg/react-auth + + + + + +## 1.0.84 (2020-10-08) + +**Note:** Version bump only for package @frontegg/react-auth diff --git a/packages/auth/README.md b/packages/auth/README.md new file mode 100644 index 000000000..45836c3f2 --- /dev/null +++ b/packages/auth/README.md @@ -0,0 +1,162 @@ + +

+ + Frontegg logo + +

+

Authentication Plugin

+
+ +Pre-built Authentication components to easily integrate Auth Components into your [React](https://reactjs.org/) App. +
+ +## Installation +Frontegg-React-Auth is available as an [npm package](https://www.npmjs.com/package/@frontegg/react-core). + +```sh +// using npm +npm install @frontegg/react-auth + +// using yarn +yarn add @frontegg/react-auth + +// NOTE: to get the latest stable use @latest. +``` +## Import + +All you need is to add pass AuthPlugin to the ``FronteggProvider``: + + +```jsx +/* imports */ +import { FronteggProvider } from '@frontegg/react-core'; +import { AuthPlugin } from '@frontegg/react-auth'; + +ReactDOM.render( + + + + +, document.querySelector('#app')); +``` + +## Options and Customizations +**Frontegg-React-Auth** provide the ability to fully customize your components +to align it with your App UI design. + +- [`header`](#header-reactnode) `` +- [`backgroundImage`](#backgroundimage-string) `` +- [`backgroundColor`](#backgroundcolor-csscolor) `` +- [`loaderComponent`](#loadercomponent-reactnode) `` +- [`routes`](#routes-string) `` + +**Advanced Customizations** + +- [`Login Component`](src/Login/README.md) + +### `header ` + +*(optional)* React Component used to customize your authentication page header +```jsx +const plugins = [ + AuthPlugin({ + header: , + //...rest options + }) +]; +``` +### `backgroundImage ` + +*(optional)* CSS Color used to for authentication page background color +```jsx +const plugins = [ + AuthPlugin({ + backgroundImage: 'https://image_url' | 'data:image/png;base64,...', + //...rest options + }) +]; +``` + +### `backgroundColor ` + +*(optional)* CSS Color used to for authentication page background color +```jsx +const plugins = [ + AuthPlugin({ + backgroundColor: '#FAFAFA' | 'red' | 'rgb(200,200,200)', + //...rest options + }) +]; +``` + +### `loaderComponent ` + +*(optional)* React Component displayed in first load while resolving the verifying the authenticated user, refreshing the token, +and to check if the user should be redirected to login page. +```jsx +const plugins = [ + AuthPlugin({ + loaderComponent: , + //...rest options + }) +]; +``` + +### `routes ` + +*(optional)* Path routes for Authentication Components, these pathes used to redirect +the user to a specific route depends on authentication state. +```jsx +const plugins = [ + AuthPlugin({ + routes: { + /** + * the page whither need to redirect in the case when a user is authenticated + */ + authenticatedUrl: '/', + /** + * the page whither need to redirect in the case when a user is not authenticated + */ + loginUrl: '/account/login', + /** + * navigating to this url, AuthProvider will logout and remove coockies + */ + logoutUrl: '/account/logout', + /** + * the page whither need to redirect in the case when a user want to activate his account + */ + activateUrl: '/account/activate', + /** + * the page in the case a user forgot his account password + */ + forgetPasswordUrl: '/account/forgot/password', + /** + * the page whither need to redirect in the case when a user redirected from reset password url + */ + resetPasswordUrl: '/account/reset/password', + }, + //...rest options + }) +]; +``` + +## Implementing custom React UI on top of Frontegg API +Implementation of custom and dedicated UI on top of the Frontegg `stateful` API is available by following our docs on the [React API section](https://github.com/frontegg/frontegg-react/tree/master/packages/auth/src/Api). + +In case you want to implement your states on top of our stateless API follow our docs on the [REST API section](https://github.com/frontegg/frontegg-react/tree/master/packages/rest-api/src/auth). + + +## Contributing + +The main purpose of this repository is to continue developing Frontegg React to making it faster and easier to use. +Read our [contributing guide](/CONTRIBUTING.md) to learn about our development process. + +**Notice** that contributions go far beyond pull requests and commits. + +## License + +This project is licensed under the terms of the [MIT license](/LICENSE). diff --git a/packages/auth/package.json b/packages/auth/package.json new file mode 100644 index 000000000..98545afe2 --- /dev/null +++ b/packages/auth/package.json @@ -0,0 +1,79 @@ +{ + "name": "@frontegg/react-auth", + "libName": "FronteggAuth", + "version": "4.0.23", + "author": "Frontegg LTD", + "main": "dist/index.js", + "module": "dist/index.esm.js", + "es2015": "dist/index.es.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "rollup -c ../../scripts/rollup.config.js && echo DONE", + "build:watch": "rollup -w -c ../../scripts/rollup.config.js", + "test": "jest --runInBand --passWithNoTests -c ../../scripts/jest.config.json --rootDir . && echo DONE" + }, + "dependencies": { + "clipboard-copy": "^3.1.0", + "jwt-encode": "1.0.1", + "react-dropzone": "^9.0.0", + "react-recaptcha-v3": "^2.0.1", + "uuid": "^8.3.2" + }, + "devDependencies": { + "@frontegg/react-core": "^4.0.23", + "@types/jwt-encode": "1.0.0", + "@types/react": "^16.9.19", + "@types/react-dom": "^16.9.8", + "@types/react-recaptcha-v3": "^1.1.1", + "@types/uuid": "^8.3.0" + }, + "peerDependencies": { + "react": ">16.8.6", + "react-dom": ">16.8.6", + "react-router-dom": "^5.1.2" + }, + "jest": { + "preset": "ts-jest", + "testEnvironment": "jsdom", + "testPathIgnorePatterns": [ + "dist/" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "prettier": { + "printWidth": 120, + "semi": true, + "singleQuote": true, + "jsxSingleQuote": true, + "trailingComma": "all", + "jsxBracketSameLine": true, + "endOfLine": "lf", + "tabWidth": 2 + }, + "standard": { + "ignore": [ + "node_modules/", + "dist/" + ], + "globals": [ + "describe", + "it", + "test", + "expect", + "afterAll", + "jest" + ] + }, + "gitHead": "24cf6de0ec5e320cb5d2cc604b8a9d33970defd2" +} diff --git a/packages/auth/src/AcceptInvitation/AcceptInvitation.tsx b/packages/auth/src/AcceptInvitation/AcceptInvitation.tsx new file mode 100644 index 000000000..5e4fa8b4b --- /dev/null +++ b/packages/auth/src/AcceptInvitation/AcceptInvitation.tsx @@ -0,0 +1,58 @@ +import React, { FC, useEffect } from 'react'; +import { ComponentsTypesWithProps, useDynamicComponents } from '@frontegg/react-core'; +import { AcceptInvitationStep } from '@frontegg/redux-store/auth'; +import { useAcceptInvitationActions, useAcceptInvitationState } from '@frontegg/react-hooks/auth'; +import { Success, SuccessProps } from './Success'; +import { Failed, FailedProps } from './Failed'; +import { authPageWrapper } from '../components'; +import { InvalidProps, Invalid } from './Invalid'; +import { Pending, PendingProps } from './Pending'; + +type Components = { + Success: SuccessProps; + Failed: FailedProps; + Invalid: InvalidProps; + Pending: PendingProps; +}; +const defaultComponents = { Success, Failed, Invalid, Pending }; + +export interface AcceptInvitationProps { + components?: ComponentsTypesWithProps; +} + +const AcceptInvitationComponent: FC = (props) => { + const { acceptInvitation } = useAcceptInvitationActions(); + const { step } = useAcceptInvitationState(); + const Dynamic = useDynamicComponents(defaultComponents, props); + + const url = new URL(window?.location.href); + const userId = url.searchParams.get('userId') || ''; + const token = url.searchParams.get('token') || ''; + + useEffect(() => { + acceptInvitation({ token, userId }); + }, [token, userId]); + + switch (step) { + case AcceptInvitationStep.invalid: + return ; + case AcceptInvitationStep.pending: + return ; + case AcceptInvitationStep.success: + return ; + case AcceptInvitationStep.failed: + return ; + default: + return null; + } +}; + +export const AcceptInvitation: FC = (props) => { + return ( +
+ +
+ ); +}; + +export const AcceptInvitationPage = authPageWrapper(AcceptInvitation); diff --git a/packages/auth/src/AcceptInvitation/Failed.tsx b/packages/auth/src/AcceptInvitation/Failed.tsx new file mode 100644 index 000000000..1f4d34a6a --- /dev/null +++ b/packages/auth/src/AcceptInvitation/Failed.tsx @@ -0,0 +1,24 @@ +import React, { FC } from 'react'; +import { useT, RendererFunctionFC, omitProps } from '@frontegg/react-core'; + +export interface FailedProps { + renderer?: RendererFunctionFC; +} + +export const Failed: FC = (props) => { + const { renderer } = props; + const { t } = useT(); + + if (renderer) { + return renderer(omitProps(props, ['renderer'])); + } + return ( + <> +
+ {t('auth.account.failed-title')} +
+ {t('auth.account.failed-description')} +
+ + ); +}; diff --git a/packages/auth/src/AcceptInvitation/Invalid.tsx b/packages/auth/src/AcceptInvitation/Invalid.tsx new file mode 100644 index 000000000..2dce672f8 --- /dev/null +++ b/packages/auth/src/AcceptInvitation/Invalid.tsx @@ -0,0 +1,22 @@ +import React, { FC } from 'react'; +import { useT, RendererFunctionFC, omitProps } from '@frontegg/react-core'; + +export interface InvalidProps { + renderer?: RendererFunctionFC; +} + +export const Invalid: FC = (props) => { + const { renderer } = props; + const { t } = useT(); + + if (renderer) { + return renderer(omitProps(props, ['renderer'])); + } + return ( +
+ {t('auth.account.invalid-title')} +
+ {t('auth.account.invalid-description')} +
+ ); +}; diff --git a/packages/auth/src/AcceptInvitation/Pending.tsx b/packages/auth/src/AcceptInvitation/Pending.tsx new file mode 100644 index 000000000..ce6354642 --- /dev/null +++ b/packages/auth/src/AcceptInvitation/Pending.tsx @@ -0,0 +1,23 @@ +import React, { FC } from 'react'; +import { useT, RendererFunctionFC, omitProps, Loader } from '@frontegg/react-core'; + +export interface PendingProps { + renderer?: RendererFunctionFC; +} + +export const Pending: FC = (props) => { + const { renderer } = props; + const { t } = useT(); + + if (renderer) { + return renderer(omitProps(props, ['renderer'])); + } + return ( + <> +
{t('auth.account.pending-title')}
+
+ +
+ + ); +}; diff --git a/packages/auth/src/AcceptInvitation/Success.tsx b/packages/auth/src/AcceptInvitation/Success.tsx new file mode 100644 index 000000000..6ab3d0431 --- /dev/null +++ b/packages/auth/src/AcceptInvitation/Success.tsx @@ -0,0 +1,30 @@ +import React, { FC, useEffect } from 'react'; +import { useT, RendererFunctionFC, omitProps } from '@frontegg/react-core'; +import { useAuthRoutes, useOnRedirectTo, useAcceptInvitationActions } from '@frontegg/react-hooks/auth'; + +export interface SuccessProps { + renderer?: RendererFunctionFC; +} + +export const Success: FC = (props) => { + const { renderer } = props; + const { t } = useT(); + const onRedirectTo = useOnRedirectTo(); + const { loginUrl } = useAuthRoutes(); + const { resetAcceptInvitationState } = useAcceptInvitationActions(); + + if (renderer) { + return renderer(omitProps(props, ['renderer'])); + } + useEffect(() => { + setTimeout(() => { + resetAcceptInvitationState(); + onRedirectTo(loginUrl); + }, 1000); + }, []); + return ( + <> +
{t('auth.account.success-title')}
+ + ); +}; diff --git a/packages/auth/src/AcceptInvitation/index.ts b/packages/auth/src/AcceptInvitation/index.ts new file mode 100644 index 000000000..6cfd5e5a7 --- /dev/null +++ b/packages/auth/src/AcceptInvitation/index.ts @@ -0,0 +1,5 @@ +export * from './AcceptInvitation'; +export * from './Invalid'; +export * from './Pending'; +export * from './Failed'; +export * from './Success'; diff --git a/packages/auth/src/AccountDropdown/AccountDropdown.tsx b/packages/auth/src/AccountDropdown/AccountDropdown.tsx new file mode 100644 index 000000000..2306f0a6a --- /dev/null +++ b/packages/auth/src/AccountDropdown/AccountDropdown.tsx @@ -0,0 +1,55 @@ +import { Button, Icon, Popup, useT } from '@frontegg/react-core'; +import React, { FC, useCallback, useEffect, useState } from 'react'; +import { useAuthRoutes, useAuthUserOrNull, useOnRedirectTo } from '@frontegg/react-hooks/auth'; +import './style.scss'; +import { AccountPopup } from './AccountPopup'; +import { AccountPopupSectionProps } from './AccountPopupSection'; + +type AccountDropdownProps = { + getSections?: (defaultSections: AccountPopupSectionProps[], closePopup: () => void) => AccountPopupSectionProps[]; + trigger?: JSX.Element; +}; + +export const AccountDropdown: FC = (props) => { + const { t } = useT(); + const onRedirectTo = useOnRedirectTo(); + const routes = useAuthRoutes(); + const user = useAuthUserOrNull(); + const [popupOpen, setPopupOpen] = useState(undefined); + const handleClose = useCallback(() => { + setPopupOpen(false); + }, []); + + useEffect(() => { + setPopupOpen(undefined); + }, [popupOpen]); + + if (!user) { + return ( + + ); + } + + const trigger = ( + + ); + + return ( + <> + } + /> + + ); +}; diff --git a/packages/auth/src/AccountDropdown/AccountPopup.tsx b/packages/auth/src/AccountDropdown/AccountPopup.tsx new file mode 100644 index 000000000..362a1afd2 --- /dev/null +++ b/packages/auth/src/AccountDropdown/AccountPopup.tsx @@ -0,0 +1,83 @@ +import React, { FC, useState } from 'react'; +import { Icon } from '@frontegg/react-core'; +import classNames from 'classnames'; +import { useAuth, useAuthUserOrNull } from '@frontegg/react-hooks/auth'; +import { AccountPopupSection, AccountPopupSectionProps } from './AccountPopupSection'; +import { AccountPopupSwitchTenant } from './AccountPopupSwitchTenant'; + +export type AccountPopupProps = { + getSections?: (defaultSections: AccountPopupSectionProps[], closePopup: () => void) => AccountPopupSectionProps[]; + closePopup: () => void; +}; +export const AccountPopup: FC = (props) => { + const [showSwitchTenant, setShowSwitchTenant] = useState(false); + const user = useAuthUserOrNull(); + const { onRedirectTo, routes } = useAuth(({ routes, onRedirectTo }) => ({ routes, onRedirectTo })); + if (!user) { + return null; + } + + const defaultSections = [ + { + items: [ + { + icon: , + title: 'Profile', + dataTestId: 'profile-btn', + onClick: () => { + onRedirectTo('/profile'); + props.closePopup(); + }, + }, + ], + }, + { + items: [ + ...((user?.tenantIds?.length ?? 0) > 1 + ? [ + { + icon: , + title: 'Switch Tenant', + dataTestId: 'switch-tenant-btn', + onClick: () => { + setShowSwitchTenant(true); + }, + }, + ] + : []), + { + icon: , + title: 'Logout', + dataTestId: 'logout-btn', + onClick: () => { + onRedirectTo(routes.logoutUrl); + props.closePopup(); + }, + }, + ], + }, + ]; + + const sections: AccountPopupSectionProps[] = + props.getSections?.(defaultSections, props.closePopup) ?? defaultSections; + + return ( +
+
+ profilePictureUrl +
+ {user.name} +
+ {user.email} +
+
+
+ {sections.map((section, index) => ( + + ))} +
+ + setShowSwitchTenant(false)} /> +
+ ); +}; diff --git a/packages/auth/src/AccountDropdown/AccountPopupSection.tsx b/packages/auth/src/AccountDropdown/AccountPopupSection.tsx new file mode 100644 index 000000000..f1c2da469 --- /dev/null +++ b/packages/auth/src/AccountDropdown/AccountPopupSection.tsx @@ -0,0 +1,24 @@ +import React, { FC, ReactElement, MouseEvent } from 'react'; +import { MenuItem } from '@frontegg/react-core'; + +export type AccountPopupSectionProps = { + title?: string; + dataTestId?: string; + items: { + icon: ReactElement; + title: string; + onClick: (e: MouseEvent) => void; + }[]; +}; + +export const AccountPopupSection: FC = (props) => { + const { title, items } = props; + return ( +
+ {title &&
{title}
} + {items.map((item, index) => ( + {item.title}} /> + ))} +
+ ); +}; diff --git a/packages/auth/src/AccountDropdown/AccountPopupSwitchTenant.tsx b/packages/auth/src/AccountDropdown/AccountPopupSwitchTenant.tsx new file mode 100644 index 000000000..e2c21fd6e --- /dev/null +++ b/packages/auth/src/AccountDropdown/AccountPopupSwitchTenant.tsx @@ -0,0 +1,61 @@ +import React, { FC } from 'react'; +import classNames from 'classnames'; +import { Button, Icon, Loader, MenuItem, Tag, useT } from '@frontegg/react-core'; +import { useAuthUser, useTenantsActions, useTenantsState } from '@frontegg/react-hooks/auth'; + +type AccountPopupSwitchTenantProps = { + show: boolean; + onClose: () => void; +}; + +type TenantInfo = { + id: string; + name: string; + tenantId: string; +}; +export const AccountPopupSwitchTenant: FC = (props) => { + const user = useAuthUser(); + const { tenants: tenantsFromState, loading } = useTenantsState(); + const { switchTenant } = useTenantsActions(); + const { t } = useT(); + const { show } = props; + const tenants: TenantInfo[] = + tenantsFromState.length > 0 + ? tenantsFromState + : (user?.tenantIds ?? []).map((tenantId: string) => ({ id: tenantId, tenantId, name: tenantId })); + return ( +
+
+ + {t('common.switchTenant')} +
+
+ {loading && } + {!loading && + tenants.map((tenant) => ( + + {tenant.name} + {tenant.tenantId === user?.tenantId && ( + + Active + + )} + + } + onClick={() => switchTenant({ tenantId: tenant.tenantId })} + /> + ))} +
+
+ ); +}; diff --git a/packages/auth/src/AccountDropdown/index.ts b/packages/auth/src/AccountDropdown/index.ts new file mode 100644 index 000000000..331ac10bf --- /dev/null +++ b/packages/auth/src/AccountDropdown/index.ts @@ -0,0 +1 @@ +export * from './AccountDropdown'; diff --git a/packages/auth/src/AccountDropdown/style.scss b/packages/auth/src/AccountDropdown/style.scss new file mode 100644 index 000000000..d5e9c3886 --- /dev/null +++ b/packages/auth/src/AccountDropdown/style.scss @@ -0,0 +1,145 @@ +.fe-account-dropdown { + height: 3.5rem; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + overflow: hidden; + max-height: 100%; + cursor: pointer; + border-radius: 0.25rem; + padding: 0 1rem; + + &__img { + height: 2.5rem; + width: 2.5rem; + border-radius: 0.5rem; + overflow: hidden; + box-shadow: var(--shadow-2); + margin-right: 1.25rem; + } + + &__name { + font-weight: bold; + margin-right: 1rem; + max-width: 160px; + overflow: hidden; + text-overflow: ellipsis; + } + + &__trigger { + } +} + +.fe-account-popup { + max-height: 50vh; + min-height: 4rem; + transition: all 300ms ease-out; + overflow: visible; + + &__container { + overflow: hidden; + z-index: 1000; + } + + &__switch-tenant-active { + min-height: 15rem; + max-height: 15rem; + } + + &__header { + display: flex; + flex-direction: row; + align-items: center; + margin-bottom: 1rem; + + img { + height: 3rem; + width: 3rem; + border-radius: 0.5rem; + overflow: hidden; + box-shadow: var(--shadow-2); + margin-right: 1.25rem; + } + + &-details { + flex: 1; + font-weight: bold; + font-size: 0.9rem; + margin-right: 2rem; + + span { + font-weight: normal; + } + } + } + + &__body { + margin: 0 -1rem; + } + + &-section { + padding: 0.5rem 0; + border-top: 1px solid rgba(0, 0, 0, 0.1); + + &:first-child { + border-top: none; + } + + &:last-child { + padding-bottom: 0; + } + + &__title { + font-size: 0.75rem; + color: var(--color-gray-5); + text-transform: uppercase; + font-weight: 700; + margin-bottom: 0.5rem; + padding: 0.5rem 1rem 0; + } + } +} + +.fe-account-switch-tenant { + position: absolute; + top: 0; + left: 100%; + width: 100%; + height: 100%; + overflow: auto; + background: var(--color-white); + transition: left 300ms ease-out; + display: flex; + flex-direction: column; + + &__visible { + left: 0; + } + + &__header { + width: 100%; + border-bottom: 1px solid var(--element-divider-color); + padding: 1rem; + align-items: center; + display: flex; + + > span { + margin-left: 0.5rem; + } + } + + &__body { + flex: 1; + overflow: auto; + width: 100%; + + .fe-active-tenant-tag { + margin-left: auto; + padding: 0.15rem 0.3rem; + border-radius: 0.5rem; + font-size: 0.75rem; + height: auto; + } + } +} diff --git a/packages/auth/src/ActivateAccount/ActivateAccount.tsx b/packages/auth/src/ActivateAccount/ActivateAccount.tsx new file mode 100644 index 000000000..912b8a4d9 --- /dev/null +++ b/packages/auth/src/ActivateAccount/ActivateAccount.tsx @@ -0,0 +1,49 @@ +import React, { FC } from 'react'; +import { ComponentsTypesWithProps, useDynamicComponents } from '@frontegg/react-core'; +import { ActivateAccountStep } from '@frontegg/redux-store/auth'; +import { useActivateAccountState } from '@frontegg/react-hooks/auth'; +import { ActivateAccountSuccessRedirect, ActivateAccountSuccessRedirectProps } from './ActivateAccountSuccessRedirect'; +import { ActivateAccountFailedRedirect, ActivateAccountFailedRedirectProps } from './ActivateAccountFailedRedirect'; +import { ActivateAccountForm, ActivateAccountFormProps } from './ActivateAccountForm'; +import { ActivateAccountResendEmail, ActivateAccountResendEmailProps } from './ActivateAccountResendEmail'; +import { authPageWrapper } from '../components'; + +type Components = { + ActivateAccountForm: ActivateAccountFormProps; + ActivateAccountSuccessRedirect: ActivateAccountSuccessRedirectProps; + ActivateAccountFailedRedirect: ActivateAccountFailedRedirectProps; + ActivateAccountResendEmail: ActivateAccountResendEmailProps; +}; +const defaultComponents = { + ActivateAccountSuccessRedirect, + ActivateAccountFailedRedirect, + ActivateAccountForm, + ActivateAccountResendEmail, +}; + +export interface ActivateAccountProps { + components?: ComponentsTypesWithProps; +} + +export const ActivateAccount: FC = (props) => { + const { step } = useActivateAccountState(); + const Dynamic = useDynamicComponents(defaultComponents, props); + + const url = new URL(window?.location.href); + const userId = url.searchParams.get('userId') || ''; + const token = url.searchParams.get('token') || ''; + + let components: any; + if (step === ActivateAccountStep.resend) { + components = ; + } else if (!userId || !token) { + components = ; + } else if (step === ActivateAccountStep.success) { + components = ; + } else { + components = ; + } + return
{components}
; +}; + +export const ActivateAccountPage = authPageWrapper(ActivateAccount); diff --git a/packages/auth/src/ActivateAccount/ActivateAccountFailedRedirect.tsx b/packages/auth/src/ActivateAccount/ActivateAccountFailedRedirect.tsx new file mode 100644 index 000000000..a9e12d431 --- /dev/null +++ b/packages/auth/src/ActivateAccount/ActivateAccountFailedRedirect.tsx @@ -0,0 +1,54 @@ +import React, { FC } from 'react'; +import { Button, Grid, omitProps, RendererFunctionFC, useT } from '@frontegg/react-core'; +import { useActivateAccountActions, useAuthRoutes, useOnRedirectTo } from '@frontegg/react-hooks/auth'; +import { ActivateAccountStep } from '@frontegg/redux-store'; + +export interface ActivateAccountFailedRedirectProps { + renderer?: RendererFunctionFC; +} + +export const ActivateAccountFailedRedirect: FC = (props) => { + const { renderer } = props; + const { t } = useT(); + const routes = useAuthRoutes(); + const onRedirectTo = useOnRedirectTo(); + const { resetActivateState, setActivateState } = useActivateAccountActions(); + + if (renderer) { + return renderer(omitProps(props, ['renderer'])); + } + return ( + <> +
+ {t('auth.activate-account.failed-title')} +
+ {t('auth.activate-account.failed-description')} +
+ + + + + + + + + + ); +}; diff --git a/packages/auth/src/ActivateAccount/ActivateAccountForm.tsx b/packages/auth/src/ActivateAccount/ActivateAccountForm.tsx new file mode 100644 index 000000000..1f7c2384f --- /dev/null +++ b/packages/auth/src/ActivateAccount/ActivateAccountForm.tsx @@ -0,0 +1,127 @@ +import React, { ComponentType, createElement, FC, RefObject, useCallback, useEffect, useState } from 'react'; +import { AuthActions, AuthState } from '@frontegg/redux-store/auth'; +import { + validatePasswordConfirmation, + validateSchema, + ErrorMessage, + FForm, + FButton, + FInput, + useT, + FFormik, + validatePasswordUsingOWASP, + Loader, +} from '@frontegg/react-core'; +import { IGetActivateAccountStrategyResponse } from '@frontegg/rest-api'; +import { useAuth, useAuthActions } from '@frontegg/react-hooks/auth'; +import { FReCaptcha } from '../components/FReCaptcha'; +import { ReCaptcha } from 'react-recaptcha-v3'; + +const { Formik } = FFormik; + +const stateMapper = ({ activateState, forgotPasswordState, signUpState }: AuthState) => ({ + activateState, + forgotPasswordState, + signUpState, +}); + +export type ActivateAccountFormRendererProps = Omit & + ReturnType & + Pick; + +export interface ActivateAccountFormProps { + renderer?: ComponentType; + userId: string; + token: string; +} + +export const ActivateAccountForm: FC = (props) => { + const [logoutLoader, setLogoutLoader] = useState(true); + const { renderer, userId, token } = props; + const { t } = useT(); + const { + activateAccount, + loadPasswordConfig, + resetForgotPasswordState, + silentLogout, + getActivateAccountStrategy, + } = useAuthActions(); + + const { + activateState: { loading: activateStateLoading, error, activationStrategy }, + forgotPasswordState: { passwordConfig }, + } = useAuth(stateMapper); + + useEffect((): (() => void) => { + const logoutCallback = () => { + setLogoutLoader(false); + const callback = (data: IGetActivateAccountStrategyResponse | null) => { + if (!data?.shouldSetPassword) { + activateAccount({ userId, token }); + } + }; + + getActivateAccountStrategy({ userId, token, callback }); + }; + + silentLogout(logoutCallback); + loadPasswordConfig({ userId }); + return resetForgotPasswordState; + }, [silentLogout, loadPasswordConfig, resetForgotPasswordState, getActivateAccountStrategy, userId, token]); + + const loading = + logoutLoader || + activationStrategy.loading || + (!activationStrategy.strategy?.shouldSetPassword && activateStateLoading); + + const recaptchaRef: RefObject = React.createRef(); + useEffect(() => { + if (recaptchaRef.current && !loading) { + error && recaptchaRef.current.execute(); + } + }, [loading, error, recaptchaRef]); + + if (renderer) { + return createElement(renderer, { ...props, loading, error, passwordConfig } as any); + } + + if (loading) { + return ; + } + + if (activationStrategy.error) return ; + + return ( + activateAccount({ userId, token, password, recaptchaToken })} + > + + + + + {t('auth.activate-account.activate-account-button')} + + + + + + ); +}; diff --git a/packages/auth/src/ActivateAccount/ActivateAccountResendEmail.tsx b/packages/auth/src/ActivateAccount/ActivateAccountResendEmail.tsx new file mode 100644 index 000000000..b59a19ca6 --- /dev/null +++ b/packages/auth/src/ActivateAccount/ActivateAccountResendEmail.tsx @@ -0,0 +1,79 @@ +import React, { ComponentType, createElement, FC } from 'react'; +import { + validateSchema, + ErrorMessage, + FForm, + FButton, + FInput, + useT, + FFormik, + validateEmail, + Button, +} from '@frontegg/react-core'; +import { AuthActions } from '@frontegg/redux-store/auth'; +import { + useActivateAccountActions, + useActivateAccountState, + useAuthRoutes, + useOnRedirectTo, +} from '@frontegg/react-hooks/auth'; + +const { Formik } = FFormik; + +export type ActivateAccountFormRendererProps = Omit & + Pick; + +export interface ActivateAccountResendEmailProps { + renderer?: ComponentType; +} + +export const ActivateAccountResendEmail: FC = (props) => { + const { renderer } = props; + const { t } = useT(); + const { loading, error, resentEmail } = useActivateAccountState(); + const { resetActivateState, resendActivationEmail } = useActivateAccountActions(); + const onRedirectTo = useOnRedirectTo(); + const routes = useAuthRoutes(); + + if (renderer) { + return createElement(renderer, { ...props, loading, error } as any); + } + + return resentEmail ? ( +
+ {t('auth.activate-account.request-sent')} + +
+ ) : ( + resendActivationEmail({ email })} + > + + + + {t('auth.activate-account.send')} + + + + + ); +}; diff --git a/packages/auth/src/ActivateAccount/ActivateAccountSuccessRedirect.tsx b/packages/auth/src/ActivateAccount/ActivateAccountSuccessRedirect.tsx new file mode 100644 index 000000000..e302daedf --- /dev/null +++ b/packages/auth/src/ActivateAccount/ActivateAccountSuccessRedirect.tsx @@ -0,0 +1,30 @@ +import React, { FC, useEffect } from 'react'; +import { useT, RendererFunctionFC, omitProps, Loader } from '@frontegg/react-core'; +import { useAuthActions, useAuthRoutes, useOnRedirectTo } from '@frontegg/react-hooks/auth'; + +export interface ActivateAccountSuccessRedirectProps { + renderer?: RendererFunctionFC; +} + +export const ActivateAccountSuccessRedirect: FC = (props) => { + const { renderer } = props; + const { t } = useT(); + const { requestAuthorize, resetActivateState } = useAuthActions(); + + if (renderer) { + return renderer(omitProps(props, ['renderer'])); + } + useEffect(() => { + requestAuthorize(true); + return resetActivateState as () => void; + }, [resetActivateState, requestAuthorize]); + + return ( + <> +
{t('auth.activate-account.activation-succeeded')}
+
+ +
+ + ); +}; diff --git a/packages/auth/src/ActivateAccount/index.ts b/packages/auth/src/ActivateAccount/index.ts new file mode 100644 index 000000000..8ce923efb --- /dev/null +++ b/packages/auth/src/ActivateAccount/index.ts @@ -0,0 +1,4 @@ +export * from './ActivateAccount'; +export * from './ActivateAccountForm'; +export * from './ActivateAccountFailedRedirect'; +export * from './ActivateAccountSuccessRedirect'; diff --git a/packages/auth/src/ApiTokens/TenantApiTokens/TenantApiTokensPage.tsx b/packages/auth/src/ApiTokens/TenantApiTokens/TenantApiTokensPage.tsx new file mode 100644 index 000000000..94db37aa1 --- /dev/null +++ b/packages/auth/src/ApiTokens/TenantApiTokens/TenantApiTokensPage.tsx @@ -0,0 +1,32 @@ +import React, { FC, useEffect } from 'react'; +import { ApiTokenType } from '@frontegg/redux-store/auth'; +import { useApiTokensActions, useApiTokensState } from '@frontegg/react-hooks/auth'; +import { ApiTokensHeader } from '../components/ApiTokensHeader'; +import { ApiTokensLayout } from '../components/ApiTokensLayout'; +import { prefixCls } from '../constants'; + +const apiTokenType: ApiTokenType = 'tenant'; + +export interface TenantApiTokensPageProps { + createdByUserIdColumn?: 'show' | 'hide'; +} + +export const TenantApiTokensPage: FC = (props) => { + const { initApiTokensData, setApiTokensState } = useApiTokensActions(); + const { apiTokenType: tokenType } = useApiTokensState(({ apiTokenType }) => ({ apiTokenType })); + const { createdByUserIdColumn } = props; + + useEffect(() => { + initApiTokensData(apiTokenType); + createdByUserIdColumn && setApiTokensState({ createdByUserIdColumn }); + }, []); + + const children = props.children ?? ( + <> + + + + ); + + return
{tokenType && children}
; +}; diff --git a/packages/auth/src/ApiTokens/UserApiTokens/UserApiTokensPage.tsx b/packages/auth/src/ApiTokens/UserApiTokens/UserApiTokensPage.tsx new file mode 100644 index 000000000..dd92f228e --- /dev/null +++ b/packages/auth/src/ApiTokens/UserApiTokens/UserApiTokensPage.tsx @@ -0,0 +1,26 @@ +import React, { FC, useEffect } from 'react'; +import { ApiTokenType } from '@frontegg/redux-store/auth'; +import { useApiTokensActions, useApiTokensState } from '@frontegg/react-hooks/auth'; +import { prefixCls } from '../constants'; +import { ApiTokensHeader } from '../components/ApiTokensHeader'; +import { ApiTokensLayout } from '../components/ApiTokensLayout'; + +const apiTokenType: ApiTokenType = 'user'; + +export const UserApiTokensPage: FC = (props) => { + const { initApiTokensData } = useApiTokensActions(); + const { apiTokenType: tokenType } = useApiTokensState(({ apiTokenType }) => ({ apiTokenType })); + + useEffect(() => { + initApiTokensData(apiTokenType); + }, []); + + const children = props.children ?? ( + <> + + + + ); + + return
{tokenType && children}
; +}; diff --git a/packages/auth/src/ApiTokens/components/ApiTokensAddDialog.tsx b/packages/auth/src/ApiTokens/components/ApiTokensAddDialog.tsx new file mode 100644 index 000000000..c96630836 --- /dev/null +++ b/packages/auth/src/ApiTokens/components/ApiTokensAddDialog.tsx @@ -0,0 +1,144 @@ +import React, { FC, useCallback } from 'react'; +import { ITeamUserRole } from '@frontegg/rest-api'; +import { ApiStateKeys, ApiTokensState } from '@frontegg/redux-store/auth'; +import { + Button, + Dialog, + ErrorMessage, + FButton, + FForm, + FFormik, + FInput, + FSelect, + Grid, + useT, +} from '@frontegg/react-core'; +import { useApiTokensActions, useApiTokensState } from '@frontegg/react-hooks/auth'; +import { prefixCls } from '../constants'; + +const { Formik } = FFormik; + +type AddTokenFormValues = { + description: string; + roles: { label: string; value: string }[]; +}; + +const stateMapper = ({ + loaders: { ADD_API_TOKEN: loading }, + roles, + apiTokenType, + showAddTokenDialog, + errors: { ADD_API_TOKEN: error }, +}: ApiTokensState) => ({ + loading, + roles, + apiTokenType, + showAddTokenDialog, + error, +}); + +export const ApiTokensAddDialog: FC = () => { + const { addUserApiToken, addTenantApiToken, setApiTokensState, setApiTokensError } = useApiTokensActions(); + const { loading, roles, apiTokenType, showAddTokenDialog, error } = useApiTokensState(stateMapper); + const { t } = useT(); + + const initialValues: AddTokenFormValues = { + description: '', + roles: [], + }; + + const closeDialog = useCallback(() => { + setApiTokensState({ showAddTokenDialog: false }); + setApiTokensError({ key: ApiStateKeys.ADD_API_TOKEN, value: false }); + }, [setApiTokensState]); + + return ( + +
+ {apiTokenType === 'user' ? t('auth.apiTokens.modal.subtitleUser') : t('auth.apiTokens.modal.subtitleTenant')} {} +
+ { + setSubmitting(true); + apiTokenType === 'user' + ? addUserApiToken({ + description, + callback: () => { + resetForm(); + setSubmitting(false); + }, + }) + : addTenantApiToken({ + description, + roleIds: roles.map((v) => v.value), + callback: () => { + resetForm(); + setSubmitting(false); + }, + }); + }} + > + + + {apiTokenType === 'tenant' && ( + ({ label: r.name, value: r.id }))} + /> + )} + + +
+ + + + + + + {t('auth.apiTokens.modal.create')} + + + +
+
+
+
+ ); +}; diff --git a/packages/auth/src/ApiTokens/components/ApiTokensDeleteDialog.tsx b/packages/auth/src/ApiTokens/components/ApiTokensDeleteDialog.tsx new file mode 100644 index 000000000..7160583ce --- /dev/null +++ b/packages/auth/src/ApiTokens/components/ApiTokensDeleteDialog.tsx @@ -0,0 +1,75 @@ +import React, { FC, useCallback } from 'react'; +import { Button, Dialog, ErrorMessage, Grid, useT } from '@frontegg/react-core'; +import { ApiStateKeys, ApiTokensState } from '@frontegg/redux-store/auth'; +import { useApiTokensState, useApiTokensActions } from '@frontegg/react-hooks/auth'; +import { prefixCls } from '../constants'; + +const stateMapper = ({ + deleteTokenDialog, + apiTokenType, + loaders: { DELETE_API_TOKEN: loading }, + errors: { DELETE_API_TOKEN: error }, +}: ApiTokensState) => ({ + ...deleteTokenDialog, + apiTokenType, + loading, + error, +}); +export const ApiTokensDeleteDialog: FC = () => { + const { t } = useT(); + const { open, clientId, apiTokenType, loading, error } = useApiTokensState(stateMapper); + const { deleteTenantApiToken, deleteUserApiToken, setApiTokensState, setApiTokensError } = useApiTokensActions(); + + const handleDeleteUser = useCallback(() => { + clientId && apiTokenType === 'user' ? deleteUserApiToken(clientId) : deleteTenantApiToken(clientId); + }, [deleteTenantApiToken, clientId]); + + const handleOnClose = useCallback(() => { + setApiTokensState({ deleteTokenDialog: { open: false, clientId } }); + setApiTokensError({ key: ApiStateKeys.DELETE_API_TOKEN, value: false }); + }, [clientId, setApiTokensState]); + + const isOpen = !!clientId && open; + + return ( + +

{t('auth.apiTokens.deleteModal.message')}

+ + +
+ + + + + + + + +
+
+ ); +}; diff --git a/packages/auth/src/ApiTokens/components/ApiTokensHeader.tsx b/packages/auth/src/ApiTokens/components/ApiTokensHeader.tsx new file mode 100644 index 000000000..7e408883b --- /dev/null +++ b/packages/auth/src/ApiTokens/components/ApiTokensHeader.tsx @@ -0,0 +1,16 @@ +import React, { FC } from 'react'; +import { PageHeader, PageHeaderProps, useT } from '@frontegg/react-core'; +import classNames from 'classnames'; + +export type ApiTokensHeaderProps = PageHeaderProps; + +export const ApiTokensHeader: FC = (props) => { + const { t } = useT(); + const customProps: Partial = { + className: classNames('fe-apiTokens__header', props.className), + title: props.title ?? t('auth.apiTokens.title'), + subTitle: props.subTitle ?? t('auth.apiTokens.subtitle'), + }; + + return ; +}; diff --git a/packages/auth/src/ApiTokens/components/ApiTokensLayout.tsx b/packages/auth/src/ApiTokens/components/ApiTokensLayout.tsx new file mode 100644 index 000000000..7f54317e6 --- /dev/null +++ b/packages/auth/src/ApiTokens/components/ApiTokensLayout.tsx @@ -0,0 +1,20 @@ +import React, { FC } from 'react'; +import { ApiTokensTableToolbar } from './ApiTokensTableToolbar'; +import { ApiTokensTableComponent } from './ApiTokensTableComponent'; +import { ApiTokensSucceessDialog } from './ApiTokensSuccessDialog'; +import { ApiTokensAddDialog } from './ApiTokensAddDialog'; +import { ApiTokensDeleteDialog } from './ApiTokensDeleteDialog'; + +export const ApiTokensLayout: FC = (props) => { + const children = props.children ?? ( + <> + + + + + + + ); + + return <>{children}; +}; diff --git a/packages/auth/src/ApiTokens/components/ApiTokensSuccessDialog.tsx b/packages/auth/src/ApiTokens/components/ApiTokensSuccessDialog.tsx new file mode 100644 index 000000000..9905a6173 --- /dev/null +++ b/packages/auth/src/ApiTokens/components/ApiTokensSuccessDialog.tsx @@ -0,0 +1,92 @@ +import React, { FC, useCallback, useState } from 'react'; +import { Button, Dialog, Input, Grid, useT, Icon } from '@frontegg/react-core'; +import copy from 'clipboard-copy'; +import { prefixCls } from '../constants'; +import { useApiTokensActions, useApiTokensState } from '@frontegg/react-hooks/auth'; + +export type copyType = 'secret' | 'clientId'; + +export const ApiTokensSucceessDialog: FC = () => { + const { t } = useT(); + const { open, secret, clientId } = useApiTokensState(({ successDialog }) => successDialog); + const { setApiTokensState } = useApiTokensActions(); + const [copiedSecret, setCopiedSecret] = useState(false); + const [copiedClientId, setCopiedClientId] = useState(false); + + const copySecret = (type: copyType) => { + copy(type === 'secret' ? secret ?? '' : clientId ?? '').then(() => displayCopied(type)); + }; + + const displayCopied = (type: copyType) => { + type === 'secret' ? setCopiedSecret(true) : setCopiedClientId(true); + setTimeout(() => { + type === 'secret' ? setCopiedSecret(false) : setCopiedClientId(false); + }, 2000); + }; + + const resetModalState = useCallback(() => { + setApiTokensState({ successDialog: { open: false, clientId, secret } }); + }, [setApiTokensState]); + + return ( + +
{t('auth.apiTokens.modal.successDescription')}
+
+ + {t('auth.apiTokens.modal.tip')} +
+ copySecret('clientId')} + data-test-id='copyID-btn' + /> + ) : ( + + ) + } + /> +
+ copySecret('secret')} /> + ) : ( + + ) + } + /> + +
+ + + + + +
+
+ ); +}; diff --git a/packages/auth/src/ApiTokens/components/ApiTokensTableComponent.tsx b/packages/auth/src/ApiTokens/components/ApiTokensTableComponent.tsx new file mode 100644 index 000000000..6bf8ba5a0 --- /dev/null +++ b/packages/auth/src/ApiTokens/components/ApiTokensTableComponent.tsx @@ -0,0 +1,73 @@ +import React, { FC, useEffect, useMemo, useState } from 'react'; +import { Loader, Table, useT } from '@frontegg/react-core'; +import { IUserApiTokensData, ITenantApiTokensData } from '@frontegg/redux-store/auth'; +import { useApiTokensState } from '@frontegg/react-hooks/auth'; +import { prefixCls, tableColumnsUser, tableColumnsTenant } from '../constants'; + +export const ApiTokensTableComponent: FC = () => { + const { + loading, + apiTokensDataUser, + apiTokensDataTenant, + apiTokenType, + searchValue, + createdByUserIdColumn, + } = useApiTokensState( + ({ + loaders: { LOAD_API_TOKENS: loading }, + apiTokensDataUser, + apiTokensDataTenant, + apiTokenType, + searchValue, + createdByUserIdColumn, + }) => ({ + loading, + apiTokensDataUser, + apiTokensDataTenant, + apiTokenType, + searchValue, + createdByUserIdColumn, + }) + ); + const [data, setData] = useState | undefined>(undefined); + const { t } = useT(); + + useEffect(() => { + apiTokenType === 'user' + ? setData( + apiTokensDataUser.filter((i: IUserApiTokensData) => { + return i.clientId?.includes(searchValue) || i.description?.includes(searchValue); + }) + ) + : setData( + apiTokensDataTenant.filter((i: ITenantApiTokensData) => { + return ( + i.clientId?.includes(searchValue) || + i.description?.includes(searchValue) || + i.createdByUserId?.includes(searchValue) + ); + }) + ); + }, [searchValue, apiTokensDataUser, apiTokensDataTenant]); + + const preparedTenantColumns = useMemo(() => { + return createdByUserIdColumn === 'hide' + ? tableColumnsTenant(t).filter((i) => i.accessor !== 'createdByUserId') + : tableColumnsTenant(t); + }, [createdByUserIdColumn, tableColumnsTenant]); + + if (!!loading || !data) { + return ; + } + + return ( +
+ ); +}; diff --git a/packages/auth/src/ApiTokens/components/ApiTokensTableToolbar.tsx b/packages/auth/src/ApiTokens/components/ApiTokensTableToolbar.tsx new file mode 100644 index 000000000..160c92009 --- /dev/null +++ b/packages/auth/src/ApiTokens/components/ApiTokensTableToolbar.tsx @@ -0,0 +1,40 @@ +import React, { FC } from 'react'; +import { Button, Grid, Input, useT } from '@frontegg/react-core'; +import { prefixCls } from '../constants'; +import { useApiTokensActions, useApiTokensState } from '@frontegg/react-hooks/auth'; + +export const ApiTokensTableToolbar: FC = () => { + const { setApiTokensState } = useApiTokensActions(); + const { searchValue } = useApiTokensState(({ searchValue }) => ({ searchValue })); + + const { t } = useT(); + + return ( + <> +
+ + + setApiTokensState({ searchValue: e.target.value })} + placeholder={`${t('common.search')}...`} + /> + + + + + +
+ + ); +}; diff --git a/packages/auth/src/ApiTokens/components/ApiTokensTebleCell.tsx b/packages/auth/src/ApiTokens/components/ApiTokensTebleCell.tsx new file mode 100644 index 000000000..dc4692459 --- /dev/null +++ b/packages/auth/src/ApiTokens/components/ApiTokensTebleCell.tsx @@ -0,0 +1,82 @@ +import React, { useCallback, useState, useEffect } from 'react'; +import { Button, CellComponent, Icon, Loader, Menu, MenuItemProps, TableCells, useT } from '@frontegg/react-core'; +import { api } from '@frontegg/rest-api'; +import { useApiTokensActions } from '@frontegg/react-hooks/auth'; + +export const getApiTokensTableCells = (column: string): CellComponent => { + switch (column) { + case 'createdByUserId': + return ApiTokensTenantCreatedBy; + case 'createdAt': + return TableCells.DateAgo; + default: + return TableCells.Title; + } +}; + +type ApiTokenCreatedByState = { + loading: boolean; + name?: string; +}; + +const ApiTokensTenantCreatedBy = ({ value }: { value: string }) => { + const { t } = useT(); + const [state, setState] = useState({ loading: true }); + const { name, loading } = state; + + useEffect(() => { + api.auth + .getUserById({ userId: value }) + .then((data) => { + setState({ loading: false, name: data.name }); + }) + .catch((error) => { + setState({ loading: false }); + }); + }, [setState]); + + return loading ? ( + + ) : ( +
{name ?? t('common.notFoundUser')}
+ ); +}; + +export const ApiTokensActions = (props: any): CellComponent => { + const { clientId } = props.row.original; + const { setApiTokensState } = useApiTokensActions(); + const { t } = useT(); + + const handleDeleteUser = useCallback(() => { + setApiTokensState({ deleteTokenDialog: { open: true, clientId } }); + }, [clientId]); + + const items: MenuItemProps[] = [ + { + icon: , + onClick: handleDeleteUser, + text: t('auth.apiTokens.deleteModal.title'), + iconClassName: 'fe-color-danger', + }, + ]; + + return ( +
+ {items.length > 0 && ( + + + + } + /> + )} +
+ ); +}; diff --git a/packages/auth/src/ApiTokens/constants.ts b/packages/auth/src/ApiTokens/constants.ts new file mode 100644 index 000000000..9d7ecc1df --- /dev/null +++ b/packages/auth/src/ApiTokens/constants.ts @@ -0,0 +1,63 @@ +import { ApiTokensActions, getApiTokensTableCells } from './components/ApiTokensTebleCell'; +import { TFunction } from 'i18next'; +export const prefixCls = 'fe-api-tokens'; + +export const tableColumnsTenant = (t: TFunction) => [ + { + accessor: 'clientId', + Header: t('common.clientId'), + sortable: true, + Cell: getApiTokensTableCells('clientId'), + }, + { + accessor: 'description', + Header: t('common.description'), + sortable: true, + Cell: getApiTokensTableCells('Description'), + }, + { + accessor: 'createdByUserId', + Header: t('common.createdBy'), + sortable: true, + Cell: getApiTokensTableCells('createdByUserId'), + }, + { + accessor: 'createdAt', + Header: t('common.createdAt'), + sortable: true, + Cell: getApiTokensTableCells('createdAt'), + }, + { + id: 'actions', + minWidth: '3.25rem', + maxWidth: '3.25rem', + Cell: ApiTokensActions, + }, +]; + +export const tableColumnsUser = (t: TFunction) => [ + { + accessor: 'clientId', + Header: t('common.clientId'), + sortable: true, + Cell: getApiTokensTableCells('clientId'), + }, + { + accessor: 'description', + Header: t('common.description'), + sortable: true, + Cell: getApiTokensTableCells('Description'), + }, + { + accessor: 'createdAt', + Header: t('common.createdAt'), + sortable: true, + Cell: getApiTokensTableCells('createdAt'), + }, + { + id: 'actions', + minWidth: '3.25rem', + maxWidth: '3.25rem', + Cell: ApiTokensActions, + }, +]; diff --git a/packages/auth/src/ApiTokens/index.ts b/packages/auth/src/ApiTokens/index.ts new file mode 100644 index 000000000..3c5990ec5 --- /dev/null +++ b/packages/auth/src/ApiTokens/index.ts @@ -0,0 +1,34 @@ +import { UserApiTokensPage } from './UserApiTokens/UserApiTokensPage'; +import { TenantApiTokensPage } from './TenantApiTokens/TenantApiTokensPage'; +import { ApiTokensHeader } from './components/ApiTokensHeader'; +import { ApiTokensLayout } from './components/ApiTokensLayout'; +import { ApiTokensAddDialog } from './components/ApiTokensAddDialog'; +import { ApiTokensSucceessDialog } from './components/ApiTokensSuccessDialog'; +import { ApiTokensDeleteDialog } from './components/ApiTokensDeleteDialog'; +import { ApiTokensTableToolbar } from './components/ApiTokensTableToolbar'; +import { ApiTokensTableComponent } from './components/ApiTokensTableComponent'; + +export * from './UserApiTokens/UserApiTokensPage'; +export * from './TenantApiTokens/TenantApiTokensPage'; + +export const TenantApiTokens = { + Page: TenantApiTokensPage, + Header: ApiTokensHeader, + Layout: ApiTokensLayout, + Toolbar: ApiTokensTableToolbar, + Table: ApiTokensTableComponent, + AddDialog: ApiTokensAddDialog, + SuccessDialog: ApiTokensSucceessDialog, + DeleteDialog: ApiTokensDeleteDialog, +}; + +export const UserApiTokens = { + Page: UserApiTokensPage, + Header: ApiTokensHeader, + Layout: ApiTokensLayout, + Toolbar: ApiTokensTableToolbar, + Table: ApiTokensTableComponent, + AddDialog: ApiTokensAddDialog, + SuccessDialog: ApiTokensSucceessDialog, + DeleteDialog: ApiTokensDeleteDialog, +}; diff --git a/packages/auth/src/ApiTokens/readme.md b/packages/auth/src/ApiTokens/readme.md new file mode 100644 index 000000000..b258f55da --- /dev/null +++ b/packages/auth/src/ApiTokens/readme.md @@ -0,0 +1,177 @@ +## Api Tokens (Single Sign On) +This collection contains built-in components to provide the ability to display your Tenant/User Api Tokens configuration, updates and etc. + +## Usage + +To use this components you need to import it from `@frontegg/react-auth` + +```tsx +import { TenantApiTokens, UserApiTokens } from '@frontegg/react-auth'; + +const AppRouter:FC = ()=> { + + return
+ {/* other routes... */} + + + +
+} +``` + +![Base Example Result](imgs/api-tokens-basic-example.png) + +## Customization + +In order to provide a **fully customizable** component, *Frontegg* team building their components with Compound Components design pattern. + +This design gives you the ability to inject your custom components inside the built-in component. + +**NOTE!**: **If you pass a child to the Api Tokens component +it will be rendered without any inner default components, then if you want to customize a specific element +you need to add the other inner default components to it, see bellow examples of how +you can override single component:** + +**You can find the default render method foreach Api Tokens component [here](#default-rendered-components)** + +### Default Rendered Components for Tenant Api Tokens component + +- [`TenantApiTokens.Page`](./TenantApiTokens/TenantApiTokensPage.tsx) + - [`TenantApiTokens.Header`](./components/ApiTokensHeader.tsx) + - [`TenantApiTokens.Layout`](./components/ApiTokensLayout.tsx) + - [`TenantApiTokens.Toolbar`](./components/ApiTokensTableToolbar.tsx) + - [`TenantApiTokens.Table`](./components/ApiTokensTableComponent.tsx) + - [`TenantApiTokens.AddDialog`](./components/ApiTokensAddDialog.tsx) + - [`TenantApiTokens.SuccessDialog`](./components/SuccessDialog.tsx) + - [`TenantApiTokens.DeleteDialog`](./components/ApiTokensDeleteDialog.tsx) + +### Default Rendered Components for User Api Tokens component + +- [`UserApiTokens.Page`](./UserApiTokens/UserApiTokensPagePage.tsx) + - [`UserApiTokens.Header`](./components/ApiTokensHeader.tsx) + - [`UserApiTokens.Layout`](./components/ApiTokensLayout.tsx) + - [`UserApiTokens.Toolbar`](./components/ApiTokensTableToolbar.tsx) + - [`UserApiTokens.Table`](./components/ApiTokensTableComponent.tsx) + - [`UserApiTokens.AddDialog`](./components/ApiTokensAddDialog.tsx) + - [`UserApiTokens.SuccessDialog`](./components/SuccessDialog.tsx) + - [`UserApiTokens.DeleteDialog`](./components/ApiTokensDeleteDialog.tsx) + + + +## Examples + +Here are some examples of how to customize the **User/Tenant Api Tokens** components: + +- [Custom header title](#custom-header-title) +- [Render without header](#render-header-title) +- [Inject custom header](#inject-custom-header) +- [Inject element inside layout](#inject-element-inside-layout) +- [Hide 'Created By' column for Tenant Api Tokens table](#hide-created-by-column-for-tenant-api-tokens-table) +- `Change dialog windows and toolbar` (coming soon) + +### Custom header title: + +In this example, we have injected the inner built-in components to the `TenantApiTokens/UserApiTokens` as `children`, +and passed `title` property to the `TenantApiTokens/UserApiTokens.Header` to override its default `title` value. + +Notice that we also added the ``, this is because how Compound Components Design works. +So you need to pass the default inner components if you don't want to override. +```tsx + +import { TenantApiTokens } from '@frontegg/react-auth'; + +render() { + return ( + + + + + ) +} +``` + +### Render without header: + +In this example, we have two options to hide the `TenantApiTokens/UserApiTokens.Header`: +1. pass hide property to the `TenantApiTokens/UserApiTokens.Header` component. +2. just remove it from `TenantApiTokens/UserApiTokens.Page` children. + +Sometimes there is a specific component +```tsx +import { TenantApiTokens } from '@frontegg/react-auth'; + +render() { + return ( +// option 1 + + + + + +// option 2 + + + + ) +} +``` + +### Inject custom header: + +```tsx +import { TenantApiTokens } from '@frontegg/react-auth'; +import { MyCustomHeader } from './MyCustomHeader'; + +render() { + return ( + + + + + ) +} + +``` + + +### Inject element inside Layout: + +```tsx +import { TenantApiTokens } from '@frontegg/react-auth'; + +render() { + return ( + + + + +
+ this element inject between Api Tokens toolbar and table. +
+ + + + +
+
+ ) +} + +``` + + +### Hide Created By column for Tenant Api Tokens table: + +```tsx +import { TenantApiTokens } from '@frontegg/react-auth'; + +render() { + return ( + + ) +} +``` + + + + diff --git a/packages/auth/src/AuthorizedContent/index.tsx b/packages/auth/src/AuthorizedContent/index.tsx new file mode 100644 index 000000000..6ee9d4d13 --- /dev/null +++ b/packages/auth/src/AuthorizedContent/index.tsx @@ -0,0 +1,53 @@ +import React, { FC } from 'react'; +import { Logger } from '@frontegg/react-core'; +import { useAuthUserOrNull } from '@frontegg/react-hooks/auth'; +import { User } from '@frontegg/redux-store'; + +export interface AuthorizationProps { + requiredRoles?: string[]; + requiredPermissions?: string[]; + render?: (isAuthorized: boolean) => React.ReactNode | null; +} + +const logger = Logger.from('AuthorizedContent'); + +export const AuthorizedContent: FC = (props) => { + let isAuthorized = true; // Initially + const user = useAuthUserOrNull(); + + if (!user?.superUser) { + if (props.requiredPermissions) { + if (!user?.permissions || user?.permissions.length === 0) { + logger.info('No permissions for user. Required permissions are - ', props.requiredPermissions); + isAuthorized = false; + } + + for (const permission of props.requiredPermissions) { + if (!user?.permissions?.find(({ key }) => key === permission)) { + logger.info(`Permissions ${permission} is missing from the list`); + isAuthorized = false; + } + } + } + + if (props.requiredRoles) { + if (!user?.roles || user?.roles.length === 0) { + logger.info('No roles for user. Required roles are - ', props.requiredRoles); + isAuthorized = false; + } + + for (const role of props.requiredRoles) { + if (!user?.roles?.find(({ key }) => key === role)) { + logger.info(`Role ${role} is missing from the list`); + isAuthorized = false; + } + } + } + } + + if (typeof props.render === 'function') { + return <>{props.render(isAuthorized)}; + } + + return isAuthorized ? <>{props.children} : null; +}; diff --git a/packages/auth/src/ForgotPassword/ForgotPassword.tsx b/packages/auth/src/ForgotPassword/ForgotPassword.tsx new file mode 100644 index 000000000..4b15bc223 --- /dev/null +++ b/packages/auth/src/ForgotPassword/ForgotPassword.tsx @@ -0,0 +1,32 @@ +import React, { FC } from 'react'; +import { useDynamicComponents, ComponentsTypesWithProps } from '@frontegg/react-core'; +import { ForgotPasswordStep } from '@frontegg/redux-store/auth'; +import { authPageWrapper } from '../components'; +import { ForgotPasswordSuccessRedirect, ForgotPasswordSuccessRedirectProps } from './ForgotPasswordSuccessRedirect'; +import { ForgotPasswordForm, ForgotPasswordFormProps } from './ForgotPasswordForm'; +import { useForgotPasswordState } from '@frontegg/react-hooks/auth'; + +type Components = { + ForgotPasswordSuccessRedirect: ForgotPasswordSuccessRedirectProps; + ForgotPasswordForm: ForgotPasswordFormProps; +}; + +export interface ForgotPasswordProps { + components?: ComponentsTypesWithProps; +} + +const defaultComponents = { ForgotPasswordSuccessRedirect, ForgotPasswordForm }; +export const ForgotPassword: FC = (props) => { + const Dynamic = useDynamicComponents(defaultComponents, props); + const { step } = useForgotPasswordState(); + + let components; + if (step === ForgotPasswordStep.success) { + components = ; + } else { + components = ; + } + + return
{components}
; +}; +export const ForgotPasswordPage = authPageWrapper(ForgotPassword); diff --git a/packages/auth/src/ForgotPassword/ForgotPasswordForm.tsx b/packages/auth/src/ForgotPassword/ForgotPasswordForm.tsx new file mode 100644 index 000000000..088822b2d --- /dev/null +++ b/packages/auth/src/ForgotPassword/ForgotPasswordForm.tsx @@ -0,0 +1,58 @@ +import React, { ComponentType, createElement, FC } from 'react'; +import { + useT, + validateEmail, + validateSchema, + ErrorMessage, + FForm, + FInput, + FButton, + FFormik, +} from '@frontegg/react-core'; +import { useForgotPasswordActions, useForgotPasswordState } from '@frontegg/react-hooks/auth'; + +const { Formik } = FFormik; + +type ForgotPasswordFormRendererProps = Omit; + +export interface ForgotPasswordFormProps { + renderer?: ComponentType; +} + +export const ForgotPasswordForm: FC = (props) => { + const { renderer } = props; + const { t } = useT(); + const { loading, email, error } = useForgotPasswordState(); + const { forgotPassword } = useForgotPasswordActions(); + if (renderer) { + return createElement(renderer, props); + } + return ( + forgotPassword({ email })} + > + + + + {t('auth.forgot-password.remind-me')} + + + + + ); +}; diff --git a/packages/auth/src/ForgotPassword/ForgotPasswordSuccessRedirect.tsx b/packages/auth/src/ForgotPassword/ForgotPasswordSuccessRedirect.tsx new file mode 100644 index 000000000..90ca52dc6 --- /dev/null +++ b/packages/auth/src/ForgotPassword/ForgotPasswordSuccessRedirect.tsx @@ -0,0 +1,34 @@ +import React, { FC } from 'react'; +import { Button, omitProps, RendererFunctionFC, useT } from '@frontegg/react-core'; +import { useAuthRoutes, useOnRedirectTo, useForgotPasswordActions } from '@frontegg/react-hooks/auth'; + +export interface ForgotPasswordSuccessRedirectProps { + renderer?: RendererFunctionFC; +} + +export const ForgotPasswordSuccessRedirect: FC = (props) => { + const { renderer } = props; + const { t } = useT(); + const onRedirectTo = useOnRedirectTo(); + const { loginUrl } = useAuthRoutes(); + const { resetForgotPasswordState } = useForgotPasswordActions(); + + if (renderer) { + return renderer(omitProps(props, ['renderer'])); + } + return ( + <> +
{t('auth.forgot-password.reset-email-sent')}
+ + + ); +}; diff --git a/packages/auth/src/ForgotPassword/index.ts b/packages/auth/src/ForgotPassword/index.ts new file mode 100644 index 000000000..9a85db86c --- /dev/null +++ b/packages/auth/src/ForgotPassword/index.ts @@ -0,0 +1,3 @@ +export * from './ForgotPassword'; +export * from './ForgotPasswordForm'; +export * from './ForgotPasswordSuccessRedirect'; diff --git a/packages/auth/src/HOCs.tsx b/packages/auth/src/HOCs.tsx new file mode 100644 index 000000000..e185772f9 --- /dev/null +++ b/packages/auth/src/HOCs.tsx @@ -0,0 +1,130 @@ +/* istanbul ignore file */ +import React, { ComponentType, FC } from 'react'; +import { Redirect, Route, RouteProps } from 'react-router-dom'; +import { AuthState } from '@frontegg/redux-store/auth'; +import { useAuth, useIsAuthenticated } from '@frontegg/react-hooks/auth'; +import { FRONTEGG_AFTER_AUTH_REDIRECT_URL } from './constants'; + +const onRedirecting = (loginUrl: string) => { + window.localStorage.setItem( + FRONTEGG_AFTER_AUTH_REDIRECT_URL, + window.location.href.substring(window.location.origin.length) + ); + return ; +}; + +/** + * ```jsx + * class MyProtectedComponent extends Component { + * render() { + * return
+ * This is Protected Component with be displayed only if the user is authenticated + *
+ * } + * } + * export default withProtectedRoute(MyProtectedComponent); + * ``` + * + * they will be redirected to the login page if not authenticated + * returned to the page they we're redirected from after login + */ +export const withProtectedRoute =

(Component: ComponentType

) => + function withProtectedRoute(props: P) { + const { isAuthenticated, isLoading, loginUrl } = useAuth((state) => ({ + isAuthenticated: state, + isLoading: state.isLoading, + loginUrl: state.routes.loginUrl, + })); + return isLoading ? null : isAuthenticated ? : onRedirecting(loginUrl); + }; + +/** + * ```jsx + * export class MyProtectedComponent extends Component { + * render() { + * return + *

My Child Components
+ * + * } + * } + * ``` + * + * they will be redirect child components to be displayed if the user is not authenticated + * the client will be redirected to the login page and returned to the page they we're + * redirected from after login + */ +export const ProtectedComponent: FC = ({ children }) => { + const { + isAuthenticated, + routes: { loginUrl }, + isLoading, + } = useAuth(({ isAuthenticated, routes, isLoading }: AuthState) => ({ isAuthenticated, routes, isLoading })); + + return isLoading ? null : isAuthenticated ? <>{children} : onRedirecting(loginUrl); +}; + +/** + * ```jsx + * export class MyApp extends Component { + * render() { + * return + * + * + * + * + * + * } + * } + * ``` + * + * they will be redirect child components to be displayed if the user is not authenticated + * the client will be redirected to the login page and returned to the page they we're + * redirected from after login + */ +export class ProtectedRoute extends React.Component { + render() { + const { component, render, children, ...routeProps } = this.props; + if (children != null) { + return ( + + {children} + + ); + } + if (render != null) { + return {render(props)}} />; + } + if (component != null) { + return ( + ( + {React.createElement(component as any, props as any)} + )} + /> + ); + } + return ; + } +} + +/** + * ```jsx + * export class MyComponent extends Component { + * render() { + * return <> + *
This 'div' will always be visible
+ * + *
This 'div' will be displayed only if the user is authenticated
+ *
+ * + * } + * } + * ``` + * + * they will be redirect child components to be displayed if the user is not authenticated + */ +export const ProtectedArea: FC = ({ children }) => { + const isAuthenticated = useIsAuthenticated(); + return isAuthenticated ? <>{children} : null; +}; diff --git a/packages/auth/src/Listener.tsx b/packages/auth/src/Listener.tsx new file mode 100644 index 000000000..69f84e5bf --- /dev/null +++ b/packages/auth/src/Listener.tsx @@ -0,0 +1,67 @@ +import React, { FC, useEffect, useRef } from 'react'; +import { AuthActions, AuthState, authStoreName } from '@frontegg/redux-store/auth'; +import { useAuth, useAuthActions } from '@frontegg/react-hooks'; +import { ContextHolder } from '@frontegg/rest-api'; +import { ListenerProps } from '@frontegg/react-core'; + +const stateMapper = ({ isAuthenticated, user, isLoading, routes, keepSessionAlive }: AuthState) => ({ + isAuthenticated, + user, + isLoading, + routes, + keepSessionAlive, +}); + +const AuthStateKey = 'fe-auth-state'; + +export const AuthListener: FC> = (props) => { + const timer = useRef(0); + const { isAuthenticated, user, isLoading, routes, keepSessionAlive } = useAuth(stateMapper); + const actions = useAuthActions(); + ContextHolder.setLogout(actions.logout, routes.logoutUrl); + + const updateSessionTimer = (firstTime: boolean = false) => { + timer.current && clearInterval(timer.current); + if (firstTime) { + actions.requestAuthorize(firstTime); + } else { + if (isAuthenticated) { + if (keepSessionAlive === true) { + const ttl = (user?.expiresIn || 20) * 1000 * 0.8; + timer.current = setInterval(() => actions.requestAuthorize(), ttl); + } else { + const ttl = (user?.expiresIn || 20) * 1000; + timer.current = setInterval(() => actions.logout(), ttl); + } + } + } + }; + + const updateAuthenticationOnStorage = () => { + if (isLoading) return; + localStorage.setItem(AuthStateKey, JSON.stringify(isAuthenticated)); + }; + + const addStorageListener = () => { + window.addEventListener('storage', (ev) => { + if (ev.key !== AuthStateKey) return; + + const authState = JSON.parse(ev.newValue || 'false'); + if (authState === true) return; + // Force refresh token + actions.resetState(); + actions.requestAuthorize(); + }); + }; + useEffect(() => { + actions.loadSecurityPolicyCaptcha(); + }, []); + useEffect(() => updateSessionTimer(true), []); + useEffect(() => updateSessionTimer(), [isAuthenticated]); + useEffect(() => updateAuthenticationOnStorage(), [isLoading, isAuthenticated]); + useEffect(() => addStorageListener(), []); + useEffect(() => { + props.resolveActions?.(authStoreName, actions); + }, [props.resolveActions, actions]); + return null; +}; diff --git a/packages/auth/src/Login/ForceEnrollMfa.tsx b/packages/auth/src/Login/ForceEnrollMfa.tsx new file mode 100644 index 000000000..1711b22f6 --- /dev/null +++ b/packages/auth/src/Login/ForceEnrollMfa.tsx @@ -0,0 +1,135 @@ +import React, { FC, useEffect, useRef } from 'react'; +import { MFAStep } from '@frontegg/redux-store/auth'; +import { + validateSchema, + validateTwoFactorCode, + omitProps, + RendererFunctionFC, + useT, + FForm, + FFormik, + Button, + Grid, + FButton, + FCheckbox, +} from '@frontegg/react-core'; +import { MFAVerifyStepErrorMessage, MFAVerifyStepForm, MFAVerifyStepMessage } from '../MFA/MFAVerifyStep'; +import { useMfaActions, useMfaState, useLoginActions, useLoginState } from '@frontegg/react-hooks/auth'; +import { MFARecoveryCodeStep } from '../MFA/MFARecoveryCodeStep'; + +const { Formik } = FFormik; +const ONE_DAY_IN_SECONDS = 60 * 60 * 24; + +export interface ForceEnrollMfaProps { + renderer?: RendererFunctionFC; +} + +export const ForceEnrollMfa: FC = (props) => { + const { t } = useT(); + const { renderer } = props; + const { requestAuthorize } = useLoginActions(); + const { step, loading, recoveryCode, mfaToken } = useMfaState(); + const { allowRememberMfaDevice, mfaDeviceExpiration } = useLoginState(); + const { setMfaState, verifyMfaAfterForce } = useMfaActions(); + + const recoveryCodeRef = useRef(''); + + useEffect(() => { + const head = document.head || document.getElementsByTagName('head')[0]; + const style = document.createElement('style'); + style.type = 'text/css'; + style.appendChild( + document.createTextNode(`:root { + --fe-auth-container-width: 500px; + }`) + ); + head.appendChild(style); + return () => { + style.remove(); + }; + }, []); + + useEffect(() => { + recoveryCodeRef.current = recoveryCode ?? ''; + }); + + if (renderer) { + return renderer(omitProps(props, ['renderer'])); + } + + if (step === MFAStep.recoveryCode) { + return ( + ( +
+
+ +
+ )} + /> + ); + } + + return ( + { + verifyMfaAfterForce({ + mfaToken: mfaToken || '', + value: token, + rememberDevice, + callback: (success) => { + if (success) { + setMfaState({ recoveryCode }); + } + setSubmitting(false); + }, + }); + }} + > + + {t('auth.mfa.verify.forceMfaMessage')} + + + {allowRememberMfaDevice && mfaDeviceExpiration && ( + + )} + +
+ + + + {t('common.verify')} + + + +
+
+
+ ); +}; diff --git a/packages/auth/src/Login/Login.tsx b/packages/auth/src/Login/Login.tsx new file mode 100644 index 000000000..07b445a60 --- /dev/null +++ b/packages/auth/src/Login/Login.tsx @@ -0,0 +1,124 @@ +import React, { FC } from 'react'; +import { ComponentsTypesWithProps, Loader, useDynamicComponents, useT, Button } from '@frontegg/react-core'; +import { LoginStep, MFAStep } from '@frontegg/redux-store/auth'; +import { + useAuth, + useAuthRoutes, + useOnRedirectTo, + useLoginActions, + useLoginState, + useMfaState, +} from '@frontegg/react-hooks/auth'; +import { authPageWrapper } from '../components'; +import { LoginSuccessRedirect, LoginSuccessRedirectProps } from './LoginSuccessRedirect'; +import { LoginWithPassword, LoginWithPasswordProps } from './LoginWithPassword'; +import { RecoverTwoFactor, RecoverTwoFactorProps } from './RecoverTwoFactor'; +import { LoginWithTwoFactor, LoginWithTwoFactorProps } from './LoginWithTwoFactor'; +import { RedirectToSSO, RedirectToSSOProps } from './RedirectToSSO'; +import { LoginWithSSOFailed, LoginWithSSOFailedProps } from './LoginWithSSOFailed'; +import { ForceEnrollMfa, ForceEnrollMfaProps } from './ForceEnrollMfa'; +import { SocialLoginsLoginWithWrapper } from '../SocialLogins'; + +type Components = { + LoginSuccessRedirect: LoginSuccessRedirectProps; + LoginWithPassword: LoginWithPasswordProps; + RecoverTwoFactor: RecoverTwoFactorProps; + LoginWithTwoFactor: LoginWithTwoFactorProps; + RedirectToSSO: RedirectToSSOProps; + LoginWithSSOFailed: LoginWithSSOFailedProps; + ForceEnrollMfa: ForceEnrollMfaProps; + SocialLogins: {}; +}; + +export interface LoginComponentProps { + components?: ComponentsTypesWithProps; +} + +export interface LoginProps { + displaySuccessMessage?: boolean; + components?: ComponentsTypesWithProps; +} + +const defaultComponents = { + LoginSuccessRedirect, + LoginWithPassword, + RecoverTwoFactor, + LoginWithTwoFactor, + RedirectToSSO, + LoginWithSSOFailed, + ForceEnrollMfa, + SocialLogins: SocialLoginsLoginWithWrapper, +}; + +export const Login: FC = (props) => { + const Dynamic = useDynamicComponents(defaultComponents, props); + + const onRedirectTo = useOnRedirectTo(); + const routes = useAuthRoutes(); + const { resetLoginState } = useLoginActions(); + const { isLoading, isAuthenticated } = useAuth(({ isLoading, isAuthenticated }) => ({ isLoading, isAuthenticated })); + const { step } = useLoginState(({ step }) => ({ step })); + const { step: mfaStep } = useMfaState(({ step }) => ({ step })); + + const { t } = useT(); + + let components = null; + if (isLoading || isAuthenticated) { + components = ; + } else if (step === LoginStep.preLogin || step === LoginStep.loginWithPassword) { + components = ( + <> + + + + ); + } else if (step === LoginStep.recoverTwoFactor) { + components = ; + } else if (step === LoginStep.loginWithTwoFactor) { + components = ; + } else if (step === LoginStep.redirectToSSO) { + components = ; + } else if (step === LoginStep.loginWithSSOFailed) { + components = ; + } else if (step === LoginStep.forceTwoFactor) { + components = ; + } else if (step === LoginStep.success && props.displaySuccessMessage) { + components = ; + } + + const showBackButton = + [LoginStep.loginWithSSOFailed, LoginStep.forceTwoFactor, LoginStep.recoverTwoFactor].includes(step) && + mfaStep !== MFAStep.recoveryCode; + + return ( +
+ {components} + {showBackButton && ( + + )} +
+ ); +}; + +const LoginPageComponent = authPageWrapper(Login); +export const LoginPage: FC = (props) => { + const { isLoading, isAuthenticated } = useAuth(({ isLoading, isAuthenticated }) => ({ isLoading, isAuthenticated })); + if (isLoading || isAuthenticated) { + return ( +
+ +
+ ); + } + return ; +}; diff --git a/packages/auth/src/Login/LoginSuccessRedirect.tsx b/packages/auth/src/Login/LoginSuccessRedirect.tsx new file mode 100644 index 000000000..8b04c145e --- /dev/null +++ b/packages/auth/src/Login/LoginSuccessRedirect.tsx @@ -0,0 +1,23 @@ +import React, { FC } from 'react'; +import { RendererFunctionFC, Loader, omitProps, useT } from '@frontegg/react-core'; + +export interface LoginSuccessRedirectProps { + renderer?: RendererFunctionFC; +} + +export const LoginSuccessRedirect: FC = (props) => { + const { renderer } = props; + const { t } = useT(); + + if (renderer) { + return renderer(omitProps(props, ['renderer'])); + } + return ( + <> +
{t('auth.login.authentication-succeeded')}
+
+ +
+ + ); +}; diff --git a/packages/auth/src/Login/LoginWithPassword.tsx b/packages/auth/src/Login/LoginWithPassword.tsx new file mode 100644 index 000000000..c192bca0d --- /dev/null +++ b/packages/auth/src/Login/LoginWithPassword.tsx @@ -0,0 +1,147 @@ +import React, { ComponentType, createElement, FC, RefObject, useCallback, useEffect } from 'react'; +import { LoginStep } from '@frontegg/redux-store/auth'; +import { + validateEmail, + validateSchema, + validatePassword, + ErrorMessage, + useT, + FForm, + FButton, + FInput, + FFormik, +} from '@frontegg/react-core'; +import { + useAuth, + useAuthRoutes, + useOnRedirectTo, + useLoginActions, + useLoginState, + useSignUpState, + useForgotPasswordActions, +} from '@frontegg/react-hooks/auth'; +import { FReCaptcha } from '../components/FReCaptcha'; +import { ReCaptcha } from 'react-recaptcha-v3'; + +const { Formik } = FFormik; + +export type LoginWithPasswordRendererProps = Omit; + +export interface LoginWithPasswordProps { + renderer?: ComponentType; +} + +const HideChildrenIfRequired: FC<{ hide: boolean }> = ({ hide, children }) => { + if (hide) { + return {children}; + } + return <>{children}; +}; + +export const LoginWithPassword: FC = (props) => { + const { renderer } = props; + const { t } = useT(); + + const onRedirectTo = useOnRedirectTo(); + const routes = useAuthRoutes(); + const { isSSOAuth } = useAuth(({ isSSOAuth }) => ({ isSSOAuth })); + const { allowSignUps } = useSignUpState(({ allowSignUps }) => ({ allowSignUps })); + const { setForgotPasswordState } = useForgotPasswordActions(); + const { loading, step, error } = useLoginState(); + const { setLoginState, login, preLogin, resetLoginState } = useLoginActions(); + + const backToPreLogin = () => setLoginState({ step: LoginStep.preLogin }); + + if (renderer) { + return createElement(renderer, props); + } + const shouldDisplayPassword = !isSSOAuth || step === LoginStep.loginWithPassword; + const shouldBackToLoginIfEmailChanged = isSSOAuth && shouldDisplayPassword; + const validationSchema: any = { email: validateEmail(t) }; + if (shouldDisplayPassword) { + validationSchema.password = validatePassword(t); + } + useEffect(() => { + if (isSSOAuth && shouldDisplayPassword) { + document.querySelector('input[name="password"]')?.focus?.(); + } + }, [shouldDisplayPassword]); + + const labelButtonProps = (values: any) => ({ + 'data-test-id': 'forgotPassBtn', + disabled: loading, + onClick: () => { + setForgotPasswordState({ email: values.email }); + resetLoginState(); + onRedirectTo(routes.forgetPasswordUrl); + }, + children: t('auth.login.forgot-password'), + }); + + const redirectToSignUp = useCallback(() => { + onRedirectTo(routes.signUpUrl, { preserveQueryParams: true }); + }, []); + + const signUpMessage = !allowSignUps ? null : ( +
+ {t('auth.login.suggest-sign-up.message')} + + {t('auth.login.suggest-sign-up.sign-up-link')} + +
+ ); + + const recaptchaRef: RefObject = React.createRef(); + useEffect(() => { + if (recaptchaRef.current && !loading) { + error && recaptchaRef.current.execute(); + } + }, [loading, error, recaptchaRef]); + + return ( + <> + {signUpMessage} + { + shouldDisplayPassword ? login({ email, password, recaptchaToken }) : preLogin({ email }); + }} + > + {({ values }) => ( + + + + + + + + + {shouldDisplayPassword ? t('auth.login.login') : t('auth.login.continue')} + + + + + + )} + + + ); +}; diff --git a/packages/auth/src/Login/LoginWithSSO.tsx b/packages/auth/src/Login/LoginWithSSO.tsx new file mode 100644 index 000000000..0653bf4c5 --- /dev/null +++ b/packages/auth/src/Login/LoginWithSSO.tsx @@ -0,0 +1,39 @@ +import React, { FC, useEffect } from 'react'; +import { authPageWrapper } from '../components'; +import { Loader, RendererFunctionFC, omitProps } from '@frontegg/react-core'; +import { LoginWithSSOFailed } from './LoginWithSSOFailed'; +import { useLoginActions } from '@frontegg/react-hooks/auth'; + +export interface LoginWithSSOProps { + renderer?: RendererFunctionFC; +} + +export const LoginWithSSO: FC = (props) => { + const { renderer } = props; + const { postLogin } = useLoginActions(); + + const url = new URL(window?.location.href); + const RelayState = url.searchParams.get('RelayState') || ''; + const SAMLResponse = url.searchParams.get('SAMLResponse') || ''; + + useEffect(() => { + if (RelayState && SAMLResponse) { + postLogin({ RelayState, SAMLResponse }); + } + }, [RelayState, SAMLResponse]); + + if (renderer) { + return renderer(omitProps(props, ['renderer'])); + } + + if (!RelayState || !SAMLResponse) { + return ; + } + return ( +
+ +
+ ); +}; + +export const LoginWithSSOPage = authPageWrapper(LoginWithSSO); diff --git a/packages/auth/src/Login/LoginWithSSOFailed.tsx b/packages/auth/src/Login/LoginWithSSOFailed.tsx new file mode 100644 index 000000000..7405616fa --- /dev/null +++ b/packages/auth/src/Login/LoginWithSSOFailed.tsx @@ -0,0 +1,21 @@ +import React, { FC } from 'react'; +import { omitProps, useT, RendererFunctionFC } from '@frontegg/react-core'; + +export interface LoginWithSSOFailedProps { + renderer?: RendererFunctionFC; +} + +export const LoginWithSSOFailed: FC = (props) => { + const { renderer } = props; + const { t } = useT(); + + if (renderer) { + return renderer(omitProps(props, ['renderer'])); + } + + return ( + <> +
{t('auth.login.login-with-sso-failed')}
+ + ); +}; diff --git a/packages/auth/src/Login/LoginWithTwoFactor.tsx b/packages/auth/src/Login/LoginWithTwoFactor.tsx new file mode 100644 index 000000000..e55527ce9 --- /dev/null +++ b/packages/auth/src/Login/LoginWithTwoFactor.tsx @@ -0,0 +1,85 @@ +import { + ErrorMessage, + FButton, + FCheckbox, + FForm, + FFormik, + FInput, + omitProps, + RendererFunctionFC, + useT, + validateSchema, + validateTwoFactorCode, +} from '@frontegg/react-core'; +import { useLoginActions, useLoginState } from '@frontegg/react-hooks/auth'; +import { LoginStep } from '@frontegg/redux-store/auth'; +import React, { FC } from 'react'; + +const { Formik } = FFormik; +const ONE_DAY_IN_SECONDS = 60 * 60 * 24; +export interface LoginWithTwoFactorProps { + renderer?: RendererFunctionFC; +} + +export const LoginWithTwoFactor: FC = (props) => { + const { renderer } = props; + const { t } = useT(); + const { loading, error, mfaToken, allowRememberMfaDevice, mfaDeviceExpiration } = useLoginState(); + const { loginWithMfa, setLoginState } = useLoginActions(); + + if (renderer) { + return renderer(omitProps(props, ['renderer'])); + } + + return ( + + loginWithMfa({ mfaToken: mfaToken || '', value: code, rememberDevice, callback: () => setSubmitting(false) }) + } + > + + + {allowRememberMfaDevice && mfaDeviceExpiration && ( + + )} + + + {t('auth.login.login')} + + +
+
{t('auth.login.disable-two-factor-title')}
+ +
+ + +
+
+ ); +}; diff --git a/packages/auth/src/Login/Logout.tsx b/packages/auth/src/Login/Logout.tsx new file mode 100644 index 000000000..c44b0a749 --- /dev/null +++ b/packages/auth/src/Login/Logout.tsx @@ -0,0 +1,30 @@ +import React, { FC, useEffect } from 'react'; +import { Loader, omitProps, RendererFunctionFC } from '@frontegg/react-core'; +import { useAuthRoutes, useLoginActions } from '@frontegg/react-hooks/auth'; + +export interface LogoutProps { + renderer?: RendererFunctionFC; +} + +export const Logout: FC = (props) => { + const { renderer } = props; + const { loginUrl } = useAuthRoutes(); + const { logout } = useLoginActions(); + + useEffect(() => { + setTimeout(() => { + logout(() => (window.location.href = loginUrl)); + }, 500); + }, []); + + if (renderer) { + return renderer(omitProps(props, ['renderer'])); + } + + return ( +
+ +
+ ); +}; +export const LogoutPage = Logout; diff --git a/packages/auth/src/Login/README.md b/packages/auth/src/Login/README.md new file mode 100644 index 000000000..1b5d73c87 --- /dev/null +++ b/packages/auth/src/Login/README.md @@ -0,0 +1,20 @@ +# Login Component + +Login components provide end-to-end integration with Authentication Service. + +Each Auth Component export two types of component. +1. Full page (includes styles, headers, etc.) +1. Standalone Component (Display components as used in the UI) + +## Usage +```jsx +import {LoginPage} from '@frontegg/react-auth' + +export class MyRouter extends React.Component { + render() { + return + + + } +}; +``` diff --git a/packages/auth/src/Login/RecoverTwoFactor.tsx b/packages/auth/src/Login/RecoverTwoFactor.tsx new file mode 100644 index 000000000..7f842d467 --- /dev/null +++ b/packages/auth/src/Login/RecoverTwoFactor.tsx @@ -0,0 +1,50 @@ +import React, { FC } from 'react'; +import { + omitProps, + validateSchema, + validateTwoFactorRecoveryCode, + ErrorMessage, + RendererFunctionFC, + useT, + FInput, + FButton, + FForm, + FFormik, +} from '@frontegg/react-core'; +import { useLoginActions, useLoginState } from '@frontegg/react-hooks/auth'; + +const { Formik } = FFormik; + +export interface RecoverTwoFactorProps { + renderer?: RendererFunctionFC; +} + +export const RecoverTwoFactor: FC = (props) => { + const { renderer } = props; + const { t } = useT(); + const { recoverMfa } = useLoginActions(); + const { loading, error, email } = useLoginState(); + + if (renderer) { + return renderer(omitProps(props, ['renderer'])); + } + + return ( + recoverMfa({ email: email ?? '', recoveryCode: code })} + > + + + + + {t('auth.login.disable-mfa')} + + + + + ); +}; diff --git a/packages/auth/src/Login/RedirectToSSO.tsx b/packages/auth/src/Login/RedirectToSSO.tsx new file mode 100644 index 000000000..941f62525 --- /dev/null +++ b/packages/auth/src/Login/RedirectToSSO.tsx @@ -0,0 +1,22 @@ +import React, { FC, ReactElement } from 'react'; +import { Loader, omitProps, RendererFunction, useT } from '@frontegg/react-core'; + +export interface RedirectToSSOProps { + renderer?: RendererFunction; +} + +export const RedirectToSSO: FC = (props: RedirectToSSOProps) => { + const { t } = useT(); + + if (props.renderer) { + return props.renderer(omitProps(props, ['renderer'])); + } + return ( + <> +
{t('auth.login.redirect-to-sso-message')}
+
+ +
+ + ); +}; diff --git a/packages/auth/src/Login/index.ts b/packages/auth/src/Login/index.ts new file mode 100644 index 000000000..44d12b9a9 --- /dev/null +++ b/packages/auth/src/Login/index.ts @@ -0,0 +1,8 @@ +export * from './Login'; +export * from './Logout'; +export * from './LoginSuccessRedirect'; +export * from './LoginWithPassword'; +export * from './LoginWithTwoFactor'; +export * from './RecoverTwoFactor'; +export * from './RedirectToSSO'; +export * from './LoginWithSSO'; diff --git a/packages/auth/src/MFA/MFAButton.tsx b/packages/auth/src/MFA/MFAButton.tsx new file mode 100644 index 000000000..9a636a1b0 --- /dev/null +++ b/packages/auth/src/MFA/MFAButton.tsx @@ -0,0 +1,85 @@ +import React, { FC, MouseEventHandler, ReactElement, useContext, useState } from 'react'; +import { Button, checkValidChildren, useT, ProxyComponent, useProxyComponent } from '@frontegg/react-core'; +import { MFAEnrollDialog } from './MFAEnrollDialog'; +import { MFADisableDialog } from './MFADisableDialog'; +import { useAuthUserOrNull } from '@frontegg/react-hooks/auth'; + +const MFAButtonContext = React.createContext({ + openEnrollDialog: () => {}, + openDisableDialog: () => {}, +}); + +type MFAButtonProps = { + children?: ReactElement<{ onClick: MouseEventHandler }>; + onClick?: MouseEventHandler; +}; + +const EnrollButton = (props: MFAButtonProps) => { + const user = useAuthUserOrNull(); + const { t } = useT(); + const { openEnrollDialog } = useContext(MFAButtonContext); + if (user?.mfaEnrolled) { + return null; + } + const children = props.children ?? ( + + ); + return React.cloneElement(children as any, { onClick: openEnrollDialog }); +}; +const DisableButton = (props: MFAButtonProps) => { + const user = useAuthUserOrNull(); + const { t } = useT(); + const { openDisableDialog } = useContext(MFAButtonContext); + + if (!user?.mfaEnrolled) { + return null; + } + const children = props.children ?? ( + + ); + return React.cloneElement(children as any, { onClick: openDisableDialog }); +}; + +type SubComponents = { + EnrollButton: typeof EnrollButton; + DisableButton: typeof EnrollButton; +}; + +export const MFAButton: FC & SubComponents = (props) => { + const [enrollOpen, setEnrollOpen] = useState(false); + const [disableOpen, setDisableOpen] = useState(false); + + const openEnrollDialog = () => setEnrollOpen(true); + const openDisableDialog = () => setDisableOpen(true); + + checkValidChildren('MFAButton', 'MFAButton', props.children, { EnrollButton, DisableButton }); + const proxyPortals = useProxyComponent(props); + const children = props.children ?? ( + <> + + + + ); + + return ( + + {children} + setEnrollOpen(false)} /> + setDisableOpen(false)} /> + + {proxyPortals} + + ); +}; + +MFAButton.EnrollButton = EnrollButton; +MFAButton.DisableButton = DisableButton; diff --git a/packages/auth/src/MFA/MFADisableDialog/MFADisableDialog.tsx b/packages/auth/src/MFA/MFADisableDialog/MFADisableDialog.tsx new file mode 100644 index 000000000..7f9166f0f --- /dev/null +++ b/packages/auth/src/MFA/MFADisableDialog/MFADisableDialog.tsx @@ -0,0 +1,56 @@ +import React, { FC, useEffect } from 'react'; +import { + Dialog, + DialogContext, + DialogProps, + FFormik, + FForm, + omitProps, + useT, + validateSchema, + validateTwoFactorCode, +} from '@frontegg/react-core'; +import { MFADisableDialogMessage } from './MFADisableDialogMessage'; +import { MFADisableDialogFooter } from './MFADisableDialogFooter'; +import { MFADisableDialogForm } from './MFADisableDialogForm'; +import { MFADisableDialogErrorMessage } from './MFADisableDialogErrorMessage'; +import { useMfaActions } from '@frontegg/react-hooks/auth'; + +const { Formik } = FFormik; + +export type MFADialogProps = DialogProps; +export const MFADisableDialog: FC = (props) => { + const { t } = useT(); + const { resetMfaState, disableMfa } = useMfaActions(); + + const dialogProps = omitProps(props, ['children']); + useEffect(() => { + props.open && resetMfaState(); + }, [props.open]); + + const children = props.children ?? ( + <> + + + + + + ); + return ( + + + { + disableMfa({ token, callback: props.onClose }); + }} + > + {children} + + + + ); +}; diff --git a/packages/auth/src/MFA/MFADisableDialog/MFADisableDialogErrorMessage.tsx b/packages/auth/src/MFA/MFADisableDialog/MFADisableDialogErrorMessage.tsx new file mode 100644 index 000000000..f150efd23 --- /dev/null +++ b/packages/auth/src/MFA/MFADisableDialog/MFADisableDialogErrorMessage.tsx @@ -0,0 +1,14 @@ +import React, { FC } from 'react'; +import { ErrorMessage } from '@frontegg/react-core'; +import { useMfaState } from '@frontegg/react-hooks/auth'; + +export const MFADisableDialogErrorMessage: FC = (props) => { + const { error } = useMfaState(({ error }) => ({ error })); + const children = props.children ?? ( + <> + + + ); + + return <>{children}; +}; diff --git a/packages/auth/src/MFA/MFADisableDialog/MFADisableDialogFooter.tsx b/packages/auth/src/MFA/MFADisableDialog/MFADisableDialogFooter.tsx new file mode 100644 index 000000000..c8f4c6fe0 --- /dev/null +++ b/packages/auth/src/MFA/MFADisableDialog/MFADisableDialogFooter.tsx @@ -0,0 +1,25 @@ +import React, { FC } from 'react'; +import { Button, FButton, Grid, useDialog, useT } from '@frontegg/react-core'; +import { useMfaState } from '@frontegg/react-hooks/auth'; + +export const MFADisableDialogFooter: FC = () => { + const { t } = useT(); + const { loading } = useMfaState(({ loading }) => ({ loading })); + const { onClose } = useDialog(); + return ( +
+ + + + + + + {t('common.disable')} + + + +
+ ); +}; diff --git a/packages/auth/src/MFA/MFADisableDialog/MFADisableDialogForm.tsx b/packages/auth/src/MFA/MFADisableDialog/MFADisableDialogForm.tsx new file mode 100644 index 000000000..b02395110 --- /dev/null +++ b/packages/auth/src/MFA/MFADisableDialog/MFADisableDialogForm.tsx @@ -0,0 +1,21 @@ +import React, { FC } from 'react'; +import { FInput, useT } from '@frontegg/react-core'; +import { useMfaState } from '@frontegg/react-hooks/auth'; + +export const MFADisableDialogForm: FC = (props) => { + const { t } = useT(); + const { loading } = useMfaState(({ loading }) => ({ loading })); + + const children = props.children ?? ( + <> + + + ); + + return <>{children}; +}; diff --git a/packages/auth/src/MFA/MFADisableDialog/MFADisableDialogMessage.tsx b/packages/auth/src/MFA/MFADisableDialog/MFADisableDialogMessage.tsx new file mode 100644 index 000000000..f0ee425a7 --- /dev/null +++ b/packages/auth/src/MFA/MFADisableDialog/MFADisableDialogMessage.tsx @@ -0,0 +1,9 @@ +import React, { FC } from 'react'; +import { useT } from '@frontegg/react-core'; + +export const MFADisableDialogMessage: FC = (props) => { + const { t } = useT(); + const children = props.children ?? <>{t('auth.mfa.disable.message')}; + + return
{children}
; +}; diff --git a/packages/auth/src/MFA/MFADisableDialog/index.ts b/packages/auth/src/MFA/MFADisableDialog/index.ts new file mode 100644 index 000000000..f9486ce22 --- /dev/null +++ b/packages/auth/src/MFA/MFADisableDialog/index.ts @@ -0,0 +1,4 @@ +export * from './MFADisableDialog'; +export * from './MFADisableDialogMessage'; +export * from './MFADisableDialogForm'; +export * from './MFADisableDialogFooter'; diff --git a/packages/auth/src/MFA/MFAEnrollDialog.tsx b/packages/auth/src/MFA/MFAEnrollDialog.tsx new file mode 100644 index 000000000..277eeda61 --- /dev/null +++ b/packages/auth/src/MFA/MFAEnrollDialog.tsx @@ -0,0 +1,38 @@ +import React, { FC, useEffect } from 'react'; +import { Dialog, DialogContext, DialogProps, omitProps, useT } from '@frontegg/react-core'; +import { MFAStep } from '@frontegg/redux-store/auth'; +import { MFAVerifyStep } from './MFAVerifyStep'; +import { MFARecoveryCodeStep } from './MFARecoveryCodeStep'; +import { useMfaActions } from '@frontegg/react-hooks/auth'; + +export type MFADialogProps = DialogProps; +export const MFAEnrollDialog: FC = (props) => { + const { t } = useT(); + const { setMfaState } = useMfaActions(); + + const dialogProps = omitProps(props, ['children']); + useEffect(() => { + props.open && setMfaState({ step: MFAStep.verify, loading: true, qrCode: null }); + }, [props.open]); + + const children = props.children ?? ( + <> + + + + ); + return ( + + + {children} + + + ); +}; diff --git a/packages/auth/src/MFA/MFALayout.tsx b/packages/auth/src/MFA/MFALayout.tsx new file mode 100644 index 000000000..9d4b58e9f --- /dev/null +++ b/packages/auth/src/MFA/MFALayout.tsx @@ -0,0 +1,39 @@ +import React, { FC } from 'react'; +import { useT, useProxyComponent, ProxyComponent } from '@frontegg/react-core'; +import { useAuthUser } from '@frontegg/react-hooks/auth'; +import { MFAButton } from './MFAButton'; + +const MFAStatus = () => { + const user = useAuthUser(); + const { t } = useT(); + return ( +
+ {user.mfaEnrolled ? ( + <> + {t('auth.mfa.two-factor')}: {t('common.enabled')} + + ) : ( + t('auth.mfa.enable-message') + )} +
+ ); +}; + +export interface MFALayoutProps extends ProxyComponent {} + +export const MFALayout: FC = (props) => { + const proxyPortals = useProxyComponent(props); + const children = props.children ?? ( + <> + + + + ); + return ( +
+
{children}
+ + {proxyPortals} +
+ ); +}; diff --git a/packages/auth/src/MFA/MFARecoveryCodeStep/MFARecoveryCodeStep.tsx b/packages/auth/src/MFA/MFARecoveryCodeStep/MFARecoveryCodeStep.tsx new file mode 100644 index 000000000..5ab5dc7cb --- /dev/null +++ b/packages/auth/src/MFA/MFARecoveryCodeStep/MFARecoveryCodeStep.tsx @@ -0,0 +1,28 @@ +import React, { ComponentType, FC } from 'react'; +import { MFAStep } from '@frontegg/redux-store/auth'; +import { MFARecoveryCodeStepMessage } from './MFARecoveryCodeStepMessage'; +import { MFARecoveryCodeStepForm } from './MFARecoveryCodeStepForm'; +import { MFARecoveryCodeStepFooter } from './MFARecoveryCodeStepFooter'; + +import { useMfaState } from '@frontegg/react-hooks/auth'; + +type MFARecoveryCodeStepProps = { + MFARecoveryCodeStepFooter?: ComponentType; +}; +export const MFARecoveryCodeStep: FC = (props) => { + const { step } = useMfaState(({ step }) => ({ step })); + + const Footer = props.MFARecoveryCodeStepFooter ?? MFARecoveryCodeStepFooter; + if (step !== MFAStep.recoveryCode) { + return null; + } + const children = props.children ?? ( + <> + + +
+ + ); + + return
{children}
; +}; diff --git a/packages/auth/src/MFA/MFARecoveryCodeStep/MFARecoveryCodeStepFooter.tsx b/packages/auth/src/MFA/MFARecoveryCodeStep/MFARecoveryCodeStepFooter.tsx new file mode 100644 index 000000000..ba6aa1cb0 --- /dev/null +++ b/packages/auth/src/MFA/MFARecoveryCodeStep/MFARecoveryCodeStepFooter.tsx @@ -0,0 +1,15 @@ +import React, { FC } from 'react'; +import { Button, useDialog, useT } from '@frontegg/react-core'; + +export const MFARecoveryCodeStepFooter: FC = () => { + const { t } = useT(); + const { onClose } = useDialog(); + return ( +
+
+ +
+ ); +}; diff --git a/packages/auth/src/MFA/MFARecoveryCodeStep/MFARecoveryCodeStepForm.tsx b/packages/auth/src/MFA/MFARecoveryCodeStep/MFARecoveryCodeStepForm.tsx new file mode 100644 index 000000000..45051605b --- /dev/null +++ b/packages/auth/src/MFA/MFARecoveryCodeStep/MFARecoveryCodeStepForm.tsx @@ -0,0 +1,40 @@ +import React, { FC, useState } from 'react'; +import { useT, Icon } from '@frontegg/react-core'; +import classNames from 'classnames'; +import copy from 'clipboard-copy'; +import { useMfaState } from '@frontegg/react-hooks/auth'; + +export const MFARecoveryCodeStepForm: FC = (props) => { + const { t } = useT(); + const { recoveryCode } = useMfaState(({ recoveryCode }) => ({ recoveryCode })); + const [copiedMgsVisible, setCopiedMgsVisible] = useState(false); + + const copyRecoverCode = () => { + copy(recoveryCode ?? '').then(displayCopiedMessage); + }; + + const displayCopiedMessage = () => { + setCopiedMgsVisible(true); + setTimeout(() => { + setCopiedMgsVisible(false); + }, 1000); + }; + + const children = props.children ?? ( + <> +
Your recovery code
+ +
+ {copiedMgsVisible ? t('common.copied') : recoveryCode} + +
+ +
+ + {t('auth.mfa.recovery-code.copy-and-save-code')} +
+ + ); + + return <>{children}; +}; diff --git a/packages/auth/src/MFA/MFARecoveryCodeStep/MFARecoveryCodeStepMessage.tsx b/packages/auth/src/MFA/MFARecoveryCodeStep/MFARecoveryCodeStepMessage.tsx new file mode 100644 index 000000000..bf44fc4fb --- /dev/null +++ b/packages/auth/src/MFA/MFARecoveryCodeStep/MFARecoveryCodeStepMessage.tsx @@ -0,0 +1,9 @@ +import React, { FC } from 'react'; +import { useT } from '@frontegg/react-core'; + +export const MFARecoveryCodeStepMessage: FC = (props) => { + const { t } = useT(); + const children = props.children ?? <>{t('auth.mfa.recovery-code.message')}; + + return
{children}
; +}; diff --git a/packages/auth/src/MFA/MFARecoveryCodeStep/index.ts b/packages/auth/src/MFA/MFARecoveryCodeStep/index.ts new file mode 100644 index 000000000..7cc97656f --- /dev/null +++ b/packages/auth/src/MFA/MFARecoveryCodeStep/index.ts @@ -0,0 +1,4 @@ +export * from './MFARecoveryCodeStep'; +export * from './MFARecoveryCodeStepMessage'; +export * from './MFARecoveryCodeStepForm'; +export * from './MFARecoveryCodeStepFooter'; diff --git a/packages/auth/src/MFA/MFAVerifyStep/MFAVerifyStep.tsx b/packages/auth/src/MFA/MFAVerifyStep/MFAVerifyStep.tsx new file mode 100644 index 000000000..6da30b7e4 --- /dev/null +++ b/packages/auth/src/MFA/MFAVerifyStep/MFAVerifyStep.tsx @@ -0,0 +1,44 @@ +import React, { FC } from 'react'; +import { FFormik, FForm, useT, validateSchema, validateTwoFactorCode } from '@frontegg/react-core'; +import { MFAStep } from '@frontegg/redux-store/auth'; +import { HideOption } from '../../interfaces'; +import { MFAVerifyStepMessage } from './MFAVerifyStepMessage'; +import { MFAVerifyStepForm } from './MFAVerifyStepForm'; +import { MFAVerifyStepErrorMessage } from './MFAVerifyStepErrorMessage'; +import { MFAVerifyStepFooter } from './MFAVerifyStepFooter'; + +import { useMfaActions, useMfaState } from '@frontegg/react-hooks/auth'; + +const { Formik } = FFormik; + +export const MFAVerifyStep: FC = (props) => { + const { t } = useT(); + const { step } = useMfaState(({ step }) => ({ step })); + const { verifyMfa } = useMfaActions(); + + if (step !== MFAStep.verify) { + return null; + } + const children = props.children ?? ( + <> + + + + + + ); + + return ( +
+ verifyMfa({ token })} + > + {children} + +
+ ); +}; diff --git a/packages/auth/src/MFA/MFAVerifyStep/MFAVerifyStepErrorMessage.tsx b/packages/auth/src/MFA/MFAVerifyStep/MFAVerifyStepErrorMessage.tsx new file mode 100644 index 000000000..d5ecdf026 --- /dev/null +++ b/packages/auth/src/MFA/MFAVerifyStep/MFAVerifyStepErrorMessage.tsx @@ -0,0 +1,14 @@ +import React, { FC } from 'react'; +import { ErrorMessage } from '@frontegg/react-core'; +import { useMfaState } from '@frontegg/react-hooks/auth'; + +export const MFAVerifyStepErrorMessage: FC = (props) => { + const { error } = useMfaState(({ error }) => ({ error })); + const children = props.children ?? ( + <> + + + ); + + return <>{children}; +}; diff --git a/packages/auth/src/MFA/MFAVerifyStep/MFAVerifyStepFooter.tsx b/packages/auth/src/MFA/MFAVerifyStep/MFAVerifyStepFooter.tsx new file mode 100644 index 000000000..043bf1a99 --- /dev/null +++ b/packages/auth/src/MFA/MFAVerifyStep/MFAVerifyStepFooter.tsx @@ -0,0 +1,25 @@ +import React, { FC } from 'react'; +import { Button, FButton, Grid, useDialog, useT } from '@frontegg/react-core'; +import { useMfaState } from '@frontegg/react-hooks/auth'; + +export const MFAVerifyStepFooter: FC = () => { + const { loading } = useMfaState(({ loading }) => ({ loading })); + const { onClose } = useDialog(); + const { t } = useT(); + return ( +
+ + + + + + + {t('common.verify')} + + + +
+ ); +}; diff --git a/packages/auth/src/MFA/MFAVerifyStep/MFAVerifyStepForm.tsx b/packages/auth/src/MFA/MFAVerifyStep/MFAVerifyStepForm.tsx new file mode 100644 index 000000000..ff46a6e63 --- /dev/null +++ b/packages/auth/src/MFA/MFAVerifyStep/MFAVerifyStepForm.tsx @@ -0,0 +1,41 @@ +import React, { FC, useEffect } from 'react'; +import { FInput, Loader, useT } from '@frontegg/react-core'; +import { useMfaActions, useMfaState } from '@frontegg/react-hooks/auth'; + +export const MFAVerifyStepForm: FC = (props) => { + const { t } = useT(); + const { loading, qrCode } = useMfaState(({ loading, qrCode }) => ({ loading, qrCode })); + const { enrollMfa } = useMfaActions(); + + useEffect(() => { + if (!qrCode) { + enrollMfa(); + } + }, [qrCode]); + const children = props.children ?? ( + <> +
    +
  1. + {t('auth.mfa.verify.scan-qr-description-1')} +  Google Authenticator  + {t('auth.mfa.verify.scan-qr-description-2')} +
    + {loading && !qrCode ? : Multi-factor QR} +
    +
  2. +
  3. + {t('auth.mfa.verify.enter-generated-code')} + +
  4. +
+ + ); + + return <>{children}; +}; diff --git a/packages/auth/src/MFA/MFAVerifyStep/MFAVerifyStepMessage.tsx b/packages/auth/src/MFA/MFAVerifyStep/MFAVerifyStepMessage.tsx new file mode 100644 index 000000000..e94fde67d --- /dev/null +++ b/packages/auth/src/MFA/MFAVerifyStep/MFAVerifyStepMessage.tsx @@ -0,0 +1,9 @@ +import React, { FC } from 'react'; +import { useT } from '@frontegg/react-core'; + +export const MFAVerifyStepMessage: FC = (props) => { + const { t } = useT(); + const children = props.children ?? <>{t('auth.mfa.verify.message')}; + + return
{children}
; +}; diff --git a/packages/auth/src/MFA/MFAVerifyStep/index.ts b/packages/auth/src/MFA/MFAVerifyStep/index.ts new file mode 100644 index 000000000..9b43a9148 --- /dev/null +++ b/packages/auth/src/MFA/MFAVerifyStep/index.ts @@ -0,0 +1,5 @@ +export * from './MFAVerifyStep'; +export * from './MFAVerifyStepMessage'; +export * from './MFAVerifyStepForm'; +export * from './MFAVerifyStepErrorMessage'; +export * from './MFAVerifyStepFooter'; diff --git a/packages/auth/src/MFA/index.ts b/packages/auth/src/MFA/index.ts new file mode 100644 index 000000000..397600dc5 --- /dev/null +++ b/packages/auth/src/MFA/index.ts @@ -0,0 +1,45 @@ +import { MFAEnrollDialog } from './MFAEnrollDialog'; +import { + MFAVerifyStep, + MFAVerifyStepMessage, + MFAVerifyStepForm, + MFAVerifyStepErrorMessage, + MFAVerifyStepFooter, +} from './MFAVerifyStep'; + +import { + MFADisableDialog, + MFADisableDialogMessage, + MFADisableDialogForm, + MFADisableDialogFooter, +} from './MFADisableDialog'; +import { + MFARecoveryCodeStep, + MFARecoveryCodeStepMessage, + MFARecoveryCodeStepForm, + MFARecoveryCodeStepFooter, +} from './MFARecoveryCodeStep'; +import { MFAButton } from './MFAButton'; +import { MFALayout } from './MFALayout'; + +export const MFA = { + Layout: MFALayout, + Button: MFAButton, + EnrollDialog: MFAEnrollDialog, + + EnrollDialogVerifyStep: MFAVerifyStep, + EnrollDialogVerifyStepMessage: MFAVerifyStepMessage, + EnrollDialogVerifyStepForm: MFAVerifyStepForm, + EnrollDialogVerifyStepErrorMessage: MFAVerifyStepErrorMessage, + EnrollDialogVerifyStepFooter: MFAVerifyStepFooter, + + EnrollDialogRecoveryCodeStep: MFARecoveryCodeStep, + EnrollDialogRecoveryCodeStepMessage: MFARecoveryCodeStepMessage, + EnrollDialogRecoveryCodeStepForm: MFARecoveryCodeStepForm, + EnrollDialogRecoveryCodeStepFooter: MFARecoveryCodeStepFooter, + + DisableDialog: MFADisableDialog, + DisableDialogMessage: MFADisableDialogMessage, + DisableDialogForm: MFADisableDialogForm, + DisableDialogFooter: MFADisableDialogFooter, +}; diff --git a/packages/auth/src/Profile/ProfileHeader.tsx b/packages/auth/src/Profile/ProfileHeader.tsx new file mode 100644 index 000000000..5c6efa654 --- /dev/null +++ b/packages/auth/src/Profile/ProfileHeader.tsx @@ -0,0 +1,12 @@ +import React, { FC } from 'react'; +import { checkRootPath, PageHeader, PageHeaderProps, useT } from '@frontegg/react-core'; +import { HideOption } from '../interfaces'; + +export const ProfileHeader: FC = (props) => { + checkRootPath('Profile.Header must be rendered inside a Profile.Page component'); + const { t } = useT(); + if (props.hide) { + return null; + } + return ; +}; diff --git a/packages/auth/src/Profile/ProfileInfoPage/ProfileBasicInformation.tsx b/packages/auth/src/Profile/ProfileInfoPage/ProfileBasicInformation.tsx new file mode 100644 index 000000000..be29af2da --- /dev/null +++ b/packages/auth/src/Profile/ProfileInfoPage/ProfileBasicInformation.tsx @@ -0,0 +1,33 @@ +import React, { FC } from 'react'; +import { FButton, FInput, Grid, useT, FFormik } from '@frontegg/react-core'; + +const { useFormikContext } = FFormik; + +export const ProfileBasicInformation: FC = () => { + const { t } = useT(); + const { isSubmitting } = useFormikContext(); + return ( +
+
{t('auth.profile.info.title2')}
+ + + + + + + + + + Update Profile + + + +
+ ); +}; diff --git a/packages/auth/src/Profile/ProfileInfoPage/ProfileImageUploader.tsx b/packages/auth/src/Profile/ProfileInfoPage/ProfileImageUploader.tsx new file mode 100644 index 000000000..9437dc5d3 --- /dev/null +++ b/packages/auth/src/Profile/ProfileInfoPage/ProfileImageUploader.tsx @@ -0,0 +1,64 @@ +import React, { ChangeEvent, FC, useCallback, useRef } from 'react'; +import { FFormik, Button, Icon, Loader, useT, FFileInput, OnError, ErrorMessage } from '@frontegg/react-core'; +import { useProfileState } from '@frontegg/react-hooks/auth'; + +const { useFormikContext, useField } = FFormik; + +const profilePictureUrl = 'profilePictureUrl'; + +export const ProfileImageUploader: FC = (props) => { + const inputRef = useRef(null); + const { loading, profile, error } = useProfileState(); + const { t } = useT(); + const { errors, submitForm, isSubmitting } = useFormikContext(); + + const [{ value: profilePhotoValue }] = useField(profilePictureUrl); + const profileImageError = error || errors[profilePictureUrl]; + + const handleUploadClick = useCallback(() => { + inputRef.current?.click?.(); + }, [inputRef]); + + const handlerOnChange = useCallback( + (e: ChangeEvent) => { + !!e.target.value && submitForm(); + }, + [submitForm] + ); + + const children = loading ? ( + + ) : ( + <> +
+ {profilePhotoValue || profile?.profilePictureUrl ? ( + Profile Image + ) : ( + + )} +
+
+
{profile?.name}
+
{profile?.email ?? ''}
+ + + + {profileImageError && } +
+ + ); + + return ( +
+
{children}
+
+ ); +}; diff --git a/packages/auth/src/Profile/ProfileInfoPage/ProfileInfoPage.tsx b/packages/auth/src/Profile/ProfileInfoPage/ProfileInfoPage.tsx new file mode 100644 index 000000000..3eb85254e --- /dev/null +++ b/packages/auth/src/Profile/ProfileInfoPage/ProfileInfoPage.tsx @@ -0,0 +1,94 @@ +import React, { FC, useCallback, useMemo } from 'react'; +import { + useT, + FForm, + FFormik, + PageTabProps, + validateEmail, + validateSchema, + validateLength, +} from '@frontegg/react-core'; +import { ProfileImageUploader } from './ProfileImageUploader'; +import { ProfileBasicInformation } from './ProfileBasicInformation'; +import { useProfileActions, useProfileState } from '@frontegg/react-hooks/auth'; + +const { Formik } = FFormik; +export const ProfileInfoPage: FC & PageTabProps = ({ children }) => { + const { profile } = useProfileState(); + const { saveProfile } = useProfileActions(); + const { t } = useT(); + + const isChildrenExist = !!children; + + const initialValues = useMemo( + () => + isChildrenExist + ? { + profilePictureUrl: profile?.profilePictureUrl ?? '', + name: profile?.name ?? '', + email: profile?.email ?? '', + } + : { + name: profile?.name ?? '', + email: profile?.email ?? '', + }, + [profile, isChildrenExist] + ); + + const profilePhotoInitialValues = useMemo( + () => + isChildrenExist + ? {} + : { + profilePictureUrl: profile?.profilePictureUrl ?? '', + }, + [profile, isChildrenExist] + ); + + const validationSchema = useMemo( + () => + validateSchema({ + name: validateLength(t('common.name'), 2, t), + email: validateEmail(t), + }), + [t] + ); + + const handlerSubmit = useCallback((values) => { + saveProfile(values); + }, []); + + return children ? ( + + +
{children}
+
+
+ ) : ( +
+ + + + + + + + + + +
+ ); +}; + +ProfileInfoPage.Title = () => useT().t('auth.profile.info.title'); +ProfileInfoPage.route = '/'; diff --git a/packages/auth/src/Profile/ProfileInfoPage/index.ts b/packages/auth/src/Profile/ProfileInfoPage/index.ts new file mode 100644 index 000000000..f992fa8fb --- /dev/null +++ b/packages/auth/src/Profile/ProfileInfoPage/index.ts @@ -0,0 +1 @@ +export * from './ProfileInfoPage'; diff --git a/packages/auth/src/Profile/ProfileMfaPage/ProfileMfaPage.tsx b/packages/auth/src/Profile/ProfileMfaPage/ProfileMfaPage.tsx new file mode 100644 index 000000000..6f53aa9c6 --- /dev/null +++ b/packages/auth/src/Profile/ProfileMfaPage/ProfileMfaPage.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { PageProps, useT } from '@frontegg/react-core'; +import { MFA } from '../../MFA'; + +export const ProfileMfaPage: PageProps = () => { + const { t } = useT(); + return ( +
+
{t('auth.mfa.two-factor')}
+ +
+ ); +}; + +ProfileMfaPage.Title = () => useT().t('auth.mfa.title'); +ProfileMfaPage.route = '/mfa'; diff --git a/packages/auth/src/Profile/ProfileMfaPage/index.ts b/packages/auth/src/Profile/ProfileMfaPage/index.ts new file mode 100644 index 000000000..723371fa2 --- /dev/null +++ b/packages/auth/src/Profile/ProfileMfaPage/index.ts @@ -0,0 +1 @@ +export * from './ProfileMfaPage'; diff --git a/packages/auth/src/Profile/ProfilePage.tsx b/packages/auth/src/Profile/ProfilePage.tsx new file mode 100644 index 000000000..f2c9beca4 --- /dev/null +++ b/packages/auth/src/Profile/ProfilePage.tsx @@ -0,0 +1,24 @@ +import React, { FC, useMemo } from 'react'; +import { checkValidChildren, RootPathContext, useRootPath } from '@frontegg/react-core'; +import { ProfileHeader } from './ProfileHeader'; +import { ProfileRouter } from './ProfileRouter'; +import { BasePageProps } from '../interfaces'; +import { reloadProfileIfNeeded } from './hooks'; + +export const ProfilePage: FC = (props) => { + const [rootPath] = useRootPath(props, '/profile'); + reloadProfileIfNeeded(); + useMemo(() => checkValidChildren('Profile.Page', 'Profile', props.children, { ProfileRouter }), [props.children]); + + const children = props.children ?? ( + <> + + + + ); + return ( + +
{children}
+
+ ); +}; diff --git a/packages/auth/src/Profile/ProfilePasswordSettingsPage/ProfilePasswordSettingsPage.tsx b/packages/auth/src/Profile/ProfilePasswordSettingsPage/ProfilePasswordSettingsPage.tsx new file mode 100644 index 000000000..b1dd603a2 --- /dev/null +++ b/packages/auth/src/Profile/ProfilePasswordSettingsPage/ProfilePasswordSettingsPage.tsx @@ -0,0 +1,109 @@ +import React, { FC, useEffect, useRef, useState } from 'react'; +import { + FFormik, + FForm, + FInput, + useT, + validateSchema, + validatePassword, + validatePasswordUsingOWASP, + validatePasswordConfirmation, + FButton, + ErrorMessage, + PageTabProps, + OnError, +} from '@frontegg/react-core'; +import { + useProfileActions, + useProfileState, + useForgotPasswordActions, + useForgotPasswordState, + useAuthUser, +} from '@frontegg/react-hooks/auth'; + +const { Formik } = FFormik; + +type ProfilePasswordSettingsPageProps = OnError; + +export const ProfilePasswordSettingsPage: FC & PageTabProps = (props) => { + const { t } = useT(); + const { onError } = props; + const { passwordConfig } = useForgotPasswordState(); + const { loadPasswordConfig } = useForgotPasswordActions(); + const { loading, error } = useProfileState(); + const { changePassword } = useProfileActions(); + const [successMessage, setSuccessMessage] = useState(null); + const changePasswordSubmitted = useRef(false); + const { id: userId } = useAuthUser(); + + useEffect(() => { + loadPasswordConfig({ userId }); + }, [loadPasswordConfig, userId]); + + useEffect(() => { + if (changePasswordSubmitted.current && !loading && !error) { + setSuccessMessage(t('auth.profile.password-settings.success-message')); + setTimeout(() => { + setSuccessMessage(null); + }, 3000); + } + }, [loading, error]); + + return ( +
+
{t('common.password')}
+ { + changePasswordSubmitted.current = true; + changePassword({ password, newPassword }); + resetForm(); + }} + > + +
+ +
+
+ + +
+ + + {successMessage || t('auth.profile.password-settings.button')} + +
+
+
+ ); +}; + +ProfilePasswordSettingsPage.Title = () => useT().t('auth.profile.password-settings.title'); +ProfilePasswordSettingsPage.route = '/password'; diff --git a/packages/auth/src/Profile/ProfilePasswordSettingsPage/index.ts b/packages/auth/src/Profile/ProfilePasswordSettingsPage/index.ts new file mode 100644 index 000000000..3c75347cc --- /dev/null +++ b/packages/auth/src/Profile/ProfilePasswordSettingsPage/index.ts @@ -0,0 +1 @@ +export * from './ProfilePasswordSettingsPage'; diff --git a/packages/auth/src/Profile/ProfileRouter.tsx b/packages/auth/src/Profile/ProfileRouter.tsx new file mode 100644 index 000000000..d55e37042 --- /dev/null +++ b/packages/auth/src/Profile/ProfileRouter.tsx @@ -0,0 +1,52 @@ +import React, { FC } from 'react'; +import { BasePageProps } from '../interfaces'; +import { reloadProfileIfNeeded } from './hooks'; +import { buildTabsFromChildren, RootPathContext, useRootPath, Logger, PageTabs } from '@frontegg/react-core'; +import { ProfileInfoPage } from './ProfileInfoPage'; +import { ProfilePasswordSettingsPage } from './ProfilePasswordSettingsPage'; +import { ProfileMfaPage } from './ProfileMfaPage'; +import { Redirect, Route, Switch } from 'react-router'; +import { useAuthUser } from '@frontegg/react-hooks/auth'; + +export const ProfileTabs = PageTabs; +const logger = Logger.from('ProfileRouter'); +export const ProfileRouter: FC = (props) => { + const { verified } = useAuthUser(); + const [rootPath, isRootPathContext] = useRootPath(props, '/profile'); + reloadProfileIfNeeded(); + + const children = props.children ?? ( + <> + , + , + , + + ); + + const [defaultTabs, invalidTabs] = buildTabsFromChildren(rootPath, children); + invalidTabs.length > 0 && + logger.error(`Children at positions [${invalidTabs.join(', ')}] should implement ProfilePage interface.`); + + if (!isRootPathContext) { + return {children}; + } + + const tabs = defaultTabs.map((tab) => { + if (!verified && tab.route.includes('password')) return { ...tab, disabled: true }; + return tab; + }); + + return ( + <> + + + {tabs.map((tab: any) => ( + + {tab.comp} + + ))} + + + + ); +}; diff --git a/packages/auth/src/Profile/README.md b/packages/auth/src/Profile/README.md new file mode 100644 index 000000000..9bb43b384 --- /dev/null +++ b/packages/auth/src/Profile/README.md @@ -0,0 +1,20 @@ +## Profile Component +This collection contains built-in components to provide the ability to display your user profile, +change password and MFA (multi-factor authentication) settings. + +## Usage + +To use this component you need to import it from `@frontegg/react-auth` + +```tsx +import { Profile } from '@frontegg/react-auth'; + +const AppRouter:FC = ()=> { + + return
+ {/* other routes... */} + + +
+} +``` diff --git a/packages/auth/src/Profile/hooks.ts b/packages/auth/src/Profile/hooks.ts new file mode 100644 index 000000000..b36a674fd --- /dev/null +++ b/packages/auth/src/Profile/hooks.ts @@ -0,0 +1,10 @@ +import { useEffect } from 'react'; +import { useProfileState, useProfileActions } from '@frontegg/react-hooks/auth'; + +export const reloadProfileIfNeeded = () => { + const { loading } = useProfileState(); + const { loadProfile } = useProfileActions(); + useEffect(() => { + !loading && loadProfile(); + }, []); +}; diff --git a/packages/auth/src/Profile/index.ts b/packages/auth/src/Profile/index.ts new file mode 100644 index 000000000..e02f26ee1 --- /dev/null +++ b/packages/auth/src/Profile/index.ts @@ -0,0 +1,16 @@ +import { ProfilePage } from './ProfilePage'; +import { ProfileHeader } from './ProfileHeader'; +import { ProfileRouter, ProfileTabs } from './ProfileRouter'; +import { ProfilePasswordSettingsPage } from './ProfilePasswordSettingsPage'; +import { ProfileMfaPage } from './ProfileMfaPage'; +import { ProfileInfoPage } from './ProfileInfoPage'; + +export const Profile = { + Page: ProfilePage, + Header: ProfileHeader, + Tabs: ProfileTabs, + Router: ProfileRouter, + InfoPage: ProfileInfoPage, + MfaPage: ProfileMfaPage, + PasswordSettingsPage: ProfilePasswordSettingsPage, +}; diff --git a/packages/auth/src/ResetPassword/ResetPassword.tsx b/packages/auth/src/ResetPassword/ResetPassword.tsx new file mode 100644 index 000000000..1dd8a7a78 --- /dev/null +++ b/packages/auth/src/ResetPassword/ResetPassword.tsx @@ -0,0 +1,47 @@ +import React, { FC, useEffect } from 'react'; +import { ComponentsTypesWithProps, useDynamicComponents } from '@frontegg/react-core'; +import { ForgotPasswordStep } from '@frontegg/redux-store/auth'; +import { ResetPasswordSuccessRedirect, ResetPasswordSuccessRedirectProps } from './ResetPasswordSuccessRedirect'; +import { ResetPasswordFailed, ResetPasswordFailedProps } from './ResetPasswordFailedRedirect'; +import { ResetPasswordForm, ResetPasswordFormProps } from './ResetPasswordForm'; +import { authPageWrapper } from '../components'; +import { useForgotPasswordActions, useForgotPasswordState } from '@frontegg/react-hooks/auth'; + +type Components = { + ResetPasswordForm: ResetPasswordFormProps; + ResetPasswordSuccessRedirect: ResetPasswordSuccessRedirectProps; + ResetPasswordFailed: ResetPasswordFailedProps; +}; + +export interface ResetPasswordProps { + components?: ComponentsTypesWithProps; +} + +const defaultComponent = { ResetPasswordForm, ResetPasswordSuccessRedirect, ResetPasswordFailed }; +export const ResetPassword: FC = (props) => { + const Dynamic = useDynamicComponents(defaultComponent, props); + const { step } = useForgotPasswordState(); + const { loadPasswordConfig, resetForgotPasswordState } = useForgotPasswordActions(); + + const url = new URL(window?.location.href); + const userId = url.searchParams.get('userId') || ''; + const token = url.searchParams.get('token') || ''; + + useEffect((): (() => void) => { + loadPasswordConfig({ userId }); + return resetForgotPasswordState; + }, [userId]); + + let components; + if (!userId || !token) { + components = ; + } else if (step === ForgotPasswordStep.success) { + components = ; + } else { + components = ; + } + + return
{components}
; +}; + +export const ResetPasswordPage = authPageWrapper(ResetPassword); diff --git a/packages/auth/src/ResetPassword/ResetPasswordFailedRedirect.tsx b/packages/auth/src/ResetPassword/ResetPasswordFailedRedirect.tsx new file mode 100644 index 000000000..a7ba15996 --- /dev/null +++ b/packages/auth/src/ResetPassword/ResetPasswordFailedRedirect.tsx @@ -0,0 +1,39 @@ +import React, { FC } from 'react'; +import { Button, omitProps, RendererFunctionFC, useT } from '@frontegg/react-core'; +import { useAuthRoutes, useOnRedirectTo, useForgotPasswordActions } from '@frontegg/react-hooks/auth'; + +export interface ResetPasswordFailedProps { + renderer?: RendererFunctionFC; +} + +export const ResetPasswordFailed: FC = (props) => { + const { renderer } = props; + const { t } = useT(); + const { loginUrl } = useAuthRoutes(); + const onRedirectTo = useOnRedirectTo(); + const { resetForgotPasswordState } = useForgotPasswordActions(); + + if (renderer) { + return renderer(omitProps(props, ['renderer'])); + } + + return ( + <> +
+ {t('auth.forgot-password.reset-password-failed-title')} +
+ {t('auth.forgot-password.reset-password-failed-description')} +
+ + + ); +}; diff --git a/packages/auth/src/ResetPassword/ResetPasswordForm.tsx b/packages/auth/src/ResetPassword/ResetPasswordForm.tsx new file mode 100644 index 000000000..37aab75f2 --- /dev/null +++ b/packages/auth/src/ResetPassword/ResetPasswordForm.tsx @@ -0,0 +1,67 @@ +import React, { FC } from 'react'; +import { + omitProps, + validatePasswordUsingOWASP, + validatePasswordConfirmation, + validateSchema, + ErrorMessage, + useT, + RendererFunctionFC, + FInput, + FButton, + FForm, + FFormik, +} from '@frontegg/react-core'; +import { useForgotPasswordActions, useForgotPasswordState } from '@frontegg/react-hooks/auth'; + +const { Formik } = FFormik; + +export interface ResetPasswordFormProps { + renderer?: RendererFunctionFC; + userId: string; + token: string; +} + +export const ResetPasswordForm: FC = (props) => { + const { renderer, userId, token } = props; + const { t } = useT(); + const { loading, error, passwordConfig } = useForgotPasswordState(); + const { resetPassword } = useForgotPasswordActions(); + + if (renderer) { + return renderer(omitProps(props, ['renderer'])); + } + + return ( + resetPassword({ userId, token, password })} + > + + + + + {t('auth.forgot-password.reset-password-button')} + + + + + ); +}; diff --git a/packages/auth/src/ResetPassword/ResetPasswordSuccessRedirect.tsx b/packages/auth/src/ResetPassword/ResetPasswordSuccessRedirect.tsx new file mode 100644 index 000000000..4a35b1a98 --- /dev/null +++ b/packages/auth/src/ResetPassword/ResetPasswordSuccessRedirect.tsx @@ -0,0 +1,34 @@ +import React, { FC, useEffect } from 'react'; +import { Loader, omitProps, RendererFunctionFC, useT } from '@frontegg/react-core'; +import { useAuthRoutes, useOnRedirectTo, useForgotPasswordActions } from '@frontegg/react-hooks/auth'; + +export interface ResetPasswordSuccessRedirectProps { + renderer?: RendererFunctionFC; +} + +export const ResetPasswordSuccessRedirect: FC = (props) => { + const { renderer } = props; + const { t } = useT(); + const { loginUrl } = useAuthRoutes(); + const onRedirectTo = useOnRedirectTo(); + const { resetForgotPasswordState } = useForgotPasswordActions(); + + useEffect(() => { + setTimeout(() => { + onRedirectTo(loginUrl); + }, 1000); + return resetForgotPasswordState; + }, [onRedirectTo, resetForgotPasswordState]); + + if (renderer) { + return renderer(omitProps(props, ['renderer'])); + } + return ( + <> +
{t('auth.forgot-password.password-has-been-changed')}
+
+ +
+ + ); +}; diff --git a/packages/auth/src/ResetPassword/index.ts b/packages/auth/src/ResetPassword/index.ts new file mode 100644 index 000000000..edbc3e190 --- /dev/null +++ b/packages/auth/src/ResetPassword/index.ts @@ -0,0 +1,4 @@ +export * from './ResetPassword'; +export * from './ResetPasswordForm'; +export * from './ResetPasswordFailedRedirect'; +export * from './ResetPasswordSuccessRedirect'; diff --git a/packages/auth/src/SSO/README.md b/packages/auth/src/SSO/README.md new file mode 100644 index 000000000..6ea869c10 --- /dev/null +++ b/packages/auth/src/SSO/README.md @@ -0,0 +1,180 @@ +## SSO (Single Sign On) +This collection contains built-in components to provide the ability to display your sso configuration, update and etc. + +## Usage + +To use this component you need to import it from `@frontegg/react-auth` + +```tsx +import { SSO } from '@frontegg/react-auth'; + +const AppRouter:FC = ()=> { + + return
+ {/* other routes... */} + + +
+} +``` + +![Base Example Result](imgs/sso-basic-example.png) + +## Customization + +In order to provide a **fully customizable** component, *Frontegg* team building their components with Compound Components design pattern. + +This design gives you the ability to inject your custom components inside the built-in component. + +**NOTE!**: **If you pass a child to the SSO component +it will be rendered without any inner default components, then if you want to customize a specific element +you need to add the other inner default components to it, see bellow examples of how +you can override single component:** + +**You can find the default render method foreach SSO component [here](#default-rendered-components)** + +### Default Rendered Components + +- [`SSO.Page`](./SSOPage.tsx#L47) + - [`SSO.Header`](./SSOPage.tsx#L15) + - [`SSO.Router`](./SSORouter.tsx#L28) + - [`SSO.OverviewPage`](./SSOOverviewPage/SSOOverviewPage.tsx#L17) + - [`SSO.NoDataPlaceholder`](./SSOOverviewPage/SSONoDataPlaceholder.tsx#L16) + - [`SSO.Steps`](./SSOOverviewPage/SSOSteps.tsx#L24) + - [`SSO.ClaimDomainPage`](./SSOClaimDomainPage/SSOClaimDomainPage.tsx) + - [`SSO.ConfigureIDPPage`](./SSOConfigureIDPPage/SSOConfigureIDPPage.tsx) + + +## Examples + +Here are some examples of how to customize the **SSO** components: + +- [Custom header title](#custom-header-title) +- [Render without header](#render-header-title) +- [Inject custom header](#inject-custom-header) +- [Inject element inside overview](#inject-element-inside-overview) +- [Custom toggle button](#inject-element-inside-overview) +- `Change SSO guide text` (coming soon) + +### Custom header title: + +In this example, we have injected the inner built-in components to the `SSO` as `children`, +and passed `title` property to the `SSO.Header` to override its default `title` value. + +Notice that we also added the ``, this is because how Compound Components Design works. +So you need to pass the default inner components if you don't want to override. +```tsx +import { SSO } from '@frontegg/react-auth'; + +render() { + + + + +} +``` + +### Render without header: + +In this example, we have two options to hide the `SSO.Header`: +1. pass hide property to the `SSO.Header` component. +2. just remove it from `SSO.Page` children. + +Sometimes there is a specific component +```tsx +import { SSO } from '@frontegg/react-auth'; + +render() { +// option 1 + + + + + +// option 2 + + + +} +``` + +### Inject custom header: + +```tsx +import { SSO } from '@frontegg/react-auth'; +import { MyCustomHeader } from './MyCustomHeader'; + +render() { + + + + +} + +``` + + +### Inject element inside overview: + +```tsx +import { SSO } from '@frontegg/react-auth'; +import { MyCustomHeader } from './MyCustomHeader'; + +render() { + + + + + + +
+ this element inject under overview page inside + the sso configuration component +
+ + + +
+ + +
+
+} + +``` + + +### Custom toggle button: + +```tsx +import { SSO, useSSOState } from '@frontegg/react-auth'; + +const MyToggle = () => { + const { toggleSSO, samlConfiguration } = useSSOState() + + return +} + +render() { + + + + + {/* here is you custom toggle */} + + + {/* bellow are the default rendered elements without the SSO.Toggle */} + + + + + +} +``` + + + + + diff --git a/packages/auth/src/SSO/SSOClaimDomainPage/SSOClaimDomainForm.tsx b/packages/auth/src/SSO/SSOClaimDomainPage/SSOClaimDomainForm.tsx new file mode 100644 index 000000000..bbc71cc93 --- /dev/null +++ b/packages/auth/src/SSO/SSOClaimDomainPage/SSOClaimDomainForm.tsx @@ -0,0 +1,51 @@ +import React, { FC } from 'react'; +import classNames from 'classnames'; +import { useT, FFormik, validateDomain, validateSchema, FInput, FForm } from '@frontegg/react-core'; +import { SSOClaimDomainProceedStep } from './SSOClaimDomainProceedStep'; +import { SSOClaimDomainValidateStep } from './SSOClaimDomainValidateStep'; +import { useSSOActions, useSSOState } from '@frontegg/react-hooks/auth'; + +const { Formik } = FFormik; +type SSOClaimDomainFormProps = { + background?: string; + className?: string; +}; + +const prefixT = 'auth.sso.claim-domain.form'; +export const SSOClaimDomainForm: FC = (props) => { + const { t } = useT(); + const { samlConfiguration } = useSSOState(({ samlConfiguration }) => ({ samlConfiguration })); + const { validateSSODomain, saveSSOConfigurations } = useSSOActions(); + + const children = props.children ?? ( + <> +
{t(`${prefixT}.title`)}
+ +
+ + + + +
+ + ); + return ( +
+ { + const callback = () => setSubmitting(false); + samlConfiguration?.domain === domain + ? validateSSODomain({ callback }) + : saveSSOConfigurations({ callback, domain }); + }} + > + {children} + +
+ ); +}; diff --git a/packages/auth/src/SSO/SSOClaimDomainPage/SSOClaimDomainGuide.tsx b/packages/auth/src/SSO/SSOClaimDomainPage/SSOClaimDomainGuide.tsx new file mode 100644 index 000000000..89b349894 --- /dev/null +++ b/packages/auth/src/SSO/SSOClaimDomainPage/SSOClaimDomainGuide.tsx @@ -0,0 +1,73 @@ +import React, { FC, ReactElement } from 'react'; +import { Icon, useT } from '@frontegg/react-core'; + +const transPrefix = 'auth.sso.claim-domain.guide'; + +const Title: FC = (props) => { + const { t } = useT(); + const children = props.children ?? t(`${transPrefix}.title`); + return
{children}
; +}; +const Description: FC = (props) => { + const { t } = useT(); + const children = props.children ?? t(`${transPrefix}.description`); + return
{children}
; +}; + +type StepsProps = (props: { children?: ReactElement }) => ReactElement | null; +const Steps: StepsProps = (props) => { + const { t } = useT(); + const steps = [ + t(`${transPrefix}.steps-0`), + t(`${transPrefix}.steps-1`), + t(`${transPrefix}.steps-2`), + t(`${transPrefix}.steps-3`), + ]; + const children = props.children ?? ; + return ( +
+ {steps?.map((step, i) => React.cloneElement(children, { key: i, children: <>{step} }))} +
+ ); +}; + +const Step: FC = ({ children }) => { + return ( +
+ + {children} +
+ ); +}; + +export type SSOClaimDomainGuideProps = { + title?: string; + description?: string; + steps?: string[]; +}; + +type SubComponents = { + Title: typeof Title; + Description: typeof Description; + Steps: typeof Steps; + Step: typeof Step; +}; + +const SSOClaimDomainGuide: FC & SubComponents = (props) => { + const children = props.children ?? ( + <> + + <Description /> + <Steps /> + </> + ); + + return <div className='fe-sso-guide'>{children}</div>; +}; + +SSOClaimDomainGuide.Title = Title; +SSOClaimDomainGuide.Description = Description; +SSOClaimDomainGuide.Steps = Steps; +SSOClaimDomainGuide.Step = Step; + +export { SSOClaimDomainGuide }; diff --git a/packages/auth/src/SSO/SSOClaimDomainPage/SSOClaimDomainPage.tsx b/packages/auth/src/SSO/SSOClaimDomainPage/SSOClaimDomainPage.tsx new file mode 100644 index 000000000..6120c3898 --- /dev/null +++ b/packages/auth/src/SSO/SSOClaimDomainPage/SSOClaimDomainPage.tsx @@ -0,0 +1,42 @@ +import React, { FC } from 'react'; +import { checkRootPath, Grid } from '@frontegg/react-core'; +import { Route } from 'react-router-dom'; +import { SSOClaimDomainForm } from './SSOClaimDomainForm'; +import { SSOClaimDomainGuide } from './SSOClaimDomainGuide'; +import { HideOption, RouteWrapper } from '../../interfaces'; +import { useSSOState } from '@frontegg/react-hooks/auth'; +import { reloadSSOIfNeeded } from '../helpers'; + +export const SSOClaimDomainComponent: FC = (props) => { + reloadSSOIfNeeded(); + const { loading } = useSSOState(({ loading }) => ({ loading })); + + if (loading) { + return null; + } + const children = props.children ?? ( + <> + <Grid container spacing={4}> + <Grid item xs={12} sm={6}> + <SSOClaimDomainGuide /> + </Grid> + <Grid item xs={12} sm={6}> + <SSOClaimDomainForm /> + </Grid> + </Grid> + </> + ); + + return <div className='fe-sso-claim-domain-page'>{children}</div>; +}; + +export const SSOClaimDomainPage: FC<RouteWrapper & HideOption> = (props) => { + const pagePath = + props.path ?? checkRootPath('SSO.ClaimDomainPage must be rendered inside a SSO.Router component') + '/domain'; + + return ( + <Route path={pagePath}> + <SSOClaimDomainComponent children={props.children} /> + </Route> + ); +}; diff --git a/packages/auth/src/SSO/SSOClaimDomainPage/SSOClaimDomainProceedStep.tsx b/packages/auth/src/SSO/SSOClaimDomainPage/SSOClaimDomainProceedStep.tsx new file mode 100644 index 000000000..97a204e61 --- /dev/null +++ b/packages/auth/src/SSO/SSOClaimDomainPage/SSOClaimDomainProceedStep.tsx @@ -0,0 +1,41 @@ +import React, { FC, useEffect } from 'react'; +import { useT, FFormik, FButton, ErrorMessage } from '@frontegg/react-core'; +import { useSSOActions, useSSOState } from '@frontegg/react-hooks/auth'; + +const { useFormikContext } = FFormik; + +export const SSOClaimDomainProceedStep: FC = (props) => { + const { t } = useT(); + const { samlConfiguration, saving, error } = useSSOState(({ samlConfiguration, saving, error }) => ({ + samlConfiguration, + saving, + error, + })); + const { setSSOState } = useSSOActions(); + const { values } = useFormikContext<{ domain: string }>(); + + useEffect(() => { + setSSOState({ error: null }); + }, [values.domain]); + + if ((samlConfiguration && values.domain === samlConfiguration?.domain) || !samlConfiguration) { + return null; + } + const children = props.children ?? ( + <> + <div className='fe-flex-spacer' /> + <ErrorMessage error={error} /> + <FButton + className='fe-self-flex-end' + variant='primary' + loading={saving} + fullWidth={false} + type='submit' + data-test-id='submitProceed-btn' + > + {t('common.proceed')} + </FButton> + </> + ); + return <>{children}</>; +}; diff --git a/packages/auth/src/SSO/SSOClaimDomainPage/SSOClaimDomainValidateStep.tsx b/packages/auth/src/SSO/SSOClaimDomainPage/SSOClaimDomainValidateStep.tsx new file mode 100644 index 000000000..f3cb236af --- /dev/null +++ b/packages/auth/src/SSO/SSOClaimDomainPage/SSOClaimDomainValidateStep.tsx @@ -0,0 +1,74 @@ +import React, { FC, useContext, useRef } from 'react'; +import { useT, FFormik, FButton, Input, ErrorMessage, Icon, ButtonProps, RootPathContext } from '@frontegg/react-core'; +import { useSSOState, useOnRedirectTo } from '@frontegg/react-hooks/auth'; + +const { useFormikContext } = FFormik; + +const prefixT = 'auth.sso.claim-domain.form'; +export const SSOClaimDomainValidateStep: FC = (props) => { + const { t } = useT(); + const rootPath = useContext(RootPathContext); + const { saving, error, samlConfiguration } = useSSOState(({ saving, error, samlConfiguration }) => ({ + saving, + error, + samlConfiguration, + })); + const onRedirectTo = useOnRedirectTo(); + const { values } = useFormikContext<{ domain: string }>(); + + const enterValidateStatus = useRef<boolean>(samlConfiguration?.validated ?? false); + + const recordName = `_saml-domain-challenge.${samlConfiguration?.domain}`; + const recordValue = samlConfiguration?.generatedVerification || ''; + + if (!samlConfiguration || values.domain !== samlConfiguration?.domain) { + return null; + } + + let submitButtonProps: ButtonProps = { + children: t('common.validate'), + variant: 'primary', + type: 'submit', + }; + if (samlConfiguration.validated) { + if (!rootPath || enterValidateStatus.current === samlConfiguration.validated) { + submitButtonProps = { + children: t('common.validated'), + disabled: true, + }; + } else { + submitButtonProps = { + children: ( + <> + {t('auth.sso.go-to-idp')} <Icon className='fe-ml-1' name='right-arrow' /> + </> + ), + variant: 'primary', + onClick: () => { + onRedirectTo(`${rootPath}/idp`); + }, + }; + } + } + + const children = props.children ?? ( + <> + <div className='fe-section-title fe-bold fe-mt-1 fe-mb-2'>{t(`${prefixT}.copy-info-to-txt-record`)}</div> + <Input inForm fullWidth readOnly value={recordName} label={t(`${prefixT}.record-name`)} /> + <Input inForm fullWidth readOnly value={recordValue} label={t(`${prefixT}.record-value`)} /> + <ErrorMessage error={error && t(`${prefixT}.validate-error`)} /> + <div className='fe-flex-spacer' /> + + <FButton + data-test-id='submit-btn' + loading={saving} + formikDisableIfNotDirty={false} + className='fe-self-flex-end fe-mt-2' + fullWidth={false} + size='large' + {...submitButtonProps} + /> + </> + ); + return <>{children}</>; +}; diff --git a/packages/auth/src/SSO/SSOClaimDomainPage/index.ts b/packages/auth/src/SSO/SSOClaimDomainPage/index.ts new file mode 100644 index 000000000..9088764cc --- /dev/null +++ b/packages/auth/src/SSO/SSOClaimDomainPage/index.ts @@ -0,0 +1,3 @@ +export * from './SSOClaimDomainPage'; +export * from './SSOClaimDomainGuide'; +export * from './SSOClaimDomainForm'; diff --git a/packages/auth/src/SSO/SSOConfigureIDPPage/SSOConfigureIDPForm.tsx b/packages/auth/src/SSO/SSOConfigureIDPPage/SSOConfigureIDPForm.tsx new file mode 100644 index 000000000..3bfde2ed8 --- /dev/null +++ b/packages/auth/src/SSO/SSOConfigureIDPPage/SSOConfigureIDPForm.tsx @@ -0,0 +1,115 @@ +import React, { FC, useEffect, useRef, useState } from 'react'; +import { FFormik, FForm, Grid, useT } from '@frontegg/react-core'; +import { HideOption } from '../../interfaces'; +import { SSOConfigureIDPStep1, SSOConfigureIDPStep2 } from './SSOConfigureIDPSteps'; +import { useSSOActions, useSSOState } from '@frontegg/react-hooks/auth'; +import { SamlVendors } from './SSOVendors'; +import { ssoConfigureIdpFormValidation, ssoConfigureIdpFormSubmit } from '../helpers'; + +const { Formik } = FFormik; + +export interface HeaderProps { + step: number; +} + +const prefixT = 'auth.sso.idp.form'; +const Title: FC = (props) => { + const { t } = useT(); + const children = props.children ?? t(`${prefixT}.title`); + return <div className='fe-sso-idp-page__title fe-mb-1'>{children}</div>; +}; + +const Header: FC<HeaderProps> = (props) => { + const { t } = useT(); + const children = props.children ?? <Title />; + return ( + <Grid container className='fe-sso-idp-page__config-header'> + <Grid item xs> + {children} + </Grid> + <Grid item className='fe-sso-idp-page__config-header-step'> + {`${t(`common.step`, { num: props.step })}`} + </Grid> + </Grid> + ); +}; + +const Progress = ({ step }: { step: number }) => { + return <div className={`fe-sso-idp-page__progress-${step}`} />; +}; + +export interface IInitialValues { + ssoEndpoint?: string; + configSaml: string; + configFile?: File[]; + signRequest?: boolean; + publicCertificate?: string; + oidcClientId?: string; + oidcSecret?: string; +} + +const initialValues: IInitialValues = { + configSaml: 'manual', + ssoEndpoint: '', + publicCertificate: '', +}; + +const initialOidcValues: IInitialValues = { + configSaml: 'manual', + oidcSecret: '', + oidcClientId: '', +}; + +export interface SSOConfigureIDPFormProps { + samlVendor: SamlVendors; +} + +export const SSOConfigureIDPForm: FC<HideOption & SSOConfigureIDPFormProps> = ({ samlVendor }) => { + const [step, goToStep] = useState(1); + const { samlConfiguration, saving } = useSSOState(({ samlConfiguration, saving }) => ({ + samlConfiguration, + saving, + })); + const { saveSSOConfigurations, saveSSOConfigurationsFile } = useSSOActions(); + const { t } = useT(); + const formikRef = useRef<FFormik.FormikProps<IInitialValues>>(null); + const initValues = samlVendor === 'Oidc' ? initialOidcValues : initialValues; + + useEffect(() => formikRef.current?.setSubmitting?.(!!saving), [saving, formikRef]); + useEffect(() => goToStep(1), [initValues]); + + return ( + <div className='fe-sso-idp-page__config'> + <Header step={step} /> + <Progress step={step} /> + <Formik + innerRef={formikRef} + initialValues={{ + ...initValues, + ...samlConfiguration, + }} + enableReinitialize + validate={(values) => + ssoConfigureIdpFormValidation({ + ...values, + samlVendor, + t, + }) + } + onSubmit={(values) => + ssoConfigureIdpFormSubmit({ + ...values, + saveSSOConfigurationsFile, + saveSSOConfigurations, + samlVendor, + }) + } + > + <FForm> + {step === 1 && <SSOConfigureIDPStep1 samlVendor={samlVendor} goToStep={goToStep} />} + {step === 2 && <SSOConfigureIDPStep2 samlVendor={samlVendor} goToStep={goToStep} />} + </FForm> + </Formik> + </div> + ); +}; diff --git a/packages/auth/src/SSO/SSOConfigureIDPPage/SSOConfigureIDPGuide.tsx b/packages/auth/src/SSO/SSOConfigureIDPPage/SSOConfigureIDPGuide.tsx new file mode 100644 index 000000000..58cf8f6f4 --- /dev/null +++ b/packages/auth/src/SSO/SSOConfigureIDPPage/SSOConfigureIDPGuide.tsx @@ -0,0 +1,76 @@ +import React, { FC, ReactElement, useState } from 'react'; +import { Icon, useT } from '@frontegg/react-core'; +import { SamlVendors } from './SSOVendors'; +import { SSODialogInstruction } from './SSODialogInstruction'; +import { Steps, Step } from './SSOConfigureIDPGuideSteps'; +const prefixT = 'auth.sso.idp.guide'; + +type StepsProps = { + samlVendor?: SamlVendors; + children?: ReactElement; +}; + +const Title: FC<StepsProps> = (props) => { + const { t } = useT(); + const children = props.children ?? `${t(`${prefixT}.title`)} (${props.samlVendor})`; + return <div className='fe-sso-idp-page__title fe-mb-1'>{children}</div>; +}; +const Description: FC = (props) => { + const { t } = useT(); + const children = props.children ?? t(`${prefixT}.description`); + return <div className='fe-sso-guide__description fe-mb-2'>{children}</div>; +}; + +export type SSOClaimDomainGuideProps = { + title?: string; + description?: string; + steps?: string[]; + samlVendor: SamlVendors; +}; + +type SubComponents = { + Title: typeof Title; + Description: typeof Description; + Steps: typeof Steps; + Step: typeof Step; +}; + +const SSOConfigureIDPGuide: FC<SSOClaimDomainGuideProps> & SubComponents = (props) => { + const [modalOpen, setModalOpen] = useState(false); + const { t } = useT(); + const onCloseModel = () => setModalOpen(false); + + const children = props.children ?? ( + <> + <Title samlVendor={props.samlVendor} /> + <Description /> + <Steps samlVendor={props.samlVendor} /> + <div className='fe-flex-spacer' /> + + {props.samlVendor !== SamlVendors.Saml && props.samlVendor !== SamlVendors.Oidc && ( + <> + <div className='fe-sso-guide__see-more'> + {t(`${prefixT}.step-by-step`)}{' '} + <div + onClick={() => { + setModalOpen(true); + }} + > + {t('common.instruction')} <Icon className='fe-ml-1' name='right-arrow' /> + </div> + </div> + <SSODialogInstruction samlVendor={props.samlVendor} open={modalOpen} onClose={onCloseModel} /> + </> + )} + </> + ); + + return <div className='fe-sso-guide'>{children}</div>; +}; + +SSOConfigureIDPGuide.Title = Title; +SSOConfigureIDPGuide.Description = Description; +SSOConfigureIDPGuide.Steps = Steps; +SSOConfigureIDPGuide.Step = Step; + +export { SSOConfigureIDPGuide }; diff --git a/packages/auth/src/SSO/SSOConfigureIDPPage/SSOConfigureIDPGuideSteps.tsx b/packages/auth/src/SSO/SSOConfigureIDPPage/SSOConfigureIDPGuideSteps.tsx new file mode 100644 index 000000000..85dc6457c --- /dev/null +++ b/packages/auth/src/SSO/SSOConfigureIDPPage/SSOConfigureIDPGuideSteps.tsx @@ -0,0 +1,51 @@ +import React, { FC, ReactElement, useMemo } from 'react'; +import { Icon, useT } from '@frontegg/react-core'; +import { SamlVendors } from './SSOVendors'; + +const prefixT = 'auth.sso.idp.guide'; + +type StepsProps = { + samlVendor?: SamlVendors; + children?: ReactElement; +}; + +export const Steps: FC<StepsProps> = (props) => { + const { samlVendor } = props; + const { t } = useT(); + const steps = useMemo(() => { + switch (samlVendor) { + case 'Oidc': + return [ + t(`${prefixT}.oidc.steps-0`), + t(`${prefixT}.oidc.steps-1`), + t(`${prefixT}.oidc.steps-2`), + t(`${prefixT}.oidc.steps-3`), + t(`${prefixT}.oidc.steps-4`), + t(`${prefixT}.oidc.steps-5`), + ]; + default: + return [ + t(`${prefixT}.steps-0`), + t(`${prefixT}.steps-1`), + t(`${prefixT}.steps-2`), + t(`${prefixT}.steps-3`), + t(`${prefixT}.steps-4`), + ]; + } + }, [samlVendor]); + + const children = props.children ?? <Step />; + return ( + <div className='fe-sso-guide__steps'> + {steps?.map((step, i) => React.cloneElement(children, { key: i, children: <>{step}</> }))} + </div> + ); +}; +export const Step: FC = ({ children }) => { + return ( + <div className='fe-sso-guide__step'> + <Icon name='right-arrow' /> + <span>{children}</span> + </div> + ); +}; diff --git a/packages/auth/src/SSO/SSOConfigureIDPPage/SSOConfigureIDPPage.tsx b/packages/auth/src/SSO/SSOConfigureIDPPage/SSOConfigureIDPPage.tsx new file mode 100644 index 000000000..c27cc0c39 --- /dev/null +++ b/packages/auth/src/SSO/SSOConfigureIDPPage/SSOConfigureIDPPage.tsx @@ -0,0 +1,46 @@ +import React, { FC, useState } from 'react'; +import { checkRootPath, Grid } from '@frontegg/react-core'; +import { Route } from 'react-router-dom'; +import { HideOption, RouteWrapper } from '../../interfaces'; +import { SSOConfigureIDPGuide } from './SSOConfigureIDPGuide'; +import { SSOConfigureIDPForm } from './SSOConfigureIDPForm'; +import { SSOConfigureIDPSelect } from './SSOConfigureIDPSelect'; +import { SamlVendors } from './SSOVendors'; +import { useSSOState } from '@frontegg/react-hooks/auth'; +import { reloadSSOIfNeeded } from '../helpers'; + +export const SSOConfigureIDPComponent: FC = (props) => { + reloadSSOIfNeeded(); + const { loading } = useSSOState(({ loading }) => ({ loading })); + const [samlVendor, setSamlVendor] = useState<SamlVendors>(SamlVendors.Saml); + if (loading) { + return null; + } + + const children = props.children ?? ( + <Grid container spacing={4}> + <Grid item xs={12} sm={4} md={3}> + <SSOConfigureIDPSelect samlVendor={samlVendor} setSamlVendor={setSamlVendor} /> + </Grid> + <Grid item xs={12} sm={8} md={4}> + <SSOConfigureIDPGuide samlVendor={samlVendor} /> + </Grid> + <Grid item xs={12} sm={12} md={5}> + <SSOConfigureIDPForm samlVendor={samlVendor} /> + </Grid> + </Grid> + ); + + return <div className='fe-sso-idp-page'>{children}</div>; +}; + +export const SSOConfigureIDPPage: FC<RouteWrapper & HideOption> = (props) => { + const pagePath = + props.path ?? checkRootPath('SSO.ConfigureIDPPage must be rendered inside a SSO.Router component') + '/idp'; + + return ( + <Route path={pagePath}> + <SSOConfigureIDPComponent children={props.children} /> + </Route> + ); +}; diff --git a/packages/auth/src/SSO/SSOConfigureIDPPage/SSOConfigureIDPSelect.tsx b/packages/auth/src/SSO/SSOConfigureIDPPage/SSOConfigureIDPSelect.tsx new file mode 100644 index 000000000..ffebc2c67 --- /dev/null +++ b/packages/auth/src/SSO/SSOConfigureIDPPage/SSOConfigureIDPSelect.tsx @@ -0,0 +1,58 @@ +import React, { FC } from 'react'; +import { Grid, useT } from '@frontegg/react-core'; +import classNames from 'classnames'; +import { AzureIcon, GoogleIcon, OktaIcon, SamlIcon, SamlVendors, OpenIdIcon } from './SSOVendors'; + +export interface SSOConfigureIDPSelectProps { + samlVendor: SamlVendors; + setSamlVendor: (key: SamlVendors) => void; +} + +type ItemProps = { + Icon: FC<React.SVGProps<SVGSVGElement>>; + label: string; + selected: boolean; + onClick: () => void; +}; + +const IDPS = [ + { Icon: SamlIcon, key: SamlVendors.Saml, label: 'SAML' }, + { Icon: OktaIcon, key: SamlVendors.Okta, label: 'Okta' }, + { Icon: AzureIcon, key: SamlVendors.Azure, label: 'Azure Active Directory' }, + { Icon: GoogleIcon, key: SamlVendors.Google, label: 'Google Gsuite' }, + { Icon: OpenIdIcon, key: SamlVendors.Oidc, label: 'Open ID Connect' }, +]; + +const prefixT = 'auth.sso.idp.select'; +const Title: FC = (props) => { + const { t } = useT(); + const children = props.children ?? t(`${prefixT}.title`); + return <div className='fe-sso-idp-page__title fe-mb-2'>{children}</div>; +}; + +const Item: FC<ItemProps> = ({ Icon, label, selected, onClick }) => { + return ( + <div className={classNames('fe-sso-idp-page__select-item', { selected })} onClick={onClick}> + <Icon width='2.5rem' height='2.5rem' /> + <div className='fe-ml-2'>{label}</div> + </div> + ); +}; + +export const SSOConfigureIDPSelect: FC<SSOConfigureIDPSelectProps> = (props) => { + const { samlVendor, setSamlVendor } = props; + return ( + <div className='fe-sso-idp-page__select'> + <Title /> + <div className='fe-sso-idp-page__select-container'> + <Grid container spacing={2}> + {IDPS.map(({ Icon, key, label }) => ( + <Grid key={key} item xs={6} sm={12}> + <Item Icon={Icon} label={label} selected={samlVendor === key} onClick={() => setSamlVendor(key)} /> + </Grid> + ))} + </Grid> + </div> + </div> + ); +}; diff --git a/packages/auth/src/SSO/SSOConfigureIDPPage/SSOConfigureIDPSteps.tsx b/packages/auth/src/SSO/SSOConfigureIDPPage/SSOConfigureIDPSteps.tsx new file mode 100644 index 000000000..71a2443a6 --- /dev/null +++ b/packages/auth/src/SSO/SSOConfigureIDPPage/SSOConfigureIDPSteps.tsx @@ -0,0 +1,193 @@ +import React, { FC, useEffect, useMemo } from 'react'; +import { + FFormik, + Button, + ErrorMessage, + FButton, + FInput, + Grid, + Icon, + Input, + SwitchToggle, + useT, +} from '@frontegg/react-core'; +import Dropzone from 'react-dropzone'; +import { useSSOState } from '@frontegg/react-hooks/auth'; +import { SamlVendors } from './SSOVendors'; + +const { useField, useFormikContext } = FFormik; + +export interface SSOConfigureIDPStepProps { + goToStep: (step: number) => void; + samlVendor: SamlVendors; +} + +export interface SSOManualConfigProps extends Pick<SSOConfigureIDPStepProps, 'samlVendor'> {} + +export const SSOConfigureIDPStep1: FC<SSOConfigureIDPStepProps> = ({ goToStep, samlVendor }) => { + const { t } = useT(); + const { samlConfiguration } = useSSOState(({ samlConfiguration }) => ({ samlConfiguration })); + + const validCallback = samlConfiguration?.acsUrl && samlConfiguration?.spEntityId; + return ( + <div className='fe-sso-idp-page__step'> + {validCallback ? ( + <> + <Input + size='large' + readOnly + inForm + fullWidth + label='ASC URL' + value={samlConfiguration?.acsUrl} + data-test-id='ASCURL-box' + /> + {samlVendor !== 'Oidc' && ( + <Input + size='large' + readOnly + inForm + fullWidth + label='Entity ID' + value={samlConfiguration?.spEntityId} + data-test-id='EntityID-box' + /> + )} + </> + ) : ( + <ErrorMessage error={t('auth.sso.idp.error-ask-your-vendor')} /> + )} + + <div className='fe-flex-spacer' /> + + <Grid container> + <Grid item xs style={{ textAlign: 'end' }}> + <Button + disabled={!validCallback} + size='large' + variant='primary' + onClick={() => goToStep(2)} + data-test-id='rightArrow-btn' + > + {t('common.next')} <Icon className='fe-ml-1' name={'right-arrow'} /> + </Button> + </Grid> + </Grid> + </div> + ); +}; + +const SSOAutomaticConfig: FC = () => { + const { t } = useT(); + const [{ value: configFile }, , { setValue: setConfigFile }] = useField('configFile'); + + return ( + <> + {configFile?.length ? ( + <> + <div className='fe-sso-dnd-title'>{t('auth.dropzone.title')}</div> + <section className='fe-sso-dnd'> + <div className='fe-sso-dnd-container'>{configFile[0].name}</div> + </section> + </> + ) : ( + <> + <div className='fe-sso-dnd-title'>{t('auth.dropzone.title')}</div> + <Dropzone onDrop={(acceptedFiles) => setConfigFile(acceptedFiles)} accept='text/xml'> + {({ getRootProps, getInputProps }) => ( + <section className='fe-sso-dnd'> + <div className='fe-sso-dnd-container'> + <div {...getRootProps()}> + <input {...getInputProps()} /> + <div className='fe-bold'>{t('auth.dropzone.dnd')}</div> + <p>{t('auth.dropzone.description')}</p> + </div> + </div> + </section> + )} + </Dropzone> + </> + )} + </> + ); +}; + +const SSOManualConfig: FC<SSOManualConfigProps> = ({ samlVendor }) => { + const { t } = useT(); + if (samlVendor === 'Oidc') { + return ( + <div className='sso-endpoint-container'> + <FInput name='oidcClientId' label='Client Id' placeholder={t('common.clientId')} /> + <FInput name='oidcSecret' label='Secret key' placeholder={t('common.secretKey')} multiline /> + </div> + ); + } + return ( + <div className='sso-endpoint-container'> + <FInput + name='ssoEndpoint' + label={t('auth.sso.idp.form.endpoint')} + placeholder={t('auth.sso.idp.form.endpoint-desc')} + /> + <FInput + name='publicCertificate' + label={t('auth.sso.idp.form.certificate')} + placeholder={t('auth.sso.idp.form.certificate-desc')} + multiline + /> + </div> + ); +}; + +export const SSOConfigureIDPStep2: FC<SSOConfigureIDPStepProps> = ({ goToStep, samlVendor }) => { + const { t } = useT(); + + const [{ value: configSaml }, , { setValue: setConfigSaml }] = useField<string>('configSaml'); + const { isValid, dirty } = useFormikContext(); + + const { samlConfiguration, saving, error } = useSSOState(({ samlConfiguration, saving, error }) => ({ + samlConfiguration, + saving, + error, + })); + + useEffect(() => { + samlVendor === 'Oidc' ? setConfigSaml('manual') : null; + }, [samlVendor]); + + const isDomainValidated = samlConfiguration?.validated ?? false; + const isIdpValidated = useMemo( + () => + samlVendor === 'Oidc' + ? !!(samlConfiguration?.oidcClientId && isDomainValidated) + : !!(samlConfiguration?.ssoEndpoint && isDomainValidated), + [samlVendor] + ); + + return ( + <div className='fe-sso-idp-page__step'> + <SwitchToggle + disabled={samlVendor === 'Oidc'} + value={configSaml !== 'auto'} + onChange={(toggle) => setConfigSaml(toggle ? 'manual' : 'auto')} + labels={[t('common.automatic'), t('common.manual')]} + /> + + {configSaml === 'auto' ? <SSOAutomaticConfig /> : <SSOManualConfig samlVendor={samlVendor} />} + <ErrorMessage error={error} separator /> + <div className='fe-flex-spacer' /> + <Grid container> + <Grid item xs> + <Button isCancel size='large' onClick={() => goToStep(1)}> + <Icon className='fe-mr-1' name={'left-arrow'} /> {t('common.back')} + </Button> + </Grid> + <Grid item xs style={{ textAlign: 'end' }}> + <FButton loading={!!saving} size='large' variant='primary' type='submit'> + {isIdpValidated && !dirty && isValid && !saving ? t('common.configured') : t('common.configure')} + </FButton> + </Grid> + </Grid> + </div> + ); +}; diff --git a/packages/auth/src/SSO/SSOConfigureIDPPage/SSODialogInstruction.tsx b/packages/auth/src/SSO/SSOConfigureIDPPage/SSODialogInstruction.tsx new file mode 100644 index 000000000..6125227b4 --- /dev/null +++ b/packages/auth/src/SSO/SSOConfigureIDPPage/SSODialogInstruction.tsx @@ -0,0 +1,282 @@ +import React, { FC, ReactNode, useContext, useMemo, useState } from 'react'; +import { + Accordion, + AccordionContent, + AccordionHeader, + Button, + Dialog, + DialogContext, + DialogProps, + Grid, + useT, +} from '@frontegg/react-core'; +import { SamlVendors } from './SSOVendors'; +import { useSSOState } from '@frontegg/react-hooks/auth'; + +type InstructionsAccordionProps = { + steps: { + text: ReactNode; + imgs: string[]; + }[]; +}; +const InstructionsAccordion: FC<InstructionsAccordionProps> = (props) => { + const { t } = useT(); + const [expanded, setExpanded] = useState(0); + const { onClose } = useContext(DialogContext); + return ( + <> + {props.steps.map((step, index) => { + const firstItem = index === 0; + const lastItem = index === props.steps.length - 1; + + return ( + <Accordion key={index} expanded={expanded === index} onClick={() => setExpanded(index)}> + <AccordionHeader>{t('common.step', { num: index + 1 })}</AccordionHeader> + <AccordionContent> + <div className='fe-sso-guide__instruction-row'> + <div className='fe-description'>{step.text}</div> + {step.imgs && step.imgs.map((imgUrl) => <img src={imgUrl} alt={'step' + index} />)} + + <Grid container> + <Grid item xs={6}> + {!firstItem && ( + <Button + data-test-id='expand-btn' + onClick={(e) => { + e.stopPropagation(); + setExpanded(index - 1); + }} + > + {t('common.back')} + </Button> + )} + </Grid> + <Grid item xs={6} className='fe-text-align-end'> + <Button + data-test-id='collapseExpand-btn' + variant='primary' + onClick={(e) => { + e.stopPropagation(); + lastItem ? onClose?.() : setExpanded(index + 1); + }} + > + {lastItem ? t('common.finish') : t('common.next')} + </Button> + </Grid> + </Grid> + </div> + </AccordionContent> + </Accordion> + ); + })} + </> + ); +}; + +const OktaInstructions = () => { + const { samlConfiguration } = useSSOState(({ samlConfiguration }) => ({ samlConfiguration })); + const steps = [ + { + text: ( + <>Navigate to your Okta admin console (make sure that the UI is set to classical and not developer console)</> + ), + imgs: ['https://assets.frontegg.com/public-frontegg-assets/SSO/OKTA/1.png'], + }, + { + text: <>Select the applications menu from the top navigation bar</>, + imgs: ['https://assets.frontegg.com/public-frontegg-assets/SSO/OKTA/2.png'], + }, + { + text: <>Search and select the SAML service provider application from the applications menu</>, + imgs: ['https://assets.frontegg.com/public-frontegg-assets/SSO/OKTA/3.png'], + }, + { + text: <>Click on the ADD application button</>, + imgs: ['https://assets.frontegg.com/public-frontegg-assets/SSO/OKTA/4.png'], + }, + { + text: ( + <>Name the application with a meaningful name (this is only for you to remember what it is) and click next</> + ), + imgs: ['https://assets.frontegg.com/public-frontegg-assets/SSO/OKTA/5.png'], + }, + { + text: ( + <> + Download the IDP metadata XML by clicking on the <b>Identity Provider metadata</b> link + </> + ), + imgs: ['https://assets.frontegg.com/public-frontegg-assets/SSO/OKTA/8.png'], + }, + { + text: ( + <> + Set <u>{samlConfiguration?.acsUrl}</u> to the <b>Assertion Consumer Service URL</b> and{' '} + <u>{samlConfiguration?.spEntityId}</u> to the <b>Service Provider Entity Id</b> + </> + ), + imgs: ['https://assets.frontegg.com/public-frontegg-assets/SSO/OKTA/6.png'], + }, + { + text: ( + <> + Under the <b>CREDENTIALS DETAILS</b> section set the <b>Application username format</b> to email and click + Done + </> + ), + imgs: ['https://assets.frontegg.com/public-frontegg-assets/SSO/OKTA/7.png'], + }, + ]; + + return <InstructionsAccordion steps={steps} />; +}; + +const AzureInstructions = () => { + const { samlConfiguration } = useSSOState(({ samlConfiguration }) => ({ samlConfiguration })); + const steps = [ + { + text: <>Navigate to your azure management portal and select Azure Active Directory from the search bar</>, + imgs: ['https://assets.frontegg.com/public-frontegg-assets/SSO/AzureAD/1.png'], + }, + { + text: ( + <> + Select <b>Enterprise applications</b> from the left side bar + </> + ), + imgs: ['https://assets.frontegg.com/public-frontegg-assets/SSO/AzureAD/2.png'], + }, + { + text: ( + <> + Click on <b>New application</b> + </> + ), + imgs: ['https://assets.frontegg.com/public-frontegg-assets/SSO/AzureAD/3.png'], + }, + { + text: ( + <> + Select <b>Non gallery application</b> + </> + ), + imgs: ['https://assets.frontegg.com/public-frontegg-assets/SSO/AzureAD/4.png'], + }, + { + text: ( + <> + Enter the display name for your application (this is logical step for you to keep track on the applications) + and click Add + </> + ), + imgs: ['https://assets.frontegg.com/public-frontegg-assets/SSO/AzureAD/5.png'], + }, + { + text: ( + <> + Click on <b>Set up single sign on</b> and select SAML + </> + ), + imgs: ['https://assets.frontegg.com/public-frontegg-assets/SSO/AzureAD/6.png'], + }, + { + text: ( + <> + Under <b>Basic SAML configuration</b> set <u>{samlConfiguration?.spEntityId}</u> as the{' '} + <b>Identifier (Entity ID)</b> + and <u>{samlConfiguration?.acsUrl}</u> as the <b>Reply URL (Assertion Consumer Service URL)</b> and click save + </> + ), + imgs: ['https://assets.frontegg.com/public-frontegg-assets/SSO/AzureAD/7.png'], + }, + { + text: <>Download the Federation Metadata XML and upload it on the IDP configuration component</>, + imgs: ['https://assets.frontegg.com/public-frontegg-assets/SSO/AzureAD/8.png'], + }, + { + text: <>Under Users and Groups, associate the users allowed to login to the application</>, + imgs: ['https://assets.frontegg.com/public-frontegg-assets/SSO/AzureAD/9.png'], + }, + ]; + + return <InstructionsAccordion steps={steps} />; +}; + +const GoogleInstructions = () => { + const { samlConfiguration } = useSSOState(({ samlConfiguration }) => ({ samlConfiguration })); + const steps = [ + { + text: <>Navigate to your GSuite admin console and select Apps from the tiles menu</>, + imgs: ['https://assets.frontegg.com/public-frontegg-assets/SSO/GSuite/1.png'], + }, + { + text: <>Click on SAML apps</>, + imgs: ['https://assets.frontegg.com/public-frontegg-assets/SSO/GSuite/2.png'], + }, + { + text: <>Click on the Plus sign</>, + imgs: ['https://assets.frontegg.com/public-frontegg-assets/SSO/GSuite/9.png'], + }, + { + text: ( + <> + On the popup select <b>Set my own custom app</b> + </> + ), + imgs: ['https://assets.frontegg.com/public-frontegg-assets/SSO/GSuite/3.png'], + }, + { + text: <>Download the IDP metadata (we will upload it on step 2)</>, + imgs: ['https://assets.frontegg.com/public-frontegg-assets/SSO/GSuite/4.png'], + }, + { + text: <>Set the application name (this is logical for you to track the SAML applications) and click Next</>, + imgs: ['https://assets.frontegg.com/public-frontegg-assets/SSO/GSuite/5.png'], + }, + { + text: ( + <> + Under <b>Service Provider Details</b> set <u>{samlConfiguration?.acsUrl}</u> as the{' '} + <b>Reply URL (Assertion Consumer Service URL)</b> and <u>{samlConfiguration?.spEntityId}</u> as the{' '} + <b>Identifier (Entity ID)</b> and click Next and Finish + </> + ), + imgs: ['https://assets.frontegg.com/public-frontegg-assets/SSO/GSuite/6.png'], + }, + { + text: ( + <> + Click <b>Edit service</b> and assign the relevant groups to the service + </> + ), + imgs: [ + 'https://assets.frontegg.com/public-frontegg-assets/SSO/GSuite/12.png', + 'https://assets.frontegg.com/public-frontegg-assets/SSO/GSuite/7.png', + ], + }, + ]; + + return <InstructionsAccordion steps={steps} />; +}; + +export const SSODialogInstruction: FC<Partial<DialogProps & { samlVendor: SamlVendors }>> = (props) => { + const { t } = useT(); + const Component = useMemo(() => { + switch (props.samlVendor) { + case SamlVendors.Azure: + return AzureInstructions; + case SamlVendors.Google: + return GoogleInstructions; + case SamlVendors.Okta: + return OktaInstructions; + } + return () => null; + }, [props.samlVendor]); + return ( + <DialogContext.Provider value={{ onClose: props.onClose }}> + <Dialog open={props.open} header={`${t('common.instruction-for')} ${props.samlVendor}`} onClose={props.onClose}> + <Component /> + </Dialog> + </DialogContext.Provider> + ); +}; diff --git a/packages/auth/src/SSO/SSOConfigureIDPPage/SSOVendors.tsx b/packages/auth/src/SSO/SSOConfigureIDPPage/SSOVendors.tsx new file mode 100644 index 000000000..1e17de0fe --- /dev/null +++ b/packages/auth/src/SSO/SSOConfigureIDPPage/SSOVendors.tsx @@ -0,0 +1,92 @@ +import React, { FC } from 'react'; + +export enum SamlVendors { + Saml = 'Saml', + Okta = 'Okta', + Azure = 'Azure', + Google = 'Google', + Oidc = 'Oidc', +} + +export const SamlIcon: FC<React.SVGProps<SVGSVGElement>> = (props) => { + return ( + <svg xmlns='http://www.w3.org/2000/svg' {...props} viewBox='0 0 20 20'> + <path + d='M8.642 11.268l-2.6-2.6a.978.978 0 00-1.382 1.384l4.232 4.233a.919.919 0 00.095.063.964.964 0 00.2.135c.012.005.023.014.035.019a.964.964 0 00.792 0c.014-.005.025-.015.039-.021a.984.984 0 00.2-.131.957.957 0 00.1-.065l4.232-4.233a.978.978 0 00-1.383-1.383l-2.6 2.6.01-11.218a9.69 9.69 0 00-1.984 0l.017 11.216z' + fill='#6a6a6a' + /> + <path d='M12.538.449v2.1a7.663 7.663 0 11-5.843 0V.456a9.62 9.62 0 105.843 0' fill='#6a6a6a' /> + </svg> + ); +}; + +export const OktaIcon: FC<React.SVGProps<SVGSVGElement>> = (props) => { + return ( + <svg xmlns='http://www.w3.org/2000/svg' {...props} viewBox='0 0 43 22'> + <path + fill='#0a7ec2' + d='M11.362 8.92a3.464 3.464 0 10-.003 6.928 3.464 3.464 0 00.003-6.928m0 5.196a1.733 1.733 0 111.732-1.732v.001c0 .956-.776 1.731-1.732 1.731M17.45 13.546a.303.303 0 01.52-.213c.864.883 2.296 2.395 2.305 2.404.031.04.076.068.125.08a.573.573 0 00.148.015h1.562a.304.304 0 00.234-.49l-2.586-2.653-.135-.142c-.298-.358-.263-.49.077-.846l2.058-2.295a.308.308 0 00-.24-.5h-1.42a.447.447 0 00-.135.021.282.282 0 00-.138.085l-1.842 1.993a.309.309 0 01-.533-.209v-3.94a.283.283 0 00-.298-.284h-1.16a.276.276 0 00-.292.272v8.72a.264.264 0 00.291.268h1.159c.158.002.29-.12.3-.277zM26.898 15.52l-.125-1.161a.278.278 0 00-.322-.244 1.733 1.733 0 01-2-1.618V10.99a.327.327 0 01.32-.335h1.564a.289.289 0 00.278-.299V9.258a.296.296 0 00-.266-.32h-1.561a.317.317 0 01-.328-.315V6.864a.284.284 0 00-.3-.28h-1.153a.268.268 0 00-.283.251V12.5a3.465 3.465 0 003.928 3.34.29.29 0 00.25-.318M35.24 14.064c-.98 0-1.125-.349-1.125-1.669V9.218c.001-.165-.132-.299-.297-.3H32.66a.305.305 0 00-.301.3v.15a3.465 3.465 0 10.542 5.648c.327.5.85.826 1.666.832.14 0 .875.025.875-.321v-1.232a.221.221 0 00-.2-.23m-4.594.051a1.733 1.733 0 110-3.465 1.733 1.733 0 010 3.465' + /> + </svg> + ); +}; + +export const AzureIcon: FC<React.SVGProps<SVGSVGElement>> = (props) => { + return ( + <svg xmlns='http://www.w3.org/2000/svg' {...props} viewBox='0 0 23 23'> + <path + d='M11.518.006c.025 0 .05.03.086.085.03.05 2.617 3.127 5.75 6.84 3.133 3.711 5.689 6.765 5.676 6.784-.006.018-2.5 2.048-5.54 4.504-3.042 2.457-5.628 4.547-5.745 4.645-.202.164-.215.17-.288.097-.043-.042-2.636-2.145-5.763-4.675S.018 13.672.03 13.641c.013-.03 2.58-3.072 5.72-6.765C8.889 3.182 11.457.128 11.469.079c.012-.049.03-.073.049-.073z' + fill='#00bef2' + /> + <path + d='M283.1 483.6c-5.8-2.1-12.8-8.1-15.7-13.7-3.6-6.9-3.3-17.7.7-26.3 3.1-6.4 3.1-6.6 1.1-8.1-1.1-.8-14.4-8.2-29.4-16.3-15-8.1-28.1-15.2-29-15.7-1.2-.7-3.2 0-6.8 2.3-11.7 7.4-23.9 6.6-33.5-2.3-6.9-6.4-8.9-10.9-8.9-20.1 0-8.9 1.8-13.5 7.5-19.2 7.7-7.7 18-10.3 27.9-7 5.4 1.8 5.5 1.8 8.9-.8 4-3 36.1-32.3 51.6-47l10.7-10.2-3.2-6.7c-6.5-13.5-3.2-28.5 8.2-37.5 6.2-4.9 10.8-6.4 19.7-6.4 20.8 0 35.3 21.8 27.5 41.3-2.1 5.4-2.1 5.5-.1 8.8 1.7 2.9 30.6 37.8 45.9 55.6 2.7 3.1 5.7 5.6 6.7 5.6s4.4-1 7.6-2.2c14.9-5.9 30.6.7 36.8 15.5 4 9.5.5 22.3-8 30-6 5.4-10.4 7.1-18.4 7.1-5.6 0-7.7-.6-13.6-3.8-4.4-2.4-7.8-3.6-9.2-3.2-2.4.6-39.3 25.9-47.5 32.5-5 4.1-5.4 5.6-2.8 11.7 2.5 6 2.2 15.4-.6 21.3-3.1 6.5-10.8 13-17.5 15-6.8 1.9-10.9 1.9-16.6-.2zm1.7-110.2v-57l-3.2-4.4c-1.8-2.4-3.5-4.4-3.8-4.4-1.3 0-65.9 58.7-65.9 59.9 0 .3 1 3.3 2.2 6.5 1.2 3.3 2.1 8 2 10.7-.1 2.7-.1 5.7-.1 6.7.1 2.3 21.7 16.1 54.1 34.8 8.9 5.2 12 6.5 13.1 5.6 1.3-1.1 1.6-12.2 1.6-58.4zm27.4 50.4c42.8-26.9 50.8-32.3 51.3-34.3.3-1.2.7-5.9.8-10.6l.3-8.4-21.8-25.9c-23.4-27.7-32-37.1-34-37.1-.7 0-4.2 2-7.8 4.4l-6.6 4.4.3 56.9c.3 51 .7 59.6 2.6 59.6.2.1 7-4 14.9-9z' + fill='white' + stroke='white' + strokeWidth='1.236' + strokeLinecap='round' + strokeLinejoin='round' + transform='matrix(.06143 0 0 .06095 -6.297 -10.13)' + /> + </svg> + ); +}; + +export const GoogleIcon: FC<React.SVGProps<SVGSVGElement>> = (props) => { + return ( + <svg xmlns='http://www.w3.org/2000/svg' {...props} viewBox='0 0 23 23'> + <path + d='M1.265 6.344c.48-1.032 1.156-1.932 1.949-2.747C5.009 1.754 7.183.573 9.756.17c3.6-.563 6.824.269 9.609 2.618.176.149.22.236.028.42-.983.943-1.95 1.901-2.924 2.854-.1.098-.167.217-.344.058-2.452-2.201-6.455-2.176-9.06.198A7.323 7.323 0 005.116 9.23c-.06-.039-.124-.074-.182-.117l-3.67-2.769' + fill='#d7282a' + fillRule='evenodd' + /> + <path + d='M5.088 13.726c.352.875.789 1.702 1.438 2.404 1.65 1.787 3.697 2.557 6.148 2.308 1.14-.116 2.177-.494 3.147-1.075.093.082.181.169.28.244 1.134.868 2.27 1.734 3.407 2.601-1.253 1.166-2.732 1.942-4.396 2.354-3.922.971-7.538.357-10.749-2.125a10.72 10.72 0 01-3.107-3.795l3.832-2.916' + fill='#45ac43' + fillRule='evenodd' + /> + <path + d='M19.508 20.208c-1.136-.867-2.273-1.733-3.408-2.601-.098-.075-.186-.162-.28-.244.77-.575 1.412-1.255 1.833-2.12.168-.344.286-.705.398-1.07.077-.25.053-.349-.264-.346-1.89.016-3.782.008-5.672.008-.4 0-.401 0-.401-.406 0-1.256.006-2.511-.006-3.767-.002-.242.042-.335.32-.334 3.487.01 6.974.007 10.462.003.188 0 .306.013.339.238.434 2.988.086 5.843-1.51 8.48-.49.808-1.076 1.546-1.811 2.16' + fill='#5d7fbe' + fillRule='evenodd' + /> + <path + d='M5.088 13.726l-3.832 2.916c-.624-1.137-.984-2.358-1.152-3.63-.29-2.203.029-4.323.983-6.344.052-.111.119-.216.178-.324l3.67 2.77c.058.042.121.077.182.116-.512 1.496-.487 2.995-.029 4.496' + fill='#f4c300' + fillRule='evenodd' + /> + </svg> + ); +}; + +export const OpenIdIcon: FC<React.SVGProps<SVGSVGElement>> = (props) => { + return ( + <svg xmlns='http://www.w3.org/2000/svg' {...props} viewBox='0 0 48 48'> + <path + fill='#9E9E9E' + d='M44,27l-1-9l-2.9,1.9c-2.7-1.7-6.1-2.9-9.9-3.5c0,0-1.9-0.4-4.4-0.4s-4.8,0.3-4.8,0.3C11.3,17.5,4,23,4,29.6C4,36.4,11.5,42,23,43v-3.9c-7.9-1.1-12.9-4.8-12.9-9.5c0-4.4,4.6-8.1,10.9-9.3c0,0,4.9-1.1,9.2,0.2c2.1,0.5,4,1.2,5.6,2.2L32,25L44,27z' + /> + <path d='M23 8L23 43 29 40 29 5z' /> + <path fill='#FF9800' d='M23 8L23 43 29 40 29 5z' /> + </svg> + ); +}; diff --git a/packages/auth/src/SSO/SSOConfigureIDPPage/index.ts b/packages/auth/src/SSO/SSOConfigureIDPPage/index.ts new file mode 100644 index 000000000..043f42ad9 --- /dev/null +++ b/packages/auth/src/SSO/SSOConfigureIDPPage/index.ts @@ -0,0 +1 @@ +export * from './SSOConfigureIDPPage'; diff --git a/packages/auth/src/SSO/SSOHeader.tsx b/packages/auth/src/SSO/SSOHeader.tsx new file mode 100644 index 000000000..1de1dd22d --- /dev/null +++ b/packages/auth/src/SSO/SSOHeader.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { useHistory } from 'react-router'; +import { checkRootPath, PageHeader, PageHeaderProps, useT } from '@frontegg/react-core'; +import { HideOption } from '../interfaces'; + +export const SSOHeader = (props: PageHeaderProps & HideOption) => { + const rootPath = checkRootPath('SSO.Header must be rendered inside a SSO.Page component'); + + const history = useHistory(); + const { t } = useT(); + if (props.hide) { + return null; + } + let onBackButtonPressed; + if (location.pathname !== rootPath) { + onBackButtonPressed = () => history.replace(rootPath!); + } + return ( + <PageHeader + title={t('auth.sso.title')} + subTitle={t('auth.sso.subtitle')} + onBackButtonClick={onBackButtonPressed} + {...props} + /> + ); +}; diff --git a/packages/auth/src/SSO/SSOManageAuthorizationPage/SSOManageAuthorizationForm.tsx b/packages/auth/src/SSO/SSOManageAuthorizationPage/SSOManageAuthorizationForm.tsx new file mode 100644 index 000000000..c47bcb816 --- /dev/null +++ b/packages/auth/src/SSO/SSOManageAuthorizationPage/SSOManageAuthorizationForm.tsx @@ -0,0 +1,44 @@ +import React, { FC } from 'react'; +import classNames from 'classnames'; +import { useT, FFormik, FForm } from '@frontegg/react-core'; +import { useSSOActions, useSSOState } from '@frontegg/react-hooks/auth'; +import { SSOManageAuthorizationSelect } from './SSOManageAuthorizationSelect'; + +const { Formik } = FFormik; + +export const SSOManageAuthorizationForm: FC = () => { + const { authorizationRoles } = useSSOState(({ authorizationRoles }) => ({ authorizationRoles })); + const { updateSSOAuthorizationRoles } = useSSOActions(); + + const prefixT = 'auth.sso.authorization'; + const Title: FC = (props) => { + const { t } = useT(); + const children = props.children ?? t(`${prefixT}.title`); + return <div className='fe-sso-authorization-page__title fe-mb-1'>{children}</div>; + }; + + const Subtitle: FC = (props) => { + const { t } = useT(); + const children = props.children ?? t(`${prefixT}.subtitle`); + return <div className='fe-sso-authorization-page__subtitle fe-mb-1'>{children}</div>; + }; + + return ( + <div className={classNames('fe-sso-authorization-page__form')}> + <Title /> + <Subtitle /> + <Formik + enableReinitialize + initialValues={{ authorizationRoles: authorizationRoles || [] }} + onSubmit={({ authorizationRoles }, { resetForm }) => { + const callback = () => resetForm({ values: { authorizationRoles } }); + updateSSOAuthorizationRoles({ callback, authorizationRoles }); + }} + > + <FForm> + <SSOManageAuthorizationSelect /> + </FForm> + </Formik> + </div> + ); +}; diff --git a/packages/auth/src/SSO/SSOManageAuthorizationPage/SSOManageAuthorizationPage.tsx b/packages/auth/src/SSO/SSOManageAuthorizationPage/SSOManageAuthorizationPage.tsx new file mode 100644 index 000000000..1092db1ce --- /dev/null +++ b/packages/auth/src/SSO/SSOManageAuthorizationPage/SSOManageAuthorizationPage.tsx @@ -0,0 +1,44 @@ +import React, { FC, useEffect } from 'react'; +import { checkRootPath, Grid } from '@frontegg/react-core'; +import { Route } from 'react-router-dom'; +import { HideOption, RouteWrapper } from '../../interfaces'; +import { useAuthTeamActions, useSSOActions, useSSOState } from '@frontegg/react-hooks/auth'; +import { SSOManageAuthorizationForm } from './SSOManageAuthorizationForm'; +import { reloadSSOIfNeeded } from '../helpers'; + +export const SSOManageAuthorizationComponent: FC<HideOption> = (props) => { + reloadSSOIfNeeded(); + const { loading } = useSSOState(({ loading }) => ({ loading })); + const { loadSSOAuthorizationRoles } = useSSOActions(); + const { loadRoles } = useAuthTeamActions(); + + useEffect(() => { + loadRoles(); + loadSSOAuthorizationRoles(); + }, []); + + if (loading) { + return null; + } + + const children = props.children ?? ( + <Grid container spacing={4}> + <Grid item xs={12} sm={12}> + <SSOManageAuthorizationForm /> + </Grid> + </Grid> + ); + + return <div className='fe-sso-authorization-page'>{children}</div>; +}; +export const SSOManageAuthorizationPage: FC<RouteWrapper & HideOption> = (props) => { + const pagePath = + props.path ?? + checkRootPath('SSO.ManageAuthorizationPage must be rendered inside a SSO.Router component') + '/authorization'; + + return ( + <Route path={pagePath}> + <SSOManageAuthorizationComponent children={props.children} /> + </Route> + ); +}; diff --git a/packages/auth/src/SSO/SSOManageAuthorizationPage/SSOManageAuthorizationSelect.tsx b/packages/auth/src/SSO/SSOManageAuthorizationPage/SSOManageAuthorizationSelect.tsx new file mode 100644 index 000000000..f22af250a --- /dev/null +++ b/packages/auth/src/SSO/SSOManageAuthorizationPage/SSOManageAuthorizationSelect.tsx @@ -0,0 +1,55 @@ +import React, { FC, useMemo } from 'react'; +import { Select, FFormik, ErrorMessage, FButton, useT } from '@frontegg/react-core'; +import { useRolesState, useSSOState } from '@frontegg/react-hooks/auth'; + +const { useFormikContext } = FFormik; + +export const SSOManageAuthorizationSelect: FC = () => { + const { t } = useT(); + const { + setFieldValue, + values: { authorizationRoles }, + dirty, + } = useFormikContext<any>(); + const { saving, error } = useSSOState(({ roles, saving, error }) => ({ roles, saving, error })); + const { roles: allRoles } = useRolesState(); + + const selectValue = useMemo(() => { + return allRoles + ?.filter((role) => authorizationRoles?.find((arole: string) => arole === role.id)) + .map((role) => ({ value: role.id, label: role.name })); + }, [authorizationRoles, allRoles]); + + const selectOptions = useMemo(() => allRoles?.map((role) => ({ value: role.id, label: role.name })), [allRoles]); + + const handleChange = (v: any[]) => { + const preparedRoles = v.map((v) => v.value); + setFieldValue('authorizationRoles', preparedRoles); + }; + + return ( + <div className='fe-sso-authorization-page__select'> + <div className='fe-sso-authorization-page__select-container'> + <Select + value={selectValue} + multiselect + options={selectOptions || []} + onChange={(_, values) => handleChange(values)} + /> + </div> + <ErrorMessage error={error} separator /> + <div className='fe-sso-authorization-page__select-footer'> + <FButton + loading={!!saving} + fullWidth={false} + size='large' + variant='primary' + type='submit' + data-test-id='submit-btn' + > + {!dirty && !!authorizationRoles.length && !saving ? t('common.configured') : t('common.configure')} + </FButton> + </div> + </div> + ); +}; diff --git a/packages/auth/src/SSO/SSOManageAuthorizationPage/index.ts b/packages/auth/src/SSO/SSOManageAuthorizationPage/index.ts new file mode 100644 index 000000000..d2708df5b --- /dev/null +++ b/packages/auth/src/SSO/SSOManageAuthorizationPage/index.ts @@ -0,0 +1 @@ +export * from './SSOManageAuthorizationPage'; diff --git a/packages/auth/src/SSO/SSOOverviewPage/SSONoDataPlaceholder.tsx b/packages/auth/src/SSO/SSOOverviewPage/SSONoDataPlaceholder.tsx new file mode 100644 index 000000000..61fdea6c4 --- /dev/null +++ b/packages/auth/src/SSO/SSOOverviewPage/SSONoDataPlaceholder.tsx @@ -0,0 +1,27 @@ +import React, { FC } from 'react'; +import { Loader, useT } from '@frontegg/react-core'; +import { HideOption } from '../../interfaces'; +import { useSSOState } from '@frontegg/react-hooks/auth'; + +export const SSONoDataPlaceholder: FC<HideOption> = (props) => { + const { t } = useT(); + const { samlConfiguration, loading } = useSSOState(({ samlConfiguration, loading }) => ({ + samlConfiguration, + loading, + })); + + if (samlConfiguration?.enabled || props.hide) { + return null; + } + if (loading) { + return <Loader center />; + } + const children = props.children ?? t('auth.sso.overview.enable-sso-message'); + return ( + <div className='fe-placeholder-box'> + <div className='fe-placeholder-box__inner'> + <span>{children}</span> + </div> + </div> + ); +}; diff --git a/packages/auth/src/SSO/SSOOverviewPage/SSOOverviewPage.tsx b/packages/auth/src/SSO/SSOOverviewPage/SSOOverviewPage.tsx new file mode 100644 index 000000000..e4f1074b1 --- /dev/null +++ b/packages/auth/src/SSO/SSOOverviewPage/SSOOverviewPage.tsx @@ -0,0 +1,24 @@ +import React, { FC } from 'react'; +import { checkRootPath } from '@frontegg/react-core'; +import { SSOSteps } from './SSOSteps'; +import { SSONoDataPlaceholder } from './SSONoDataPlaceholder'; +import { Route } from 'react-router-dom'; + +const SSOOverviewPage: FC = (props) => { + const rootPath = checkRootPath('SSOOverviewPage should be rendered inside SSORouter component'); + + const children = props.children ?? ( + <> + <SSOSteps /> + <SSONoDataPlaceholder /> + </> + ); + + return ( + <Route exact path={`${rootPath}`}> + {children} + </Route> + ); +}; + +export { SSOOverviewPage }; diff --git a/packages/auth/src/SSO/SSOOverviewPage/SSOStep.tsx b/packages/auth/src/SSO/SSOOverviewPage/SSOStep.tsx new file mode 100644 index 000000000..ec09391fb --- /dev/null +++ b/packages/auth/src/SSO/SSOOverviewPage/SSOStep.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { RendererFunction, Icon, WithT } from '@frontegg/react-core'; + +export interface SSOStepProps extends Pick<WithT, 't'> { + renderer?: RendererFunction<SSOStepProps>; + num: number; + title: string; + subtitle?: string; + optional?: boolean; + configured: boolean; + to: string; +} + +export class SSOStep extends React.Component<SSOStepProps> { + render() { + const { t, num, title, subtitle, configured, to, optional } = this.props; + + return ( + <Link to={to} className='fe-sso-step'> + <div className='fe-sso-step__inner'> + <div className='fe-sso-step__header'> + <span> + {t('common.step', { num })} {optional && `(${t('common.optional')})`} + </span> + <span className='fe-sso-step__checkmark' data-test-id='IDP-checkmark'> + {configured && <Icon name='checkmark' data-test-id='domain-checkmark' />} + </span> + </div> + <div className='fe-sso-step__title'>{title}</div> + <div className='fe-sso-step__subtitle'>{subtitle}</div> + </div> + {configured ? ( + <div className='fe-sso-step__info'> + <span>{t('common.configured')}</span> + <span style={{ color: '#4183c4' }}>{t('common.edit')}</span> + </div> + ) : ( + <div className='fe-sso-step__info'>{t('common.not-configured')}</div> + )} + </Link> + ); + } +} diff --git a/packages/auth/src/SSO/SSOOverviewPage/SSOSteps.tsx b/packages/auth/src/SSO/SSOOverviewPage/SSOSteps.tsx new file mode 100644 index 000000000..a1ddb4c09 --- /dev/null +++ b/packages/auth/src/SSO/SSOOverviewPage/SSOSteps.tsx @@ -0,0 +1,72 @@ +import React, { FC } from 'react'; +import { useT, Loader, checkRootPath } from '@frontegg/react-core'; +import { SSOStep } from './SSOStep'; +import { HideOption } from '../../interfaces'; +import { useSSOState } from '@frontegg/react-hooks/auth'; + +export interface SSOStepsProps extends HideOption { + authorizationStep?: boolean; +} + +export const SSOSteps: FC<SSOStepsProps> = (props) => { + const { t } = useT(); + const rootPath = checkRootPath('SSOSteps should be rendered inside SSO component'); + const { samlConfiguration, loading, roles, authorizationRoles } = useSSOState( + ({ samlConfiguration, loading, roles, authorizationRoles }) => ({ + samlConfiguration, + loading, + roles, + authorizationRoles, + }) + ); + + if (!samlConfiguration?.enabled || props.hide) { + return null; + } + if (loading) { + return <Loader center />; + } + + const isDomainValidated = samlConfiguration?.validated ?? false; + const isIdpValidated = (samlConfiguration?.ssoEndpoint && isDomainValidated) as boolean; + const isAuthorizationValidated = !!authorizationRoles?.length; + const authorizationValue = roles + ?.filter((r) => authorizationRoles?.find((ar) => ar === r.id)) + .map((v) => v.name) + .slice(0, 2) + .join(', '); + const claimValue = samlConfiguration?.domain ?? ''; + const idpValue = samlConfiguration?.acsUrl ?? ''; + + return ( + <div className='fe-sso-steps'> + <SSOStep + num={1} + t={t} + to={`${rootPath}/domain`} + title={t('auth.sso.overview.claim-domain')} + subtitle={claimValue} + configured={isDomainValidated} + /> + <SSOStep + num={2} + t={t} + to={`${rootPath}/idp`} + title={t('auth.sso.overview.configure-your-idp')} + subtitle={idpValue} + configured={isIdpValidated} + /> + {props.authorizationStep && ( + <SSOStep + num={3} + t={t} + optional + to={`${rootPath}/authorization`} + title={t('auth.sso.overview.manage-authorization')} + subtitle={!!authorizationRoles?.length && authorizationValue ? `${authorizationValue}...` : ''} + configured={isAuthorizationValidated} + /> + )} + </div> + ); +}; diff --git a/packages/auth/src/SSO/SSOOverviewPage/index.ts b/packages/auth/src/SSO/SSOOverviewPage/index.ts new file mode 100644 index 000000000..845b24b94 --- /dev/null +++ b/packages/auth/src/SSO/SSOOverviewPage/index.ts @@ -0,0 +1,3 @@ +export * from './SSOOverviewPage'; +export * from './SSOSteps'; +export * from './SSONoDataPlaceholder'; diff --git a/packages/auth/src/SSO/SSOPage.tsx b/packages/auth/src/SSO/SSOPage.tsx new file mode 100644 index 000000000..4e16bc20c --- /dev/null +++ b/packages/auth/src/SSO/SSOPage.tsx @@ -0,0 +1,27 @@ +import React, { FC } from 'react'; +import { SSORouter } from './SSORouter'; +import { useRootPath, RootPathContext, useProxyComponent, ProxyComponent } from '@frontegg/react-core'; +import { SSOHeader } from './SSOHeader'; +import { BasePageProps } from '../interfaces'; + +export type SSOPageProps = BasePageProps & ProxyComponent; +export const SSOPage: FC<SSOPageProps> = (props) => { + const [rootPath] = useRootPath(props, '/sso'); + const proxyPortals = useProxyComponent(props); + + const children = props.children ?? ( + <> + <SSOHeader /> + <SSORouter /> + </> + ); + + return ( + <RootPathContext.Provider value={rootPath}> + <div className='fe-sso-page'> + {children} + {proxyPortals} + </div> + </RootPathContext.Provider> + ); +}; diff --git a/packages/auth/src/SSO/SSORouter.tsx b/packages/auth/src/SSO/SSORouter.tsx new file mode 100644 index 000000000..041566204 --- /dev/null +++ b/packages/auth/src/SSO/SSORouter.tsx @@ -0,0 +1,39 @@ +import React, { FC } from 'react'; +import { checkValidChildren, RootPathContext, useRootPath } from '@frontegg/react-core'; +import { SSOOverviewPage } from './SSOOverviewPage'; +import { SSOClaimDomainPage } from './SSOClaimDomainPage'; +import { SSOConfigureIDPPage } from './SSOConfigureIDPPage'; +import { SSOManageAuthorizationPage } from './SSOManageAuthorizationPage'; +import { reloadSSOIfNeeded } from './helpers'; +import { SSOToggle } from './SSOToggle'; +import { BasePageProps } from '../interfaces'; + +export type SSOProps = BasePageProps; + +const SSORouter: FC<SSOProps> = (props) => { + const [rootPath, isRootPathContext] = useRootPath(props, '/sso'); + reloadSSOIfNeeded(); + checkValidChildren('SSO.Router', 'SSO', props.children, { + SSOOverviewPage, + SSOClaimDomainPage, + SSOConfigureIDPPage, + SSOManageAuthorizationPage, + }); + + const children = props.children ?? ( + <> + <SSOToggle /> + <SSOOverviewPage /> + <SSOClaimDomainPage /> + <SSOConfigureIDPPage /> + <SSOManageAuthorizationPage /> + </> + ); + + if (!isRootPathContext) { + return <RootPathContext.Provider value={rootPath}>{children}</RootPathContext.Provider>; + } + return <>{children}</>; +}; + +export { SSORouter }; diff --git a/packages/auth/src/SSO/SSOToggle.tsx b/packages/auth/src/SSO/SSOToggle.tsx new file mode 100644 index 000000000..96fa1c585 --- /dev/null +++ b/packages/auth/src/SSO/SSOToggle.tsx @@ -0,0 +1,33 @@ +import React, { FC, useCallback } from 'react'; +import { SwitchToggle, SwitchToggleProps, useT } from '@frontegg/react-core'; +import { reloadSSOIfNeeded } from './helpers'; +import { HideOption } from '../interfaces'; +import { useSSOActions, useSSOState } from '@frontegg/react-hooks/auth'; + +export const SSOToggle: FC<SwitchToggleProps & HideOption> = (props) => { + reloadSSOIfNeeded(); + const { samlConfiguration, loading } = useSSOState(); + const { saveSSOConfigurations } = useSSOActions(); + + const { t } = useT(); + if (props.hide) { + return null; + } + const samlEnabled = samlConfiguration?.enabled ?? false; + const onEnabledDisabledChanged = useCallback(() => { + saveSSOConfigurations({ ...samlConfiguration, enabled: !samlConfiguration?.enabled }); + }, [samlConfiguration]); + + return ( + <div className='fe-center fe-mt-4'> + <SwitchToggle + data-test-id='sso-toggle' + loading={loading} + value={samlEnabled} + labels={[t('common.disabled'), t('common.enabled')]} + onChange={onEnabledDisabledChanged} + {...props} + /> + </div> + ); +}; diff --git a/packages/auth/src/SSO/helpers.ts b/packages/auth/src/SSO/helpers.ts new file mode 100644 index 000000000..982051e8d --- /dev/null +++ b/packages/auth/src/SSO/helpers.ts @@ -0,0 +1,93 @@ +import { useEffect } from 'react'; +import { validateRequired, validateSchemaSync, validateUrl } from '@frontegg/react-core'; +import { SSOActions } from '@frontegg/redux-store/auth'; +import { useSSOActions, useSSOState } from '@frontegg/react-hooks/auth'; +import { SamlVendors } from './SSOConfigureIDPPage/SSOVendors'; +import { IInitialValues } from './SSOConfigureIDPPage/SSOConfigureIDPForm'; + +export interface IssoConfigureIdpFormValidation extends IInitialValues { + t: any; + samlVendor: SamlVendors; +} + +export interface IssoConfigureIdpFormSubmit extends IInitialValues { + samlVendor: SamlVendors; + saveSSOConfigurations: SSOActions['saveSSOConfigurations']; + saveSSOConfigurationsFile: SSOActions['saveSSOConfigurationsFile']; +} + +export const reloadSSOIfNeeded = () => { + const { samlConfiguration, loading } = useSSOState(({ samlConfiguration, loading }) => ({ + samlConfiguration, + loading, + })); + const { loadSSOConfigurations } = useSSOActions(); + useEffect(() => { + if (loading && !samlConfiguration) { + loadSSOConfigurations(); + } + }, []); +}; + +export const ssoConfigureIdpFormValidation = ({ + configSaml, + configFile, + ssoEndpoint, + publicCertificate, + oidcClientId, + oidcSecret, + samlVendor, + t, +}: IssoConfigureIdpFormValidation) => { + if (configSaml === 'auto' && samlVendor !== SamlVendors.Oidc) { + return validateSchemaSync( + { + configFile: validateRequired(t('auth.sso.form.metadata-file'), t), + }, + { configFile } + ); + } else if (samlVendor === SamlVendors.Oidc) { + return validateSchemaSync( + { + oidcClientId: validateRequired(t('common.clientId'), t), + oidcSecret: validateRequired(t('common.secretKey'), t), + }, + { oidcClientId, oidcSecret } + ); + } else { + return validateSchemaSync( + { + ssoEndpoint: validateUrl(t('auth.sso.idp.form.endpoint'), t), + publicCertificate: validateRequired(t('auth.sso.idp.form.certificate'), t), + }, + { ssoEndpoint, publicCertificate } + ); + } +}; + +export const ssoConfigureIdpFormSubmit = ({ + ssoEndpoint, + configSaml, + publicCertificate, + configFile, + oidcClientId, + oidcSecret, + saveSSOConfigurations, + saveSSOConfigurationsFile, + samlVendor, +}: IssoConfigureIdpFormSubmit) => { + if (configSaml === 'auto') { + saveSSOConfigurationsFile?.(configFile!); + } else if (samlVendor === SamlVendors.Oidc) { + saveSSOConfigurations?.({ + oidcClientId, + oidcSecret, + samlVendor: SamlVendors.Oidc, + } as any); + } else { + saveSSOConfigurations?.({ + ssoEndpoint, + publicCertificate, + }); + } +}; diff --git a/packages/auth/src/SSO/index.ts b/packages/auth/src/SSO/index.ts new file mode 100644 index 000000000..c05625278 --- /dev/null +++ b/packages/auth/src/SSO/index.ts @@ -0,0 +1,31 @@ +import { SSORouter } from './SSORouter'; +import { SSOPage } from './SSOPage'; +import { SSOHeader } from './SSOHeader'; +import { SSOToggle } from './SSOToggle'; +import { SSOOverviewPage, SSOSteps, SSONoDataPlaceholder } from './SSOOverviewPage'; +import { + SSOClaimDomainComponent, + SSOClaimDomainPage, + SSOClaimDomainGuide, + SSOClaimDomainForm, +} from './SSOClaimDomainPage'; +import { SSOConfigureIDPComponent, SSOConfigureIDPPage } from './SSOConfigureIDPPage'; +import { SSOManageAuthorizationComponent, SSOManageAuthorizationPage } from './SSOManageAuthorizationPage'; + +export const SSO = { + Page: SSOPage, + Header: SSOHeader, + Router: SSORouter, + Toggle: SSOToggle, + OverviewPage: SSOOverviewPage, + ClaimDomainPage: SSOClaimDomainPage, + ClaimDomainComponent: SSOClaimDomainComponent, + ConfigureIDPPage: SSOConfigureIDPPage, + ConfigureIDPComponent: SSOConfigureIDPComponent, + ManageAuthorizationPage: SSOManageAuthorizationPage, + ManageAuthorizationComponent: SSOManageAuthorizationComponent, + Steps: SSOSteps, + NoDataPlaceholder: SSONoDataPlaceholder, + ClaimDomainGuide: SSOClaimDomainGuide, + ClaimDomainForm: SSOClaimDomainForm, +}; diff --git a/packages/auth/src/SignUp/SignUp.tsx b/packages/auth/src/SignUp/SignUp.tsx new file mode 100644 index 000000000..1743e49ef --- /dev/null +++ b/packages/auth/src/SignUp/SignUp.tsx @@ -0,0 +1,68 @@ +import React, { FC, useCallback, useEffect } from 'react'; +import { ComponentsTypesWithProps, useDynamicComponents } from '@frontegg/react-core'; +import { useAuthRoutes, useOnRedirectTo, useSecurityPolicyActions, useSignUpState } from '@frontegg/react-hooks/auth'; +import { SignUpStage } from '@frontegg/redux-store/auth'; +import { SignUpForm } from './SignUpForm'; +import { authPageWrapper } from '../components'; +import { SignUpSuccess } from './SignUpSuccess'; +import { SocialLoginActionWrapperProps, SocialLoginsSignUpWithWrapper } from '../SocialLogins'; + +type Components = { + SignUpForm: {}; + SocialLogins: SocialLoginActionWrapperProps; +}; + +export interface SignUpCheckbox { + required: boolean; + content: () => JSX.Element; +} + +export interface SignUpProps { + components?: ComponentsTypesWithProps<Components>; + withCompanyName?: boolean; + signUpConsent?: SignUpCheckbox; + marketingMaterialConsent?: SignUpCheckbox; +} + +const defaultComponents = { + SignUpForm, + SocialLogins: SocialLoginsSignUpWithWrapper, +}; + +export const SignUp: FC<SignUpProps> = (props) => { + const signUpState = useSignUpState(); + const routes = useAuthRoutes(); + const onRedirectTo = useOnRedirectTo(); + + const Dynamic = useDynamicComponents(defaultComponents, props); + const { loadVendorPasswordConfig } = useSecurityPolicyActions(); + + const redirectToLogin = useCallback(() => { + onRedirectTo(routes.loginUrl); + }, []); + + if (!signUpState.firstLoad && !signUpState.allowSignUps) { + redirectToLogin(); + } + + useEffect(() => { + loadVendorPasswordConfig(); + }, []); + + switch (signUpState.stage) { + case SignUpStage.SignUpSuccess: + return <SignUpSuccess />; + case SignUpStage.SignUp: + default: + return ( + <Dynamic.SignUpForm + withCompanyName={props.withCompanyName} + signUpConsent={props.signUpConsent} + marketingMaterialConsent={props.marketingMaterialConsent} + SocialLogins={Dynamic.SocialLogins} + /> + ); + } +}; + +export const SignUpPageComponent = authPageWrapper(SignUp); diff --git a/packages/auth/src/SignUp/SignUpForm.tsx b/packages/auth/src/SignUp/SignUpForm.tsx new file mode 100644 index 000000000..9b6630b26 --- /dev/null +++ b/packages/auth/src/SignUp/SignUpForm.tsx @@ -0,0 +1,194 @@ +import React, { ComponentType, FC, RefObject, useCallback, useEffect } from 'react'; +import { + FForm, + FFormik, + FInput, + FCheckbox, + useT, + validateLength, + validateSchema, + validateEmail, + ErrorMessage, + validateCheckbox, + validatePasswordUsingOWASP, + Button, +} from '@frontegg/react-core'; +import { + useAuthRoutes, + useOnRedirectTo, + useSecurityPolicyState, + useSignUpActions, + useSignUpState, +} from '@frontegg/react-hooks/auth'; +import { FReCaptcha } from '../components/FReCaptcha'; +import { SignUpCheckbox } from './SignUp'; +import { SocialLoginActionWrapperProps } from '../SocialLogins'; +import { ReCaptcha } from 'react-recaptcha-v3'; + +const { Formik } = FFormik; + +export interface SignUpFormProps { + withCompanyName?: boolean; + signUpConsent?: SignUpCheckbox; + marketingMaterialConsent?: SignUpCheckbox; + SocialLogins: ComponentType<SocialLoginActionWrapperProps>; +} + +export const SignUpForm: FC<SignUpFormProps> = ({ + withCompanyName = true, + signUpConsent, + marketingMaterialConsent, + SocialLogins, +}) => { + const { t } = useT(); + + const routes = useAuthRoutes(); + const onRedirectTo = useOnRedirectTo(); + const { signUpUser } = useSignUpActions(); + const { loading, error, allowNotVerifiedUsersLogin } = useSignUpState(); + const { + passwordPolicy: { policy }, + } = useSecurityPolicyState(); + + const getCheckboxDetails = useCallback((checkbox?: SignUpCheckbox) => { + const isVisible = checkbox?.hasOwnProperty('content'); + const isRequired = isVisible && checkbox?.required !== false; + + return { isVisible, isRequired }; + }, []); + + const redirectToLogin = useCallback(() => { + onRedirectTo(routes.loginUrl, { preserveQueryParams: true }); + }, []); + + const signUpConsentDetails = getCheckboxDetails(signUpConsent); + const marketingMaterialConsentDetails = getCheckboxDetails(marketingMaterialConsent); + const recaptchaRef: RefObject<ReCaptcha> = React.createRef(); + useEffect(() => { + if (recaptchaRef.current && !loading) { + error && recaptchaRef.current.execute(); + } + }, [loading, error, recaptchaRef]); + + return ( + <> + <div> + {t('auth.sign-up.suggest-login.message')} + <span onClick={redirectToLogin} className={'fe-sign-up__back-to-login-link'}> + {t('auth.sign-up.suggest-login.login-link')} + </span> + </div> + + <Formik + initialValues={{ + email: '', + name: '', + companyName: '', + recaptchaToken: '', + acceptedTermsOfService: signUpConsentDetails.isVisible ? false : undefined, + allowMarketingMaterial: marketingMaterialConsentDetails.isVisible ? false : undefined, + password: allowNotVerifiedUsersLogin ? '' : undefined, + }} + onSubmit={({ acceptedTermsOfService, allowMarketingMaterial, ...values }, b) => { + const metadata = JSON.stringify({ + acceptedTermsOfService, + allowMarketingMaterial, + }); + + if (withCompanyName) { + signUpUser({ ...values, metadata }); + } else { + signUpUser({ ...values, companyName: values.name, metadata }); + } + }} + validationSchema={validateSchema({ + email: validateEmail(t), + name: validateLength('name', 3, t), + companyName: withCompanyName && validateLength('Company Name', 3, t), + acceptedTermsOfService: signUpConsentDetails.isRequired && validateCheckbox(), + allowMarketingMaterial: marketingMaterialConsentDetails.isRequired && validateCheckbox(), + password: allowNotVerifiedUsersLogin && validatePasswordUsingOWASP(policy), + })} + > + {({ values: { acceptedTermsOfService, allowMarketingMaterial }, dirty, errors, touched, setFieldTouched }) => { + const isValid = !errors.name && !errors.email && !errors.password && !errors.companyName; + const showTermsError = errors.acceptedTermsOfService && touched.acceptedTermsOfService; + const showMarketingError = errors.allowMarketingMaterial && touched.allowMarketingMaterial; + + return ( + <FForm> + <FInput name='name' size='large' placeholder={t('auth.sign-up.form.name')} data-test-id='name-box' /> + <FInput + name='email' + type={'email'} + size='large' + placeholder={t('auth.sign-up.form.email')} + data-test-id='email-box' + /> + {allowNotVerifiedUsersLogin && ( + <FInput + size='large' + type='password' + name='password' + placeholder={t('auth.login.enter-your-password')} + data-testid='password-box' + /> + )} + {withCompanyName && ( + <FInput + name='companyName' + size='large' + placeholder={t('auth.sign-up.form.company-name')} + data-test-id='compenyName-box' + /> + )} + {signUpConsentDetails.isVisible && ( + <FCheckbox + name='acceptedTermsOfService' + renderLabel={signUpConsent?.content} + className={'fe-sign-up__checkbox'} + /> + )} + {marketingMaterialConsentDetails.isVisible && ( + <FCheckbox + name='allowMarketingMaterial' + renderLabel={marketingMaterialConsent?.content} + className={'fe-sign-up__checkbox'} + /> + )} + + {showTermsError && <div className='fe-sign-up__error'>{t('auth.sign-up.form.terms-error')}</div>} + {showMarketingError && <div className='fe-sign-up__error'>{t('auth.sign-up.form.marketing-error')}</div>} + + <Button + type='submit' + fullWidth + variant='primary' + loading={loading} + disabled={!(isValid && dirty)} + data-test-id='signupSubmit-btn' + > + {t('auth.sign-up.form.submit-button')} + </Button> + + <ErrorMessage error={error} /> + <FReCaptcha recaptchaRef={recaptchaRef} action='sign_up' /> + <SocialLogins + isValid={() => { + setFieldTouched('acceptedTermsOfService', true, true); + setFieldTouched('allowMarketingMaterial', true, true); + + return !( + (signUpConsentDetails.isRequired && !acceptedTermsOfService) || + (marketingMaterialConsentDetails.isRequired && !allowMarketingMaterial) + ); + }} + state={{ acceptedTermsOfService, allowMarketingMaterial }} + /> + </FForm> + ); + }} + </Formik> + </> + ); +}; diff --git a/packages/auth/src/SignUp/SignUpSuccess.tsx b/packages/auth/src/SignUp/SignUpSuccess.tsx new file mode 100644 index 000000000..bccb3ac46 --- /dev/null +++ b/packages/auth/src/SignUp/SignUpSuccess.tsx @@ -0,0 +1,37 @@ +import React, { FC, useEffect, useMemo } from 'react'; +import { useT } from '@frontegg/react-core'; +import { useSignUpActions, useAuthRoutes, useOnRedirectTo, useSignUpState } from '@frontegg/react-hooks/auth'; + +export const SignUpSuccess: FC = () => { + const { t } = useT(); + const { shouldActivate } = useSignUpState(); + const routes = useAuthRoutes(); + const onRedirectTo = useOnRedirectTo(); + const { resetSignUpStateSoft } = useSignUpActions(); + + const message: string = useMemo(() => { + if (shouldActivate) { + return t('auth.sign-up.success.activate-message'); + } + return t('auth.sign-up.success.go-to-login-message'); + }, [shouldActivate]); + + useEffect((): (() => void) => { + if (!shouldActivate) { + const url = new URL(window?.location.href); + const redirectUrl = url.searchParams.get('redirectUrl'); + + setTimeout(() => onRedirectTo(redirectUrl || routes.authenticatedUrl), 3000); + } + return resetSignUpStateSoft; + }, [shouldActivate, routes, resetSignUpStateSoft]); + + return ( + <> + <div className={'fe-center fe-sign-up__success-container'}> + <h2>{t('auth.sign-up.success.title')}</h2> + <div className='fe-sign-up__success-message'>{message}</div> + </div> + </> + ); +}; diff --git a/packages/auth/src/SignUp/index.tsx b/packages/auth/src/SignUp/index.tsx new file mode 100644 index 000000000..33150b406 --- /dev/null +++ b/packages/auth/src/SignUp/index.tsx @@ -0,0 +1,3 @@ +export * from './SignUp'; +export * from './SignUpForm'; +export * from './SignUpSuccess'; diff --git a/packages/auth/src/SocialLogins/FacebookLogin/FacebookIcon.tsx b/packages/auth/src/SocialLogins/FacebookLogin/FacebookIcon.tsx new file mode 100644 index 000000000..f875adcc8 --- /dev/null +++ b/packages/auth/src/SocialLogins/FacebookLogin/FacebookIcon.tsx @@ -0,0 +1,16 @@ +import React, { FC } from 'react'; + +export const FacebookIcon: FC<React.SVGProps<SVGSVGElement>> = (props) => { + return ( + <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1365 1365' fill='white' {...props}> + <g xmlns='http://www.w3.org/2000/svg' transform='matrix(1.3333333,0,0,-1.3333333,0,1365.3333)' id='g10'> + <g transform='scale(0.1)' id='g12'> + <path + xmlns='http://www.w3.org/2000/svg' + d='m 10240,5120 c 0,2827.7 -2292.3,5120 -5120,5120 C 2292.3,10240 0,7947.7 0,5120 0,2564.46 1872.31,446.301 4320,62.1992 V 3640 H 3020 v 1480 h 1300 v 1128 c 0,1283.2 764.38,1992 1933.9,1992 560.17,0 1146.1,-100 1146.1,-100 V 6880 H 6754.38 C 6118.35,6880 5920,6485.33 5920,6080.43 V 5120 H 7340 L 7113,3640 H 5920 V 62.1992 C 8367.69,446.301 10240,2564.46 10240,5120' + /> + </g> + </g> + </svg> + ); +}; diff --git a/packages/auth/src/SocialLogins/FacebookLogin/index.tsx b/packages/auth/src/SocialLogins/FacebookLogin/index.tsx new file mode 100644 index 000000000..fa5a1ce30 --- /dev/null +++ b/packages/auth/src/SocialLogins/FacebookLogin/index.tsx @@ -0,0 +1,45 @@ +import React, { FC, useCallback } from 'react'; +import { SocialLoginButton } from '../SocialLoginButton'; +import { FacebookIcon } from './FacebookIcon'; +import { FronteggContext, SocialLoginProviders } from '@frontegg/rest-api'; +import { UrlCreatorConfigType, useRedirectUrl, useSocialLoginContext } from '../hooks'; + +const createFacebookUrl = ({ clientId, redirectUrl, state }: UrlCreatorConfigType): string => { + const searchParams: URLSearchParams = new URLSearchParams({ + scope: 'email', + client_id: clientId, + redirect_uri: redirectUrl, + response_type: 'code', + state, + }); + const url: URL = new URL('https://www.facebook.com/v10.0/dialog/oauth'); + url.search = searchParams.toString(); + return url.toString(); +}; + +const FacebookLogin: FC = (props) => { + const { action, state, isValid } = useSocialLoginContext(); + + const redirectUrl: string | null = useRedirectUrl(createFacebookUrl, SocialLoginProviders.Facebook, state); + + const defaultButton = ( + <SocialLoginButton name={SocialLoginProviders.Facebook} action={action}> + <FacebookIcon /> + </SocialLoginButton> + ); + + const handleLogin = useCallback(async () => { + const valid = (await isValid?.()) ?? true; + if (redirectUrl && valid) { + FronteggContext.onRedirectTo(redirectUrl, { replace: true, refresh: true }); + } + }, [redirectUrl, isValid]); + + if (redirectUrl) { + return <div onClick={handleLogin}>{props.children || defaultButton}</div>; + } + + return null; +}; + +export default FacebookLogin; diff --git a/packages/auth/src/SocialLogins/GithubLogin/GithubIcon.tsx b/packages/auth/src/SocialLogins/GithubLogin/GithubIcon.tsx new file mode 100644 index 000000000..236596d78 --- /dev/null +++ b/packages/auth/src/SocialLogins/GithubLogin/GithubIcon.tsx @@ -0,0 +1,9 @@ +import React, { FC } from 'react'; + +export const GithubIcon: FC<React.SVGProps<SVGSVGElement>> = (props) => { + return ( + <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white' {...props}> + <path d='M12 0C5.374 0 0 5.373 0 12c0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.509 11.509 0 0112 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12z' /> + </svg> + ); +}; diff --git a/packages/auth/src/SocialLogins/GithubLogin/index.tsx b/packages/auth/src/SocialLogins/GithubLogin/index.tsx new file mode 100644 index 000000000..5af7503b9 --- /dev/null +++ b/packages/auth/src/SocialLogins/GithubLogin/index.tsx @@ -0,0 +1,44 @@ +import React, { FC, useCallback } from 'react'; +import { SocialLoginButton } from '../SocialLoginButton'; +import { GithubIcon } from './GithubIcon'; +import { FronteggContext, SocialLoginProviders } from '@frontegg/rest-api'; +import { UrlCreatorConfigType, useRedirectUrl, useSocialLoginContext } from '../hooks'; + +const createGithubUrl = ({ clientId, redirectUrl, state }: UrlCreatorConfigType): string => { + const searchParams: URLSearchParams = new URLSearchParams({ + client_id: clientId, + redirect_uri: redirectUrl, + scope: 'read:user user:email', + state, + }); + const url: URL = new URL('https://github.com/login/oauth/authorize'); + url.search = searchParams.toString(); + return url.toString(); +}; + +const GithubLogin: FC = (props) => { + const { action, state, isValid } = useSocialLoginContext(); + + const redirectUrl: string | null = useRedirectUrl(createGithubUrl, SocialLoginProviders.Github, state); + + const defaultButton = ( + <SocialLoginButton name={SocialLoginProviders.Github} action={action}> + <GithubIcon /> + </SocialLoginButton> + ); + + const handleLogin = useCallback(async () => { + const valid = (await isValid?.()) ?? true; + if (redirectUrl && valid) { + FronteggContext.onRedirectTo(redirectUrl, { replace: true, refresh: true }); + } + }, [redirectUrl, isValid]); + + if (redirectUrl) { + return <div onClick={handleLogin}>{props.children || defaultButton}</div>; + } + + return null; +}; + +export default GithubLogin; diff --git a/packages/auth/src/SocialLogins/GoogleLogin/GoogleIcon.tsx b/packages/auth/src/SocialLogins/GoogleLogin/GoogleIcon.tsx new file mode 100644 index 000000000..638e1c3dd --- /dev/null +++ b/packages/auth/src/SocialLogins/GoogleLogin/GoogleIcon.tsx @@ -0,0 +1,11 @@ +import React, { FC } from 'react'; + +export const GoogleIcon: FC = () => { + return ( + <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48' preserveAspectRatio='none' fill='white'> + <g> + <path d='M 25.996094 48 C 13.3125 48 2.992188 37.683594 2.992188 25 C 2.992188 12.316406 13.3125 2 25.996094 2 C 31.742188 2 37.242188 4.128906 41.488281 7.996094 L 42.261719 8.703125 L 34.675781 16.289063 L 33.972656 15.6875 C 31.746094 13.78125 28.914063 12.730469 25.996094 12.730469 C 19.230469 12.730469 13.722656 18.234375 13.722656 25 C 13.722656 31.765625 19.230469 37.269531 25.996094 37.269531 C 30.875 37.269531 34.730469 34.777344 36.546875 30.53125 L 24.996094 30.53125 L 24.996094 20.175781 L 47.546875 20.207031 L 47.714844 21 C 48.890625 26.582031 47.949219 34.792969 43.183594 40.667969 C 39.238281 45.53125 33.457031 48 25.996094 48 Z' /> + </g> + </svg> + ); +}; diff --git a/packages/auth/src/SocialLogins/GoogleLogin/index.tsx b/packages/auth/src/SocialLogins/GoogleLogin/index.tsx new file mode 100644 index 000000000..59eadb1fc --- /dev/null +++ b/packages/auth/src/SocialLogins/GoogleLogin/index.tsx @@ -0,0 +1,47 @@ +import React, { FC } from 'react'; +import { FronteggContext, SocialLoginProviders } from '@frontegg/rest-api'; +import { SocialLoginButton } from '../SocialLoginButton'; +import { GoogleIcon } from './GoogleIcon'; +import { UrlCreatorConfigType, useRedirectUrl, useSocialLoginContext } from '../hooks'; +import { useCallback } from 'react'; + +const createGoogleUrl = ({ clientId, redirectUrl, state }: UrlCreatorConfigType): string => { + const searchParams: URLSearchParams = new URLSearchParams({ + client_id: clientId, + redirect_uri: redirectUrl, + response_type: 'code', + include_granted_scopes: 'true', + scope: 'https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email', + state, + }); + const url: URL = new URL('https://accounts.google.com/o/oauth2/v2/auth'); + url.search = searchParams.toString(); + return url.toString(); +}; + +const LoginWithGoogle: FC = (props) => { + const { action, state, isValid } = useSocialLoginContext(); + + const redirectUrl = useRedirectUrl(createGoogleUrl, SocialLoginProviders.Google, state); + + const defaultButton = ( + <SocialLoginButton action={action} name={SocialLoginProviders.Google}> + <GoogleIcon /> + </SocialLoginButton> + ); + + const handleLogin = useCallback(() => { + const valid = isValid?.() ?? true; + if (redirectUrl && valid) { + FronteggContext.onRedirectTo(redirectUrl, { refresh: true }); + } + }, [redirectUrl, isValid]); + + if (redirectUrl) { + return <div onClick={handleLogin}>{props.children || defaultButton}</div>; + } + + return null; +}; + +export default LoginWithGoogle; diff --git a/packages/auth/src/SocialLogins/MicrosoftLogin/MicrosoftIcon.tsx b/packages/auth/src/SocialLogins/MicrosoftLogin/MicrosoftIcon.tsx new file mode 100644 index 000000000..2b2794351 --- /dev/null +++ b/packages/auth/src/SocialLogins/MicrosoftLogin/MicrosoftIcon.tsx @@ -0,0 +1,12 @@ +import React, { FC } from 'react'; + +export const MicrosoftIcon: FC<React.SVGProps<SVGSVGElement>> = (props) => { + return ( + <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 19 19' fill='white' {...props}> + <rect x='0' y='0' width='9' height='9' /> + <rect x='0' y='10' width='9' height='9' /> + <rect x='10' y='0' width='9' height='9' /> + <rect x='10' y='10' width='9' height='9' /> + </svg> + ); +}; diff --git a/packages/auth/src/SocialLogins/MicrosoftLogin/index.tsx b/packages/auth/src/SocialLogins/MicrosoftLogin/index.tsx new file mode 100644 index 000000000..a2286c020 --- /dev/null +++ b/packages/auth/src/SocialLogins/MicrosoftLogin/index.tsx @@ -0,0 +1,54 @@ +import { FronteggContext, SocialLoginProviders } from '@frontegg/rest-api'; +import React, { FC, useCallback } from 'react'; +import { v4 as uuid } from 'uuid'; +import { FRONTEGG_CODE_VERIFIER } from '../../constants'; +import { UrlCreatorConfigType, useRedirectUrl, useSocialLoginContext } from '../hooks'; +import { SocialLoginButton } from '../SocialLoginButton'; +import { MicrosoftIcon } from './MicrosoftIcon'; + +const codeVerifier = `${uuid()}${uuid()}`; + +const createMicrosoftUrl = ({ clientId, redirectUrl, state }: UrlCreatorConfigType): string => { + localStorage.setItem(FRONTEGG_CODE_VERIFIER, codeVerifier); + + const searchParams: URLSearchParams = new URLSearchParams({ + client_id: clientId, + response_type: 'code', + redirect_uri: redirectUrl, + response_mode: 'query', + scope: 'openid profile email', + code_challenge: codeVerifier, + state, + code_challenge_type: 'S256', + }); + const url: URL = new URL('https://login.microsoftonline.com/common/oauth2/v2.0/authorize'); + url.search = searchParams.toString(); + return url.toString(); +}; + +const LoginWithMicrosoft: FC = (props) => { + const { action, state, isValid } = useSocialLoginContext(); + + const redirectUrl = useRedirectUrl(createMicrosoftUrl, SocialLoginProviders.Microsoft, state); + + const defaultButton = ( + <SocialLoginButton action={action} name={SocialLoginProviders.Microsoft}> + <MicrosoftIcon /> + </SocialLoginButton> + ); + + const handleLogin = useCallback(async () => { + const valid = (await isValid?.()) ?? true; + if (redirectUrl && valid) { + FronteggContext.onRedirectTo(redirectUrl, { refresh: true }); + } + }, [redirectUrl, isValid]); + + if (redirectUrl) { + return <div onClick={handleLogin}>{props.children || defaultButton}</div>; + } + + return null; +}; + +export default LoginWithMicrosoft; diff --git a/packages/auth/src/SocialLogins/SocailLoginsSuccess.tsx b/packages/auth/src/SocialLogins/SocailLoginsSuccess.tsx new file mode 100644 index 000000000..13d7a40cf --- /dev/null +++ b/packages/auth/src/SocialLogins/SocailLoginsSuccess.tsx @@ -0,0 +1,94 @@ +import React, { FC, useEffect } from 'react'; +import { Button, Loader, useT } from '@frontegg/react-core'; +import { useLocation } from 'react-router-dom'; +import { ISocialLoginCallbackState, SocialLoginsActions } from './types'; +import { authPageWrapper } from '../components'; +import { useAuthRoutes, useOnRedirectTo, useSocialLoginActions, useSocialLoginState } from '@frontegg/react-hooks/auth'; +import { useRedirectUri } from './hooks'; +import { FRONTEGG_CODE_VERIFIER } from '../constants'; +import { useHistory } from 'react-router-dom'; +import { parse } from 'uuid'; + +export const SocialLoginsSuccess: FC = () => { + const routes = useAuthRoutes(); + const onRedirectTo = useOnRedirectTo(); + const socialLoginState = useSocialLoginState(); + const { resetSocialLoginsState, setSocialLoginError, loginViaSocialLogin } = useSocialLoginActions(); + const location = useLocation(); + const { t } = useT(); + const redirectUri = useRedirectUri(); + const { replace: historyReplace } = useHistory(); + + useEffect((): void => { + const params: URLSearchParams = new URLSearchParams(location.search); + const state = params.get('state'); + const code = params.get('code'); + const codeVerifier: any = localStorage.getItem(FRONTEGG_CODE_VERIFIER); + localStorage.removeItem(FRONTEGG_CODE_VERIFIER); + + let parsedState: ISocialLoginCallbackState; + const error = t('auth.social-logins.error.invalid-callback-url'); + + if (!state || !code) { + setSocialLoginError({ error }); + return; + } + + try { + parsedState = JSON.parse(state); + if (parsedState.afterAuthRedirectUrl) + historyReplace({ pathname: location.pathname, search: `?redirectUrl=${parsedState.afterAuthRedirectUrl}` }); + } catch (e) { + setSocialLoginError({ error }); + return; + } + + if (!parsedState.action || !parsedState.provider) { + setSocialLoginError({ error }); + return; + } + + switch (parsedState.action) { + case SocialLoginsActions.Login: + case SocialLoginsActions.SignUp: + loginViaSocialLogin({ + code, + redirectUri, + codeVerifier, + provider: parsedState.provider, + metadata: JSON.stringify({ + allowMarketingMaterial: parsedState.allowMarketingMaterial, + acceptedTermsOfService: parsedState.acceptedTermsOfService, + }), + }); + break; + default: + setSocialLoginError({ error }); + } + }, []); + + if (socialLoginState.firstLoad || socialLoginState.loading) { + return ( + <div className={'fe-center'}> + <Loader /> + </div> + ); + } + + return ( + <> + <div className='fe-error-message'>{socialLoginState.error}</div> + <Button + fullWidth={true} + onClick={() => { + resetSocialLoginsState(); + onRedirectTo(routes.loginUrl); + }} + > + {t('auth.login.back-to-login')} + </Button> + </> + ); +}; + +export const SocialLoginsSuccessPageComponent = authPageWrapper(SocialLoginsSuccess); diff --git a/packages/auth/src/SocialLogins/SocialLoginButton.tsx b/packages/auth/src/SocialLogins/SocialLoginButton.tsx new file mode 100644 index 000000000..240d2bfc7 --- /dev/null +++ b/packages/auth/src/SocialLogins/SocialLoginButton.tsx @@ -0,0 +1,30 @@ +import React, { FC } from 'react'; +import { fronteggElements as FE, useT } from '@frontegg/react-core'; +import { SocialLoginProviders } from '@frontegg/rest-api'; +import { SocialLoginsActions } from './types'; +import classNames from 'classnames'; + +export interface SocialLoginButtonProps { + name: SocialLoginProviders; + action: SocialLoginsActions; +} + +export const SocialLoginButton: FC<SocialLoginButtonProps> = (props) => { + const { t } = useT(); + const { name, children, action } = props; + + const providerName: string = name.charAt(0).toUpperCase() + name.slice(1); + + return ( + <FE.Button + className={`fe-social-login__button fe-social-login__button__${name.toLowerCase()}`} + data-test-id={`${name.toLowerCase()}SocialLogin-btn`} + fullWidth={true} + > + <div className={'fe-row fe-center'}> + <div>{children}</div> + <div>{t(`auth.social-logins.${action.toLowerCase()}.button-text`, { providerName })}</div> + </div> + </FE.Button> + ); +}; diff --git a/packages/auth/src/SocialLogins/SocialLoginContext.ts b/packages/auth/src/SocialLogins/SocialLoginContext.ts new file mode 100644 index 000000000..9e987a013 --- /dev/null +++ b/packages/auth/src/SocialLogins/SocialLoginContext.ts @@ -0,0 +1,4 @@ +import { createContext } from 'react'; +import { ISocialLoginsContext, SocialLoginsActions } from './types'; + +export const SocialLoginsContext = createContext<ISocialLoginsContext>({ action: SocialLoginsActions.Login }); diff --git a/packages/auth/src/SocialLogins/SocialLogins.tsx b/packages/auth/src/SocialLogins/SocialLogins.tsx new file mode 100644 index 000000000..502baea01 --- /dev/null +++ b/packages/auth/src/SocialLogins/SocialLogins.tsx @@ -0,0 +1,57 @@ +import React, { FC, ReactNode, useEffect } from 'react'; +import { Loader } from '@frontegg/react-core'; +import { ISocialLoginCallbackState, SocialLoginsActions } from './types'; +import GoogleLogin from './GoogleLogin'; +import GithubLogin from './GithubLogin'; +import MicrosoftLogin from './MicrosoftLogin'; +import { SocialLoginsContext } from './SocialLoginContext'; +import FacebookLogin from './FacebookLogin'; +import { useSocialLoginActions, useSocialLoginState } from '@frontegg/react-hooks/auth'; + +export interface SocialLoginsProps { + action: SocialLoginsActions; + children?: ReactNode; + state?: Partial<ISocialLoginCallbackState>; + isValid?: () => boolean; +} + +export type SocialLoginsWithCompoundComponents = FC<SocialLoginsProps> & { + Google: FC; + Github: FC; + Microsoft: FC; + Facebook: FC; +}; + +export const SocialLogins: SocialLoginsWithCompoundComponents = (props: SocialLoginsProps) => { + const { firstLoad, socialLoginsConfig, error } = useSocialLoginState(); + const { loadSocialLoginsConfiguration } = useSocialLoginActions(); + + useEffect(() => { + if (firstLoad) { + loadSocialLoginsConfiguration(); + } + }, [firstLoad]); + + if (error) { + return <div className='fe-error-message'>{error}</div>; + } + + if (firstLoad) { + return <Loader />; + } + + if (!socialLoginsConfig?.length || !socialLoginsConfig.some(({ active }) => active)) { + return null; + } + + return ( + <SocialLoginsContext.Provider value={{ action: props.action, state: props.state, isValid: props.isValid }}> + {props.children} + </SocialLoginsContext.Provider> + ); +}; + +SocialLogins.Google = GoogleLogin; +SocialLogins.Github = GithubLogin; +SocialLogins.Microsoft = MicrosoftLogin; +SocialLogins.Facebook = FacebookLogin; diff --git a/packages/auth/src/SocialLogins/SocialLoginsWithWrapper.tsx b/packages/auth/src/SocialLogins/SocialLoginsWithWrapper.tsx new file mode 100644 index 000000000..c0467f13c --- /dev/null +++ b/packages/auth/src/SocialLogins/SocialLoginsWithWrapper.tsx @@ -0,0 +1,38 @@ +import React, { FC } from 'react'; +import { SocialLogins, SocialLoginsProps } from './SocialLogins'; +import { ISocialLoginCallbackState, SocialLoginsActions } from './types'; + +export const SocialLoginsWithWrapper: FC<SocialLoginsProps> = (props) => { + return ( + <> + <div className={'fe-col fe-center'}> + <SocialLogins action={props.action} state={props.state} isValid={props.isValid}> + <div className={'fe-social-login__or-container'}> + <span>OR</span> + </div> + {props.children || ( + <> + <SocialLogins.Google /> + <SocialLogins.Github /> + <SocialLogins.Microsoft /> + <SocialLogins.Facebook /> + </> + )} + </SocialLogins> + </div> + </> + ); +}; + +export interface SocialLoginActionWrapperProps { + state?: Partial<ISocialLoginCallbackState>; + isValid?: () => boolean; +} + +export const SocialLoginsLoginWithWrapper: FC<SocialLoginActionWrapperProps> = ({ state }) => { + return <SocialLoginsWithWrapper action={SocialLoginsActions.Login} state={state} />; +}; + +export const SocialLoginsSignUpWithWrapper: FC<SocialLoginActionWrapperProps> = ({ state, isValid }) => { + return <SocialLoginsWithWrapper action={SocialLoginsActions.SignUp} state={state} isValid={isValid} />; +}; diff --git a/packages/auth/src/SocialLogins/hooks.ts b/packages/auth/src/SocialLogins/hooks.ts new file mode 100644 index 000000000..7ff6209e4 --- /dev/null +++ b/packages/auth/src/SocialLogins/hooks.ts @@ -0,0 +1,64 @@ +import { useContext, useMemo } from 'react'; +import { ISocialLoginProviderConfiguration, SocialLoginProviders } from '@frontegg/rest-api'; +import { ISocialLoginCallbackState, ISocialLoginsContext } from './types'; +import { useAuthRoutes, useSocialLoginState } from '@frontegg/react-hooks/auth'; +import { SocialLoginsContext } from './SocialLoginContext'; + +export type UrlCreatorConfigType = ISocialLoginProviderConfiguration & { state: string }; + +export const createSocialLoginState = (state: ISocialLoginCallbackState): string => JSON.stringify(state); + +export const useRedirectUri = (): string => { + const routes = useAuthRoutes(); + return useMemo<string>(() => { + return `${window.location.origin}${routes.socialLoginCallbackUrl}`; + }, [window.location.origin, routes.socialLoginCallbackUrl]); +}; + +export const useRedirectUrl = ( + urlCreator: (config: UrlCreatorConfigType) => string, + socialLoginType: any, + state?: Partial<ISocialLoginCallbackState> +): string | null => { + const { action } = useSocialLoginContext(); + const { socialLoginsConfig } = useSocialLoginState(); + const config = useMemo( + () => socialLoginsConfig?.find(({ type }) => type.toLowerCase() === socialLoginType.toLowerCase()), + [socialLoginsConfig] + ); + + const redirectUri = useRedirectUri(); + + const redirectUrl: string | undefined = useMemo(() => { + const url = new URL(window?.location.href); + const afterAuthRedirectUrl = url.searchParams.get('redirectUrl'); + + if (config) { + return urlCreator({ + ...config, + redirectUrl: redirectUri, + state: createSocialLoginState({ + provider: socialLoginType, + action, + afterAuthRedirectUrl: afterAuthRedirectUrl || undefined, + ...state, + }), + } as any); + } + }, [config?.clientId, config?.redirectUrl, action, state]); + + if (!config?.active || !redirectUrl) { + return null; + } + + return redirectUrl; +}; + +export const useSocialLoginContext = (): ISocialLoginsContext => { + const context = useContext(SocialLoginsContext); + + if (!context) { + throw new Error('Social Login compound component cannot be rendered outside SocialLogins component'); + } + return context; +}; diff --git a/packages/auth/src/SocialLogins/index.tsx b/packages/auth/src/SocialLogins/index.tsx new file mode 100644 index 000000000..9d50719f1 --- /dev/null +++ b/packages/auth/src/SocialLogins/index.tsx @@ -0,0 +1,7 @@ +export * from './SocialLoginsWithWrapper'; +export * from './SocialLogins'; +export * from './SocailLoginsSuccess'; +export * from './SocialLoginContext'; +export * from './types'; + +export * from './hooks'; diff --git a/packages/auth/src/SocialLogins/types.ts b/packages/auth/src/SocialLogins/types.ts new file mode 100644 index 000000000..d93639e7c --- /dev/null +++ b/packages/auth/src/SocialLogins/types.ts @@ -0,0 +1,21 @@ +import { SocialLoginProviders } from '@frontegg/rest-api'; + +export interface ISocialLoginCallbackState { + provider: SocialLoginProviders; + action: SocialLoginsActions; + afterAuthRedirectUrl?: string; + allowNotVerifiedUsersLogin?: boolean; + allowMarketingMaterial?: boolean; + acceptedTermsOfService?: boolean; +} + +export interface ISocialLoginsContext { + action: SocialLoginsActions; + state?: Partial<ISocialLoginCallbackState>; + isValid?: () => boolean; +} + +export enum SocialLoginsActions { + Login = 'login', + SignUp = 'signUp', +} diff --git a/packages/auth/src/Team/TeamAddUserDialog.tsx b/packages/auth/src/Team/TeamAddUserDialog.tsx new file mode 100644 index 000000000..f2bc3c34b --- /dev/null +++ b/packages/auth/src/Team/TeamAddUserDialog.tsx @@ -0,0 +1,131 @@ +import React, { FC, useEffect, useState } from 'react'; +import { + Button, + Dialog, + ErrorMessage, + FButton, + FForm, + FFormik, + FInput, + FSelect, + Grid, + useT, + validateEmail, + validateSchema, +} from '@frontegg/react-core'; +import { useAuthUserOrNull, useAuthTeamActions, useAuthTeamState } from '@frontegg/react-hooks/auth'; +import { checkRoleAccess } from './helpers'; + +type TRoles = { + label: string; + value: string; +}; + +const { Formik } = FFormik; + +type AddUserFormValues = { + name: string; + email: string; + roles: { label: string; value: string }[]; +}; +export const TeamAddUserDialog: FC = () => { + const user = useAuthUserOrNull(); + const [roleOptionsToDisplay, setRoleOptionsToDisplay] = useState<TRoles[]>([]); + const { open, error, loading, roles } = useAuthTeamState(({ addUserDialogState, roles }) => ({ + ...addUserDialogState, + roles, + })); + const { addUser, closeAddUserDialog } = useAuthTeamActions(); + const { t } = useT(); + + useEffect(() => { + const rolesWithAccess = checkRoleAccess(roles, user); + setRoleOptionsToDisplay(rolesWithAccess); + }, [roles]); + + const initialValues: AddUserFormValues = { + name: '', + email: '', + roles: [], + }; + + return ( + <Dialog open={open} size={'tiny'} onClose={closeAddUserDialog} header={t('auth.team.add-dialog.title')}> + <Formik + validationSchema={validateSchema({ + email: validateEmail(t), + })} + initialValues={initialValues} + onSubmit={({ name, email, roles }, { setSubmitting }) => { + setSubmitting(true); + addUser({ + name, + email, + roleIds: roles.map((v) => v.value), + callback: () => setSubmitting(false), + }); + }} + > + <FForm> + <FInput + label={t('common.name')} + size='large' + name='name' + disabled={loading} + placeholder={t('common.enter-name')} + data-test-id='name-box' + /> + <FInput + label={t('common.email')} + size='large' + name='email' + disabled={loading} + placeholder={t('common.enter-email')} + data-test-id='email-box' + /> + {!!roleOptionsToDisplay.length && ( + <FSelect + size='large' + label={t('common.roles')} + multiselect + name='roles' + disabled={loading} + placeholder={t('common.select')} + options={roleOptionsToDisplay} + data-test-id='roles-dropdown' + /> + )} + <ErrorMessage error={error} /> + <div className='fe-dialog__footer'> + <Grid container> + <Grid xs item> + <Button + size='large' + isCancel + fullWidth={false} + disabled={loading} + onClick={() => closeAddUserDialog()} + data-test-id='x-btn' + > + {t('common.cancel')} + </Button> + </Grid> + <Grid xs item className='fe-text-align-end'> + <FButton + type='submit' + size='large' + fullWidth={false} + variant='primary' + loading={loading} + data-test-id='invite-btn' + > + {t('common.invite')} + </FButton> + </Grid> + </Grid> + </div> + </FForm> + </Formik> + </Dialog> + ); +}; diff --git a/packages/auth/src/Team/TeamDeleteUserDialog.tsx b/packages/auth/src/Team/TeamDeleteUserDialog.tsx new file mode 100644 index 000000000..8cce42168 --- /dev/null +++ b/packages/auth/src/Team/TeamDeleteUserDialog.tsx @@ -0,0 +1,59 @@ +import React, { FC, useCallback } from 'react'; +import { Button, Dialog, ErrorMessage, Grid, useT } from '@frontegg/react-core'; +import { useAuthTeamActions, useAuthTeamState, useAuthUserOrNull } from '@frontegg/react-hooks/auth'; + +export interface TeamDeleteUserDialogProps { + open?: boolean; +} + +export const TeamDeleteUserDialog: FC<TeamDeleteUserDialogProps> = () => { + const { t } = useT(); + const user = useAuthUserOrNull(); + const { open, error, loading, userId, email } = useAuthTeamState( + ({ deleteUserDialogState }) => deleteUserDialogState + ); + const { deleteUser, closeDeleteUserDialog } = useAuthTeamActions(); + const isMe = user?.id === userId; + + const handleDeleteUser = useCallback(() => { + userId && deleteUser({ userId }); + }, [deleteUser, userId]); + + const isOpen = !!userId && open; + + return ( + <Dialog open={isOpen} size={'tiny'} onClose={closeDeleteUserDialog} header={t('auth.team.deleteDialog.title')}> + <p>{t('auth.team.deleteDialog.message', { email: isMe ? t('common.yourself') : email })}</p> + <ErrorMessage error={error} /> + + <div className='fe-dialog__footer'> + <Grid container> + <Grid xs item> + <Button + size='large' + isCancel + fullWidth={false} + disabled={loading} + onClick={() => closeDeleteUserDialog()} + data-test-id='cancel-btn' + > + {t('common.cancel')} + </Button> + </Grid> + <Grid xs item className='fe-text-align-end'> + <Button + size='large' + fullWidth={false} + variant='danger' + loading={loading} + onClick={handleDeleteUser} + data-test-id='delete-btn' + > + {isMe ? t('auth.team.leaveTeam') : t('common.delete')} + </Button> + </Grid> + </Grid> + </div> + </Dialog> + ); +}; diff --git a/packages/auth/src/Team/TeamHeader.tsx b/packages/auth/src/Team/TeamHeader.tsx new file mode 100644 index 000000000..77d039e8d --- /dev/null +++ b/packages/auth/src/Team/TeamHeader.tsx @@ -0,0 +1,19 @@ +import React, { FC } from 'react'; +import { PageHeader, PageHeaderProps, useT } from '@frontegg/react-core'; +import classNames from 'classnames'; +import { useAuth } from '@frontegg/react-hooks/auth'; + +export type TeamHeaderProps = PageHeaderProps; + +export const TeamHeader: FC<TeamHeaderProps> = (props) => { + const { t } = useT(); + const { loaders, totalItems } = useAuth((state) => state.teamState); + const customProps: Partial<PageHeaderProps> = { + className: classNames('fe-team__header', props.className), + title: props.title ?? t('auth.team.title'), + subTitle: + props.subTitle ?? + (loaders.USERS ? t('common.loading') : t('auth.team.subtitle', { totalItems: `${totalItems ?? 0}` })), + }; + return <PageHeader {...props} {...customProps} />; +}; diff --git a/packages/auth/src/Team/TeamLayout.tsx b/packages/auth/src/Team/TeamLayout.tsx new file mode 100644 index 000000000..278d29a3d --- /dev/null +++ b/packages/auth/src/Team/TeamLayout.tsx @@ -0,0 +1,17 @@ +import React, { FC } from 'react'; +import { TeamTableToolbar } from './TeamTableToolbar'; +import { TeamTable } from './TeamTable'; +import { TeamAddUserDialog } from './TeamAddUserDialog'; +import { TeamDeleteUserDialog } from './TeamDeleteUserDialog'; + +export const TeamLayout: FC = (props) => { + const children = props.children ?? ( + <> + <TeamTableToolbar /> + <TeamTable /> + <TeamAddUserDialog /> + <TeamDeleteUserDialog /> + </> + ); + return <div className='fe-team__layout'>{children}</div>; +}; diff --git a/packages/auth/src/Team/TeamPage.tsx b/packages/auth/src/Team/TeamPage.tsx new file mode 100644 index 000000000..3d18f247a --- /dev/null +++ b/packages/auth/src/Team/TeamPage.tsx @@ -0,0 +1,25 @@ +import React, { FC, useMemo } from 'react'; +import { checkValidChildren, useProxyComponent, ProxyComponent } from '@frontegg/react-core'; +import { BasePageProps } from '../interfaces'; +import { TeamLayout } from './TeamLayout'; +import { TeamHeader } from './TeamHeader'; + +export type TeamPageProps = BasePageProps & ProxyComponent; + +export const TeamPage: FC<TeamPageProps> = (props) => { + const proxyPortals = useProxyComponent(props); + useMemo(() => checkValidChildren('Team.Page', 'Team', props.children, { TeamLayout }), [props.children]); + + const children = props.children ?? ( + <> + <TeamHeader /> + <TeamLayout /> + </> + ); + return ( + <> + <div className='fe-team__page'>{children}</div> + {proxyPortals} + </> + ); +}; diff --git a/packages/auth/src/Team/TeamTable.tsx b/packages/auth/src/Team/TeamTable.tsx new file mode 100644 index 000000000..1e67a590e --- /dev/null +++ b/packages/auth/src/Team/TeamTable.tsx @@ -0,0 +1,121 @@ +import React, { FC, useEffect, useMemo, useState } from 'react'; +import { Table, TableColumnProps, useT } from '@frontegg/react-core'; +import { TeamState } from '@frontegg/redux-store/auth'; +import { useAuthUserOrNull, useAuthTeamActions, useAuthTeamState } from '@frontegg/react-hooks/auth'; +import { + TeamTableActions, + TeamTableAvatarCell, + TeamTableDescriptionCell, + TeamTableJoinedTeam, + TeamTableLastLogin, + TeamTableTitleCell, + TeamTableRoles, +} from './TeamTableCells'; +import { checkRoleAccess } from './helpers'; + +type TRoles = { + label: string; + value: string; +}; + +const stateMapper = ({ users, loaders, totalItems, pageSize, totalPages, errors, roles, sort, filter }: TeamState) => ({ + users, + loaders, + totalItems, + pageSize, + totalPages, + errors, + roles, + sort, + filter, +}); + +export const TeamTable: FC = () => { + const [roleOptionsToDisplay, setRoleOptionsToDisplay] = useState<TRoles[]>([]); + const { users, loaders, totalItems, sort, pageSize, totalPages, roles } = useAuthTeamState(stateMapper); + const { loadUsers } = useAuthTeamActions(); + const user = useAuthUserOrNull(); + const { t } = useT(); + + useEffect(() => { + loadUsers({ pageOffset: 0 }); + }, [loadUsers]); + + useEffect(() => { + const rolesWithAccess = checkRoleAccess(roles, user); + setRoleOptionsToDisplay(rolesWithAccess); + }, [roles, setRoleOptionsToDisplay]); + + const teamTableColumns: TableColumnProps[] = useMemo( + () => [ + { + accessor: 'profileImage', + minWidth: '2.25rem', + maxWidth: '2.25rem', + Cell: TeamTableAvatarCell, + }, + { + accessor: 'name', + Header: t('common.name') ?? '', + sortable: true, + Cell: TeamTableTitleCell(user?.id, t('common.me')), + }, + { + accessor: 'email', + Header: t('common.email') ?? '', + sortable: true, + Cell: TeamTableDescriptionCell, + }, + ...(roles.length > 0 + ? [ + { + accessor: 'roleIds', + minWidth: 220, + Header: t('common.roles') ?? '', + Cell: TeamTableRoles( + roles.map((r) => ({ label: r.name, value: r.id })), + roleOptionsToDisplay + ), + }, + ] + : []), + { + accessor: 'createdAt', + Header: t('common.joinedTeam') ?? '', + sortable: true, + Cell: TeamTableJoinedTeam(t('common.pendingApproval')), + }, + { + accessor: 'lastLogin', + Header: t('common.lastLogin') ?? '', + sortable: true, + Cell: TeamTableLastLogin, + }, + { + id: 'actions', + minWidth: '3.25rem', + maxWidth: '3.25rem', + Cell: TeamTableActions(user?.id), + }, + ], + [roles, roleOptionsToDisplay] + ); + + return ( + <div className='fe-team__table'> + <Table + data={users} + totalData={totalItems || users.length} + columns={teamTableColumns} + rowKey={'id'} + pageSize={pageSize} + pageCount={totalPages || 1} + pagination='pages' + loading={!!loaders.USERS} + sortBy={sort} + onSortChange={(sortBy) => loadUsers({ sort: sortBy, pageOffset: 0 })} + onPageChange={(pageSize, pageOffset) => loadUsers({ pageSize, pageOffset })} + /> + </div> + ); +}; diff --git a/packages/auth/src/Team/TeamTableCells.tsx b/packages/auth/src/Team/TeamTableCells.tsx new file mode 100644 index 000000000..961432624 --- /dev/null +++ b/packages/auth/src/Team/TeamTableCells.tsx @@ -0,0 +1,192 @@ +import { + Button, + CellComponent, + Checkbox, + Icon, + Menu, + MenuItem, + MenuItemProps, + Popup, + TableCells, + Tag, + useT, +} from '@frontegg/react-core'; +import React, { useCallback, useMemo } from 'react'; +import { useAuthTeamActions, useAuthTeamState } from '@frontegg/react-hooks/auth'; +import classNames from 'classnames'; + +const LEAVE_TEAM_OPTION = false; +const numberPermissionsToShow = 3; + +export const TeamTableAvatarCell: CellComponent = TableCells.Avatar; +export const TeamTableTitleCell = (me?: string, meText?: string): CellComponent => (props) => { + const value = `${props.value} ${props.row.original.id === me ? meText : ''}`; + return <TableCells.Title {...props} value={value} />; +}; +export const TeamTableDescriptionCell: CellComponent = TableCells.Description; +export const TeamTableJoinedTeam = (pendingText: string): CellComponent => (props) => { + const { + row: { + original: { lastLogin }, + }, + } = props; + if (lastLogin) { + return <TableCells.DateAgo {...props} />; + } else { + return ( + <Tag variant='primary' size='small'> + {pendingText} + </Tag> + ); + } +}; +export const TeamTableLastLogin: CellComponent = TableCells.DateAgo; + +export const TeamTableActions = (me?: string): CellComponent => (props) => { + const { id: userId, email, lastLogin } = props.row.original; + const { t } = useT(); + const { resendActivationLink, openDeleteUserDialog } = useAuthTeamActions(); + const loaders = useAuthTeamState((state) => state.loaders); + const resendLoading = loaders.RESEND_ACTIVATE_LINK || loaders.UPDATE_USER || loaders.DELETE_USER; + const deleteLoading = + typeof loaders.DELETE_USER === 'string' ? loaders.DELETE_USER === userId : !!loaders.DELETE_USER; + const isMe = me === userId; + + const handleSendActivationLink = useCallback(() => { + resendActivationLink({ userId }); + }, [userId]); + + const handleDeleteUser = useCallback(() => { + openDeleteUserDialog({ userId, email }); + }, [userId, email]); + + const items: MenuItemProps[] = useMemo(() => { + const items = []; + + if (!lastLogin) { + items.push({ + icon: <Icon name='send' />, + onClick: handleSendActivationLink, + text: t('auth.team.resendActivation'), + loading: !!resendLoading, + }); + } + if (!isMe || LEAVE_TEAM_OPTION) { + items.push({ + icon: <Icon name='delete' />, + onClick: handleDeleteUser, + text: isMe ? t('auth.team.leaveTeam') : t('auth.team.deleteUser'), + iconClassName: 'fe-color-danger', + loading: !!deleteLoading, + }); + } + return items; + }, [lastLogin, isMe, resendLoading, deleteLoading]); + + return ( + <div + style={{ + minWidth: props.column.minWidth, + maxWidth: props.column.maxWidth, + }} + > + {items.length > 0 && ( + <Menu + items={items} + trigger={ + <Button iconButton size='small' transparent> + <Icon name='vertical-dots' data-test-id='dots-btn' /> + </Button> + } + /> + )} + </div> + ); +}; + +type TRoles = { + label: string; + value: string; +}; + +export const TeamTableRoles = (allRolesOptions?: TRoles[], roleOptionsToDisplay?: TRoles[]): CellComponent => ( + props +) => { + const { t } = useT(); + const { id: userId } = props.row.original; + const { updateUser } = useAuthTeamActions(); + const { loading } = useAuthTeamState(({ loaders }) => ({ + loading: loaders.UPDATE_USER, + })); + const permissions = allRolesOptions?.filter((role) => props.value.indexOf(role.value) !== -1) || []; + + const checked = useCallback((role) => permissions?.some((p) => p.value === role.value), [permissions]); + const onUpdateUser = useCallback( + (role: TRoles) => { + const { createdAt, customData, lastLogin, tenantId, vendorId, activatedForTenant, ...data } = props.row.original; + updateUser({ + ...data, + roleIds: checked(role) + ? [...props.row.original.roleIds.filter((r: string) => r !== role.value)] + : [...props.row.original.roleIds, role.value], + }); + }, + [props.row.original] + ); + + const permissionsToShow = useMemo(() => { + return permissions.slice(0, numberPermissionsToShow); + }, [permissions]); + + const permissionsCounter = useMemo(() => { + const permissionsToShowInCounter = permissions.length - numberPermissionsToShow; + if (permissions.length > numberPermissionsToShow) return `${permissionsToShowInCounter} ${t('common.more')}`; + }, [permissions]); + + return ( + <div className='fe-flex fe-full-width fe-flex-no-wrap'> + <div className='fe-flex'> + {permissionsToShow.map((permission) => ( + <Tag className='fe-mr-1 fe-mb-1 fe-mt-1' size='small' key={permission.value}> + {permission.label} + </Tag> + ))} + {permissionsCounter} + </div> + {!!roleOptionsToDisplay?.length && ( + <Popup + className='fe-team__roles-popup' + content={() => ( + <div + className={classNames('fe-team__roles-dropdown', { + 'fe-team__roles-dropdown-disabled': loading === userId, + })} + > + {roleOptionsToDisplay?.map((role) => ( + <MenuItem + key={role.label} + withIcons={true} + icon={<Checkbox checked={checked(role)} />} + onClick={loading === userId ? undefined : () => onUpdateUser(role)} + text={role.label} + /> + ))} + </div> + )} + action='click' + trigger={ + <Button + className='fe-team__roles-dropdown-button' + transparent + size='small' + iconButton + data-test-id='rolesDropDown-btn' + > + <Icon name='down-arrow' /> + </Button> + } + /> + )} + </div> + ); +}; diff --git a/packages/auth/src/Team/TeamTableFilters.tsx b/packages/auth/src/Team/TeamTableFilters.tsx new file mode 100644 index 000000000..b75d87e7f --- /dev/null +++ b/packages/auth/src/Team/TeamTableFilters.tsx @@ -0,0 +1,56 @@ +import { Button, FilterComponent, Grid, Select, useT } from '@frontegg/react-core'; +import React, { useMemo, useState } from 'react'; +import { useAuthTeamState } from '@frontegg/react-hooks/auth'; + +export const TeamRolesFilter: FilterComponent = ({ value, setFilterValue, closePopup }) => { + const { t } = useT(); + const { roles } = useAuthTeamState(({ roles }) => ({ roles })); + const roleOptions = useMemo(() => roles.map((role) => ({ label: role.name, value: role.id })), [roles]); + const [selectedRoles, setSelectedRoles] = useState<string[]>(value || []); + + return ( + <div className='fe-team__filter-popup-md'> + <div className='fe-team__filter-title fe-mb-2'>Filter by permissions</div> + <Select + fullWidth + size='small' + multiselect + value={selectedRoles} + onChange={(e, newValues) => { + setSelectedRoles(newValues); + }} + options={roleOptions} + /> + <div className='fe-mt-2'> + <Grid container spacing={2}> + <Grid item xs={5}> + <Button + data-test-id='options-btn' + fullWidth + onClick={() => { + setFilterValue(null); + closePopup?.(); + }} + > + {(value && value.length > 0) || selectedRoles.length > 0 ? t('common.clear') : t('common.cancel')} + </Button> + </Grid> + <Grid item xs={2} /> + <Grid item xs={5}> + <Button + data-test-id='selectroles-btn' + fullWidth + variant='primary' + onClick={() => { + setFilterValue(selectedRoles); + closePopup?.(); + }} + > + {t('common.filter')} + </Button> + </Grid> + </Grid> + </div> + </div> + ); +}; diff --git a/packages/auth/src/Team/TeamTableToolbar.tsx b/packages/auth/src/Team/TeamTableToolbar.tsx new file mode 100644 index 000000000..3af633efb --- /dev/null +++ b/packages/auth/src/Team/TeamTableToolbar.tsx @@ -0,0 +1,51 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { Button, Grid, Icon, Input, useT, useDebounce } from '@frontegg/react-core'; +import { useAuthTeamActions, useAuthTeamState } from '@frontegg/react-hooks/auth'; + +export const TeamTableToolbar = () => { + const { filter: filters } = useAuthTeamState((state) => ({ filter: state.filter || [] })); + const { openAddUserDialog, loadUsers } = useAuthTeamActions(); + const [inputValue, setInputValue] = useState<string | undefined>(undefined); + const searchValue = useDebounce(inputValue, 400); + const didMountRef = useRef(true); + const { t } = useT(); + + useEffect(() => { + if (didMountRef.current) { + didMountRef.current = false; + return; + } + const newFilters = []; + if (inputValue) { + newFilters.push({ + id: 'searchFilter', + value: searchValue, + }); + } + + loadUsers({ + pageOffset: 0, + filter: [...filters.filter((f) => f.id !== 'searchFilter'), ...newFilters], + }); + }, [searchValue]); + + return ( + <div className='fe-team__table-toolbar'> + <Grid container> + <Grid item md={3} xs={6}> + <Input + type={'search'} + placeholder={t('auth.team.search-users')} + fullWidth + onChange={(e) => setInputValue(e.target.value)} + /> + </Grid> + <Grid item md={9} xs={6} className='fe-text-align-end'> + <Button variant='primary' size='large' onClick={() => openAddUserDialog()} data-test-id='inviteUser-btn'> + {t('auth.team.invite-user')} <Icon className='fe-ml-1' name='person-add' /> + </Button> + </Grid> + </Grid> + </div> + ); +}; diff --git a/packages/auth/src/Team/helpers.ts b/packages/auth/src/Team/helpers.ts new file mode 100644 index 000000000..7ee884d9c --- /dev/null +++ b/packages/auth/src/Team/helpers.ts @@ -0,0 +1,38 @@ +import { ContextHolder, ITeamUserRole } from '@frontegg/rest-api'; +import { User } from '@frontegg/redux-store/auth'; + +type TRoles = { + label: string; + value: string; +}; + +const getRoleLevel = (roleId: string, roles: ITeamUserRole[]) => { + if (!roles) return Infinity; + const roleSettings = roles.find((role) => role.id === roleId); + return roleSettings?.permissionLevel ?? Infinity; +}; + +const getMaxRoleLevel = (roleIds: string[], roles: ITeamUserRole[]) => { + if (!roleIds) return Infinity; + // map roleIds array to numeric levels array, using provider roles settings + const levelsArr: number[] = roleIds.map((roleId) => getRoleLevel(roleId, roles)); + return levelsArr.length ? Math.min(...levelsArr) : Infinity; +}; + +export const checkRoleAccess = (roles: ITeamUserRole[], user: User | null): TRoles[] => { + const context = ContextHolder.getContext(); + let currnetUserRoleLevel: number; + const currentUserRolesIds = user?.roles.map((r) => r.id); + + if (context.currentUserRoles && context.currentUserRoles.length > 0) { + currnetUserRoleLevel = getMaxRoleLevel(context.currentUserRoles, roles); + } else if (currentUserRolesIds && currentUserRolesIds.length > 0) { + currnetUserRoleLevel = getMaxRoleLevel(currentUserRolesIds, roles); + } + + if (roles) { + const rolesWithAccess = roles.filter((role) => (role.permissionLevel ?? Infinity) >= currnetUserRoleLevel); + return rolesWithAccess.map((r) => ({ label: r.name, value: r.id })); + } + return []; +}; diff --git a/packages/auth/src/Team/index.ts b/packages/auth/src/Team/index.ts new file mode 100644 index 000000000..b867f369e --- /dev/null +++ b/packages/auth/src/Team/index.ts @@ -0,0 +1,17 @@ +import { TeamPage } from './TeamPage'; +import { TeamLayout } from './TeamLayout'; +import { TeamHeader } from './TeamHeader'; +import { TeamTableToolbar } from './TeamTableToolbar'; +import { TeamTable } from './TeamTable'; +import { TeamAddUserDialog } from './TeamAddUserDialog'; +import { TeamDeleteUserDialog } from './TeamDeleteUserDialog'; + +export const Team = { + Page: TeamPage, + Header: TeamHeader, + Layout: TeamLayout, + Table: TeamTable, + TableToolbar: TeamTableToolbar, + AddUserDialog: TeamAddUserDialog, + DeleteUserDialog: TeamDeleteUserDialog, +}; diff --git a/packages/auth/src/components/AuthRoutes.tsx b/packages/auth/src/components/AuthRoutes.tsx new file mode 100644 index 000000000..e48dd0406 --- /dev/null +++ b/packages/auth/src/components/AuthRoutes.tsx @@ -0,0 +1,210 @@ +import React, { FC, useMemo, createElement } from 'react'; +import { Route, Switch } from 'react-router-dom'; +import { Logger } from '@frontegg/react-core'; +import { AuthState } from '@frontegg/redux-store/auth'; +import { LoginPage, LogoutPage, LoginWithSSOPage, Login, Logout, LoginWithSSO } from '../Login'; +import { ActivateAccount, ActivateAccountPage } from '../ActivateAccount'; +import { AcceptInvitation, AcceptInvitationPage } from '../AcceptInvitation'; +import { ForgotPassword, ForgotPasswordPage } from '../ForgotPassword'; +import { ResetPassword, ResetPasswordPage } from '../ResetPassword'; +import { AuthPageProps } from '../interfaces'; +import { useAuth } from '@frontegg/react-hooks/auth'; +import { SocialLoginsSuccess, SocialLoginsSuccessPageComponent } from '../SocialLogins'; +import { SignUp, SignUpPageComponent } from '../SignUp'; + +const stateMapper = ({ routes, isLoading, header, loaderComponent, ssoACS }: AuthState) => ({ + routes, + isLoading, + header, + loaderComponent, + ssoACS, +}); + +const logger = Logger.from('AuthRoutes'); + +export const AuthRoutes: FC<AuthPageProps> = (props) => { + const { + header, + headerImg, + loaderComponent, + children, + pageComponent, + pageHeader, + pageProps: perPageProps, + ...rest + } = props; + const { routes, isLoading, header: defaultHeader, loaderComponent: defaultLoaderComponent, ssoACS } = useAuth( + stateMapper + ); + const defaultComps = { + header: defaultHeader, + loaderComponent: defaultLoaderComponent, + }; + + const samlCallbackPath = useMemo(() => { + const acsUrl = routes.samlCallbackUrl ?? ssoACS; + if (!isLoading && acsUrl) { + try { + return new URL(acsUrl).pathname; + } catch (e) { + return null; + } + } + return null; + }, [isLoading, ssoACS]); + + const pageProps = { + ...rest, + ...defaultComps, + ...(loaderComponent !== undefined ? { loaderComponent } : {}), + }; + + const computedPerPageProps = { + loginProps: { + ...perPageProps?.login, + header: perPageProps?.login?.header ?? pageHeader?.login ?? header ?? defaultComps.header, + headerImg: perPageProps?.login?.headerImg ?? headerImg, + }, + logoutProps: { + ...perPageProps?.logout, + header: perPageProps?.logout?.header ?? pageHeader?.logout ?? header ?? defaultComps.header, + headerImg: perPageProps?.logout?.headerImg ?? headerImg, + }, + forgotPasswordProps: { + ...perPageProps?.forgotPassword, + header: perPageProps?.forgotPassword?.header ?? pageHeader?.forgotPassword ?? header ?? defaultComps.header, + headerImg: perPageProps?.forgotPassword?.headerImg ?? headerImg, + }, + resetPasswordProps: { + ...perPageProps?.resetPassword, + header: perPageProps?.resetPassword?.header ?? pageHeader?.resetPassword ?? header ?? defaultComps.header, + headerImg: perPageProps?.resetPassword?.headerImg ?? headerImg, + }, + activateAccountProps: { + ...perPageProps?.activateAccount, + header: perPageProps?.activateAccount?.header ?? pageHeader?.activateAccount ?? header ?? defaultComps.header, + headerImg: perPageProps?.activateAccount?.headerImg ?? headerImg, + }, + acceptInvitationProps: { + ...perPageProps?.acceptInvitation, + header: perPageProps?.acceptInvitation?.header ?? pageHeader?.acceptInvitation ?? header ?? defaultComps.header, + headerImg: perPageProps?.acceptInvitation?.headerImg ?? headerImg, + }, + loginWithSSOProps: { + ...perPageProps?.loginWithSSO, + header: perPageProps?.loginWithSSO?.header ?? pageHeader?.loginWithSSO ?? header ?? defaultComps.header, + headerImg: perPageProps?.loginWithSSO?.headerImg ?? headerImg, + }, + socialLoginsSuccessProps: { + ...perPageProps?.socialLoginsSuccess, + header: + perPageProps?.socialLoginsSuccess?.header ?? pageHeader?.socialLoginsSuccess ?? header ?? defaultComps.header, + headerImg: perPageProps?.socialLoginsSuccess?.headerImg ?? headerImg, + }, + signUp: { + ...perPageProps?.signUp, + header: perPageProps?.signUp?.header ?? pageHeader?.signUp ?? header ?? defaultComps.header, + headerImg: perPageProps?.signUp?.headerImg ?? headerImg, + }, + }; + + if (pageProps.loaderComponent && isLoading) { + return <>{pageProps.loaderComponent}</>; + } + + const router = [ + { + id: 'login', + path: routes.loginUrl, + defaultComponent: LoginPage, + standaloneComponent: Login, + props: computedPerPageProps.loginProps, + }, + { + id: 'logout', + path: routes.logoutUrl, + defaultComponent: LogoutPage, + standaloneComponent: Logout, + props: computedPerPageProps.logoutProps, + }, + { + id: 'forgotPassword', + path: routes.forgetPasswordUrl, + defaultComponent: ForgotPasswordPage, + standaloneComponent: ForgotPassword, + props: computedPerPageProps.forgotPasswordProps, + }, + { + id: 'resetPassword', + path: routes.resetPasswordUrl, + defaultComponent: ResetPasswordPage, + standaloneComponent: ResetPassword, + props: computedPerPageProps.resetPasswordProps, + }, + { + id: 'activateAccount', + path: routes.activateUrl, + defaultComponent: ActivateAccountPage, + standaloneComponent: ActivateAccount, + props: computedPerPageProps.activateAccountProps, + }, + { + id: 'acceptInvitation', + path: routes.acceptInvitationUrl, + defaultComponent: AcceptInvitationPage, + standaloneComponent: AcceptInvitation, + props: computedPerPageProps.acceptInvitationProps, + }, + { + id: 'socialLoginCallback', + path: routes.socialLoginCallbackUrl, + defaultComponent: SocialLoginsSuccessPageComponent, + standaloneComponent: SocialLoginsSuccess, + props: computedPerPageProps.socialLoginsSuccessProps, + }, + { + id: 'signUp', + path: routes.signUpUrl, + defaultComponent: SignUpPageComponent, + standaloneComponent: SignUp, + props: computedPerPageProps.signUp, + }, + + ...(samlCallbackPath + ? [ + { + id: 'loginWithSSO', + path: samlCallbackPath || '', + defaultComponent: LoginWithSSOPage, + standaloneComponent: LoginWithSSO, + props: computedPerPageProps.loginWithSSOProps, + }, + ] + : []), + ]; + + return ( + <Switch> + {router.map((route) => { + const routeProps = { + key: route.path, + exact: true, + path: route.path, + }; + const wrapperProps: any = { + ...pageProps, + ...route.props, + pageId: route.id, + }; + if (pageComponent) { + wrapperProps.children = createElement(route.standaloneComponent as any); + return <Route {...routeProps} render={() => createElement(pageComponent, wrapperProps)} />; + } else { + return <Route {...routeProps} render={() => createElement(route.defaultComponent as any, wrapperProps)} />; + } + })} + + <Route path='*' component={() => <>{children}</>} /> + </Switch> + ); +}; diff --git a/packages/auth/src/components/FReCaptcha.tsx b/packages/auth/src/components/FReCaptcha.tsx new file mode 100644 index 000000000..8abc5cda3 --- /dev/null +++ b/packages/auth/src/components/FReCaptcha.tsx @@ -0,0 +1,54 @@ +import React, { FC, useEffect, RefObject } from 'react'; +import { loadReCaptcha, ReCaptcha } from 'react-recaptcha-v3'; +import { useCallback } from 'react'; +import { FFormik } from '@frontegg/react-core'; +import { useSecurityPolicyState } from '@frontegg/react-hooks/auth'; + +const { useField } = FFormik; + +interface IReCaptchaProps { + action: string; + fieldName?: string; + recaptchaRef: RefObject<ReCaptcha>; +} + +const unload = (recaptchaSiteKey?: string) => { + if (!recaptchaSiteKey) { + return; + } + const nodeBadge = document.querySelector('.grecaptcha-badge'); + if (nodeBadge && nodeBadge.parentNode) { + document.body.removeChild(nodeBadge.parentNode); + } + const scriptSelector = "script[src='https://www.google.com/recaptcha/api.js?render=" + recaptchaSiteKey + "']"; + const script = document.querySelector(scriptSelector); + if (script) { + script.remove(); + } +}; + +export const FReCaptcha: FC<IReCaptchaProps> = ({ action, recaptchaRef, fieldName = 'recaptchaToken' }) => { + const { policy } = useSecurityPolicyState((state) => state.captchaPolicy); + const [, , { setValue }] = useField(fieldName); + + useEffect(() => { + if (policy?.enabled) loadReCaptcha(policy?.siteKey || ''); + + return () => unload(policy?.siteKey); + }, [policy?.enabled, policy?.siteKey]); + + if (!policy?.enabled || !policy?.siteKey) { + return null; + } + + const handleCallback = useCallback( + (token: string) => { + setValue(token, false); + }, + [setValue] + ); + + if (!policy || !policy.enabled) return null; + + return <ReCaptcha ref={recaptchaRef} sitekey={policy?.siteKey} verifyCallback={handleCallback} action={action} />; +}; diff --git a/packages/auth/src/components/authPageWrapper.tsx b/packages/auth/src/components/authPageWrapper.tsx new file mode 100644 index 000000000..a7d32ee33 --- /dev/null +++ b/packages/auth/src/components/authPageWrapper.tsx @@ -0,0 +1,20 @@ +import React, { ComponentType } from 'react'; +import ReactDOM from 'react-dom'; +import { HeaderProps } from '../interfaces'; + +const DEFAULT_IMAGE = 'https://assets.frontegg.com/public-frontegg-assets/logo-transparent.png'; + +export const authPageWrapper = <P extends {}>(Component: ComponentType<P>): ComponentType<P & HeaderProps> => ( + props: P & HeaderProps +) => { + const header = props.header ?? <img src={props.headerImg ?? DEFAULT_IMAGE} alt='logo' />; + const component = ( + <div className='fe-login-page'> + <div className='fe-login-container'> + <div className='fe-login-header'>{header}</div> + <Component {...props} /> + </div> + </div> + ); + return ReactDOM.createPortal(component, document.body); +}; diff --git a/packages/auth/src/components/index.ts b/packages/auth/src/components/index.ts new file mode 100644 index 000000000..f3d16b596 --- /dev/null +++ b/packages/auth/src/components/index.ts @@ -0,0 +1 @@ +export * from './authPageWrapper'; diff --git a/packages/auth/src/constants.ts b/packages/auth/src/constants.ts new file mode 100644 index 000000000..578dac649 --- /dev/null +++ b/packages/auth/src/constants.ts @@ -0,0 +1,36 @@ +import { v4 as uuid } from 'uuid'; +import jwtEncode from 'jwt-encode'; + +export const FRONTEGG_AFTER_AUTH_REDIRECT_URL = 'FRONTEGG_AFTER_AUTH_REDIRECT_URL'; +export const FRONTEGG_CODE_VERIFIER = 'FRONTEGG_CODE_VERIFIER'; + +const expiresIn = 365000; +const iat = new Date(); +const expires = new Date(iat.getMilliseconds() + expiresIn); +const userId = uuid(); +const aud = uuid(); +const token = { + sub: userId, + name: 'Test User 1', + email: 'test+1@frontegg.com', + email_verified: true, + roles: ['admin'], + permissions: ['fe.*'], + metadata: {}, + profilePictureUrl: '', + tenantId: 'my-tenant-id', + tenantIds: ['my-tenant-id'], + type: 'userToken', + iat: iat.getMilliseconds(), + exp: expires.getMilliseconds(), + aud, + iss: 'https://test.frontegg.com', +}; + +export const refreshTokenResponse = { + accessToken: jwtEncode(token, 'secret'), + expires, + expiresIn, + refreshToken: '', + mfaRequired: false, +}; diff --git a/packages/auth/src/index.scss b/packages/auth/src/index.scss new file mode 100644 index 000000000..ce6208fc9 --- /dev/null +++ b/packages/auth/src/index.scss @@ -0,0 +1,9 @@ +@import './styles/common'; +@import './styles/login'; +@import './styles/sso'; +@import './styles/profile'; +@import './styles/mfa'; +@import './styles/team'; +@import './styles/socialLogins'; +@import './styles/signUp'; +@import './styles/apiTokens.scss'; diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts new file mode 100644 index 000000000..1063834de --- /dev/null +++ b/packages/auth/src/index.ts @@ -0,0 +1,46 @@ +import React from 'react'; +import auth from '@frontegg/redux-store/auth'; +import { PluginConfig, Loader } from '@frontegg/react-core'; +import { AuthListener } from './Listener'; +import { AuthPluginOptions } from './interfaces'; +import './index.scss'; +import { AuthRoutes } from './components/AuthRoutes'; + +export * from '@frontegg/react-hooks/auth'; +export * from './HOCs'; +export * from './components'; +export * from './Login'; +export * from './ActivateAccount'; +export * from './AcceptInvitation'; +export * from './ForgotPassword'; +export * from './ResetPassword'; +export * from './SSO'; +export * from './Profile'; +export * from './MFA'; +export * from './Team'; +export * from './AccountDropdown'; +export * from './SocialLogins'; +export * from './ApiTokens'; +export * from './AuthorizedContent'; +export * from './SignUp'; + +export { AuthRoutes }; +export const AuthPlugin = (options?: AuthPluginOptions): PluginConfig => ({ + storeName: auth.storeName, + preloadedState: { + ...auth.initialState, + ...options, + loaderComponent: options?.loaderComponent ?? React.createElement(Loader, { center: true }), + routes: { + ...auth.initialState.routes, + ...options?.routes, + }, + }, + reducer: auth.reducer, + sagas: auth.sagas, + Listener: AuthListener, + WrapperComponent: + options?.injectAuthRoutes ?? true + ? (props) => React.createElement(AuthRoutes, { ...options, ...props }) + : undefined, +}); diff --git a/packages/auth/src/interfaces.ts b/packages/auth/src/interfaces.ts new file mode 100644 index 000000000..bfba97ada --- /dev/null +++ b/packages/auth/src/interfaces.ts @@ -0,0 +1,114 @@ +import { ComponentType, ReactNode } from 'react'; +import { LoginProps, LoginWithSSOProps, LogoutProps } from './Login'; +import { ForgotPasswordProps } from './ForgotPassword'; +import { ResetPasswordProps } from './ResetPassword'; +import { ActivateAccountProps } from './ActivateAccount'; +import { AcceptInvitationProps } from './AcceptInvitation'; +import { SignUpProps } from './SignUp'; + +export type AuthPageRoutes = { + /** + * the page whither need to redirect in the case when a user is authenticated + * @default: url before redirect to login or '/' + */ + authenticatedUrl: string; + /** + * the page whither need to redirect in the case when a user is not authenticated + */ + loginUrl: string; + /** + * navigating to this url, AuthProvider will logout and remove coockies + */ + logoutUrl: string; + /** + * the page whither need to redirect in the case when a user want to activate his account + */ + activateUrl: string; + /** + * the page whether need to redirect in the case when a user want to accept invite to tanent + */ + acceptInvitationUrl: string; + /** + * the page in the case a user forgot his account password + */ + forgetPasswordUrl: string; + /** + * the page whither need to redirect in the case when a user redirected from reset password url + */ + resetPasswordUrl: string; + /** + * the url to reach the idp redirect after successful SAML response + */ + samlCallbackUrl?: string; + /** + * the url to reach the idp redirect after successful SAML response + */ + socialLoginCallbackUrl?: string; + /** + * sign up page + */ + signUpUrl: string; +}; + +export interface PerPageHeader { + login?: ReactNode; + logout?: ReactNode; + forgotPassword?: ReactNode; + resetPassword?: ReactNode; + activateAccount?: ReactNode; + acceptInvitation?: ReactNode; + loginWithSSO?: ReactNode; + socialLoginsSuccess?: ReactNode; + signUp?: ReactNode; +} + +export type HeaderProps = { header?: ReactNode; headerImg?: string }; + +export interface PerPageProps { + login?: LoginProps & HeaderProps; + logout?: LogoutProps & HeaderProps; + forgotPassword?: ForgotPasswordProps & HeaderProps; + resetPassword?: ResetPasswordProps & HeaderProps; + activateAccount?: ActivateAccountProps & HeaderProps; + acceptInvitation?: AcceptInvitationProps & HeaderProps; + loginWithSSO?: LoginWithSSOProps & HeaderProps; + socialLoginsSuccess?: HeaderProps; + signUp?: SignUpProps & HeaderProps; +} + +export type PageComponentProps = HeaderProps & { + pageId: + | 'login' + | 'logout' + | 'forgotPassword' + | 'resetPassword' + | 'activateAccount' + | 'acceptInvitation' + | 'loginWithSSO'; + children?: ReactNode; +}; + +export interface AuthPageProps { + header?: ReactNode; + headerImg?: string; + pageHeader?: PerPageHeader; + pageProps?: PerPageProps; + loaderComponent?: ReactNode; + pageComponent?: ComponentType<PageComponentProps>; + injectAuthRoutes?: boolean; // default: true + keepSessionAlive?: boolean; // default: false +} + +export interface HideOption { + hide?: boolean; +} + +export interface RouteWrapper { + path?: string; +} + +export interface BasePageProps { + rootPath?: string; +} + +export type AuthPluginOptions = AuthPageProps & { routes?: Partial<AuthPageRoutes> }; diff --git a/packages/auth/src/styles/apiTokens.scss b/packages/auth/src/styles/apiTokens.scss new file mode 100644 index 000000000..c78b04360 --- /dev/null +++ b/packages/auth/src/styles/apiTokens.scss @@ -0,0 +1,71 @@ +.fe-api-tokens { + &__table-toolbar { + padding: 1rem 2rem; + } + + &__dialog { + &-description { + margin-top: 1.5rem; + font-weight: 500; + margin-bottom: 2rem; + } + + &-spacer { + height: 1.5rem; + } + } + + &__dialog-add, + &__dialog-success { + .fe-dialog-body { + padding: 0 2rem 2rem 2rem; + } + } + + &__dialog-add { + .fe-api-tokens__dialog-description { + font-weight: normal; + } + } + + &__dialog-success { + &-input { + svg { + cursor: pointer; + width: 1.5rem; + fill: #d0d3e0; + } + } + + &-tip { + display: flex; + align-items: center; + background-color: #ededfe; + padding: 2.5rem 2rem; + margin-bottom: 1.5rem; + border-radius: 4px; + + svg { + fill: #5a6ff5; + } + + span { + max-width: 84%; + font-size: 0.875rem; + color: #5a6ff5; + margin-left: 1.5rem; + } + } + } + + &__dialog-delete { + &-message { + text-align: center; + font-size: 1.1rem; + } + } + + &__copy-icon { + height: 1rem; + } +} diff --git a/packages/auth/src/styles/common.scss b/packages/auth/src/styles/common.scss new file mode 100644 index 000000000..e72a700aa --- /dev/null +++ b/packages/auth/src/styles/common.scss @@ -0,0 +1,88 @@ +.fe-login-page { + position: absolute; + top: 0; + left: 0; + background: var(--fe-auth-background, var(--color-white)); + z-index: 1000; + width: 100vw; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; + min-height: 500px; + overflow: auto; + + from { + box-sizing: border-box; + width: 100%; + } +} + +.grecaptcha-badge { + z-index: 100000000; +} +.fe-login-container { + display: flex; + flex-direction: column; + justify-content: space-evenly; + max-width: 90%; + min-height: 300px; + box-shadow: 0 1.4px 4.5px rgba(0, 0, 0, 0.016), 0 4.3px 12.5px rgba(0, 0, 0, 0.032), + 0 10.3px 30.1px rgba(0, 0, 0, 0.055), 0 29px 100px rgba(0, 0, 0, 0.14); + background: var(--fe-auth-container-background, var(--color-white)); + border-radius: var(--fe-auth-container-border-radius, 0.5rem); + padding: var(--fe-auth-container-paddig, 2rem); + width: var(--fe-auth-container-width, 400px); +} + +.fe-login-header { + display: flex; + justify-content: center; + margin: 1rem 0 3rem; + align-items: center; + overflow-x: hidden; + + > * { + max-width: 100%; + max-height: 50px; + } +} + +.fe-login-header { + display: flex; + justify-content: center; + margin: 1rem 0 3rem; + align-items: center; + overflow-x: hidden; + + > * { + max-width: 100%; + } +} + +.fe-section-title { + color: var(--color-gray-7); + margin-bottom: 1rem; + font-size: 1rem; +} + +.fe { + &-flex { + display: flex; + flex-direction: row; + flex-flow: wrap; + align-items: center; + } + + &-full-width { + width: 100%; + } + + &-flex-no-wrap { + flex-wrap: nowrap; + } + + &-flex-spacer { + flex: 1; + } +} diff --git a/packages/auth/src/styles/login.scss b/packages/auth/src/styles/login.scss new file mode 100644 index 000000000..c1c60b398 --- /dev/null +++ b/packages/auth/src/styles/login.scss @@ -0,0 +1,44 @@ +.fe-login-component, +.fe-forgot-password-component, +.fe-accept-invitation-component, +.fe-activate-account-component { + flex: 1; + display: flex; + flex-direction: column; + + .fe-recover-two-factor { + a { + text-decoration: underline; + cursor: pointer; + } + } + + &__back-to-login { + margin-top: 1rem; + } + + .ui.loader.inline { + display: block; + margin: 2rem auto 1rem; + } + + &__back-to-sign-up-link { + cursor: pointer; + color: var(--color-primary); + + &:hover { + color: var(--color-primary-75); + } + } +} + +@media screen and (max-width: 400px) { + .fe-login-page { + padding-top: 10vh; + justify-content: flex-start; + + .fe-login-container { + box-shadow: none; + } + } +} diff --git a/packages/auth/src/styles/mfa.scss b/packages/auth/src/styles/mfa.scss new file mode 100644 index 000000000..2b89ac750 --- /dev/null +++ b/packages/auth/src/styles/mfa.scss @@ -0,0 +1,97 @@ +.fe-mfa { + &__verify-form-ol { + padding-left: 1rem; + margin-top: 1.5rem; + margin-bottom: 0; + + .fe-input { + margin: 1rem 0 !important; + } + } + + &__qr { + margin: 2rem auto; + width: 13rem; + height: 13rem; + border: 1px solid #d4dde9; + border-radius: 0.25rem; + padding: 1.5rem; + position: relative; + + img { + max-width: 100%; + max-height: 100%; + } + } + + &__recovery-code { + height: 5rem; + background-color: #f9fafc; + border-radius: 0.25rem; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + padding: 0 2rem; + margin: 1rem 0 2rem; + font-weight: bold; + font-size: 1.3rem; + + &.copied { + color: #d0d3e0; + } + + i.copy { + color: #d0d3e0; + cursor: pointer; + width: 2.5rem; + height: 2.5rem; + text-align: center; + line-height: 2.5rem; + margin-right: -0.5rem; + + &:hover { + background: #edeef0; + border-radius: 50%; + } + } + + &-step { + .fe-dialog__footer { + display: flex; + } + } + } + + &__recovery-note { + display: flex; + flex-direction: row; + padding: 2rem; + background: rgba(90, 111, 245, 0.1); + color: #5a6ff5; + border-radius: 0.25rem; + align-items: center; + + > .fe-icon { + fill: currentColor; + font-size: 1.3rem; + margin-right: 1.5rem; + } + + span { + flex: 1; + } + } + + &__content-title { + color: var(--color-gray-5); + margin-bottom: 2rem; + font-weight: bold; + text-align: center; + font-size: 1rem; + + span { + color: #333; + } + } +} diff --git a/packages/auth/src/styles/profile.scss b/packages/auth/src/styles/profile.scss new file mode 100644 index 000000000..b4ca5d750 --- /dev/null +++ b/packages/auth/src/styles/profile.scss @@ -0,0 +1,124 @@ +.fe-profile-page { + .fe-page-header { + border-bottom: none; + margin-bottom: 0; + padding-bottom: 0; + min-height: auto; + } +} + +.fe-profile-mfa-page, +.fe-profile-password-page { + padding: 2rem; +} + +.fe-profile-mfa-page { + align-items: center; + text-align: center; + display: flex; + flex-direction: column; +} + +.fe-profile-info { + display: flex; + flex-direction: row; + width: 80rem; + margin: auto; + max-width: 90%; +} + +.fe-profile-image { + &-uploader { + .fe-card-content { + display: flex; + flex-direction: row; + padding: 2.5rem; + } + } + + &-container { + border-radius: 50%; + background: var(--color-blue-gray-0); + width: 10rem; + height: 10rem; + position: relative; + display: flex; + justify-content: center; + align-items: center; + + .fe-icon { + fill: var(--color-gray-0); + color: var(--color-gray-0); + font-size: 2.5rem; + line-height: 2.5rem; + } + + img { + object-fit: cover; + object-position: center; + width: 100%; + height: 100%; + border-radius: 50%; + overflow: hidden; + } + + .fe-icon.fe-profile-image-remove { + position: absolute; + top: 0.5rem; + right: 0.5rem; + color: var(--color-white); + background: var(--color-red-5); + border-radius: 50%; + font-size: 0.85rem; + line-height: 2rem; + text-align: center; + width: 2rem; + height: 2rem; + cursor: pointer; + + &:hover { + background: var(--color-red-7); + } + } + } + + &-details { + flex: 1; + padding: 0 2.5rem; + justify-content: center; + + .fe-profile-name { + font-size: 1.4rem; + font-weight: bold; + line-height: 1.5; + margin-bottom: 0rem; + text-overflow: ellipsis; + } + + .fe-profile-email { + margin-bottom: 2rem; + } + + input[type='file'] { + display: none; + } + } + + &-note { + font-size: 1rem; + line-height: 1.5; + margin-top: 1rem; + color: var(--color-gray-5); + } +} + +.fe-profile-basic-information { + flex: 1; + padding: 2rem; +} + +@media screen and (max-width: 75rem) { + .fe-profile-info { + flex-direction: column; + } +} diff --git a/packages/auth/src/styles/signUp.scss b/packages/auth/src/styles/signUp.scss new file mode 100644 index 000000000..877ee36bb --- /dev/null +++ b/packages/auth/src/styles/signUp.scss @@ -0,0 +1,32 @@ +.fe-sign-up { + &__back-to-login-link { + cursor: pointer; + color: var(--color-primary); + + &:hover { + color: var(--color-primary-75); + } + } + + &__success-container { + margin-bottom: 3em; + } + + &__error { + margin-bottom: 1rem; + color: var(--color-danger); + font-size: var(--element-font-size-sm); + } + + &__checkbox { + margin-bottom: 1rem; + + .fe-checkbox__content { + display: flex; + + input { + width: 1.5rem; + } + } + } +} diff --git a/packages/auth/src/styles/socialLogins.scss b/packages/auth/src/styles/socialLogins.scss new file mode 100644 index 000000000..35536dfbb --- /dev/null +++ b/packages/auth/src/styles/socialLogins.scss @@ -0,0 +1,54 @@ +.fe-social-login { + &__or-container { + width: 100%; + height: 0.8em; + border-bottom: 1px solid var(--color-gray-4); + text-align: center; + margin: 2.5em 0; + + span { + font-size: 1em; + background-color: var(--color-white); + padding: 1em; + color: var(--color-gray-5); + border: 1px solid var(--color-gray-4); + border-radius: 2em; + } + } + + &__button { + margin: 0.2em 0; + border: 1px solid var(--element-border-color) !important; + color: var(--color-white); + + div { + div:last-of-type { + flex-grow: 5; + padding-right: 1.5rem; + } + } + + svg { + width: 1.5rem; + height: 1.5rem; + margin-right: 1em; + vertical-align: middle; + } + + &__google { + background-color: var(--color-google); + } + + &__github { + background-color: var(--color-github); + } + + &__microsoft { + background-color: var(--color-microsoft); + } + + &__facebook { + background-color: var(--color-facebook); + } + } +} diff --git a/packages/auth/src/styles/sso.scss b/packages/auth/src/styles/sso.scss new file mode 100644 index 000000000..465c5454e --- /dev/null +++ b/packages/auth/src/styles/sso.scss @@ -0,0 +1,404 @@ +.fe-sso-overview { + margin: 2rem auto; +} + +.fe-sso-steps { + display: flex; + justify-content: center; + flex-wrap: wrap; + width: 75rem; + max-width: 95%; + margin: 3rem auto 4rem auto; + + * { + text-decoration: none; + } + + .fe-sso-step { + box-sizing: border-box; + width: 20rem; + padding: 1rem; + border-radius: 0.5rem; + background-color: #f9fafc; + border: 3px solid #f9fafc; + margin: 2rem; + overflow: hidden; + transition: border 200ms ease-in; + + &__inner { + width: 100%; + height: 11rem; + position: relative; + border-radius: 0.5rem; + background-color: #fff; + margin-bottom: 1rem; + } + + &:hover { + border: 3px solid var(--color-primary-dark); + } + + &__header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; + padding: 1rem 1rem 0; + font-weight: bold; + + span { + font-size: 1rem; + color: #3c4a5a; + text-transform: capitalize; + } + + .fe-sso-step__checkmark { + box-sizing: content-box; + width: 1.25rem; + height: 1.25rem; + border-radius: 0.25rem; + background-color: #f9fafc; + border: 1px solid #f1f1f1; + position: relative; + padding: 0.25rem; + + .fe-icon { + fill: var(--color-primary); + margin: 0; + width: 100%; + height: 100%; + position: absolute; + left: 0; + top: 0; + } + } + } + + &__title, + &__subtitle { + text-overflow: ellipsis; + white-space: pre-wrap; + width: 100%; + overflow: hidden; + text-align: center; + font-weight: bold; + color: #666; + font-size: 1.25rem; + } + + &__subtitle { + margin-top: 1.5rem; + padding: 0 1.5rem; + font-size: 1rem; + color: #3c4a5a; + font-weight: bold; + } + + &__info { + display: flex; + justify-content: space-around; + color: #3c4a5a; + } + } +} + +.fe-sso-guide { + padding: 1rem 0; + flex: 1; + display: flex; + flex-direction: column; + height: 100%; + + &__title { + color: var(--color-gray-9); + font-weight: bold; + font-size: 1.1rem; + } + + &__description { + color: var(--color-gray-6); + font-size: 1rem; + } + + &__steps { + > * { + margin-bottom: 0.5rem; + } + } + + &__step { + background: var(--color-white); + color: var(--color-gray-9); + padding: 0.9rem; + border-radius: var(--element-border-radius-tiny); + display: flex; + align-items: center; + + .fe-icon { + margin-right: 0.5rem; + color: var(--color-gray-3); + font-size: 1.2rem; + } + } + + &__see-more { + font-size: 1rem; + font-weight: bold; + color: var(--color-gray-7); + line-height: 2; + display: flex; + flex-direction: row; + align-items: center; + + > div { + padding: 0 1rem; + cursor: pointer; + color: var(--color-primary); + display: flex; + flex-direction: row; + align-items: center; + + &:hover { + color: var(--color-primary-light); + } + } + } + + &__instruction-row { + width: 100%; + padding-bottom: 1rem; + + .fe-description { + font-size: 1.1rem; + margin-bottom: 2rem; + line-height: 1.5; + + u { + color: var(--color-primary); + background: var(--color-gray-0); + padding: 0.05rem 0.25rem; + font-family: monospace; + font-size: 1rem; + display: inline-block; + } + } + + > img { + max-height: 300px; + max-width: 90%; + display: block; + border: 1px solid var(--color-gray-4); + border-radius: 0.5rem; + overflow: hidden; + margin: auto auto 2rem; + } + } +} + +.fe-sso-claim-domain { + &-page { + border-radius: var(--element-border-radius-sm); + padding: 1rem 1rem 1rem 2rem; + background-color: var(--color-gray-0); + display: flex; + width: 70rem; + max-width: 80%; + margin: 2rem auto; + } + + &-form { + flex: 1; + background: var(--fe-sso-calim-domain-header, var(--color-white)); + border-radius: 0.5rem; + + .fe-form, + .fe-form > form { + display: flex; + flex-direction: column; + height: 100%; + } + + &__header { + height: 4rem; + display: flex; + align-items: center; + border-bottom: 1px solid var(--color-gray-2); + padding: 0 1.5rem; + font-weight: bold; + font-size: 1.1rem; + } + + &__body { + flex: 1; + display: flex; + flex-direction: column; + padding: 2rem; + } + } +} + +.fe-sso-authorization-page { + border-radius: var(--element-border-radius-sm); + padding: 2rem; + background-color: var(--color-gray-0); + display: flex; + flex-direction: column; + width: 50rem; + max-width: 80%; + margin: 2rem auto; + &__title { + color: var(--color-gray-9); + font-weight: bold; + font-size: 1.1rem; + } + &__subtitle { + color: var(--color-gray-6); + font-size: 1rem; + } + + &__select { + margin-top: 2rem; + &-container { + margin-bottom: 3rem; + } + &-footer { + text-align: end; + } + } +} + +@media screen and (max-width: 800px) { + .fe-sso-claim-domain-page { + flex-direction: column; + } +} + +.fe-sso-idp-page { + margin: 2rem auto; + padding: 1rem 1rem 1rem 1.75rem; + background-color: var(--color-gray-0); + width: 78rem; + max-width: 100%; + + &__title { + color: var(--color-gray-9); + font-weight: bold; + font-size: 1.1rem; + } + + &__select { + padding-top: 1rem; + } + + &__select-container { + background-color: var(--color-blue-gray-0); + padding: 1rem; + border-radius: 0.5rem; + } + + &__select-item { + background-color: var(--color-white); + height: 6rem; + width: 100%; + padding: 1rem; + cursor: pointer; + color: var(--color-gray-9); + overflow: hidden; + border: 2px solid #ededf0; + border-radius: 0.5rem; + display: flex; + flex-direction: revert; + flex: 1; + align-items: center; + font-size: 1rem; + + &:hover, + &.selected { + border-color: var(--color-primary); + } + } + + &__config { + padding-top: 1rem; + width: 100%; + height: 100%; + background-color: var(--color-white); + display: flex; + flex-direction: column; + + .fe-form { + flex: 1; + + form { + height: 100%; + } + } + + &-header { + padding: 1rem 2.5rem; + + &-step { + font-size: 0.875rem; + color: var(--color-gray-6); + } + } + } + + .sso-endpoint-container { + margin-top: 1rem; + } + + &__progress-1, + &__progress-2 { + width: 50%; + height: 2px; + background: var(--color-primary); + transition: width 300ms; + } + + &__progress-2 { + width: 100%; + } + + &__step { + flex: 1; + display: flex; + flex-direction: column; + padding: 2rem; + height: 100%; + } +} + +.fe-sso-dnd { + background-color: var(--color-gray-0); + padding: 1rem; + height: 13.5rem; + width: 100%; + border-radius: 4px; + color: var(--color-gray-9); + margin-bottom: 2rem; + + &-title { + margin-top: 1rem; + color: var(--color-gray-9); + font-size: 0.875rem; + margin-bottom: 0.5rem; + } + + &-container { + flex: 1; + width: 100%; + height: 100%; + border: 1px dashed #d4dde9; + border-radius: 4px; + display: flex; + justify-content: center; + align-items: center; + text-align: center; + + p { + margin-top: 1rem; + max-width: 9.5rem; + } + } +} diff --git a/packages/auth/src/styles/team.scss b/packages/auth/src/styles/team.scss new file mode 100644 index 000000000..236e91e08 --- /dev/null +++ b/packages/auth/src/styles/team.scss @@ -0,0 +1,74 @@ +.fe-team { + &__page { + display: flex; + flex-direction: column; + overflow: hidden; + position: absolute; + width: 100%; + height: 100%; + } + + &__layout { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + } + + &__table { + flex: 1; + overflow: hidden; + } + + &__table-toolbar { + padding: 1rem 2rem; + } + + &__filter-popup-md { + width: 20rem; + } + + &__filter-title { + font-size: 1rem; + font-weight: bold; + } + + &__roles-dropdown-button.fe-button { + min-width: 3rem; + } + + &__roles-popup.fe-popup__container { + padding: 0; + + .fe-team__roles-dropdown { + display: flex; + flex-direction: column; + padding: 0.5rem 0; + + &-disabled { + opacity: 0.6; + cursor: wait; + } + + .fe-menu-item { + padding-right: 2rem; + text-align: left; + } + + .fe-checkbox.fe-menu-item__icon { + height: 1.75rem; + max-width: calc(100% - 2.5rem); + + .fe-checkbox__input { + width: 1.25rem; + height: 1.25rem; + } + + input:checked + .fe-checkbox__input .fe-icon { + width: 1.25rem; + height: 1.25rem; + } + } + } + } +} diff --git a/packages/auth/src/tests/activate-account-flow.cy-spec.tsx b/packages/auth/src/tests/activate-account-flow.cy-spec.tsx new file mode 100644 index 000000000..de5f0786f --- /dev/null +++ b/packages/auth/src/tests/activate-account-flow.cy-spec.tsx @@ -0,0 +1,142 @@ +import React from 'react'; +import { mount } from 'cypress-react-unit-test'; +import { AuthPlugin } from '../index'; +import { + IDENTITY_SERVICE, + mockAuthApi, + mountOptions, + navigateTo, + PASSWORD, + submitButtonSelector, + TestFronteggWrapper, +} from '../../../../cypress/helpers'; +import { FRONTEGG_AFTER_AUTH_REDIRECT_URL } from '../constants'; + +const defaultAuthPlugin = { + routes: { + authenticatedUrl: '/', + loginUrl: '/account/login', + logoutUrl: '/account/logout', + activateUrl: '/account/activate', + acceptInvitationUrl: '/account/invitation/accept', + forgetPasswordUrl: '/account/forget-password', + resetPasswordUrl: '/account/reset-password', + }, +}; + +describe('Activate Account Tests', () => { + it('ActivateAccount Page should display error if userId or token not found', () => { + cy.server(); + mockAuthApi(false, false); + mount(<TestFronteggWrapper plugins={[AuthPlugin(defaultAuthPlugin)]}>Home</TestFronteggWrapper>, mountOptions); + navigateTo(defaultAuthPlugin.routes.activateUrl); + + cy.get('.fe-error-message').contains('Activation failed').should('be.visible'); + cy.contains('Back to login').should('be.visible').click(); + + cy.location().should((loc) => { + expect(loc.pathname).to.eq(defaultAuthPlugin.routes.loginUrl); + }); + }); + + it('ActivateAccount Page should display success and redirect to login page', () => { + cy.server(); + mockAuthApi(false, false); + cy.route({ + method: 'POST', + url: `${IDENTITY_SERVICE}/resources/users/v1/activate`, + status: 200, + response: {}, + delay: 200, + }).as('activateAccount'); + + mount(<TestFronteggWrapper plugins={[AuthPlugin(defaultAuthPlugin)]}>Home</TestFronteggWrapper>, mountOptions); + + const userId = '1111-userId-1111'; + const token = '1111-token-1111'; + navigateTo(defaultAuthPlugin.routes.activateUrl + `?userId=${userId}&token=${token}`); + + cy.get('.fe-error-message').should('not.be.exist'); + + const passwordSelector = 'input[name="password"]'; + const confirmPasswordSelector = 'input[name="confirmPassword"]'; + + cy.get(submitButtonSelector).should('be.disabled'); + cy.get(passwordSelector).focus().clear().type('1111').blur(); + cy.get(passwordSelector).parents('.field').should('have.class', 'error'); + cy.get(passwordSelector).focus().clear().type(PASSWORD).blur(); + cy.get(submitButtonSelector).should('be.disabled'); + cy.get(confirmPasswordSelector) + .focus() + .clear() + .type(PASSWORD + '1') + .blur(); + cy.get(submitButtonSelector).should('be.disabled'); + + cy.get(confirmPasswordSelector).parents('.field').should('have.class', 'error'); + cy.get(confirmPasswordSelector).focus().clear().type(PASSWORD).blur(); + + cy.get(passwordSelector).parents('.field').should('not.have.class', 'error'); + cy.get(confirmPasswordSelector).parents('.field').should('not.have.class', 'error'); + + cy.get(submitButtonSelector).should('not.be.disabled').click(); + mockAuthApi(true, false); + cy.wait('@activateAccount') + .its('request.body') + .should('deep.equal', { userId, token, password: PASSWORD, recaptchaToken: '' }); + cy.location().should((loc) => { + expect(loc.pathname).to.eq(defaultAuthPlugin.routes.authenticatedUrl); + }); + }); + + it('ActivateAccount Page should display success and redirect to after auth redirect url', () => { + cy.server(); + mockAuthApi(false, false); + cy.route({ + method: 'POST', + url: `${IDENTITY_SERVICE}/resources/users/v1/activate`, + status: 200, + response: {}, + delay: 200, + }).as('activateAccount'); + + mount(<TestFronteggWrapper plugins={[AuthPlugin(defaultAuthPlugin)]}>Home</TestFronteggWrapper>, mountOptions); + + const userId = '1111-userId-1111'; + const token = '1111-token-1111'; + navigateTo(defaultAuthPlugin.routes.activateUrl + `?userId=${userId}&token=${token}`); + + cy.get('.fe-error-message').should('not.be.exist'); + + const passwordSelector = 'input[name="password"]'; + const confirmPasswordSelector = 'input[name="confirmPassword"]'; + + cy.get(submitButtonSelector).should('be.disabled'); + cy.get(passwordSelector).focus().clear().type('1111').blur(); + cy.get(passwordSelector).parents('.field').should('have.class', 'error'); + cy.get(passwordSelector).focus().clear().type(PASSWORD).blur(); + cy.get(submitButtonSelector).should('be.disabled'); + cy.get(confirmPasswordSelector) + .focus() + .clear() + .type(PASSWORD + '1') + .blur(); + cy.get(submitButtonSelector).should('be.disabled'); + + cy.get(confirmPasswordSelector).parents('.field').should('have.class', 'error'); + cy.get(confirmPasswordSelector).focus().clear().type(PASSWORD).blur(); + + cy.get(passwordSelector).parents('.field').should('not.have.class', 'error'); + cy.get(confirmPasswordSelector).parents('.field').should('not.have.class', 'error'); + + cy.get(submitButtonSelector).should('not.be.disabled').click(); + mockAuthApi(true, false); + cy.window().then((win) => win.localStorage.setItem(FRONTEGG_AFTER_AUTH_REDIRECT_URL, '/after-login-redirect')); + cy.wait('@activateAccount') + .its('request.body') + .should('deep.equal', { userId, token, password: PASSWORD, recaptchaToken: '' }); + cy.location().should((loc) => { + expect(loc.pathname).to.eq('/after-login-redirect'); + }); + }); +}); diff --git a/packages/auth/src/tests/forget-password-flow.cy-spec.tsx b/packages/auth/src/tests/forget-password-flow.cy-spec.tsx new file mode 100644 index 000000000..68fc0a291 --- /dev/null +++ b/packages/auth/src/tests/forget-password-flow.cy-spec.tsx @@ -0,0 +1,186 @@ +import React from 'react'; +import { mount } from 'cypress-react-unit-test'; +import { AuthPlugin } from '../index'; +import { + checkEmailValidation, + EMAIL_1, + emailInputSelector, + IDENTITY_SERVICE, + METADATA_SERVICE, + mockAuthApi, + mountOptions, + navigateTo, + PASSWORD, + submitButtonSelector, + TestFronteggWrapper, +} from '../../../../cypress/helpers'; + +const defaultAuthPlugin = { + routes: { + authenticatedUrl: '/', + loginUrl: '/account/login', + logoutUrl: '/account/logout', + activateUrl: '/account/activate', + acceptInvitationUrl: '/account/invitation/accept', + forgetPasswordUrl: '/account/forget-password', + resetPasswordUrl: '/account/reset-password', + }, +}; + +describe('Forgot Password Tests', () => { + it('NO SAML, should display forget password if click on forget password button', () => { + cy.server(); + mockAuthApi(false, false); + mount(<TestFronteggWrapper plugins={[AuthPlugin(defaultAuthPlugin)]}>Home</TestFronteggWrapper>, mountOptions); + navigateTo(defaultAuthPlugin.routes.loginUrl); + + cy.wait(['@refreshToken', '@metadata']); + cy.get('.loader').should('not.be.visible'); + + cy.get(emailInputSelector).focus().clear().type(EMAIL_1).blur(); + cy.get('[data-test-id="forgotPassBtn"]').click(); + + cy.location().should((loc) => { + expect(loc.pathname).to.eq(defaultAuthPlugin.routes.forgetPasswordUrl); + }); + + cy.get(submitButtonSelector).should('not.be.disabled'); + cy.get(emailInputSelector).should('have.value', EMAIL_1); + }); + + it('WITH SAML, should display forget password if click on forget password button', () => { + cy.server(); + mockAuthApi(false, true); + cy.route({ + method: 'POST', + url: `${IDENTITY_SERVICE}/resources/auth/v2/user/sso/prelogin`, + status: 400, + response: { address: null }, + delay: 200, + }).as('preLogin'); + + mount(<TestFronteggWrapper plugins={[AuthPlugin(defaultAuthPlugin)]}>Home</TestFronteggWrapper>, mountOptions); + navigateTo(defaultAuthPlugin.routes.loginUrl); + + cy.wait(['@refreshToken', '@metadata']); + cy.get('.loader').should('not.be.visible'); + + const emailSelector = '[name="email"]'; + cy.get(emailSelector).focus().clear().type(EMAIL_1).blur(); + cy.get('button[type="submit"]').click(); + + cy.wait(['@preLogin']); + cy.get('button[type="submit"]').should('not.be.disabled'); + + cy.get('[data-test-id="forgotPassBtn"]').click(); + cy.location().should((loc) => { + expect(loc.pathname).to.eq(defaultAuthPlugin.routes.forgetPasswordUrl); + }); + + cy.get(submitButtonSelector).should('not.be.disabled'); + cy.get(emailInputSelector).should('have.value', EMAIL_1); + }); + + it('should display error message if api request failed', () => { + cy.server(); + mockAuthApi(false, false); + + mount(<TestFronteggWrapper plugins={[AuthPlugin(defaultAuthPlugin)]}>Home</TestFronteggWrapper>, mountOptions); + navigateTo(defaultAuthPlugin.routes.forgetPasswordUrl); + + cy.get(submitButtonSelector).should('be.disabled'); + checkEmailValidation(); + cy.get(submitButtonSelector).should('not.be.disabled').click(); + + cy.get('.fe-error-message').contains('Unknown error occurred').should('be.visible'); + }); + + it('should display success message if api request succeeded', () => { + cy.server(); + mockAuthApi(false, false); + cy.route({ + method: 'POST', + url: `${IDENTITY_SERVICE}/resources/users/v1/passwords/reset`, + status: 200, + response: {}, + delay: 200, + }).as('forgotPassword'); + + mount(<TestFronteggWrapper plugins={[AuthPlugin(defaultAuthPlugin)]}>Home</TestFronteggWrapper>, mountOptions); + navigateTo(defaultAuthPlugin.routes.forgetPasswordUrl); + + cy.get(submitButtonSelector).should('be.disabled'); + checkEmailValidation(); + cy.get(submitButtonSelector).should('not.be.disabled').click(); + cy.wait('@forgotPassword').its('request.body').should('deep.equal', { email: EMAIL_1 }); + + cy.contains('A password reset email has been sent to your registered email address').should('be.visible'); + cy.contains('Back to login').should('be.visible').click(); + + cy.location().should((loc) => { + expect(loc.pathname).to.eq(defaultAuthPlugin.routes.loginUrl); + }); + }); + + it('ResetPassword Page should display error if userId or token not found', () => { + cy.server(); + mockAuthApi(false, false); + mount(<TestFronteggWrapper plugins={[AuthPlugin(defaultAuthPlugin)]}>Home</TestFronteggWrapper>, mountOptions); + navigateTo(defaultAuthPlugin.routes.resetPasswordUrl); + + cy.get('.fe-error-message').contains('Reset Password Failed').should('be.visible'); + cy.contains('Back to login').should('be.visible').click(); + + cy.location().should((loc) => { + expect(loc.pathname).to.eq(defaultAuthPlugin.routes.loginUrl); + }); + }); + + it('ResetPassword Page should display success and redirect to login page', () => { + cy.server(); + mockAuthApi(false, false); + cy.route({ + method: 'POST', + url: `${IDENTITY_SERVICE}/resources/users/v1/passwords/reset/verify`, + status: 200, + response: {}, + delay: 200, + }).as('resetPassword'); + + mount(<TestFronteggWrapper plugins={[AuthPlugin(defaultAuthPlugin)]}>Home</TestFronteggWrapper>, mountOptions); + + const userId = '1111-userId-1111'; + const token = '1111-token-1111'; + navigateTo(defaultAuthPlugin.routes.resetPasswordUrl + `?userId=${userId}&token=${token}`); + + cy.get('.fe-error-message').should('not.be.exist'); + + const passwordSelector = 'input[name="password"]'; + const confirmPasswordSelector = 'input[name="confirmPassword"]'; + + cy.get(submitButtonSelector).should('be.disabled'); + cy.get(passwordSelector).focus().clear().type('1111').blur(); + cy.get(passwordSelector).parents('.field').should('have.class', 'error'); + cy.get(passwordSelector).focus().clear().type(PASSWORD).blur(); + cy.get(submitButtonSelector).should('be.disabled'); + cy.get(confirmPasswordSelector) + .focus() + .clear() + .type(PASSWORD + '1') + .blur(); + cy.get(submitButtonSelector).should('be.disabled'); + + cy.get(confirmPasswordSelector).parents('.field').should('have.class', 'error'); + cy.get(confirmPasswordSelector).focus().clear().type(PASSWORD).blur(); + + cy.get(passwordSelector).parents('.field').should('not.have.class', 'error'); + cy.get(confirmPasswordSelector).parents('.field').should('not.have.class', 'error'); + + cy.get(submitButtonSelector).should('not.be.disabled').click(); + cy.wait('@resetPassword').its('request.body').should('deep.equal', { userId, token, password: PASSWORD }); + + cy.location().should((loc) => { + expect(loc.pathname).to.eq(defaultAuthPlugin.routes.loginUrl); + }); + }); +}); diff --git a/packages/auth/src/tests/login-customize.cy-spec.tsx b/packages/auth/src/tests/login-customize.cy-spec.tsx new file mode 100644 index 000000000..367f6fc6c --- /dev/null +++ b/packages/auth/src/tests/login-customize.cy-spec.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { mount } from 'cypress-react-unit-test'; +import { AuthPlugin } from '../index'; +import { + IDENTITY_SERVICE, + METADATA_SERVICE, + mountOptions, + navigateTo, + TestFronteggWrapper, +} from '../../../../cypress/helpers'; + +const defaultAuthPlugin = { + routes: { + authenticatedUrl: '/', + loginUrl: '/account/login', + logoutUrl: '/account/logout', + activateUrl: '/account/activate', + acceptInvitationUrl: '/account/invitation/accept', + forgetPasswordUrl: '/account/forget-password', + resetPasswordUrl: '/account/reset-password', + }, +}; + +/* eslint-env mocha */ +describe('Login Customize Tests', () => { + it('Global Custom Header', () => { + cy.server(); + cy.route({ + method: 'POST', + url: `${IDENTITY_SERVICE}/resources/auth/v1/user/token/refresh`, + status: 401, + response: 'Unauthorized', + }); + cy.route({ method: 'GET', url: `${METADATA_SERVICE}?entityName=saml`, status: 200, response: { rows: [] } }); + + mount( + <TestFronteggWrapper + plugins={[ + AuthPlugin({ + ...defaultAuthPlugin, + header: <div className='custom-header'>MY HEADER</div>, + }), + ]} + > + Home + </TestFronteggWrapper>, + mountOptions + ); + + navigateTo('/account/login'); + + cy.get('.custom-header').contains('MY HEADER').should('be.visible'); + }); +}); diff --git a/packages/auth/src/tests/login-flow.cy-spec.tsx b/packages/auth/src/tests/login-flow.cy-spec.tsx new file mode 100644 index 000000000..3f0db2e61 --- /dev/null +++ b/packages/auth/src/tests/login-flow.cy-spec.tsx @@ -0,0 +1,716 @@ +import React from 'react'; +import { mount } from 'cypress-react-unit-test'; +import { AuthPlugin, LoginStep } from '../index'; +import { FRONTEGG_AFTER_AUTH_REDIRECT_URL, refreshTokenResponse } from '../constants'; +import { + checkEmailValidation, + EMAIL_1, + IDENTITY_SERVICE, + METADATA_SERVICE, + mockAuthApi, + mountOptions, + navigateTo, + TestFronteggWrapper, + ACCESS_TOKEN, + mockAuthMe, +} from '../../../../cypress/helpers'; + +const defaultAuthPlugin = { + routes: { + authenticatedUrl: '/', + loginUrl: '/account/login', + logoutUrl: '/account/logout', + activateUrl: '/account/activate', + acceptInvitationUrl: '/account/invitation/accept', + forgetPasswordUrl: '/account/forget-password', + resetPasswordUrl: '/account/reset-password', + socialLoginCallbackUrl: '/account/social/success', + signUpUrl: '/account/sign-up', + }, +}; + +const EMAIL_2 = 'test2@frontegg.com'; +const PASSWORD = 'ValidPassword123!'; +const SSO_PATH = '/my-test-sso-login'; +const GOOGLE_AUTH_RESPONSE = '?state=%7B%22provider%22:%22google%22,%22action%22:%22login%22%7D&code=google_auth_code'; +const MFA_TOKEN = 'mfaToken'; +const RECOVERY_CODE = '123412341234'; + +const getGoogleAuthUrl = (origin): string => { + const search = new URLSearchParams({ + client_id: 'google_client_id', + redirect_uri: `${origin}/account/social/success`, + response_type: 'code', + include_granted_scopes: 'true', + scope: 'https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email', + state: '{"provider":"google","action":"login"}', + }); + return `/account/https://accounts.google.com/o/oauth2/v2/auth?${search.toString()}`; +}; + +const checkPasswordValidation = () => { + const passwordSelector = '[name="password"]'; + cy.get(passwordSelector).focus().clear().type('not').blur(); + cy.contains('Password must be at least 6 characters').should('be.visible'); + cy.get(passwordSelector).focus().clear().type(PASSWORD).blur(); + cy.contains('Password must be at least 6 characters').should('not.be.exist'); +}; +/* eslint-env mocha */ +describe('Login Tests', () => { + it('Login, NO SAML', () => { + cy.server(); + mockAuthApi(false, false); + mockAuthMe(); + mount(<TestFronteggWrapper plugins={[AuthPlugin(defaultAuthPlugin)]}>Home</TestFronteggWrapper>, mountOptions); + + navigateTo(defaultAuthPlugin.routes.loginUrl); + cy.wait(['@refreshToken', '@metadata']); + cy.get('.loader').should('not.be.visible'); + + const submitSelector = 'button[type=submit]'; + + cy.get(submitSelector).contains('Login').should('be.disabled'); + checkEmailValidation(); + cy.get(submitSelector).contains('Login').should('be.disabled'); + + checkPasswordValidation(); + cy.get(submitSelector).contains('Login').should('not.be.disabled'); + + cy.route({ + method: 'POST', + url: `${IDENTITY_SERVICE}/resources/auth/v1/user`, + status: 401, + response: { errors: ['invalid auth'] }, + delay: 200, + }).as('login'); + + cy.get(submitSelector).contains('Login').click(); + + cy.wait('@login') + .its('request.body') + .should('deep.equal', { email: EMAIL_1, password: PASSWORD, recaptchaToken: '' }); + cy.contains('invalid auth').should('be.visible'); + + cy.route({ + method: 'POST', + url: `${IDENTITY_SERVICE}/resources/auth/v1/user`, + status: 200, + response: { accessToken: ACCESS_TOKEN, refreshToken: 'refreshToken' }, + delay: 200, + }).as('login'); + + cy.window().then((win) => win.localStorage.removeItem(FRONTEGG_AFTER_AUTH_REDIRECT_URL)); + cy.get(submitSelector).contains('Login').click(); + + cy.wait('@login') + .its('request.body') + .should('deep.equal', { email: EMAIL_1, password: PASSWORD, recaptchaToken: '' }); + + // cy.contains('Authentication Succeeded').should('be.visible'); + cy.contains('Home').should('be.visible'); + cy.location().should((loc) => { + expect(loc.pathname).to.eq('/'); + }); + }); + + it('Login, check after login url', () => { + cy.server(); + mockAuthApi(false, false); + mockAuthMe(); + cy.window().then((win) => win.localStorage.setItem(FRONTEGG_AFTER_AUTH_REDIRECT_URL, '/after-login-redirect')); + cy.route({ + method: 'POST', + url: `${IDENTITY_SERVICE}/resources/auth/v1/user`, + status: 200, + response: { accessToken: ACCESS_TOKEN, refreshToken: 'refreshToken' }, + delay: 200, + }).as('login'); + mount(<TestFronteggWrapper plugins={[AuthPlugin(defaultAuthPlugin)]}>Home</TestFronteggWrapper>, mountOptions); + + navigateTo(defaultAuthPlugin.routes.loginUrl); + cy.wait(['@refreshToken', '@metadata']); + cy.get('.loader').should('not.be.visible'); + + const emailSelector = '[name="email"]'; + const passwordSelector = '[name="password"]'; + const submitSelector = 'button[type=submit]'; + + cy.get(emailSelector).focus().clear().type(EMAIL_1).blur(); + cy.get(passwordSelector).focus().clear().type(PASSWORD).blur(); + cy.get(submitSelector).contains('Login').should('not.be.disabled'); + + cy.get(submitSelector).click(); + + cy.wait('@login') + .its('request.body') + .should('deep.equal', { email: EMAIL_1, password: PASSWORD, recaptchaToken: '' }); + + cy.contains('Home').should('be.visible'); + cy.location().should((loc) => { + expect(loc.pathname).to.eq('/after-login-redirect'); + }); + }); + + it('Login, WITH SAML tenant, NO SAML email', () => { + cy.server(); + mockAuthApi(false, true); + mockAuthMe(); + cy.route({ + method: 'POST', + url: `${IDENTITY_SERVICE}/resources/auth/v2/user/sso/prelogin`, + status: 400, + response: { address: null }, + delay: 200, + }).as('preLogin'); + cy.route({ + method: 'GET', + url: `${IDENTITY_SERVICE}/resources/configurations/v1/mfa-policy/allow-remember-device?mfaToken=${MFA_TOKEN}`, + status: 200, + response: { isAllowedToRemember: false, mfaDeviceExpiration: 0 }, + delay: 200, + }).as('checkIfAllowToRememberDevice'); + cy.route({ + method: 'POST', + url: `${IDENTITY_SERVICE}/resources/auth/v1/user`, + status: 200, + response: { accessToken: ACCESS_TOKEN, refreshToken: 'refreshToken' }, + delay: 200, + }).as('login'); + mount(<TestFronteggWrapper plugins={[AuthPlugin(defaultAuthPlugin)]}>Home</TestFronteggWrapper>, { + ...mountOptions, + alias: 'providerComponent', + }); + + navigateTo(defaultAuthPlugin.routes.loginUrl); + cy.wait(['@refreshToken', '@metadata']); + cy.get('.loader').should('not.be.visible'); + + const emailSelector = '[name="email"]'; + const passwordSelector = '[name="password"]'; + const submitSelector = 'button[type=submit]'; + + cy.get(submitSelector).contains('Continue').should('be.disabled'); + checkEmailValidation(); + cy.get(submitSelector).contains('Continue').should('not.be.disabled'); + cy.get(passwordSelector).should('not.be.visible'); + + cy.get(submitSelector).contains('Continue').click(); + cy.get(submitSelector).should('not.have.class', 'loading'); + + cy.get(passwordSelector).should('be.visible'); + checkPasswordValidation(); + cy.get(submitSelector).contains('Login').should('not.be.disabled'); + + // change email should reset the login back to preLogin + cy.get(emailSelector).focus().clear().type(EMAIL_1).blur(); + cy.get(passwordSelector).should('not.be.visible'); + cy.get(submitSelector).contains('Continue').click(); + + cy.wait('@preLogin').its('request.body').should('deep.equal', { email: EMAIL_1 }); + + cy.get(passwordSelector).should('be.visible'); + cy.get(passwordSelector).focus().clear().type(PASSWORD).blur(); + cy.get(submitSelector).contains('Login').should('not.be.disabled'); + + cy.window().then((win) => win.localStorage.removeItem(FRONTEGG_AFTER_AUTH_REDIRECT_URL)); + cy.get(submitSelector).click(); + + cy.wait('@login') + .its('request.body') + .should('deep.equal', { email: EMAIL_1, password: PASSWORD, recaptchaToken: '' }); + + // cy.contains('Authentication Succeeded').should('be.visible'); + cy.contains('Home').should('be.visible'); + cy.location().should((loc) => { + expect(loc.pathname).to.eq('/'); + }); + }); + + it('Login, WITH SAML tenant, WITH email', () => { + cy.server(); + mockAuthApi(false, true); + cy.route({ + method: 'POST', + url: `${IDENTITY_SERVICE}/resources/auth/v2/user/sso/prelogin`, + status: 200, + response: { address: SSO_PATH }, + delay: 200, + }).as('preLogin'); + + mount(<TestFronteggWrapper plugins={[AuthPlugin(defaultAuthPlugin)]}>Home</TestFronteggWrapper>, { + ...mountOptions, + alias: 'providerComponent', + }); + + navigateTo(defaultAuthPlugin.routes.loginUrl); + cy.wait(['@refreshToken', '@metadata']); + cy.get('.loader').should('not.be.visible'); + + const emailSelector = '[name="email"]'; + const passwordSelector = '[name="password"]'; + const submitSelector = 'button[type=submit]'; + + cy.get(submitSelector).contains('Continue').should('be.disabled'); + checkEmailValidation(); + cy.get(submitSelector).contains('Continue').should('not.be.disabled'); + + cy.get(passwordSelector).should('not.be.visible'); + cy.get(submitSelector).contains('Continue').click(); + cy.get(submitSelector).should('have.class', 'loading'); + + cy.get(passwordSelector).should('not.be.visible'); + + cy.wait('@preLogin').its('request.body').should('deep.equal', { email: EMAIL_1 }); + + cy.location().should((loc) => { + expect(loc.pathname).to.eq(SSO_PATH); + }); + }); + + it('Login, WITH SAML tenant, WITH email, with two-factor', () => { + cy.server(); + mockAuthApi(false, true); + cy.route({ + method: 'POST', + url: `${IDENTITY_SERVICE}/resources/auth/v2/user/sso/prelogin`, + status: 200, + response: { address: SSO_PATH }, + delay: 200, + }).as('preLogin'); + cy.route({ + method: 'GET', + url: `${IDENTITY_SERVICE}/resources/configurations/v1/mfa-policy/allow-remember-device?mfaToken=${MFA_TOKEN}`, + status: 200, + response: { isAllowedToRemember: false, mfaDeviceExpiration: 0 }, + }).as('checkIfAllowToRememberDevice'); + + mount(<TestFronteggWrapper plugins={[AuthPlugin(defaultAuthPlugin)]}>Home</TestFronteggWrapper>, { + ...mountOptions, + alias: 'providerComponent', + }); + + navigateTo(defaultAuthPlugin.routes.loginUrl); + cy.wait(['@refreshToken', '@metadata']); + cy.get('.loader').should('not.be.visible'); + + const passwordSelector = '[name="password"]'; + const submitSelector = 'button[type=submit]'; + const codeSelector = '[name="code"]'; + + cy.get(submitSelector).contains('Continue').should('be.disabled'); + checkEmailValidation(); + cy.get(submitSelector).contains('Continue').should('not.be.disabled'); + + cy.get(passwordSelector).should('not.be.visible'); + cy.get(submitSelector).contains('Continue').click(); + cy.get(submitSelector).should('have.class', 'loading'); + + cy.get(passwordSelector).should('not.be.visible'); + + cy.wait('@preLogin').its('request.body').should('deep.equal', { email: EMAIL_1 }); + + cy.location().should((loc) => { + expect(loc.pathname).to.eq(SSO_PATH); + }); + + cy.route({ + method: 'POST', + url: `${IDENTITY_SERVICE}/resources/auth/v1/user/token/refresh`, + status: 200, + response: { + mfaRequired: true, + mfaToken: MFA_TOKEN, + }, + }).as('refreshTokenForMfa'); + cy.wait(1000); + navigateTo(defaultAuthPlugin.routes.authenticatedUrl); + + mount(<TestFronteggWrapper plugins={[AuthPlugin(defaultAuthPlugin)]}>Home</TestFronteggWrapper>, { + ...mountOptions, + alias: 'providerComponent', + }); + + cy.wait(['@refreshTokenForMfa', '@checkIfAllowToRememberDevice']); + + cy.contains('Please enter the 6 digit code from your authenticator app').should('be.visible'); + + const validCode = '123123'; + cy.get(codeSelector).focus().type('111').blur(); + cy.get(codeSelector).parents('.field').should('have.class', 'error'); + cy.get(submitSelector).contains('Login').should('be.disabled'); + cy.get(codeSelector).focus().clear().type(validCode).blur(); + cy.get(codeSelector).parents('.field').should('not.have.class', 'error'); + cy.get(submitSelector).contains('Login').should('not.be.disabled'); + + cy.route({ + method: 'POST', + url: `${IDENTITY_SERVICE}/resources/auth/v1/user/mfa/verify`, + status: 400, + response: { errors: ['invalid code'] }, + delay: 200, + }).as('verifyMfa'); + + cy.get(submitSelector).contains('Login').click(); + cy.wait('@verifyMfa') + .its('request.body') + .should('deep.equal', { mfaToken: MFA_TOKEN, value: validCode, rememberDevice: false }); + cy.contains('invalid code').should('be.visible'); + + cy.route({ + method: 'POST', + url: `${IDENTITY_SERVICE}/resources/auth/v1/user/mfa/verify`, + status: 200, + response: { accessToken: ACCESS_TOKEN, refreshToken: 'refreshToken' }, + delay: 200, + }).as('verifyMfa'); + cy.route({ + method: 'GET', + url: `${IDENTITY_SERVICE}/resources/users/v2/me`, + status: 200, + response: { name: 'name', email: 'email' }, + delay: 200, + }).as('me'); + cy.get(submitSelector).contains('Login').click(); + cy.wait('@verifyMfa') + .its('request.body') + .should('deep.equal', { mfaToken: MFA_TOKEN, value: validCode, rememberDevice: false }); + cy.wait('@me'); + + cy.location().should((loc) => { + expect(loc.pathname).to.eq('/'); + }); + }); + + it('Login, NO SAML, Two-Factor', () => { + cy.server(); + mockAuthApi(false, false); + mockAuthMe(); + cy.route({ + method: 'POST', + url: `${IDENTITY_SERVICE}/resources/auth/v1/user`, + status: 200, + response: { mfaToken: MFA_TOKEN, mfaRequired: true }, + delay: 200, + }).as('login'); + cy.route({ + method: 'GET', + url: `${IDENTITY_SERVICE}/resources/configurations/v1/mfa-policy/allow-remember-device?mfaToken=${MFA_TOKEN}`, + status: 200, + response: { isAllowedToRemember: true, mfaDeviceExpiration: 60 * 60 * 24 * 3 }, + delay: 200, + }).as('checkIfAllowToRememberDevice'); + + mount(<TestFronteggWrapper plugins={[AuthPlugin(defaultAuthPlugin)]}>Home</TestFronteggWrapper>, mountOptions); + + navigateTo(defaultAuthPlugin.routes.loginUrl); + cy.wait(['@refreshToken', '@metadata']); + cy.get('.loader').should('not.be.visible'); + + const submitSelector = 'button[type=submit]'; + const codeSelector = '[name="code"]'; + const rememberDeviceSelector = '[name="rememberDevice"]'; + + cy.get(submitSelector).contains('Login').should('be.disabled'); + checkEmailValidation(); + cy.get(submitSelector).contains('Login').should('be.disabled'); + + checkPasswordValidation(); + cy.get(submitSelector).contains('Login').should('not.be.disabled'); + + cy.window().then((win) => win.localStorage.removeItem(FRONTEGG_AFTER_AUTH_REDIRECT_URL)); + cy.get(submitSelector).contains('Login').click(); + + cy.wait('@login') + .its('request.body') + .should('deep.equal', { email: EMAIL_1, password: PASSWORD, recaptchaToken: '' }); + + cy.contains('Please enter the 6 digit code from your authenticator app').should('be.visible'); + + const validCode = '123123'; + cy.get(codeSelector).focus().type('111').blur(); + cy.get(codeSelector).parents('.field').should('have.class', 'error'); + cy.get(submitSelector).contains('Login').should('be.disabled'); + cy.get(codeSelector).focus().clear().type(validCode).blur(); + cy.get(codeSelector).parents('.field').should('not.have.class', 'error'); + cy.get(submitSelector).contains('Login').should('not.be.disabled'); + + cy.route({ + method: 'POST', + url: `${IDENTITY_SERVICE}/resources/auth/v1/user/mfa/verify`, + status: 400, + response: { errors: ['invalid code'] }, + delay: 200, + }).as('verifyMfa'); + cy.get(submitSelector).contains('Login').click(); + cy.wait('@verifyMfa') + .its('request.body') + .should('deep.equal', { mfaToken: MFA_TOKEN, value: validCode, rememberDevice: false }); + cy.contains('invalid code').should('be.visible'); + cy.contains(`Don't ask again on this device for 3 days`).should('be.visible'); + cy.get(rememberDeviceSelector).should('be.visible'); + cy.route({ + method: 'POST', + url: `${IDENTITY_SERVICE}/resources/auth/v1/user/mfa/verify`, + status: 200, + response: { accessToken: ACCESS_TOKEN, refreshToken: 'refreshToken' }, + delay: 200, + }).as('verifyMfa'); + cy.get(submitSelector).contains('Login').click(); + cy.wait('@verifyMfa') + .its('request.body') + .should('deep.equal', { mfaToken: MFA_TOKEN, value: validCode, rememberDevice: false }); + + cy.location().should((loc) => { + expect(loc.pathname).to.eq('/'); + }); + }); + + it('Login, NO SAML, Recover Two-Factor', () => { + cy.server(); + mockAuthApi(false, false); + cy.route({ + method: 'POST', + url: `${IDENTITY_SERVICE}/resources/auth/v1/user`, + status: 200, + response: { mfaToken: MFA_TOKEN, mfaRequired: true }, + delay: 200, + }).as('login'); + cy.route({ + method: 'GET', + url: `${IDENTITY_SERVICE}/resources/configurations/v1/mfa-policy/allow-remember-device?mfaToken=${MFA_TOKEN}`, + status: 200, + response: { isAllowedToRemember: false, mfaDeviceExpiration: 0 }, + delay: 200, + }).as('checkIfAllowToRememberDevice'); + + mount(<TestFronteggWrapper plugins={[AuthPlugin(defaultAuthPlugin)]}>Home</TestFronteggWrapper>, mountOptions); + + navigateTo(defaultAuthPlugin.routes.loginUrl); + cy.wait(['@refreshToken', '@metadata']); + cy.get('.loader').should('not.be.visible'); + + const submitSelector = 'button[type=submit]'; + const codeSelector = '[name="code"]'; + + cy.get(submitSelector).contains('Login').should('be.disabled'); + checkEmailValidation(); + cy.get(submitSelector).contains('Login').should('be.disabled'); + checkPasswordValidation(); + cy.get(submitSelector).contains('Login').should('not.be.disabled'); + + cy.get(submitSelector).contains('Login').click(); + cy.wait('@login') + .its('request.body') + .should('deep.equal', { email: EMAIL_1, password: PASSWORD, recaptchaToken: '' }); + + cy.contains('Please enter the 6 digit code from your authenticator app').should('be.visible'); + cy.get('[test-id="recover-two-factor-button"]').click(); + + cy.contains('Please enter your MFA recovery code').should('be.visible'); + + cy.get(codeSelector).focus().clear().type(RECOVERY_CODE).blur(); + + cy.route({ + method: 'POST', + url: `${IDENTITY_SERVICE}/resources/auth/v1/user/mfa/recover`, + status: 400, + response: { errors: ['invalid recovery code'] }, + delay: 200, + }).as('recoverMfa'); + + cy.get(submitSelector).contains('Disable MFA').click(); + cy.wait('@recoverMfa').its('request.body').should('deep.equal', { recoveryCode: RECOVERY_CODE, email: EMAIL_1 }); + cy.contains('invalid recovery code').should('be.visible'); + + cy.route({ + method: 'POST', + url: `${IDENTITY_SERVICE}/resources/auth/v1/user/mfa/recover`, + status: 200, + response: {}, + delay: 200, + }).as('recoverMfa'); + cy.get(submitSelector).contains('Disable MFA').click(); + cy.wait('@recoverMfa').its('request.body').should('deep.equal', { recoveryCode: RECOVERY_CODE, email: EMAIL_1 }); + + cy.location().should((loc) => { + expect(loc.pathname).to.eq('/account/login'); + }); + }); + + it('Login with Social Login', () => { + cy.server(); + mockAuthApi(false, false, true); + cy.route({ + method: 'POST', + url: `${IDENTITY_SERVICE}/resources/auth/v2/user/sso/prelogin`, + status: 200, + response: { address: SSO_PATH }, + delay: 200, + }).as('preLogin'); + + mount(<TestFronteggWrapper plugins={[AuthPlugin(defaultAuthPlugin)]}>Home</TestFronteggWrapper>, { + ...mountOptions, + alias: 'providerComponent', + }); + + navigateTo(defaultAuthPlugin.routes.loginUrl); + cy.wait([ + '@refreshToken', + '@metadata', + '@socialLogin', + '@publicConfigurations', + '@publicAuthStrategyConfigurations', + ]); + cy.get('.loader').should('not.be.visible'); + + cy.location('origin').then((origin) => { + const loginWithGoogleSelector = '[data-test-id="googleSocialLogin-btn"]'; + cy.get(loginWithGoogleSelector).contains('Login with Google').should('not.be.disabled').click(); + cy.location().should((loc) => { + expect(loc.pathname + loc.search).to.eq(getGoogleAuthUrl(origin)); + }); + + const redirectUri = origin + defaultAuthPlugin.routes.socialLoginCallbackUrl; + + cy.route({ + method: 'POST', + url: `${IDENTITY_SERVICE}/resources/auth/v1/user/sso/google/postlogin?code=google_auth_code&redirectUri=${redirectUri}`, + status: 200, + delay: 200, + response: {}, + }).as('submitSocialLogin'); + cy.route({ + method: 'POST', + url: `${IDENTITY_SERVICE}/resources/auth/v1/user/token/refresh`, + status: 200, + response: refreshTokenResponse, + }).as('refreshToken'); + + mockAuthMe(); + navigateTo(defaultAuthPlugin.routes.socialLoginCallbackUrl + GOOGLE_AUTH_RESPONSE); + + cy.get('.loader').should('not.be.visible'); + cy.wait(['@submitSocialLogin', '@refreshToken', '@meTenants', '@me']); + + cy.wait(1000); + + cy.location().should((loc) => { + expect(loc.pathname).to.eq('/'); + }); + }); + }); + + // it('Login with Social, with two-factor', () => { + // cy.server(); + // mockAuthApi(false, false, true); + // cy.route({ + // method: 'POST', + // url: `${IDENTITY_SERVICE}/resources/auth/v2/user/sso/prelogin`, + // status: 200, + // response: { address: SSO_PATH }, + // delay: 200, + // }).as('preLogin'); + // + // mount(<TestFronteggWrapper plugins={[AuthPlugin(defaultAuthPlugin)]}>Home</TestFronteggWrapper>, { + // ...mountOptions, + // alias: 'providerComponent', + // }); + // + // navigateTo(defaultAuthPlugin.routes.loginUrl); + // cy.wait(['@refreshToken', '@metadata', '@socialLogin', '@publicConfigurations']); + // cy.get('.loader').should('not.be.visible'); + // + // cy.location('origin').then((origin) => { + // const loginWithGoogleSelector = '[data-test-id="googleSocialLogin-btn"]'; + // cy.get(loginWithGoogleSelector).contains('Login with Google').should('not.be.disabled').click(); + // cy.location().should((loc) => { + // expect(loc.pathname + loc.search).to.eq(getGoogleAuthUrl(origin)); + // }); + // + // navigateTo(defaultAuthPlugin.routes.socialLoginCallbackUrl + GOOGLE_AUTH_RESPONSE); + // + // cy.get('.loader').should('not.be.visible'); + // + // const redirectUri = origin + defaultAuthPlugin.routes.socialLoginCallbackUrl; + // cy.route({ + // method: 'POST', + // url: `${IDENTITY_SERVICE}/resources/auth/v1/user/sso/google/postlogin?code=google_auth_code?redirectUri=${redirectUri}`, + // status: 200, + // delay: 200, + // response: {}, + // }).as('submitSocialLogin'); + // cy.route({ + // method: 'POST', + // url: `${IDENTITY_SERVICE}/resources/auth/v1/user/token/refresh`, + // status: 200, + // response: { + // mfaRequired: true, + // mfaToken: MFA_TOKEN, + // }, + // }).as('refreshToken'); + // + // cy.wait(['@submitSocialLogin', '@refreshToken']); + // }); + // + // cy.contains('Please enter the 6 digit code from your authenticator app').should('be.visible'); + // + // const submitSelector = 'button[type=submit]'; + // const codeSelector = '[name="code"]'; + // + // const validCode = '123123'; + // cy.get(codeSelector).focus().type('111').blur(); + // cy.get(codeSelector).parents('.field').should('have.class', 'error'); + // cy.get(submitSelector).contains('Login').should('be.disabled'); + // cy.get(codeSelector).focus().clear().type(validCode).blur(); + // cy.get(codeSelector).parents('.field').should('not.have.class', 'error'); + // cy.get(submitSelector).contains('Login').should('not.be.disabled'); + // + // cy.route({ + // method: 'POST', + // url: `${IDENTITY_SERVICE}/resources/auth/v1/user/mfa/verify`, + // status: 400, + // response: { errors: ['invalid code'] }, + // delay: 200, + // }).as('verifyMfa'); + // cy.get(submitSelector).contains('Login').click(); + // cy.wait('@verifyMfa').its('request.body').should('deep.equal', { mfaToken: MFA_TOKEN, value: validCode }); + // cy.contains('invalid code').should('be.visible'); + // + // mockAuthMe(); + // cy.route({ + // method: 'POST', + // url: `${IDENTITY_SERVICE}/resources/auth/v1/user/mfa/verify`, + // status: 200, + // response: { accessToken: ACCESS_TOKEN, refreshToken: 'refreshToken' }, + // delay: 200, + // }).as('verifyMfa'); + // cy.get(submitSelector).contains('Login').click(); + // cy.wait('@verifyMfa').its('request.body').should('deep.equal', { mfaToken: MFA_TOKEN, value: validCode }); + // + // cy.location().should((loc) => { + // expect(loc.pathname).to.eq('/'); + // }); + // }); + + it('Logout Component', () => { + cy.server(); + cy.route({ + method: 'POST', + url: `${IDENTITY_SERVICE}/resources/auth/v1/user/token/refresh`, + status: 200, + response: { accessToken: ACCESS_TOKEN }, + }); + cy.route({ method: 'GET', url: `${METADATA_SERVICE}?entityName=saml`, status: 200, response: { rows: [] } }); + cy.route({ method: 'POST', url: `${IDENTITY_SERVICE}/resources/auth/v1/logout`, status: 200, response: 'LOGOUT' }); + + navigateTo(defaultAuthPlugin.routes.logoutUrl); + mount(<TestFronteggWrapper plugins={[AuthPlugin(defaultAuthPlugin)]}>Home</TestFronteggWrapper>, { + ...mountOptions, + alias: 'providerComponent', + }); + + cy.location().should((loc) => { + expect(loc.pathname).to.eq(defaultAuthPlugin.routes.loginUrl); + }); + }); +}); diff --git a/packages/auth/tsconfig.json b/packages/auth/tsconfig.json new file mode 100644 index 000000000..150a85181 --- /dev/null +++ b/packages/auth/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "declarationDir": "./dist" + }, + "include": [ + "./src/**/*.tsx", + "./src/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist", + "src/**/*.stories.tsx", + "src/**/*.test.tsx", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.spec.tsx", + "src/**/*.cy-spec.ts", + "src/**/*.cy-spec.tsx" + ] +} + diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md new file mode 100644 index 000000000..ab1e51d43 --- /dev/null +++ b/packages/cli/CHANGELOG.md @@ -0,0 +1,440 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [2.8.3](https://github.com/frontegg/frontegg-react/compare/v2.8.1...v2.8.3) (2021-06-23) + + +### Bug Fixes + +* align all dependencies versions ([f1d5c48](https://github.com/frontegg/frontegg-react/commit/f1d5c48aba34827ea06a554cfb61734f1a40c93b)) +* update libraries version ([77da072](https://github.com/frontegg/frontegg-react/commit/77da072c67cb11e6cccb86b044a3a1a22b09625c)) + + + + + +## [2.8.2](https://github.com/frontegg/frontegg-react/compare/v2.8.1...v2.8.2) (2021-06-23) + + +### Bug Fixes + +* update libraries version ([77da072](https://github.com/frontegg/frontegg-react/commit/77da072c67cb11e6cccb86b044a3a1a22b09625c)) + + + + + +# [2.0.0](https://github.com/frontegg/frontegg-react/compare/v1.28.0...v2.0.0) (2021-04-11) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +## [1.22.1](https://github.com/frontegg/frontegg-react/compare/v1.22.0...v1.22.1) (2021-02-16) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +# [1.22.0](https://github.com/frontegg/frontegg-react/compare/v1.21.1...v1.22.0) (2021-02-13) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +## [1.21.1](https://github.com/frontegg/frontegg-react/compare/v1.21.0...v1.21.1) (2021-02-04) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +# [1.21.0](https://github.com/frontegg/frontegg-react/compare/v1.20.1...v1.21.0) (2021-02-04) + + +### Bug Fixes + +* Remove deprecated @frontegg/react support ([48f493c](https://github.com/frontegg/frontegg-react/commit/48f493cafb98dfcf66096c6f2a577c067c5c8bdf)) + + + + + +## [1.20.1](https://github.com/frontegg/frontegg-react/compare/v1.20.0...v1.20.1) (2021-02-02) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +# [1.20.0](https://github.com/frontegg/frontegg-react/compare/v1.19.1...v1.20.0) (2021-02-01) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +## [1.19.1](https://github.com/frontegg/frontegg-react/compare/v1.19.0...v1.19.1) (2021-01-20) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +# [1.19.0](https://github.com/frontegg/frontegg-react/compare/v1.18.6...v1.19.0) (2021-01-20) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +## [1.18.6](https://github.com/frontegg/frontegg-react/compare/v1.18.5...v1.18.6) (2021-01-19) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +## [1.18.5](https://github.com/frontegg/frontegg-react/compare/v1.18.4...v1.18.5) (2021-01-18) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +## [1.18.4](https://github.com/frontegg/frontegg-react/compare/v1.18.3...v1.18.4) (2021-01-18) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +## [1.18.3](https://github.com/frontegg/frontegg-react/compare/v1.18.2...v1.18.3) (2021-01-17) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +## [1.18.2](https://github.com/frontegg/frontegg-react/compare/v1.18.1...v1.18.2) (2021-01-15) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +## [1.18.1](https://github.com/frontegg/frontegg-react/compare/v1.18.0...v1.18.1) (2021-01-15) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +# [1.18.0](https://github.com/frontegg/frontegg-react/compare/v1.17.3...v1.18.0) (2021-01-14) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +## [1.17.3](https://github.com/frontegg/frontegg-react/compare/v1.17.2...v1.17.3) (2021-01-13) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +## [1.17.2](https://github.com/frontegg/frontegg-react/compare/v1.17.1...v1.17.2) (2021-01-12) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +## [1.17.1](https://github.com/frontegg/frontegg-react/compare/v1.17.0...v1.17.1) (2021-01-05) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +# [1.17.0](https://github.com/frontegg/frontegg-react/compare/v1.16.2...v1.17.0) (2021-01-03) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +## [1.16.2](https://github.com/frontegg/frontegg-react/compare/v1.16.0...v1.16.2) (2020-12-27) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +## [1.16.1](https://github.com/frontegg/frontegg-react/compare/v1.16.0...v1.16.1) (2020-12-27) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +# [1.16.0](https://github.com/frontegg/frontegg-react/compare/v1.15.2...v1.16.0) (2020-12-24) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +## [1.15.2](https://github.com/frontegg/frontegg-react/compare/v1.15.1...v1.15.2) (2020-12-24) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +## [1.15.1](https://github.com/frontegg/frontegg-react/compare/v1.15.0...v1.15.1) (2020-12-21) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +# [1.15.0](https://github.com/frontegg/frontegg-react/compare/v1.14.1...v1.15.0) (2020-12-20) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +## [1.14.1](https://github.com/frontegg/frontegg-react/compare/v1.14.0...v1.14.1) (2020-12-17) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +# [1.14.0](https://github.com/frontegg/frontegg-react/compare/v1.13.2...v1.14.0) (2020-12-16) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +## [1.13.2](https://github.com/frontegg/frontegg-react/compare/v1.13.1...v1.13.2) (2020-12-13) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +## [1.13.1](https://github.com/frontegg/frontegg-react/compare/v1.13.0...v1.13.1) (2020-12-10) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +# [1.13.0](https://github.com/frontegg/frontegg-react/compare/v1.12.0...v1.13.0) (2020-12-09) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +# [1.12.0](https://github.com/frontegg/frontegg-react/compare/v1.11.1...v1.12.0) (2020-12-09) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +## [1.11.1](https://github.com/frontegg/frontegg-react/compare/v1.11.0...v1.11.1) (2020-12-02) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +# [1.11.0](https://github.com/frontegg/frontegg-react/compare/v1.10.0...v1.11.0) (2020-11-30) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +# [1.10.0](https://github.com/frontegg/frontegg-react/compare/v1.9.0...v1.10.0) (2020-11-27) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +# [1.9.0](https://github.com/frontegg/frontegg-react/compare/v1.8.0...v1.9.0) (2020-11-25) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +# [1.8.0](https://github.com/frontegg/frontegg-react/compare/v1.7.0...v1.8.0) (2020-11-23) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +# [1.7.0](https://github.com/frontegg/frontegg-react/compare/v1.6.1...v1.7.0) (2020-11-22) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +## [1.6.2](https://github.com/frontegg/frontegg-react/compare/v1.6.1...v1.6.2) (2020-11-19) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +## [1.6.1](https://github.com/frontegg/frontegg-react/compare/v1.6.0...v1.6.1) (2020-11-15) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +# [1.6.0](https://github.com/frontegg/frontegg-react/compare/v1.5.0...v1.6.0) (2020-11-12) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +# [1.5.0](https://github.com/frontegg/frontegg-react/compare/v1.4.0...v1.5.0) (2020-11-10) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +# [1.4.0](https://github.com/frontegg/frontegg-react/compare/v1.3.0...v1.4.0) (2020-11-09) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +# [1.3.0](https://github.com/frontegg/frontegg-react/compare/v1.2.0...v1.3.0) (2020-11-04) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +# [1.2.0](https://github.com/frontegg/frontegg-react/compare/v1.1.0...v1.2.0) (2020-10-25) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +# [1.1.0](https://github.com/frontegg/frontegg-react/compare/v1.0.89...v1.1.0) (2020-10-14) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +## [1.0.89](https://github.com/frontegg/frontegg-react/compare/v1.0.88...v1.0.89) (2020-10-13) + + +### Bug Fixes + +* **cli:** fix missing property in frontegg/react-cli ([2d45c3f](https://github.com/frontegg/frontegg-react/commit/2d45c3f2c44c4531e72434cb7935a42c28012992)), closes [#44](https://github.com/frontegg/frontegg-react/issues/44) + + + + + +## [1.0.88](https://github.com/frontegg/frontegg-react/compare/v1.0.87...v1.0.88) (2020-10-11) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +## [1.0.87](https://github.com/frontegg/frontegg-react/compare/v1.0.86...v1.0.87) (2020-10-09) + + +### Bug Fixes + +* **cli:** add missing tslib to cli package json ([de0bc6e](https://github.com/frontegg/frontegg-react/commit/de0bc6e2f7558077eef8c7c1aeb815ff561f000e)) + + + + + +## 1.0.85 (2020-10-08) + +**Note:** Version bump only for package @frontegg/react-cli + + + + + +## 1.0.84 (2020-10-08) + +**Note:** Version bump only for package @frontegg/react-cli diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 000000000..a13d8b7d4 --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,27 @@ +{ + "name": "@frontegg/react-cli", + "version": "4.0.23", + "author": "Frontegg LTD", + "scripts": { + "build": "export NODE_ENV='production'; rm -rf ./dist && rollup -c ../../scripts/rollup-cli.config.js && echo DONE", + "build:watch": "rollup -w -c ../../scripts/rollup-cli.config.js", + "test": "jest --runInBand --passWithNoTests -c ../../scripts/jest.config.json --rootDir . && echo DONE" + }, + "bin": { + "frontegg": "dist/index.js" + }, + "dependencies": { + "chalk": "^4.1.0", + "clear": "^0.1.0", + "figlet": "^1.5.0", + "handlebars": "^4.7.6", + "history": "^4.9.0", + "prompts": "^2.3.2", + "tslib": "^2.0.1", + "yargs": "^15.4.1" + }, + "devDependencies": { + "@types/node": "^13.9.1" + }, + "gitHead": "00c0bd425a211916eec1170b8359cf302727b338" +} diff --git a/packages/cli/src/helpers.ts b/packages/cli/src/helpers.ts new file mode 100644 index 000000000..732562482 --- /dev/null +++ b/packages/cli/src/helpers.ts @@ -0,0 +1,88 @@ +import fs from 'fs'; +import path from 'path'; +import chalk from 'chalk'; + +export const usingTypescript = (): boolean => fs.existsSync(path.join(process.cwd(), 'tsconfig.json')); +export const usingYarn = (): boolean => { + let cwd = process.cwd(); + let packageLockFound = false; + do { + packageLockFound = fs.existsSync(path.join(cwd, 'package-lock.json')); + if ( + !packageLockFound && + (fs.existsSync(path.join(cwd, 'lerna.json')) || + fs.existsSync(path.join(cwd, 'yarn.lock')) || + fs.existsSync(path.join(cwd, 'node_modules')) || + fs.existsSync(path.join(cwd, '.git'))) + ) { + packageLockFound = false; + break; + } + cwd = path.join(cwd, '../'); + } while (!packageLockFound); + packageLockFound ? console.log(chalk.yellow('--using-npm')) : console.log(chalk.yellow('--using-yarn')); + return !packageLockFound; +}; +export const getPackageJson = (): any => + JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), { encoding: 'utf8' })); +export const isFileExistsInSrc = (fileName: string): boolean => + fs.existsSync(path.join(process.cwd(), 'src', fileName)); + +export const copyToOld = (fileName: string) => + fs.renameSync( + path.join(process.cwd(), 'src', fileName), + path.join(process.cwd(), 'src', `old${fileName.substring(0, 1).toUpperCase()}${fileName.substring(1)}`) + ); +export const createFileInSrc = (fileName: string, data: string) => + fs.writeFileSync(path.join(process.cwd(), 'src', fileName), data, { encoding: 'utf8' }); + +const coreName = '@frontegg/react-core'; +export const extractVersion = (v: string): string => { + let version = v.trim(); + if (version.indexOf(coreName) !== -1) { + version = version.substring(version.lastIndexOf(coreName) + coreName.length); + } + if (version.indexOf('@') !== -1) { + version = version.substring(version.indexOf('@') + 1); + } + if (version.startsWith('^')) { + version = version.substring(1); + } + if (version.indexOf('\n') !== -1) { + version = version.substring(0, version.indexOf('\n')); + } + if (version.indexOf(' ') !== -1) { + version = version.substring(0, version.indexOf(' ')); + } + return version.trim(); +}; + +export const createLoader = () => { + const text = 'Frontegg-React'; + const gap = text.length; + const p: any = []; + for (let i = 0; i < gap * 3; i++) { + const preSpace = i < gap ? 0 : i - gap; + const postSpace = i <= gap ? 2 * gap - i : i < 2 * gap ? 2 * gap + 1 - i : 0; + + let t = text; + if (i < gap) { + t = text.substring(gap - i); + } else if (i > 2 * gap) { + t = text.substring(0, 3 * gap - i); + } + p.push(`[${Array(preSpace).join('.')}${t}${Array(postSpace).join('.')}]`); + } + let x = 0; + return setInterval(() => { + process.stdout.write('\r' + p[x++]); + x = x < p.length ? x : 0; + }, 100); +}; + +export const printVersions = (installedPackages: string[], lastVersion: string) => { + console.log(chalk.yellow('You are UP-TO-DATE! :D'), '\nversion:'); + installedPackages.forEach((p) => { + console.log(chalk.green(` - ${p}@${lastVersion}`)); + }); +}; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts new file mode 100644 index 000000000..ed3da1ca8 --- /dev/null +++ b/packages/cli/src/index.ts @@ -0,0 +1,33 @@ +import yargs from 'yargs'; +import initProject from './initProject'; +import updatePackages from './updatePackages'; +import chalk from 'chalk'; +import figlet from 'figlet'; +import clear from 'clear'; + +let valid = false; +const hookCommand = (fn: any) => (args: any) => { + valid = true; + fn(args); +}; + +clear(); +console.log('\n'); +console.log(chalk.hex('#243c4b')(figlet.textSync('Frontegg - React', { horizontalLayout: 'full' }))); +console.log('\n React pre-built Component for faster and simpler integration with Frontegg services.\n'); +console.log( + '-------------------------------------------------------------------------------------------------------------' +); + +const argv = yargs + .usage('Usage: $0 <command> [options]') + .command('init', 'Initialize and inject Frontegg Provider to the project', hookCommand(initProject)) + .command('update', 'Update frontegg packages using npm update', hookCommand(updatePackages)) + .demandCommand(1, 1, '', '') + .option('latest', { + describe: 'frontegg update --latest; force update to latest version. (this may break components)', + }) + .help('h') + .alias('h', 'help') + .alias('f', 'force') + .epilog(`Frontegg LTD Copyright ${new Date().getFullYear()}`).argv; diff --git a/packages/cli/src/initProject.ts b/packages/cli/src/initProject.ts new file mode 100644 index 000000000..bde48335a --- /dev/null +++ b/packages/cli/src/initProject.ts @@ -0,0 +1,148 @@ +import { + copyToOld, + createFileInSrc, + createLoader, + isFileExistsInSrc, + printVersions, + usingTypescript, + usingYarn, +} from './helpers'; +import chalk from 'chalk'; +import prompts from 'prompts'; +import handlebars from 'handlebars'; +import withFronteggTemplate from './withFronteggTemplate'; +import { exec, execSync } from 'child_process'; +import path from 'path'; + +const uiLibraryCss: any = { + semantic: 'https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css', + bootstrap: 'not supported', + antd: 'not supported', +}; +const buildSelectedPluginsJS = (selectedPluginsJS: string[]): object => { + const js = []; + const importJs = []; + if (selectedPluginsJS.indexOf('auth') !== -1) { + js.push( + ` AuthPlugin({\n /* auth options, find more information at https://github.com/frontegg/frontegg-react/tree/master/packages/auth */\n }),` + ); + importJs.push(`import { AuthPlugin } from '@frontegg/react-auth';`); + } + + if (selectedPluginsJS.indexOf('audits') !== -1) { + js.push( + ` AuditsPlugin({\n /* audits options, find more information at https://github.com/frontegg/frontegg-react/tree/master/packages/audits */\n }),` + ); + importJs.push(`import { AuditsPlugin } from '@frontegg/react-audits';`); + } + + if (selectedPluginsJS.indexOf('audits') !== -1) { + js.push( + ` ConnectivityPlugin(), /* find more information at https://github.com/frontegg/frontegg-react/tree/master/packages/connectivity */` + ); + importJs.push(`import { ConnectivityPlugin } from '@frontegg/react-connectivity';`); + } + + return { + plugins: js.join('\n'), + imports: importJs.join('\n'), + }; +}; +export default async ({ argv }: any) => { + const isTypescript = usingTypescript(); + const constants = { + fileName: 'withFrontegg.js', + installCommand: 'npm install --save', + }; + if (isTypescript) { + constants.fileName = 'withFrontegg.tsx'; + } + if (usingYarn()) { + constants.installCommand = 'yarn add'; + } + + console.log(chalk.cyan('Initializing Frontegg React...')); + + if (isFileExistsInSrc(constants.fileName) && !argv.force) { + console.log(chalk.green('withFrontegg file already exists in src folder. moved to oldWithFrontegg')); + copyToOld(constants.fileName); + } + + const { selectedPlugins, withRouter, baseUrl, uiLibrary } = await prompts([ + { + type: 'multiselect', + name: 'selectedPlugins', + message: 'Select the plugins you want to install', + choices: [ + { title: 'Authentication Plugin (for secure access integration)', value: 'auth', selected: true }, + { title: 'Audits Plugin (for audit logs integration)', value: 'audits', selected: false }, + { title: 'Connectivity Plugin (for webhooks integration)', value: 'connectivity', selected: false }, + { title: 'Notifications Plugin (coming-soon)', value: 'connectivity', selected: false, disabled: true }, + { title: 'Reports Plugin (coming-soon)', value: 'connectivity', selected: false, disabled: true }, + ], + instructions: '\n\n Space to select. Return to submit', + } as any, + { + type: 'select', + name: 'uiLibrary', + message: 'Which UI Library do you use in your project?', + choices: [ + { title: 'Frontegg (recommended)', value: 'frontegg' }, + { title: 'Semantic', value: 'semantic' }, + { title: 'Material UI', value: 'material-ui' }, + { title: 'Bootstrap (coming soon)', value: 'bootstrap', disabled: true }, + { title: 'Antd (coming soon)', value: 'antd', disabled: true }, + ], + initial: 0, + }, + { + type: 'text', + name: 'baseUrl', + message: 'What is your server address?', + initial: 'http://localhost:8080', + }, + ]); + const loader = createLoader(); + + const jsFile = handlebars.compile(withFronteggTemplate)({ + typescript: isTypescript, + withRouter, + baseUrl, + uiLibrary, + ...buildSelectedPluginsJS(selectedPlugins), + }); + + const toInstall = [ + `@frontegg/rest-api`, + `@frontegg/react-core`, + `@frontegg/react-elements-${uiLibrary}`, + ...selectedPlugins.map((pluginName: string) => `@frontegg/react-${pluginName}`), + ]; + + const lastVersion = execSync('npm view @frontegg/react-core version', { encoding: 'utf8' }).trim(); + const command = `${constants.installCommand} ${toInstall.map((d) => `${d}@${lastVersion}`).join(' ')}`; + + createFileInSrc(constants.fileName, jsFile); + const filePath = path.join(process.cwd(), 'src', constants.fileName); + const exec1 = exec(command); + exec1.on('exit', () => { + clearInterval(loader); + process.stdout.write('\r \n'); + printVersions(toInstall, lastVersion); + + console.log(chalk.black(`\nGenerated ${constants.fileName} location:`), chalk.yellow(filePath)); + + console.log( + chalk.yellow(`\n---------------------------------------------------------------------------------------------------- +==> NEXT STEP: +==> 1. wrap you entire application with this HOC (withFrontegg) +${ + uiLibraryCss[uiLibrary] && + `==> 2. add this link to index html (only if you are not using ${uiLibrary} in your project) + <link rel="stylesheet" href="${uiLibraryCss[uiLibrary]}">` +} +----------------------------------------------------------------------------------------------------\n`) + ); + process.exit(0); + }); +}; diff --git a/packages/cli/src/updatePackages.ts b/packages/cli/src/updatePackages.ts new file mode 100644 index 000000000..6e51bd947 --- /dev/null +++ b/packages/cli/src/updatePackages.ts @@ -0,0 +1,65 @@ +import { createLoader, extractVersion, getPackageJson, printVersions, usingYarn } from './helpers'; +import { exec, execSync } from 'child_process'; +import chalk from 'chalk'; + +const packages = [ + '@frontegg/rest-api', + '@frontegg/react-core', + '@frontegg/react-auth', + '@frontegg/react-audits', + '@frontegg/react-connectivity', + '@frontegg/react-elements-semantic', + '@frontegg/react-elements-material-ui', +]; +export default ({ argv }: any) => { + const pkg = getPackageJson(); + const installedPackages = Object.keys(pkg.dependencies || {}).filter((dep) => packages.indexOf(dep) !== -1); + + if (installedPackages.length === 0) { + throw Error('package.json missing @frontegg/react- dependencies'); + } + + console.log(chalk.cyan('checking for updates...')); + + const commands = { + getCurrentVersion: 'npm list --depth=0 | grep @frontegg/react-core', + getLatestVersion: (version: string, latest: boolean) => + `npm view @frontegg/react-core@'${latest ? '>' : '^'}${version}' version`, + updateVersion: 'npm install --save', + }; + if (usingYarn()) { + commands.getCurrentVersion = 'yarn list --depth=0 --pattern @frontegg/react-core'; + commands.updateVersion = 'yarn add'; + } + + const currentVersion = extractVersion(execSync(commands.getCurrentVersion).toString('utf8')); + const lastVersion = extractVersion(execSync(commands.getLatestVersion(currentVersion, argv.latest)).toString('utf8')); + + if (currentVersion === lastVersion) { + printVersions(installedPackages, currentVersion); + return; + } + + console.log(chalk.cyan('updating frontegg packages:'), chalk.red(currentVersion), '->', chalk.green(lastVersion)); + const updateCommand = `${commands.updateVersion} ${installedPackages.map((p) => `${p}@${lastVersion}`).join(' ')}`; + + console.log(chalk.gray(`> exec: ${updateCommand}`)); + const loader = createLoader(); + const exec1 = exec(updateCommand); + const commandLogs = []; + exec1.stdout?.on('data', (data) => { + commandLogs.push(chalk.blue(`> ${data.toString()}`)); + }); + + exec1.stderr?.on('data', (data) => { + commandLogs.push(chalk.red(`> ${data.toString()}`)); + }); + exec1.on('exit', () => { + clearInterval(loader); + process.stdout.write('\r \n'); + if (exec1.exitCode === 0) { + printVersions(installedPackages, lastVersion); + } + process.exit(exec1.exitCode !== null ? exec1.exitCode : 0); + }); +}; diff --git a/packages/cli/src/withFronteggTemplate.ts b/packages/cli/src/withFronteggTemplate.ts new file mode 100644 index 000000000..c37cd256b --- /dev/null +++ b/packages/cli/src/withFronteggTemplate.ts @@ -0,0 +1,35 @@ +export default `import React{{#if typescript}}, { ComponentType }{{/if}} from 'react'; +import { {{#if typescript}}ContextOptions, PluginConfig, {{/if}}FronteggProvider } from '@frontegg/react-core'; +{{#if uiLibrary}}import { uiLibrary } from '@frontegg/react-elements-{{uiLibrary}}';{{/if}} +{{{imports}}} + +/** + * use this object to config Frontegg global context object + */ +const contextOptions{{#if typescript}}: ContextOptions{{/if}} = { + baseUrl: \`{{baseUrl}}\`, + requestCredentials: 'include', +}; + +const plugins{{#if typescript}}: PluginConfig[]{{/if}} = [ + // add frontegg plugin here +{{plugins}} +]; + +/** + * Wrap you entire application with this HOC. + * NOTE: Make sure to remove any BrowserRouter in your application if you use \`\`\`withRouter\`\`\` option + */ +{{#if typescript}} +export const withFrontegg = <P extends {}>(AppComponent: ComponentType<P>) => (props: P) => { +{{else}} +export const withFrontegg = (AppComponent) => (props) => { +{{/if}} + return <FronteggProvider + plugins={plugins} + context={contextOptions} + {{#if uiLibrary}}uiLibrary={uiLibrary}{{/if}} + > + <AppComponent {...props}/> + </FronteggProvider>; +};`; diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json new file mode 100644 index 000000000..8a769826a --- /dev/null +++ b/packages/cli/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": [ + "es6", + "es2015" + ], + "strict": true, + "types": [ + "node" + ], + "moduleResolution": "node", + "esModuleInterop": true, + "resolveJsonModule": true + }, + "include": [ + "./**/*.ts" + ], + "exclude": [ + "../../node_modules", + "node_modules", + "dist", + "src/**/*.stories.tsx", + "src/**/*.test.tsx", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.spec.tsx" + ] +} + diff --git a/packages/connectivity/CHANGELOG.md b/packages/connectivity/CHANGELOG.md new file mode 100644 index 000000000..bbb135afd --- /dev/null +++ b/packages/connectivity/CHANGELOG.md @@ -0,0 +1,659 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [2.8.12](https://github.com/frontegg/frontegg-react/compare/v2.8.11...v2.8.12) (2021-07-20) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +## [2.8.10](https://github.com/frontegg/frontegg-react/compare/v2.8.9...v2.8.10) (2021-07-08) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +## [2.8.9](https://github.com/frontegg/frontegg-react/compare/v2.8.8...v2.8.9) (2021-07-08) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +## [2.8.8](https://github.com/frontegg/frontegg-react/compare/v2.8.7...v2.8.8) (2021-07-06) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +## [2.8.7](https://github.com/frontegg/frontegg-react/compare/v2.8.6...v2.8.7) (2021-07-01) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +## [2.8.6](https://github.com/frontegg/frontegg-react/compare/v2.8.5...v2.8.6) (2021-06-30) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +## [2.8.5](https://github.com/frontegg/frontegg-react/compare/v2.8.4...v2.8.5) (2021-06-30) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +## [2.8.4](https://github.com/frontegg/frontegg-react/compare/v2.8.3...v2.8.4) (2021-06-29) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +## [2.8.3](https://github.com/frontegg/frontegg-react/compare/v2.8.1...v2.8.3) (2021-06-23) + + +### Bug Fixes + +* align all dependencies versions ([f1d5c48](https://github.com/frontegg/frontegg-react/commit/f1d5c48aba34827ea06a554cfb61734f1a40c93b)) +* update libraries version ([77da072](https://github.com/frontegg/frontegg-react/commit/77da072c67cb11e6cccb86b044a3a1a22b09625c)) + + + + + +## [2.8.2](https://github.com/frontegg/frontegg-react/compare/v2.8.1...v2.8.2) (2021-06-23) + + +### Bug Fixes + +* update libraries version ([77da072](https://github.com/frontegg/frontegg-react/commit/77da072c67cb11e6cccb86b044a3a1a22b09625c)) + + + + + +## [2.8.1](https://github.com/frontegg/frontegg-react/compare/v2.8.0...v2.8.1) (2021-06-22) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +# [2.8.0](https://github.com/frontegg/frontegg-react/compare/v2.7.2...v2.8.0) (2021-06-21) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +## [2.7.2](https://github.com/frontegg/frontegg-react/compare/v2.7.1...v2.7.2) (2021-06-14) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +# [2.7.0](https://github.com/frontegg/frontegg-react/compare/v2.6.0...v2.7.0) (2021-06-07) + + +### Bug Fixes + +* **connectivity:** fix the documentation ([02946a9](https://github.com/frontegg/frontegg-react/commit/02946a928360e12045ad5f23ba4717d8cbbf499b)) +* **connectivity:** remove the fitContent property. fix scrolling of the container ([b2a7c8f](https://github.com/frontegg/frontegg-react/commit/b2a7c8f13425c90d51b22cce613c71819a9c9f64)) + + + + + +# [2.6.0](https://github.com/frontegg/frontegg-react/compare/v2.5.2...v2.6.0) (2021-05-27) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +## [2.5.2](https://github.com/frontegg/frontegg-react/compare/v2.5.1...v2.5.2) (2021-05-24) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +## [2.5.1](https://github.com/frontegg/frontegg-react/compare/v2.5.0...v2.5.1) (2021-05-23) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +# [2.5.0](https://github.com/frontegg/frontegg-react/compare/v2.4.0...v2.5.0) (2021-05-21) + + +### Bug Fixes + +* **connectivity:** add overflow auto to connectivity page FR-3005 ([fd4b341](https://github.com/frontegg/frontegg-react/commit/fd4b34177d0da2b386325b4144bba2bad4a235d8)) + + +### Features + +* **connectivity:** add new paraneter fitConntent ([d31c281](https://github.com/frontegg/frontegg-react/commit/d31c28122b09b2b41ebcab1cc89d5a5f0bc93d17)) + + + + + +# [2.4.0](https://github.com/frontegg/frontegg-react/compare/v2.3.2...v2.4.0) (2021-05-19) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +## [2.3.2](https://github.com/frontegg/frontegg-react/compare/v2.3.1...v2.3.2) (2021-05-10) + + +### Bug Fixes + +* **connectivity:** fix UI glitches ([a78c4f0](https://github.com/frontegg/frontegg-react/commit/a78c4f0587a606cc529909d35a24d98ab3e66f01)) + + + + + +## [2.3.1](https://github.com/frontegg/frontegg-react/compare/v2.3.0...v2.3.1) (2021-05-10) + + +### Bug Fixes + +* **connectivity:** fix connectivity slack UI ([b214466](https://github.com/frontegg/frontegg-react/commit/b2144661bad8a6d827f4e6fc652fae1b9eae7dde)) + + + + + +# [2.3.0](https://github.com/frontegg/frontegg-react/compare/v2.2.2...v2.3.0) (2021-05-07) + + +### Bug Fixes + +* **connectivity:** fix save data in the slack configurattionn ([23ff145](https://github.com/frontegg/frontegg-react/commit/23ff1452cafc91debd4ee99ee473798e37e5d739)) + + + + + +## [2.2.2](https://github.com/frontegg/frontegg-react/compare/v2.2.1...v2.2.2) (2021-04-29) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +## [2.2.1](https://github.com/frontegg/frontegg-react/compare/v2.2.0...v2.2.1) (2021-04-28) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +# [2.2.0](https://github.com/frontegg/frontegg-react/compare/v2.1.0...v2.2.0) (2021-04-28) + + +### Bug Fixes + +* **connectivity:** FR-2311 validate webhook secret key length and format error message ([#358](https://github.com/frontegg/frontegg-react/issues/358)) ([589fccb](https://github.com/frontegg/frontegg-react/commit/589fccbf8c34704cbf16f246cda96cdcd5b85f92)) + + +### Features + +* **connectivity:** FR-2586 format dates on webhook page ([#357](https://github.com/frontegg/frontegg-react/issues/357)) ([80a6832](https://github.com/frontegg/frontegg-react/commit/80a683273d49517583967ed04fc589da74e8d020)) + + + + + +# [2.1.0](https://github.com/frontegg/frontegg-react/compare/v2.0.0...v2.1.0) (2021-04-13) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +# [2.0.0](https://github.com/frontegg/frontegg-react/compare/v1.28.0...v2.0.0) (2021-04-11) + + +### Bug Fixes + +* **connectivity:** FR-2310 - fix test hook form filling ([d54e053](https://github.com/frontegg/frontegg-react/commit/d54e053d3b40059558f91012c3cc82a709da96ff)) +* **connectivity:** FR-2312 - merge with master ([41b7a2b](https://github.com/frontegg/frontegg-react/commit/41b7a2b7147c249aac61352f2fc70bb068e6ccaf)) +* FR-2312 - fixstyle status button (webhooks); add success for theme' ([1c4ca9d](https://github.com/frontegg/frontegg-react/commit/1c4ca9de4ced5a567740bb2d812c104a51435c3a)) + + + + + +# [1.28.0](https://github.com/frontegg/frontegg-react/compare/v1.27.0...v1.28.0) (2021-03-22) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +# [1.27.0](https://github.com/frontegg/frontegg-react/compare/v1.26.0...v1.27.0) (2021-03-18) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +# [1.26.0](https://github.com/frontegg/frontegg-react/compare/v1.25.0...v1.26.0) (2021-03-17) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +# [1.25.0](https://github.com/frontegg/frontegg-react/compare/v1.24.0...v1.25.0) (2021-03-07) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +# [1.24.0](https://github.com/frontegg/frontegg-react/compare/v1.23.1...v1.24.0) (2021-03-03) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +## [1.23.1](https://github.com/frontegg/frontegg-react/compare/v1.23.0...v1.23.1) (2021-02-21) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +## [1.22.1](https://github.com/frontegg/frontegg-react/compare/v1.22.0...v1.22.1) (2021-02-16) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +# [1.22.0](https://github.com/frontegg/frontegg-react/compare/v1.21.1...v1.22.0) (2021-02-13) + + +### Bug Fixes + +* **connectivity:** fix scroll for the event table ([7871686](https://github.com/frontegg/frontegg-react/commit/78716867a077d30d9cf8698424e25f08f8e4591d)) + + + + + +## [1.21.1](https://github.com/frontegg/frontegg-react/compare/v1.21.0...v1.21.1) (2021-02-04) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +# [1.21.0](https://github.com/frontegg/frontegg-react/compare/v1.20.1...v1.21.0) (2021-02-04) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +## [1.20.1](https://github.com/frontegg/frontegg-react/compare/v1.20.0...v1.20.1) (2021-02-02) + + +### Bug Fixes + +* **connectivity:** fix the delete dialog message disappers ([f40e50a](https://github.com/frontegg/frontegg-react/commit/f40e50a36ee9f184f0d8b5bddffa6842ed827605)) + + + + + +# [1.20.0](https://github.com/frontegg/frontegg-react/compare/v1.19.1...v1.20.0) (2021-02-01) + + +### Bug Fixes + +* **connectivity:** fix icons on the list of platform ([8de499c](https://github.com/frontegg/frontegg-react/commit/8de499c6e50faf29ad2198f9d0661d38d7201394)) +* **connectivity:** fix send the security parameter for the webhook configuration ([cbc8bbf](https://github.com/frontegg/frontegg-react/commit/cbc8bbf9e347d5561169f0cfbf724b3f6d1c6e29)) + + + + + +## [1.19.1](https://github.com/frontegg/frontegg-react/compare/v1.19.0...v1.19.1) (2021-01-20) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +# [1.19.0](https://github.com/frontegg/frontegg-react/compare/v1.18.6...v1.19.0) (2021-01-20) + + +### Bug Fixes + +* **connectivity:** fix sorting the Status column in the webhooks list ([c7d1428](https://github.com/frontegg/frontegg-react/commit/c7d14282d349c7cd7a952a431361faee5d53f893)) +* **connectivity:** remove old dead code on the webhooks list ([609bfe0](https://github.com/frontegg/frontegg-react/commit/609bfe0116c6b8d64cabc14a9aaa3cca9ff5b8da)) +* **connectivity:** remove warnings in the devTools for some svg elements ([dde9731](https://github.com/frontegg/frontegg-react/commit/dde973150f9dce8043b6d99d9b7c8339fb014df6)) + + +### Features + +* **connectivity:** add error message from the server to the webhook component ([7b53d9f](https://github.com/frontegg/frontegg-react/commit/7b53d9f66eed8e32127abbb4ca95a3d60c743a43)) +* **connectivity:** add loading when changes status in the webhooks list ([b834ac6](https://github.com/frontegg/frontegg-react/commit/b834ac6570eff62d5629445fac4417baee9d5ea0)) +* **connectivity:** add sorting data by columns in the webhooks list ([6fc81b6](https://github.com/frontegg/frontegg-react/commit/6fc81b654a164654cde7821f430621c0344d5974)) +* **connectivity:** change behaviors of select catagory and envents ([368849e](https://github.com/frontegg/frontegg-react/commit/368849e427b384b8203fa6ab28cf6dc25be522fd)) +* **connectivity:** Disabled the V mark if no one of events is active. ([7cea078](https://github.com/frontegg/frontegg-react/commit/7cea078dc70939dc9c3b2fa28af495d1e2d988d9)) + + +### Performance Improvements + +* **connectivity:** move handlers to the useCallback hook in the AccordionCategories ([b89826b](https://github.com/frontegg/frontegg-react/commit/b89826bbd009d05c9f7a52c8d2ccaadae3a2b896)) + + + + + +## [1.18.6](https://github.com/frontegg/frontegg-react/compare/v1.18.5...v1.18.6) (2021-01-19) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +## [1.18.5](https://github.com/frontegg/frontegg-react/compare/v1.18.4...v1.18.5) (2021-01-18) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +## [1.18.4](https://github.com/frontegg/frontegg-react/compare/v1.18.3...v1.18.4) (2021-01-18) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +## [1.18.3](https://github.com/frontegg/frontegg-react/compare/v1.18.2...v1.18.3) (2021-01-17) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +## [1.18.2](https://github.com/frontegg/frontegg-react/compare/v1.18.1...v1.18.2) (2021-01-15) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +## [1.18.1](https://github.com/frontegg/frontegg-react/compare/v1.18.0...v1.18.1) (2021-01-15) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +# [1.18.0](https://github.com/frontegg/frontegg-react/compare/v1.17.3...v1.18.0) (2021-01-14) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +## [1.17.3](https://github.com/frontegg/frontegg-react/compare/v1.17.2...v1.17.3) (2021-01-13) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +## [1.17.2](https://github.com/frontegg/frontegg-react/compare/v1.17.1...v1.17.2) (2021-01-12) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +## [1.17.1](https://github.com/frontegg/frontegg-react/compare/v1.17.0...v1.17.1) (2021-01-05) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +# [1.17.0](https://github.com/frontegg/frontegg-react/compare/v1.16.2...v1.17.0) (2021-01-03) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +## [1.16.2](https://github.com/frontegg/frontegg-react/compare/v1.16.0...v1.16.2) (2020-12-27) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +## [1.16.1](https://github.com/frontegg/frontegg-react/compare/v1.16.0...v1.16.1) (2020-12-27) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +# [1.16.0](https://github.com/frontegg/frontegg-react/compare/v1.15.2...v1.16.0) (2020-12-24) + + +### Bug Fixes + +* **connectivity:** fix some design and behavioral issues ([fd5d16d](https://github.com/frontegg/frontegg-react/commit/fd5d16d1e114fa8445ea5a0548d7b4cc00a530f0)) + + + + + +## [1.15.2](https://github.com/frontegg/frontegg-react/compare/v1.15.1...v1.15.2) (2020-12-24) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +## [1.15.1](https://github.com/frontegg/frontegg-react/compare/v1.15.0...v1.15.1) (2020-12-21) + + +### Bug Fixes + +* **connectivity:** fix bug of edit the email and sms events ([e34788c](https://github.com/frontegg/frontegg-react/commit/e34788c0b58dc07d20509b4addb39d33a57e1dea)) + + + + + +# [1.15.0](https://github.com/frontegg/frontegg-react/compare/v1.14.1...v1.15.0) (2020-12-20) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +## [1.14.1](https://github.com/frontegg/frontegg-react/compare/v1.14.0...v1.14.1) (2020-12-17) + + +### Bug Fixes + +* Fix create new webhooks button typo ([0be76c3](https://github.com/frontegg/frontegg-react/commit/0be76c38a771996ad849a027b752fb7107b9d3db)) + + + + + +# [1.14.0](https://github.com/frontegg/frontegg-react/compare/v1.13.2...v1.14.0) (2020-12-16) + + +### Bug Fixes + +* **connectivity:** fix actions for saving data ([c92d051](https://github.com/frontegg/frontegg-react/commit/c92d051f309e3cbb5862bec73c6e12dd5b96da67)) +* **connectivity:** FR-1005 fix the alignment of the line data in the webhook table ([bccfa8b](https://github.com/frontegg/frontegg-react/commit/bccfa8b96b372f1762e6b681fcccfc9d556fe9d3)) + + + + + +## [1.13.2](https://github.com/frontegg/frontegg-react/compare/v1.13.1...v1.13.2) (2020-12-13) + + +### Bug Fixes + +* **connectivity:** fix the enabled edit data form the SMS and Email webhooks ([6314a4e](https://github.com/frontegg/frontegg-react/commit/6314a4ea2eb1be8b1a0043077de3ceb4f410bd68)) + + + + + +## [1.13.1](https://github.com/frontegg/frontegg-react/compare/v1.13.0...v1.13.1) (2020-12-10) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +# [1.13.0](https://github.com/frontegg/frontegg-react/compare/v1.12.0...v1.13.0) (2020-12-09) + + +### Bug Fixes + +* **connectivity:** fix show platform if the it dosen't have any events FR-984 ([6319b02](https://github.com/frontegg/frontegg-react/commit/6319b0201888b7226d600d43cb2bfc65f1d24b20)) + + + + + +# [1.12.0](https://github.com/frontegg/frontegg-react/compare/v1.11.1...v1.12.0) (2020-12-09) + + +### Bug Fixes + +* **connectivity:** fix color of the Install button FR-975 ([ac9d61e](https://github.com/frontegg/frontegg-react/commit/ac9d61e7cc329410954f4830a476602a3ab73e49)) +* **connectivity:** fix styles for the material UI ([ca1258d](https://github.com/frontegg/frontegg-react/commit/ca1258d85a29d40b9de59407c41f7e755f1e4206)) + + +### Features + +* **connectivity:** add UI design for the semantic library ([986c86c](https://github.com/frontegg/frontegg-react/commit/986c86cc1d3bc5f35b8a409cbdc9fba7737bb522)) + + + + + +## [1.11.1](https://github.com/frontegg/frontegg-react/compare/v1.11.0...v1.11.1) (2020-12-02) + + +### Bug Fixes + +* **connectivity:** fix styles for separate components ([3b4b8d3](https://github.com/frontegg/frontegg-react/commit/3b4b8d3909942716889f72830796294e846c45d0)) + + + + + +# [1.11.0](https://github.com/frontegg/frontegg-react/compare/v1.10.0...v1.11.0) (2020-11-30) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +# [1.10.0](https://github.com/frontegg/frontegg-react/compare/v1.9.0...v1.10.0) (2020-11-27) + + +### Features + +* **connectivity:** add listener ([e463fae](https://github.com/frontegg/frontegg-react/commit/e463faeb097a07959b88cbf4a535bb1871b9e6b3)) + + + + + +# [1.9.0](https://github.com/frontegg/frontegg-react/compare/v1.8.0...v1.9.0) (2020-11-25) + +**Note:** Version bump only for package @frontegg/react-connectivity + + + + + +# [1.8.0](https://github.com/frontegg/frontegg-react/compare/v1.7.0...v1.8.0) (2020-11-23) + + +### Features + +* Add Connectivity Plugin ([#98](https://github.com/frontegg/frontegg-react/issues/98)) ([db77431](https://github.com/frontegg/frontegg-react/commit/db77431b6d744b93431430543019fedd1c0dbae2)) diff --git a/packages/connectivity/README.md b/packages/connectivity/README.md new file mode 100644 index 000000000..fc9244c58 --- /dev/null +++ b/packages/connectivity/README.md @@ -0,0 +1,78 @@ + +<p align="center"> + <a href="https://www.frontegg.com/" rel="noopener" target="_blank"> + <img style="margin-top:40px" height="50" src="https://frontegg.com/wp-content/uploads/2020/04/logo_frrontegg.svg" alt="Frontegg logo"> + </a> +</p> +<h1 align="center">Connectivity Plugin</h1> +<div align="center"> + +Pre-built Table to easily integrate Connectivity Services into your [React](https://reactjs.org/) App. +</div> + +## Installation + +Frontegg-React-Connectivity is available as an [npm package](https://www.npmjs.com/package/@frontegg/react-connectivity). + +```sh +// using npm +npm install @frontegg/react-connectivity + +// using yarn +yarn add @frontegg/react-connectivity + +// NOTE: to get the latest stable use @latest. +``` + +## Usage + +All you need is to pass AuditsPlugin to the ``FronteggProvider``: + +```jsx +/* imports */ +import { FronteggProvider } from '@frontegg/react-core'; +import { ConnectivityPlugin } from '@frontegg/react-connectivity'; + +const plugins = [ConnectivityPlugin()]; + +ReactDOM.render( +<BrowserRouter> + <FronteggProvider + context={/* context options */} + plugins={plugins}> + <App /> + </FronteggProvider> +</BrowserRouter>, document.querySelector('#app')); +``` + +Then add `ConnectivityPage` component to your route: + + ```jsx + import { ConnectivityPage } from '@frontegg/react-connectivity'; + + <Route exact={false} path={'/connectivity'} component={ConnectivityPage}/> + + // or if you want to add special parameters for the ConnectivityPage component + + <Router exact={false} path={'/somewhere/connectivity''}> + <ConnectivityPage rootPath='/somewhere/connectivity' /> + </Router> + + ``` + +## Parameters + - rootPath - a custom root path for the component by default it's `/connectivity` + - className - a className for the whole container + - headClassName - a className for the header component + - contentClassName - a className for the container of the table + +## Contributing + +The main purpose of this repository is to continue developing Frontegg React to making it faster and easier to use. +Read our [contributing guide](/CONTRIBUTING.md) to learn about our development process. + +**Notice** that contributions go far beyond pull requests and commits. + +## License + +This project is licensed under the terms of the [MIT license](/LICENSE). diff --git a/packages/connectivity/package.json b/packages/connectivity/package.json new file mode 100644 index 000000000..7b4b38a1b --- /dev/null +++ b/packages/connectivity/package.json @@ -0,0 +1,68 @@ +{ + "name": "@frontegg/react-connectivity", + "libName": "FronteggConnectivity", + "version": "4.0.23", + "author": "Frontegg LTD", + "main": "dist/index.js", + "module": "dist/index.esm.js", + "es2015": "dist/index.es.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "rollup -c ../../scripts/rollup.config.js && echo DONE", + "build:watch": "rollup -w -c ../../scripts/rollup.config.js", + "test": "jest --runInBand --passWithNoTests -c ../../scripts/jest.config.json --rootDir . && echo DONE" + }, + "dependencies": { + "@frontegg/react-core": "^4.0.23", + "@reduxjs/toolkit": "^1.4.0", + "classnames": "^2.2.6", + "react": ">16.8.6" + }, + "devDependencies": { + "@types/react": "^16.9.19" + }, + "jest": { + "preset": "ts-jest", + "testEnvironment": "jsdom", + "testPathIgnorePatterns": [ + "dist/" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "prettier": { + "printWidth": 120, + "semi": true, + "singleQuote": true, + "jsxSingleQuote": true, + "trailingComma": "all", + "jsxBracketSameLine": true, + "endOfLine": "lf", + "tabWidth": 2 + }, + "standard": { + "ignore": [ + "node_modules/", + "dist/" + ], + "globals": [ + "describe", + "it", + "test", + "expect", + "afterAll", + "jest" + ] + }, + "gitHead": "00c0bd425a211916eec1170b8359cf302727b338" +} diff --git a/packages/connectivity/src/components/ConnectivityContent.tsx b/packages/connectivity/src/components/ConnectivityContent.tsx new file mode 100644 index 000000000..fa1c473d0 --- /dev/null +++ b/packages/connectivity/src/components/ConnectivityContent.tsx @@ -0,0 +1,22 @@ +import React, { FC, useContext } from 'react'; +import { Route } from 'react-router-dom'; +import classnames from 'classnames'; +import { RootPathContext } from '@frontegg/react-core'; +import { IRootPath } from '../interfaces'; +import { ConnectivityTable } from './ConnectivityTable'; +import { ConnectivitySlackAuthSuccess } from './ConnectivitySlackAuthSuccess'; + +export interface ConnectivityContentProps extends IRootPath { + className?: string; +} + +export const ConnectivityContent: FC<ConnectivityContentProps> = ({ className }) => { + const path = useContext(RootPathContext); + + return ( + <div className={classnames('fe-connectivity-context', className)}> + <Route exact path={`${path}`} component={ConnectivityTable} /> + <Route path={`${path}/success`} component={ConnectivitySlackAuthSuccess} /> + </div> + ); +}; diff --git a/packages/connectivity/src/components/ConnectivityForms/ConnectivityEmail.tsx b/packages/connectivity/src/components/ConnectivityForms/ConnectivityEmail.tsx new file mode 100644 index 000000000..0d1ab0b70 --- /dev/null +++ b/packages/connectivity/src/components/ConnectivityForms/ConnectivityEmail.tsx @@ -0,0 +1,5 @@ +import React, { FC } from 'react'; +import { IConnectivityComponent } from '../../interfaces'; +import { ConnectivityForm } from './ConnectivityForm'; + +export const ConnectivityEmail: FC<IConnectivityComponent> = () => <ConnectivityForm form='email' />; diff --git a/packages/connectivity/src/components/ConnectivityForms/ConnectivityForm.tsx b/packages/connectivity/src/components/ConnectivityForms/ConnectivityForm.tsx new file mode 100644 index 000000000..b1e7ddfef --- /dev/null +++ b/packages/connectivity/src/components/ConnectivityForms/ConnectivityForm.tsx @@ -0,0 +1,223 @@ +import React, { FC, useCallback, useMemo, useState } from 'react'; +import classnames from 'classnames'; +import { + Icon, + useT, + Table, + Button, + Loader, + FFormik, + useSearch, + FormikAutoSave, + TableColumnProps, +} from '@frontegg/react-core'; +import { IEmailSMSConfigResponse } from '@frontegg/rest-api'; +import { IConnectivityComponent, IEventFormData, ITableFormData } from '../../interfaces'; +import { filterCategories } from '../../utils'; +import { FConnectivityCheckBox } from '../../elements/ConnectivityCheckBox'; +import { InputEmailOrPhone } from '../../elements/InputEmailOrPhone'; +import { useConnectivityActions, useConnectivityState } from '@frontegg/react-hooks'; + +interface IConnectivityForm extends IConnectivityComponent { + form: 'email' | 'sms'; +} + +export const ConnectivityForm: FC<IConnectivityForm> = ({ form }) => { + const { t } = useT(); + + const [close, setClose] = useState<number[]>([]); + const [isFiltering, setIsFiltering] = useState<boolean>(false); + + const { categories, channelMap: _channelMap, isSaving, isLoading, ...connectivity } = useConnectivityState(); + const { postDataAction } = useConnectivityActions(); + const data = connectivity[form]; + const channelMap = _channelMap?.[form]; + + const validate = useCallback( + (values) => { + const errors = values?.data?.map((data: ITableFormData) => { + const { events = [] } = data; + return { + events: events.map(({ recipients }) => { + if ( + recipients + .filter((e) => !!e) + .some( + (e) => + !(form === 'email' ? /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(e) : /^\+?\d{12}$/.test(e)) + ) + ) { + return { + recipients: t( + form === 'email' ? 'connectivity.recipients.wrongEmail' : 'connectivity.recipients.wrongPhone' + ), + }; + } + return null; + }), + }; + }); + return errors?.filter(({ events }: { events: string[] }) => events.filter((el: string) => !!el).length).length + ? { data: errors } + : {}; + }, + + [t, form] + ); + + const saveData = useCallback( + (formData: ITableFormData[], setSubmitting) => { + const newData: IEmailSMSConfigResponse[] = formData.reduce( + (acc: IEmailSMSConfigResponse[], curr: ITableFormData) => { + const { events = [] } = curr; + return [ + ...acc, + ...events.map( + ({ enabled, eventKey, recipients, subscriptions }): IEmailSMSConfigResponse => ({ + enabled, + eventKey, + subscriptions: [{ ...subscriptions, enabled, recipients: recipients.filter((el) => !!el) }], + }), + [] + ), + ]; + }, + [] + ); + if (JSON.stringify(newData) !== JSON.stringify(data)) { + postDataAction({ platform: form, data: newData }); + } else { + setSubmitting(false); + } + }, + [data] + ); + + const cleanCategory = filterCategories(categories, channelMap); + + const tablesData: ITableFormData[] | undefined = useMemo( + () => + cleanCategory && + data && + cleanCategory.map(({ id, name, events, index }) => { + return { + id, + name, + index, + events: + events?.map(({ id, displayName, key }: any) => { + const { + subscriptions: [{ recipients, ...subscriptions }], + ...config + } = data.find(({ eventKey }) => eventKey === key) || { + subscriptions: [{ recipients: [], name: '', id: '' }], + eventKey: key, + enabled: false, + }; + return { + id, + displayName, + ...config, + recipients, + subscriptions, + }; + }) ?? [], + }; + }), + [cleanCategory, data] + ); + + const columns = useMemo( + () => + (tablesData || [])?.map( + ({ name, index }, idx) => + [ + { + accessor: 'displayName', + Header: () => ( + <Button + transparent + iconButton + className='fe-connectivity-accordion-button' + onClick={() => { + !isFiltering && setClose(close.includes(idx) ? close.filter((e) => e !== idx) : [...close, idx]); + }} + > + <Icon name={!close.includes(idx) || isFiltering ? 'down-arrow' : 'right-arrow'} /> + {name} + </Button> + ), + }, + { + accessor: 'enabled', + Header: t('common.enable'), + Cell: ({ row: { index: rowIndex } }) => ( + <FConnectivityCheckBox name={`data[${index}].events[${rowIndex}].enabled`} /> + ), + maxWidth: 50, + minWidth: 50, + }, + { + accessor: 'events', + Header: t(form === 'email' ? 'common.emails' : 'common.phones'), + Cell: ({ row: { index: rowIndex } }) => ( + <InputEmailOrPhone + dataIdx={index} + eventIdx={rowIndex} + placeholder={t(form === 'email' ? 'connectivity.enterEmail' : 'connectivity.enterPhone')} + /> + ), + }, + ] as TableColumnProps<IEventFormData>[] + ), + [tablesData, t, close, isFiltering] + ); + + const [filterTableData, Search] = useSearch({ + data: tablesData, + filteredBy: 'name', + filterFunction: (allData: ITableFormData[], regexp, isEmpty) => { + const result = isEmpty + ? allData + : (allData + .map(({ name, events, ...cat }) => { + const eventsFiltered = events?.filter(({ eventKey }) => regexp.test(eventKey)) ?? []; + return regexp.test(name) || eventsFiltered.length + ? { ...cat, name, events: regexp.test(name) ? events : eventsFiltered } + : null; + }) + .filter((e) => !!e) as ITableFormData[]); + setIsFiltering(!isEmpty); + return result; + }, + }); + + if (isLoading) { + return <Loader center />; + } + + return ( + <FFormik.Formik + initialValues={{ data: tablesData }} + validate={validate} + onSubmit={(val, { setSubmitting }) => val.data && saveData(val.data, setSubmitting)} + > + <FFormik.Form> + <FormikAutoSave isSaving={isSaving} /> + {Search} + {filterTableData.map(({ id, events, index }, idx) => ( + <Table + rowKey='id' + key={id} + columns={columns[index]} + data={events || []} + totalData={events?.length || 0} + className={classnames('fe-connectivity-table-accordion', { + 'fe-connectivity-open': !close.includes(idx) || isFiltering, + })} + /> + ))} + </FFormik.Form> + </FFormik.Formik> + ); +}; diff --git a/packages/connectivity/src/components/ConnectivityForms/ConnectivitySMS.tsx b/packages/connectivity/src/components/ConnectivityForms/ConnectivitySMS.tsx new file mode 100644 index 000000000..23dca1ac8 --- /dev/null +++ b/packages/connectivity/src/components/ConnectivityForms/ConnectivitySMS.tsx @@ -0,0 +1,5 @@ +import React, { FC } from 'react'; +import { IConnectivityComponent } from '../../interfaces'; +import { ConnectivityForm } from './ConnectivityForm'; + +export const ConnectivitySMS: FC<IConnectivityComponent> = () => <ConnectivityForm form='sms' />; diff --git a/packages/connectivity/src/components/ConnectivityForms/ConnectivitySlack.tsx b/packages/connectivity/src/components/ConnectivityForms/ConnectivitySlack.tsx new file mode 100644 index 000000000..c1d7c2d87 --- /dev/null +++ b/packages/connectivity/src/components/ConnectivityForms/ConnectivitySlack.tsx @@ -0,0 +1,197 @@ +import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import classnames from 'classnames'; +import { + useT, + Icon, + Table, + Loader, + Button, + FFormik, + FormikAutoSave, + TableColumnProps, + useSearch, +} from '@frontegg/react-core'; +import { ISlackConfigurations, ISlackSubscription } from '@frontegg/rest-api'; +import { IConnectivityComponent, ISlackEventData, ISlackTableData } from '../../interfaces'; +import { filterCategories } from '../../utils'; +import { SelectSlack } from '../../elements/SelectSlack'; +import { FConnectivityCheckBox } from '../../elements/ConnectivityCheckBox'; +import { ConnectivitySlackAuth } from './ConnectivitySlackAuth'; +import { MessageSlack } from '../../elements/MessageSlack'; +import { useConnectivityActions, useConnectivityState } from '@frontegg/react-hooks'; + +export const ConnectivitySlack: FC<IConnectivityComponent> = () => { + const { t } = useT(); + const [close, setClose] = useState<number[]>([]); + const [isFiltering, setIsFiltering] = useState<boolean>(false); + + const { + categories, + channelMap: _channelMap, + slack, + isSaving, + slackChannels: _slackChannels, + } = useConnectivityState(); + const slackChannels = _slackChannels?.data; + const channelMap = _channelMap?.slack; + const isLoading = _slackChannels?.isLoading; + + const cleanCategory = filterCategories(categories, channelMap); + + const { slackSubscriptions } = slack ?? { slackSubscriptions: null }; + + const tablesData: ISlackTableData[] | undefined = useMemo( + () => + (cleanCategory && + slackSubscriptions && + cleanCategory.map(({ id, name, events, index }) => ({ + id, + name, + index, + events: (events || []).map(({ id: eventId, displayName, key }) => ({ + displayName, + isActive: false, + slackEvents: [ + { + eventKey: key, + }, + ], + ...slackSubscriptions.find(({ slackEvents }) => + (slackEvents || []).some(({ eventKey }) => eventKey === key) + ), + eventId, + })), + }))) || + undefined, + [cleanCategory] + ); + + const columns = useMemo( + () => + (tablesData || []).map( + ({ name, index }, idx) => + [ + { + accessor: 'displayName', + Header: () => ( + <Button + transparent + iconButton + className='fe-connectivity-accordion-button' + onClick={() => { + !isFiltering && setClose(close.includes(idx) ? close.filter((e) => e !== idx) : [...close, idx]); + }} + > + <Icon name={!close.includes(idx) || isFiltering ? 'down-arrow' : 'right-arrow'} /> + {name} + </Button> + ), + }, + { + accessor: 'isActive', + Header: t('common.enabled'), + Cell: ({ row: { index: rowIndex } }) => ( + <FConnectivityCheckBox name={`data[${index}].events[${rowIndex}].isActive`} /> + ), + maxWidth: 50, + minWidth: 50, + }, + { + accessor: 'slackEvents', + Header: t('common.channels'), + Cell: ({ row: { index: rowIndex } }) => <SelectSlack eventIdx={rowIndex} dataIdx={index} />, + }, + { + accessor: 'non', + Header: t('common.message'), + Cell: ({ row: { index: rowIndex } }) => <MessageSlack eventIdx={rowIndex} dataIdx={index} />, + }, + ] as TableColumnProps<ISlackEventData>[] + ), + [t, close, isFiltering] + ); + + const { loadSlackActions, cleanSlackData, postDataAction } = useConnectivityActions(); + + useEffect(() => { + console.log('Load'); + loadSlackActions(); + return () => { + cleanSlackData(); + }; + }, []); + + const [filterTableData, Search] = useSearch({ + data: tablesData, + filteredBy: 'name', + filterFunction: (allData: ISlackTableData[], regexp, isEmpty) => { + const result = isEmpty + ? allData + : (allData + .map(({ name, events, ...cat }) => { + const eventsFiltered = events?.filter(({ displayName }) => regexp.test(displayName)) ?? []; + return regexp.test(name) || eventsFiltered.length + ? { ...cat, name, events: regexp.test(name) ? events : eventsFiltered } + : null; + }) + .filter((e) => !!e) as ISlackTableData[]); + setIsFiltering(!isEmpty); + return result; + }, + }); + + const saveData = useCallback( + ({ data }: { data?: ISlackTableData[] }) => { + console.log('Saving data'); + if (!slack || !data) return; + const { id } = slack; + const newData: ISlackConfigurations = { + id, + // @ts-ignore + slackSubscriptions: data.reduce((acc: ISlackSubscription[], curr: ISlackTableData) => { + const { events = [] } = curr; + return [ + ...acc, + ...events.map(({ isActive, id, slackEvents }) => ({ + id, + isActive, + slackEvents, + })), + ]; + }, []), + }; + + postDataAction({ platform: 'slack', data: newData }); + }, + [slack] + ); + + if (isLoading) { + return <Loader center />; + } + + if (!isLoading && !slackChannels?.length && !tablesData?.length) { + return <ConnectivitySlackAuth />; + } + + return ( + <FFormik.Formik initialValues={{ data: tablesData }} onSubmit={saveData} enableReinitialize> + <FFormik.Form> + <FormikAutoSave isSaving={isSaving} debounceMs={50} /> + {Search} + {filterTableData.map(({ id, events, index }, idx) => ( + <Table + rowKey='eventId' + key={id} + columns={columns[index]} + data={events || []} + totalData={events?.length || 0} + className={classnames('fe-connectivity-table-accordion', { + 'fe-connectivity-open': !close.includes(idx) || isFiltering, + })} + /> + ))} + </FFormik.Form> + </FFormik.Formik> + ); +}; diff --git a/packages/connectivity/src/components/ConnectivityForms/ConnectivitySlackAuth.tsx b/packages/connectivity/src/components/ConnectivityForms/ConnectivitySlackAuth.tsx new file mode 100644 index 000000000..dd81eb19b --- /dev/null +++ b/packages/connectivity/src/components/ConnectivityForms/ConnectivitySlackAuth.tsx @@ -0,0 +1,61 @@ +import React, { FC, useEffect, useMemo } from 'react'; +import { Grid, Loader } from '@frontegg/react-core'; +import { SlackSvg } from '../../elements/Svgs'; +import { useConnectivityActions, useConnectivityState } from '@frontegg/react-hooks'; + +const defaultScope = ['chat:write', 'channels:read', 'chat:write.public'].join(','); + +export const ConnectivitySlackAuth: FC<any> = () => { + const { loadScope } = useConnectivityActions(); + const { + slackChannels: { isLoadingScope, clientId = '' }, + } = useConnectivityState(); + + useEffect(() => { + loadScope(); + }, []); + + const redirectUrl = (() => { + const url = new URL(window.location.href); + url.search = ''; + url.hash = ''; + return `${url.toString()}/success`; + })(); + + const query = useMemo(() => { + return new URLSearchParams({ client_id: clientId ?? '', scope: defaultScope, redirect_uri: redirectUrl }); + }, [clientId]); + + if (isLoadingScope) { + return <Loader center />; + } + + if (!isLoadingScope && !clientId) { + return <> Required configure the connectivity</>; + } + + return ( + <Grid container justifyContent='center'> + <div className='fe-slack-auth'> + <div className='fe-slack-auth-container'> + <div className='fe-slack-auth__txt-strong'> + Slack integration allow your Slack account to be notified + <br /> + when certain events happen. + </div> + <div className='fe-slack-auth__txt'> + When the specified events happen, we’ll send a customized + <br /> + message to the Slack channels of your choice. + </div> + <div className='fe-slack-auth__txt-strong'>The first stage would be to connect your Slack Account.</div> + <br /> + <a href={`https://slack.com/oauth/v2/authorize?${query.toString()}`} className='fe-slack-auth__btn'> + <SlackSvg /> + <span>Connect with Slack</span> + </a> + </div> + </div> + </Grid> + ); +}; diff --git a/packages/connectivity/src/components/ConnectivityForms/index.ts b/packages/connectivity/src/components/ConnectivityForms/index.ts new file mode 100644 index 000000000..63f673c29 --- /dev/null +++ b/packages/connectivity/src/components/ConnectivityForms/index.ts @@ -0,0 +1,3 @@ +export * from './ConnectivitySMS'; +export * from './ConnectivitySlack'; +export * from './ConnectivityEmail'; diff --git a/packages/connectivity/src/components/ConnectivityHeader.tsx b/packages/connectivity/src/components/ConnectivityHeader.tsx new file mode 100644 index 000000000..edc3ceb61 --- /dev/null +++ b/packages/connectivity/src/components/ConnectivityHeader.tsx @@ -0,0 +1,9 @@ +import React, { FC } from 'react'; +import { PageHeader, PageHeaderProps, useT } from '@frontegg/react-core'; + +export interface ConnectivityHeadersProps extends Pick<PageHeaderProps, 'className' | 'titleClassName'> {} + +export const ConnectivityHeader: FC<ConnectivityHeadersProps> = (props) => { + const { t } = useT(); + return <PageHeader {...props} title={t('connectivity.headerTitle')} subTitle={t('connectivity.headerSubTitle')} />; +}; diff --git a/packages/connectivity/src/components/ConnectivityListener.tsx b/packages/connectivity/src/components/ConnectivityListener.tsx new file mode 100644 index 000000000..7b098f5bc --- /dev/null +++ b/packages/connectivity/src/components/ConnectivityListener.tsx @@ -0,0 +1,17 @@ +import React, { FC, useEffect } from 'react'; +import { ListenerProps } from '@frontegg/react-core'; +import connectivity from '@frontegg/redux-store/connectivity'; +import { useConnectivityActions } from '@frontegg/react-hooks'; + +export const ConnectivityListener: FC<ListenerProps<typeof connectivity.actions>> = (props) => { + const { initData } = useConnectivityActions(); + const { storeName, actions } = connectivity; + useEffect(() => { + initData(); + }, [initData]); + + useEffect(() => { + props.resolveActions?.(storeName, actions); + }, [props.resolveActions, actions, storeName]); + return null; +}; diff --git a/packages/connectivity/src/components/ConnectivityPage.tsx b/packages/connectivity/src/components/ConnectivityPage.tsx new file mode 100644 index 000000000..faf3863ce --- /dev/null +++ b/packages/connectivity/src/components/ConnectivityPage.tsx @@ -0,0 +1,45 @@ +import React, { FC, useEffect } from 'react'; +import classnames from 'classnames'; +import { ConnectivityContent, ConnectivityContentProps } from './ConnectivityContent'; +import { ConnectivityHeader, ConnectivityHeadersProps } from './ConnectivityHeader'; +import { RootPathContext } from '@frontegg/react-core'; +import { defaultRootPath } from '../consts'; +import { useConnectivityActions } from '@frontegg/react-hooks'; +export interface IConnectivityPage + extends Omit<ConnectivityContentProps, 'className'>, + Omit<ConnectivityHeadersProps, 'className'> { + className?: string; + headClassName?: string; + contentClassName?: string; +} + +export const ConnectivityPage: FC<IConnectivityPage> = ({ + children, + className, + rootPath = defaultRootPath, + headClassName, + titleClassName, + contentClassName, + ...contentProps +}) => { + const { loadDataAction, initData } = useConnectivityActions(); + useEffect(() => { + loadDataAction(); + return () => { + initData(); + }; + }, []); + + return ( + <RootPathContext.Provider value={rootPath}> + <div className={classnames('fe-connectivity-page', className)}> + {children ?? ( + <> + <ConnectivityContent className={contentClassName} {...contentProps} /> + <ConnectivityHeader className={headClassName} titleClassName={titleClassName} /> + </> + )} + </div> + </RootPathContext.Provider> + ); +}; diff --git a/packages/connectivity/src/components/ConnectivityPanel.tsx b/packages/connectivity/src/components/ConnectivityPanel.tsx new file mode 100644 index 000000000..baa6d3c81 --- /dev/null +++ b/packages/connectivity/src/components/ConnectivityPanel.tsx @@ -0,0 +1,32 @@ +import { Button, useT } from '@frontegg/react-core'; +import React, { FC, useRef } from 'react'; +import { IConnectivityComponent } from '../interfaces'; + +export interface IConnectivityPanel extends IConnectivityComponent { + show: boolean; +} + +export const ConnectivityPanel: FC<IConnectivityPanel> = ({ children, show, onClose }) => { + const { t } = useT(); + const divRef = useRef<HTMLDivElement>(null); + + return show ? ( + <div className='fe-connectivity-panel' ref={divRef}> + <div> + <Button + data-test-id='eventsBtn' + className='fe-connectivity-panel-btn' + transparent + onClick={() => onClose && onClose()} + fullWidth + > + <div className='fe-connectivity-panel-close'> + <span className='fe-connectivity-panel-close-icon fe-mr-3'>✕</span> + {t('common.events')} + </div> + </Button> + </div> + {children} + </div> + ) : null; +}; diff --git a/packages/connectivity/src/components/ConnectivitySlackAuthSuccess.tsx b/packages/connectivity/src/components/ConnectivitySlackAuthSuccess.tsx new file mode 100644 index 000000000..0f008d49d --- /dev/null +++ b/packages/connectivity/src/components/ConnectivitySlackAuthSuccess.tsx @@ -0,0 +1,26 @@ +import { Loader, RootPathContext } from '@frontegg/react-core'; +import React, { FC, useContext, useLayoutEffect } from 'react'; +import { useLocation, Redirect } from 'react-router-dom'; +import { useConnectivityState, useConnectivityActions } from '@frontegg/react-hooks'; + +export const ConnectivitySlackAuthSuccess: FC = () => { + const { postCodeAction } = useConnectivityActions(); + + const path = useContext(RootPathContext); + const { search } = useLocation(); + const { isSaving } = useConnectivityState(); + + useLayoutEffect(() => { + if (search) { + const query = new URLSearchParams(search); + if (query.has('code')) { + postCodeAction(query.get('code') || ''); + } + } + }, [search]); + + if (isSaving) { + return <Loader center />; + } + return <Redirect to={{ pathname: path || '/', state: { open: 'slack' } }} />; +}; diff --git a/packages/connectivity/src/components/ConnectivityTable.tsx b/packages/connectivity/src/components/ConnectivityTable.tsx new file mode 100644 index 000000000..6f393ea16 --- /dev/null +++ b/packages/connectivity/src/components/ConnectivityTable.tsx @@ -0,0 +1,133 @@ +import React, { FC, useCallback, useLayoutEffect, useMemo, useState } from 'react'; +import classnames from 'classnames'; +import { useHistory } from 'react-router-dom'; +import { Grid, Icon, useT, Table, Button, Loader, TableColumnProps, CellComponent } from '@frontegg/react-core'; +import { IConnectivityData, TPlatform } from '../interfaces'; +import { ConnectivityPanel } from './ConnectivityPanel'; +import { platformForm } from '../consts'; +import { CheckSvg, channelsSvgs } from '../elements/Svgs'; +import { useConnectivityState } from '@frontegg/react-hooks'; + +interface ILocationState { + open: TPlatform; +} + +const cssPrefix = 'fe-connectivity-platform'; + +interface IData extends IConnectivityData { + isSelect: boolean; +} + +export const ConnectivityTable: FC = () => { + const { t } = useT(); + const { + replace: historyReplace, + location: { state: locationState, ...location }, + } = useHistory<ILocationState>(); + const [edit, setEdit] = useState<IData | null>(null); + + const { isLoading, list } = useConnectivityState(); + const data = useMemo(() => list.map((el: any) => ({ ...el, isSelect: locationState?.open === el.key })), [ + list, + locationState, + ]); + + const platformCell = useCallback( + ({ value, row: { original } }): CellComponent<IData> => { + const ChannelImage = channelsSvgs[original.image]; + return ( + <Button + data-test-id='editBtn' + transparent + fullWidth + className={classnames(cssPrefix, { + 'fe-connectivity-active': original.isSelect, + })} + onClick={() => { + setEdit(original); + historyReplace({ ...location, state: { open: original.key } }); + }} + > + <div className={`${cssPrefix}-icon`}> + <ChannelImage /> + </div> + {!edit && <div className={`${cssPrefix}-title`}>{t(value)}</div>} + <Icon className={`${cssPrefix}-right-arrow`} name='right-arrow' /> + </Button> + ); + }, + [historyReplace, setEdit, location, edit] + ); + + const actionCell = useCallback( + ({ row }): CellComponent<IData> => ( + <Button + data-test-id='keyBtn' + className='fe-connectivity-button' + variant={row.original.active ? 'primary' : 'secondary'} + onClick={() => { + setEdit(row.original); + historyReplace({ ...location, state: { open: row.original.key } }); + }} + > + {row.original.active ? t('common.configure') : t('common.install')} + </Button> + ), + [historyReplace, setEdit, location] + ); + + const columns: TableColumnProps<IData>[] = useMemo( + () => [ + { + accessor: 'platform', + Header: () => <span id='fe-connectivity-firstColumn'>{t('common.channels')}</span>, + maxWidth: 90, + Cell: platformCell, + }, + { + accessor: 'active', + Header: t('common.active') || '', + Cell: ({ value }) => ( + <CheckSvg className={classnames(`${cssPrefix}-check`, { [`${cssPrefix}-check-active`]: value })} /> + ), + }, + { + accessor: 'events', + Header: t('common.events') || '', + Cell: ({ value }) => <span className='fe-circle'>{value}</span>, + }, + { + accessor: 'actions', + Cell: actionCell, + maxWidth: 80, + }, + ], + [actionCell, platformCell] + ); + + useLayoutEffect(() => { + locationState && data?.length && setEdit(data.find(({ key }: any) => key === locationState.open) ?? null); + }, [locationState, data]); + + const onCloseEdit = () => { + locationState && historyReplace(location); + setEdit(null); + }; + + if (isLoading) { + return <Loader center />; + } + + return ( + <> + <Grid container className='fe-connectivity-list' direction='column'> + <div className={classnames({ ['fe-connectivity-panel-shown']: !!edit })}> + <Table rowKey='id' columns={columns} data={data} totalData={list.length} /> + <ConnectivityPanel show={!!edit} onClose={onCloseEdit}> + {edit && React.createElement(platformForm[edit.key], { onClose: onCloseEdit })} + </ConnectivityPanel> + </div> + </Grid> + </> + ); +}; diff --git a/packages/connectivity/src/components/ConnectivityWebhooks/ConnectivityWebhooks.tsx b/packages/connectivity/src/components/ConnectivityWebhooks/ConnectivityWebhooks.tsx new file mode 100644 index 000000000..1debb9c8a --- /dev/null +++ b/packages/connectivity/src/components/ConnectivityWebhooks/ConnectivityWebhooks.tsx @@ -0,0 +1,17 @@ +import React, { FC } from 'react'; +import { useHistory } from 'react-router-dom'; +import { ConnectivityWebhooksEdit } from './ConnectivityWebhooksEdit'; +import { ConnectivityWebhooksList } from './ConnectivityWebhooksList'; +import { IWebhookLocationState } from './interfaces'; + +export const ConnectivityWebhooks: FC = () => { + const { + location: { state: locationState }, + } = useHistory<IWebhookLocationState>(); + + return !locationState?.view || locationState.view === 'list' ? ( + <ConnectivityWebhooksList /> + ) : ( + <ConnectivityWebhooksEdit /> + ); +}; diff --git a/packages/connectivity/src/components/ConnectivityWebhooks/ConnectivityWebhooksEdit.tsx b/packages/connectivity/src/components/ConnectivityWebhooks/ConnectivityWebhooksEdit.tsx new file mode 100644 index 000000000..afa98b9d2 --- /dev/null +++ b/packages/connectivity/src/components/ConnectivityWebhooks/ConnectivityWebhooksEdit.tsx @@ -0,0 +1,56 @@ +import React, { FC, useCallback, useLayoutEffect, useMemo } from 'react'; +import { Icon, Tabs, TabItem, usePrevious, useT } from '@frontegg/react-core'; +import { useHistory } from 'react-router-dom'; +import { ConnectivityWebhooksForm } from './ConnectivityWebhooksForm'; +import { ConnectivityWebhooksLog } from './ConnectivityWebhooksLog'; +import { IWebhookLocationState } from './interfaces'; +import { useConnectivityState } from '@frontegg/react-hooks'; + +const itemsArray = ['common.detail', 'common.logs']; + +export const ConnectivityWebhooksEdit: FC = () => { + const { t } = useT(); + const items: TabItem[] = useMemo(() => itemsArray.map((el) => ({ Title: t(el) })), [t]); + + const { + replace: historyReplace, + location: { state: locationState, ...location }, + } = useHistory<IWebhookLocationState>(); + + const { error, webhook, isSaving } = useConnectivityState(); + + const prevIsSaving = usePrevious(isSaving); + const preparedWebhook = webhook?.data ?? webhook; + const data = useMemo(() => webhook && preparedWebhook?.find(({ _id }) => _id === locationState.id), [ + webhook?.data, + locationState, + ]); + + const onBack = useCallback(() => { + historyReplace({ ...location, state: { ...locationState, view: 'list', id: undefined } }); + }, [historyReplace, location, locationState]); + + useLayoutEffect(() => { + !error && prevIsSaving && !isSaving && onBack(); + }, [prevIsSaving, isSaving, onBack, error]); + + const onChangeTab = useCallback( + (_event: React.MouseEvent<HTMLDivElement>, activeIndex: number) => { + historyReplace({ ...location, state: { ...locationState, view: activeIndex === 1 ? 'log' : 'edit' } }); + }, + [historyReplace, location, locationState] + ); + + return ( + <div className='fe-connectivity__content'> + <div className='fe-connectivity__content-heading'> + <span onClick={onBack} className={'fe-back-button fe-block'}> + <Icon data-test-id='backBtn' name='back' /> + </span> + {data?.displayName ?? t('connectivity.addNewHook')} + </div> + {data && <Tabs items={items} activeTab={locationState.view === 'edit' ? 0 : 1} onTabChange={onChangeTab} />} + {locationState.view === 'edit' ? <ConnectivityWebhooksForm data={data ?? null} /> : <ConnectivityWebhooksLog />} + </div> + ); +}; diff --git a/packages/connectivity/src/components/ConnectivityWebhooks/ConnectivityWebhooksForm.tsx b/packages/connectivity/src/components/ConnectivityWebhooks/ConnectivityWebhooksForm.tsx new file mode 100644 index 000000000..1b2e513cd --- /dev/null +++ b/packages/connectivity/src/components/ConnectivityWebhooks/ConnectivityWebhooksForm.tsx @@ -0,0 +1,159 @@ +import React, { FC, useCallback, useEffect, useState } from 'react'; +import { + Grid, + useT, + Popup, + Button, + Dialog, + FInput, + FButton, + FFormik, + validateUrl, + validateSchema, + validateRequired, + validateArrayLength, + validateLength, +} from '@frontegg/react-core'; +import { initialValues } from './consts'; +import { IWebhooksSaveData } from '@frontegg/rest-api'; +import { AccordingCategories } from '../../elements/AccordingCategories'; +import { SelectWebhook } from '../../elements/SelectWebhook'; +import { filterCategories } from '../../utils'; +import { ConnectivityWebhooksTestForm } from './ConnectivityWebhooksTestFrom'; +import { useConnectivityActions, useConnectivityState } from '@frontegg/react-hooks'; + +export interface IConnectivityWebhooksForm { + data: IWebhooksSaveData | null; +} + +export const ConnectivityWebhooksForm: FC<IConnectivityWebhooksForm> = ({ data }) => { + const { t } = useT(); + const [openTestDialog, setOpenTestDialog] = useState(false); + + const { cleanWebhookTestMessage, cleanWebhookTestData, cleanError, postDataAction } = useConnectivityActions(); + const { error, categories, channelMap, testResult, isSaving } = useConnectivityState(); + + useEffect(() => { + return () => { + cleanWebhookTestData(); + }; + }, []); + + const toggleTestDialog = useCallback(() => { + if (openTestDialog) { + cleanWebhookTestMessage(); + } + setOpenTestDialog(!openTestDialog); + }, [setOpenTestDialog, openTestDialog]); + + const validationSchema = validateSchema({ + displayName: validateRequired(t('common.displayName'), t), + url: validateUrl('URL', t), + eventKeys: validateArrayLength(t, t('connectivity.events')), + secret: validateLength('Secret Key', 8, t), + }); + + const cleanCategory = filterCategories(categories, channelMap && channelMap.webhook); + + return ( + <> + <FFormik.Formik + validationSchema={validationSchema} + initialValues={{ ...initialValues, ...data, secret: data?.secret || '' }} + onSubmit={(val, { setSubmitting }) => { + cleanError(); + postDataAction({ + platform: 'webhook', + data: { ...val, secret: val.secret ? val.secret : null }, + }); + setSubmitting(false); + }} + > + {({ values }) => ( + <> + <FFormik.Form> + <Grid container wrap='nowrap'> + <Grid item className='fe-connectivity-webhook-settings' xs={6}> + <div className='fe-section-title fe-bold fe-mb-3'>{t('connectivity.generalSettings')}</div> + <FInput + data-test-id='displayNameBox' + label={t('common.displayName')} + name='displayName' + placeholder={t('connectivity.inputName')} + /> + <FInput + data-test-id='descriptionBox' + label={t('common.description')} + name='description' + multiline + placeholder={t('connectivity.shortDescription')} + /> + <FInput data-test-id='urlBox' label='URL' name='url' placeholder='https://' /> + <FInput + label={ + <label> + {t('common.secretKey')} + <Popup + trigger={<span className='fe-connectivity-webhook-help'>?</span>} + position={{ vertical: 'center', horizontal: 'right' }} + action='hover' + content={ + <div className='fe-connectivity-webhook-help-block'>{t('connectivity.secretKeyHelp')}</div> + } + /> + </label> + } + name='secret' + placeholder='Secret key' + /> + <Grid container justifyContent='space-between'> + <Grid> + <FButton data-test-id='submitBtn' type='submit' variant='primary' loading={isSaving}> + {data ? t('connectivity.updateHook').toUpperCase() : t('connectivity.addHook').toUpperCase()} + </FButton> + </Grid> + <Grid> + <Button data-test-id='testHookBtn' size='large' onClick={toggleTestDialog}> + {t('connectivity.testHook').toUpperCase()} + </Button> + </Grid> + {error && ( + <Grid xs={12} className='fe-error-message'> + {error} + </Grid> + )} + </Grid> + </Grid> + <Grid item className='fe-connectivity-webhook-settings' xs={6}> + <div className='fe-section-title fe-bold fe-mb-2'>{t('connectivity.eventSettings')}</div> + <div className='fe-connectivity-webhook-settings__frame'> + <div className='fe-connectivity-webhook-settings__frame-title'> + {t('connectivity.selectEvents')} + </div> + <SelectWebhook cleanCategory={cleanCategory} /> + <div className='fe-connectivity-webhook-settings__frame-title fe-mt-2'> + {t('connectivity.manageCategories')} + </div> + <AccordingCategories cleanCategory={cleanCategory} /> + </div> + </Grid> + </Grid> + </FFormik.Form> + <Dialog + header={t('connectivity.testHook')} + className={`fe-connectivity-webhook-dialog-${testResult?.status ?? ''}`} + open={openTestDialog} + onClose={toggleTestDialog} + > + <ConnectivityWebhooksTestForm + toggleTestDialog={toggleTestDialog} + secret={values?.secret ?? ''} + url={values?.url ?? ''} + /> + </Dialog> + </> + )} + </FFormik.Formik> + </> + ); +}; diff --git a/packages/connectivity/src/components/ConnectivityWebhooks/ConnectivityWebhooksList.tsx b/packages/connectivity/src/components/ConnectivityWebhooks/ConnectivityWebhooksList.tsx new file mode 100644 index 000000000..5a2faeed6 --- /dev/null +++ b/packages/connectivity/src/components/ConnectivityWebhooks/ConnectivityWebhooksList.tsx @@ -0,0 +1,231 @@ +import React, { FC, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import moment from 'moment'; +import { useHistory } from 'react-router-dom'; +import { IWebhooksConfigurations, IWebhooksSaveData } from '@frontegg/rest-api'; +import { + Icon, + Menu, + useT, + Grid, + Table, + Button, + Loader, + Dialog, + useSearch, + TableColumnProps, +} from '@frontegg/react-core'; +import { IWebhookLocationState } from './interfaces'; +import { filterCategories, selectedEvents } from '../../utils'; +import { ConnectivityCheckBox } from '../../elements/ConnectivityCheckBox'; +import { useConnectivityActions, useConnectivityState } from '@frontegg/react-hooks'; + +interface IEventCount { + name: string; + count: number; +} + +interface IWebhooksFullConfigurations extends IWebhooksConfigurations { + groupEvents: IEventCount[]; + totalEvents: number; +} + +export const ConnectivityWebhooksList: FC = () => { + const prevSaving = useRef<{ isSaving: boolean }>({ isSaving: false }); + const { t } = useT(); + const { + replace: historyReplace, + location: { state: locationState, ...location }, + } = useHistory<IWebhookLocationState>(); + + const [remove, onRemove] = useState<IWebhooksConfigurations | null>(null); + + const { postDataAction, deleteWebhookConfigAction } = useConnectivityActions(); + const { webhook: webhookState, isSaving, categories, channelMap, isLoading, processIds } = useConnectivityState(); + + useLayoutEffect(() => { + remove && prevSaving.current.isSaving && onRemove(null); + prevSaving.current.isSaving = isSaving; + }, [isSaving, onRemove, remove, prevSaving]); + const cleanCatagories = filterCategories(categories, channelMap && channelMap.webhook); + + const preparedWebhookState = webhookState?.data ?? webhookState; + const webhook = useMemo( + () => + preparedWebhookState?.map((elm) => { + const eventObject = selectedEvents(elm.eventKeys); + const data = cleanCatagories?.reduce<IEventCount[]>((acc, cur) => { + if (eventObject?.names.includes(cur.name)) { + return [...acc, { name: cur.name, count: cur.events?.length ?? 0 }]; + } else { + const evs = cur.events?.filter(({ key }) => eventObject?.eventKeys.includes(key)); + if (evs?.length) { + return [...acc, { name: cur.name, count: evs.length }]; + } + } + return acc; + }, []); + return { + ...elm, + groupEvents: data ?? [], + totalEvents: data?.reduce((acc, cur) => acc + (cur.count || 0), 0) ?? 0, + }; + }), + [webhookState, cleanCatagories] + ); + + const [data, Search] = useSearch({ filteredBy: 'displayName', data: webhook }); + + const onEdit = useCallback( + (id?: string) => { + historyReplace({ ...location, state: { ...locationState, view: 'edit', id } }); + }, + [Location, locationState] + ); + + const onNewEvent = useCallback(() => { + historyReplace({ ...location, state: { ...locationState, view: 'edit' } }); + }, [Location, locationState]); + + const onChangeStatus = useCallback((data: IWebhooksSaveData) => { + postDataAction({ platform: 'webhook', data: { ...data, isActive: !data.isActive } }); + }, []); + + const columns: TableColumnProps<IWebhooksFullConfigurations>[] = useMemo( + () => [ + { + accessor: 'displayName', + Header: t('common.title').toUpperCase(), + sortable: true, + Cell: ({ value, row }) => ( + <div + className='fe-connectivity-webhook-cell fe-connectivity-webhook-cell-link' + onClick={() => onEdit(row.original._id)} + > + <div>{value}</div> + {row.original.description && ( + <div className='fe-connectivity-webhook-description'>{row.original.description}</div> + )} + </div> + ), + }, + { + accessor: 'isActive', + Header: t('common.status').toUpperCase(), + sortable: true, + Cell: ({ value, row }) => + processIds.includes(row.original._id) ? ( + <Loader /> + ) : ( + <ConnectivityCheckBox value={value} onChange={() => onChangeStatus(row.original)} /> + ), + sortType: 'basic', + maxWidth: 70, + minWidth: 70, + }, + { + accessor: 'totalEvents', + sortable: true, + Header: t('common.events').toUpperCase(), + Cell: ({ value, row: { original } }) => ( + <div className='fe-connectivity-webhook-cell'> + <div className='fe-connectivity-webhook-row'> + {original.groupEvents && + !!original.groupEvents.length && + original.groupEvents.map(({ name, count }, idx) => ( + <span key={idx} className='fe-connectivity-webhook-event'> + {name}({count}) + </span> + ))} + </div> + <div className='fe-connectivity-webhook-description'>{value} total</div> + </div> + ), + }, + { + accessor: 'invocations', + sortable: true, + Header: t('common.invocations').toUpperCase(), + }, + { + accessor: 'createdAt', + sortable: true, + Header: t('common.createdAt').toUpperCase(), + Cell: ({ value }) => { + const date = moment.utc(value).local(); + return ( + <div className='fe-connectivity-webhook-cell'> + <div>{date.fromNow()}</div> + <div className='fe-connectivity-webhook-description'>{date.format('D/M/YYYY hh:mm A')}</div> + </div> + ); + }, + }, + { + accessor: 'action', + maxWidth: 40, + minWidth: 50, + Cell: ({ row }) => ( + <Menu + className='fe-connectivity-panel-menu' + trigger={ + <Button data-test-id='menuBtn' iconButton className='fe-connectivity-panel-menu-button'> + <Icon name='vertical-dots' size='small' /> + </Button> + } + items={[ + { + text: t('common.edit'), + icon: <Icon name='edit' />, + onClick: () => { + onEdit(row.original._id); + }, + }, + { text: t('common.remove'), icon: <Icon name='delete' />, onClick: () => onRemove(row.original) }, + ]} + /> + ), + }, + ], + [t, onEdit, onRemove, onChangeStatus, processIds] + ); + + if (isLoading) { + return <Loader center />; + } + + return ( + <div className='fe-connectivity-webhook-list'> + {Search} + <Button data-test-id='addBtn' className='fe-connectivity-webhook-add' variant='primary' onClick={onNewEvent}> + {t('connectivity.addNewHook')} + </Button> + <Table rowKey='_id' columns={columns as any} data={data} totalData={webhook?.length || 0} /> + <Dialog header={t('connectivity.deleteWebhook')} open={!!remove} onClose={() => onRemove(null)}> + {!!remove && ( + <> + <div className='fe-mb-4'>{t('connectivity.queryDeleteWebhook', { name: remove?.displayName })}</div> + <div className='fe-connectivity-webhook-dialog-action'> + <Grid container spacing={2} justifyContent='flex-end'> + <Grid item> + <Button data-test-id='cancelBtn' variant='default' onClick={() => onRemove(null)}> + Cancel + </Button> + </Grid> + <Grid item> + <Button + data-test-id='acceptBtn' + variant='danger' + loading={isSaving} + onClick={() => deleteWebhookConfigAction({ webhookId: remove._id })} + > + Accept + </Button> + </Grid> + </Grid> + </div> + </> + )} + </Dialog> + </div> + ); +}; diff --git a/packages/connectivity/src/components/ConnectivityWebhooks/ConnectivityWebhooksLog.tsx b/packages/connectivity/src/components/ConnectivityWebhooks/ConnectivityWebhooksLog.tsx new file mode 100644 index 000000000..3cda451eb --- /dev/null +++ b/packages/connectivity/src/components/ConnectivityWebhooks/ConnectivityWebhooksLog.tsx @@ -0,0 +1,168 @@ +import React, { FC, useCallback, useLayoutEffect, useMemo, useState } from 'react'; +import moment from 'moment'; +import { useHistory } from 'react-router-dom'; +import { Button, Dialog, Grid, Icon, Table, TableColumnProps, useT } from '@frontegg/react-core'; +import { IWebhookLocationState } from './interfaces'; +import { IWebhookLog } from '@frontegg/rest-api'; +import { useConnectivityActions, useConnectivityState } from '@frontegg/react-hooks'; + +enum TriggerType { + RETRY = 'RETRY', + EVENT = 'EVENT', +} +interface IWebhookData extends IWebhookLog { + now: string; + date: string; + status: JSX.Element; + triggerType: TriggerType; +} + +const defaultPageSize = 7; +export const ConnectivityWebhooksLog: FC = () => { + const { t } = useT(); + + const validateStatus = (status: string) => { + const result = +status >= 200 && +status <= 399 ? 'success' : 'failed'; + return <span className={`fe-connectivity-webhook-status fe-status-${result}`}>{t(`common.${result}`)}</span>; + }; + + const formatDate = (date: string) => { + const momentDate = moment.utc(date).local(); + return { now: momentDate.fromNow(), date: momentDate.format('D/M/YYYY hh:mm A') }; + }; + + const [moreInfo, setMoreInfo] = useState<IWebhookData | null>(null); + + const { webhookLogs } = useConnectivityState(); + const { loadWebhookLogsAction, cleanWebhookLogsData, postWebhookRetryAction } = useConnectivityActions(); + + const data = useMemo( + () => + webhookLogs?.rows?.map((log) => ({ + ...log, + ...formatDate(log.createdAt), + status: validateStatus(log.statusCode), + })) ?? [], + [webhookLogs] + ); + + const retryHandler = useCallback((e: React.MouseEvent<HTMLElement>) => { + const { id } = e.currentTarget?.dataset; + if (id) { + postWebhookRetryAction(id); + } + }, []); + + const columns: TableColumnProps<IWebhookData>[] = useMemo( + () => [ + { + accessor: 'now', + Header: t('common.createdAt') || '', + Cell: ({ row }) => { + const isEventRetry = row.original?.triggerType === TriggerType.RETRY; + return ( + <> + {row.original?.now}{' '} + {isEventRetry && ( + <Icon className='fe-connectivity-webhook-logs__retry-icon' size='small' name='refresh' /> + )} + </> + ); + }, + maxWidth: 50, + }, + { + accessor: 'date', + maxWidth: 50, + }, + { + accessor: 'status', + Header: t('common.status') || '', + }, + { + accessor: 'body', + Cell: ({ row }) => ( + <Button + data-test-id='moreInfoBtn' + transparent + onClick={() => setMoreInfo(row.original)} + className='fe-connectivity-webhook-detail' + > + {t('common.detail').toUpperCase()} + </Button> + ), + maxWidth: 50, + }, + { + accessor: 'action', + Cell: ({ row }) => ( + <Button + data-test-id='retryBtn' + transparent + className='fe-connectivity-webhook-retry' + onClick={retryHandler} + data-id={row.original.id} + > + <Icon name='refresh' /> + </Button> + ), + maxWidth: 25, + }, + ], + [t, setMoreInfo, retryHandler] + ); + + const { isLoading, count } = useMemo(() => webhookLogs ?? { isLoading: true, count: 0 }, [webhookLogs]); + + const { + location: { state: locationState }, + } = useHistory<IWebhookLocationState>(); + + const loadData = useCallback( + (pageSize: number, page: number) => { + locationState.id && loadWebhookLogsAction(locationState.id, page * pageSize, pageSize); + }, + [locationState.id] + ); + + useLayoutEffect(() => { + loadData(defaultPageSize, 0); + return () => { + cleanWebhookLogsData(); + }; + }, [loadData]); + + return ( + <> + <Table + columns={columns} + data={data} + rowKey='id' + totalData={count ?? 0} + pagination='pages' + loading={isLoading} + pageSize={defaultPageSize} + pageCount={((count ?? 0) / defaultPageSize) | 0 || 1} + onPageChange={loadData} + /> + <Dialog + open={!!moreInfo} + onClose={() => setMoreInfo(null)} + className='fe-connectivity-webhook-dialog-more' + header={ + moreInfo && ( + <Grid container justifyContent='space-between' alignItems='center'> + <Grid item> + <h3>{t('common.logDetails')}</h3> + {moreInfo.now} <span className='fe-connectivity-webhook-dialog-more-date'>{moreInfo.date}</span> + </Grid> + <Grid>{moreInfo.status}</Grid> + </Grid> + ) + } + > + {moreInfo && <pre>{JSON.stringify(JSON.parse(moreInfo.body), null, 2)}</pre>} + </Dialog> + </> + ); +}; diff --git a/packages/connectivity/src/components/ConnectivityWebhooks/ConnectivityWebhooksTestFrom.tsx b/packages/connectivity/src/components/ConnectivityWebhooks/ConnectivityWebhooksTestFrom.tsx new file mode 100644 index 000000000..41c07bdb6 --- /dev/null +++ b/packages/connectivity/src/components/ConnectivityWebhooks/ConnectivityWebhooksTestFrom.tsx @@ -0,0 +1,104 @@ +import React, { FC, useEffect, useMemo } from 'react'; +import { + Grid, + useT, + Button, + FInput, + FButton, + FFormik, + validateSchema, + validateObject, + Input, + ErrorMessage, +} from '@frontegg/react-core'; +import { useConnectivityActions, useConnectivityState } from '@frontegg/react-hooks'; + +export interface IConnectivityWebhookTestForm { + secret: string; + url: string; + toggleTestDialog: () => void; +} + +export const ConnectivityWebhooksTestForm: FC<IConnectivityWebhookTestForm> = ({ secret, url, toggleTestDialog }) => { + const { t } = useT(); + const { isTesting, testResult } = useConnectivityState(); + const { cleanWebhookTestData, postWebhookTestAction } = useConnectivityActions(); + + useEffect(() => { + return () => { + cleanWebhookTestData(); + }; + }, [cleanWebhookTestData]); + + const validationSchema = validateSchema({ + payload: validateObject(t('common.payload'), t), + }); + + const btnVariant = useMemo(() => { + switch (testResult?.status) { + case 'failed': + return 'danger'; + case 'success': + return 'success'; + default: + return undefined; + } + }, [testResult?.status]); + + return ( + <FFormik.Formik + validationSchema={validationSchema} + initialValues={{ payload: '' }} + onSubmit={({ payload }, { setSubmitting }) => { + postWebhookTestAction({ + url, + secret: secret ? secret : null, + payload: { ...JSON.parse(payload) }, + }); + setSubmitting(false); + }} + > + <FFormik.Form> + <Grid container wrap='nowrap'> + <Grid container className='fe-connectivity-webhook-test-settings' direction='column' xs={12}> + <Input data-test-id='urlBox' className='fe-mb-1' label='URL' placeholder='https://' value={url} disabled /> + <Input + data-test-id='secretBox' + className='fe-mb-1' + label={t('common.secretKey')} + placeholder={t('common.secretKey')} + value={secret} + disabled + /> + <FInput + data-test-id='testBox' + className='fe-connectivity-webhook-test-payload' + label={t('connectivity.json')} + name='payload' + multiline + placeholder={t('connectivity.json')} + /> + </Grid> + </Grid> + + {testResult?.status === 'failed' && <ErrorMessage error={testResult.message} />} + {testResult?.status === 'success' && <div className='fe-success-message fe-center'>{testResult.message}</div>} + + <div className='fe-dialog__footer'> + <Grid container justifyContent='space-between'> + <Grid> + <Button data-test-id='closeBtn' onClick={toggleTestDialog} size='large'> + {t('common.close')} + </Button> + </Grid> + <Grid> + <FButton size='large' data-test-id='submitBtn' variant={btnVariant} loading={isTesting} type='submit'> + {testResult?.status?.toUpperCase() ?? t('connectivity.testHook').toUpperCase()} + </FButton> + </Grid> + </Grid> + </div> + </FFormik.Form> + </FFormik.Formik> + ); +}; diff --git a/packages/connectivity/src/components/ConnectivityWebhooks/consts.ts b/packages/connectivity/src/components/ConnectivityWebhooks/consts.ts new file mode 100644 index 000000000..264a7f0d6 --- /dev/null +++ b/packages/connectivity/src/components/ConnectivityWebhooks/consts.ts @@ -0,0 +1,10 @@ +import { IWebhooksSaveData } from '@frontegg/rest-api'; + +export const initialValues: IWebhooksSaveData = { + description: '', + displayName: '', + url: '', + secret: '', + eventKeys: [], + isActive: true, +}; diff --git a/packages/connectivity/src/components/ConnectivityWebhooks/index.ts b/packages/connectivity/src/components/ConnectivityWebhooks/index.ts new file mode 100644 index 000000000..645675821 --- /dev/null +++ b/packages/connectivity/src/components/ConnectivityWebhooks/index.ts @@ -0,0 +1 @@ +export * from './ConnectivityWebhooks'; diff --git a/packages/connectivity/src/components/ConnectivityWebhooks/interfaces.ts b/packages/connectivity/src/components/ConnectivityWebhooks/interfaces.ts new file mode 100644 index 000000000..610436134 --- /dev/null +++ b/packages/connectivity/src/components/ConnectivityWebhooks/interfaces.ts @@ -0,0 +1,14 @@ +import { ICategory } from '@frontegg/rest-api'; +import { TPlatform } from '../../interfaces'; + +export interface IWebhookComponent { + cleanCategory?: ICategory[]; +} + +export type TWebhookView = 'list' | 'edit' | 'log'; + +export interface IWebhookLocationState { + open: TPlatform; + view?: TWebhookView; + id?: string; +} diff --git a/packages/connectivity/src/consts.ts b/packages/connectivity/src/consts.ts new file mode 100644 index 000000000..69e8ee35d --- /dev/null +++ b/packages/connectivity/src/consts.ts @@ -0,0 +1,68 @@ +import { api, IEmailSMSConfigResponse, ISlackConfigurations, IWebhooksConfigurations } from '@frontegg/rest-api'; +import { FC } from 'react'; +import { ConnectivitySlack, ConnectivitySMS, ConnectivityEmail } from './components/ConnectivityForms'; +import { ConnectivityWebhooks } from './components/ConnectivityWebhooks'; +import { EmailSvg, SlackSvg, SmsSvg, WebhookSvg } from './elements/Svgs'; +import { TPlatform, IConnectivityComponent } from './interfaces'; + +export const type2ApiGet: Record<TPlatform | 'categories' | 'channelMap', any> = { + slack: api.connectivity.getSlackConfiguration, + email: api.connectivity.getEmailConfiguration, + sms: api.connectivity.getSMSConfiguration, + webhook: api.connectivity.getWebhooksConfigurations, + categories: api.connectivity.getCategories, + channelMap: api.connectivity.getChannelMaps, +}; + +export const type2ApiPost: Record<TPlatform, any> = { + slack: api.connectivity.postSlackConfiguration, + email: api.connectivity.postEmailConfiguration, + sms: api.connectivity.postSMSConfiguration, + webhook: api.connectivity.postWebhooksConfiguration, +}; + +export const defaultRootPath = '/connectivity'; + +export const channels: TPlatform[] = ['email', 'slack', 'sms', 'webhook']; + +export const channels2Platform: Record< + TPlatform, + { + title: string; + events(data: IEmailSMSConfigResponse[] | ISlackConfigurations | IWebhooksConfigurations[]): number; + isActive(data: IEmailSMSConfigResponse[] | ISlackConfigurations | IWebhooksConfigurations[]): boolean; + image: FC<React.SVGProps<SVGSVGElement>>; + } +> = { + sms: { + title: 'connectivity.sms', + events: (data) => (data as IEmailSMSConfigResponse[])?.length || 0, + isActive: (data) => (data as IEmailSMSConfigResponse[])?.some(({ enabled }) => enabled) ?? false, + image: SmsSvg, + }, + email: { + title: 'common.email', + events: (data) => (data as IEmailSMSConfigResponse[])?.length || 0, + isActive: (data) => (data as IEmailSMSConfigResponse[])?.some(({ enabled }) => enabled) ?? false, + image: EmailSvg, + }, + slack: { + title: 'connectivity.slack', + events: (data) => (data as ISlackConfigurations)?.slackSubscriptions?.length || 0, + isActive: (data) => !!(data as ISlackConfigurations)?.slackSubscriptions.some(({ isActive }) => isActive) ?? false, + image: SlackSvg, + }, + webhook: { + title: 'connectivity.webhook', + events: (data) => (data as IWebhooksConfigurations[])?.length || 0, + isActive: (data) => (data as IWebhooksConfigurations[])?.some(({ isActive }) => isActive) ?? false, + image: WebhookSvg, + }, +}; + +export const platformForm: Record<TPlatform, FC<IConnectivityComponent>> = { + sms: ConnectivitySMS, + webhook: ConnectivityWebhooks, + slack: ConnectivitySlack, + email: ConnectivityEmail, +}; diff --git a/packages/connectivity/src/elements/AccordingCategories.tsx b/packages/connectivity/src/elements/AccordingCategories.tsx new file mode 100644 index 000000000..e311a38a7 --- /dev/null +++ b/packages/connectivity/src/elements/AccordingCategories.tsx @@ -0,0 +1,118 @@ +import React, { FC, useCallback, useMemo, ChangeEvent } from 'react'; +import { + Grid, + Icon, + useT, + FFormik, + Checkbox, + Accordion, + AccordionHeader, + AccordionContent, +} from '@frontegg/react-core'; +import { selectedEvents } from '../utils'; +import { IWebhookComponent } from '../components/ConnectivityWebhooks/interfaces'; + +export const AccordingCategories: FC<IWebhookComponent> = ({ cleanCategory }) => { + const { t } = useT(); + const [{ value }, {}, { setValue }] = FFormik.useField<string[]>('eventKeys'); + + const eventObject = selectedEvents(value); + + const extendCategory = useMemo( + () => + cleanCategory?.map(({ name, events, ...props }) => ({ + ...props, + events, + name, + selected: eventObject?.names.includes(name) + ? events?.length ?? 0 + : events?.filter(({ key }) => eventObject?.eventKeys.includes(key)).length, + })), + [cleanCategory, eventObject] + ); + + const handleCategoryChange = useCallback( + (e: ChangeEvent<HTMLInputElement>) => { + const category = extendCategory?.find(({ id }) => id === e.target.dataset.category); + if (!category) return; + + const template = `${category.name}.*`; + if (eventObject?.names.includes(category.name)) { + setValue([ + ...value.filter((el) => el !== template), + ...(category.events?.filter(({ key }) => !value.includes(key)).map(({ key }) => key) ?? []), + ]); + } else { + const keys = category.events?.map(({ key }) => key) ?? []; + setValue([...value.filter((key) => !keys.includes(key)), template]); + } + }, + [extendCategory, eventObject, value] + ); + + const handleEventChange = useCallback( + (e: ChangeEvent<HTMLInputElement>) => { + const parent = extendCategory?.find(({ id }) => id === e.target.dataset.category); + const key = e.target.dataset.key; + if (!parent || !key) return; + + const template = `${parent.name}.*`; + if (eventObject?.names.includes(parent.name)) { + setValue([ + ...value.filter((el) => el !== template), + ...(parent.events?.filter((el) => el.key !== key).map(({ key }) => key) ?? []), + ]); + } else if (eventObject?.eventKeys.includes(key)) { + setValue(value.filter((el) => el !== key)); + } else { + setValue([...value, key]); + } + }, + [extendCategory, eventObject, value] + ); + + return ( + <div> + {extendCategory?.map((category) => { + const { id, name, events, selected } = category; + return ( + <Accordion key={id} className='fe-connectivity-webhook-accordion'> + <AccordionHeader> + <Grid container alignItems='center' wrap='nowrap'> + <Icon name='right-arrow' className='fe-connectivity-webhook-accordion-icon' /> + <Grid className='fe-connectivity-webhook-according-events-name fe-ml-1'>{name}</Grid> + <Grid> + {t('common.selected')}: {selected} + </Grid> + </Grid> + </AccordionHeader> + <AccordionContent> + <div> + <Checkbox + checked={eventObject?.names.includes(name)} + className='fe-connectivity-webhook-check fe-check-all' + onChange={handleCategoryChange} + data-category={id} + label={t('connectivity.selectAll', { name: name.toUpperCase() })} + /> + </div> + {events?.map(({ id, displayName, key }) => ( + <div key={id}> + <Checkbox + className='fe-connectivity-webhook-check fe-check-item' + label={displayName} + size='large' + onChange={handleEventChange} + data-category={category.id} + data-key={key} + checked={eventObject?.names.includes(name) || eventObject?.eventKeys.includes(key)} + /> + </div> + ))} + </AccordionContent> + </Accordion> + ); + })} + </div> + ); +}; diff --git a/packages/connectivity/src/elements/ConnectivityCheckBox.tsx b/packages/connectivity/src/elements/ConnectivityCheckBox.tsx new file mode 100644 index 000000000..969929210 --- /dev/null +++ b/packages/connectivity/src/elements/ConnectivityCheckBox.tsx @@ -0,0 +1,14 @@ +import React, { FC } from 'react'; +import { FFormik, SwitchToggle, SwitchToggleProps } from '@frontegg/react-core'; + +export interface IConnectivityCheckBox { + name: string; +} + +export const FConnectivityCheckBox: FC<IConnectivityCheckBox> = ({ name }) => { + const [{ value, ...inputProps }, {}, { setValue }] = FFormik.useField(name); + + return <ConnectivityCheckBox {...inputProps} name={name} value={!!value} onChange={(e) => setValue(!value)} />; +}; + +export const ConnectivityCheckBox: FC<SwitchToggleProps> = (props) => <SwitchToggle {...props} />; diff --git a/packages/connectivity/src/elements/InputEmailOrPhone.tsx b/packages/connectivity/src/elements/InputEmailOrPhone.tsx new file mode 100644 index 000000000..418ea260f --- /dev/null +++ b/packages/connectivity/src/elements/InputEmailOrPhone.tsx @@ -0,0 +1,28 @@ +import { FFormik, FInputChip } from '@frontegg/react-core'; +import React, { FC } from 'react'; +import { IFormikEditComponent, ITableFormData } from '../interfaces'; + +export const InputEmailOrPhone: FC<IFormikEditComponent & { placeholder?: string }> = ({ + eventIdx, + dataIdx, + placeholder, +}) => { + const name = `data[${dataIdx}].events[${eventIdx}].recipients`; + const enabledName = `data[${dataIdx}].events[${eventIdx}].enabled`; + const [{ value }, {}, { setValue }] = FFormik.useField<string[]>(name); + const [{ value: enabled }, {}, { setValue: setEnabled }] = FFormik.useField<boolean>(enabledName); + + return ( + <FInputChip + name={name} + fullWidth + placeholder={placeholder} + className='fe-connectivity-table-input' + onChange={(newValue) => { + !enabled && value.length < newValue.length && setEnabled(true); + enabled && newValue.length === 0 && setEnabled(false); + setValue(newValue); + }} + /> + ); +}; diff --git a/packages/connectivity/src/elements/MessageSlack.tsx b/packages/connectivity/src/elements/MessageSlack.tsx new file mode 100644 index 000000000..a18f27aa9 --- /dev/null +++ b/packages/connectivity/src/elements/MessageSlack.tsx @@ -0,0 +1,14 @@ +import { FFormik, FInput } from '@frontegg/react-core'; +import React, { FC } from 'react'; +import { IFormikEditComponent, ISlackTableData } from '../interfaces'; + +export const MessageSlack: FC<IFormikEditComponent> = ({ eventIdx, dataIdx }) => { + return ( + <FInput + dontDisableSaving + fullWidth + name={`data[${dataIdx}].events[${eventIdx}].slackEvents[0].message`} + className='fe-connectivity-table-input' + /> + ); +}; diff --git a/packages/connectivity/src/elements/SelectSlack.tsx b/packages/connectivity/src/elements/SelectSlack.tsx new file mode 100644 index 000000000..ae0623a2d --- /dev/null +++ b/packages/connectivity/src/elements/SelectSlack.tsx @@ -0,0 +1,33 @@ +import React, { FC, useMemo } from 'react'; +import { FFormik, Select, SelectOptionProps, useSelector } from '@frontegg/react-core'; +import { IFormikEditComponent, IPluginState, ISlackTableData } from '../interfaces'; + +export const SelectSlack: FC<IFormikEditComponent> = ({ eventIdx, dataIdx }) => { + const [{ value, ...inputProps }, {}, { setValue }] = FFormik.useField( + `data[${dataIdx}].events[[${eventIdx}].slackEvents[0].channelIds` + ); + const { slackChannels } = useSelector( + ({ + connectivity: { + slackChannels: { data: slackChannels }, + }, + }: IPluginState) => ({ + slackChannels, + }) + ); + + const slackOptions: SelectOptionProps<string>[] = useMemo( + () => (slackChannels || [])?.map(({ name, id }) => ({ label: name, value: id })), + [slackChannels] + ); + + return ( + <Select + multiselect + {...inputProps} + options={slackOptions} + value={value?.map((elm: string) => slackOptions.find(({ value }) => value === elm))} + onChange={(e, newValue) => setValue(newValue.map(({ value }) => value))} + /> + ); +}; diff --git a/packages/connectivity/src/elements/SelectWebhook.tsx b/packages/connectivity/src/elements/SelectWebhook.tsx new file mode 100644 index 000000000..1f6acf441 --- /dev/null +++ b/packages/connectivity/src/elements/SelectWebhook.tsx @@ -0,0 +1,46 @@ +import React, { FC, useCallback, useMemo } from 'react'; +import { FFormik, Select, SelectOptionProps } from '@frontegg/react-core'; +import { IWebhookComponent } from '../components/ConnectivityWebhooks/interfaces'; + +export const SelectWebhook: FC<IWebhookComponent> = ({ cleanCategory }) => { + const [{ value: formikValue }, {}, { setValue }] = FFormik.useField<string[]>('eventKeys'); + + const options = useMemo( + () => + cleanCategory?.reduce((acc: SelectOptionProps<string>[], cur) => { + const template = `${cur.name}.*`; + if (formikValue.includes(template)) { + return [...acc, { value: template, label: template }]; + } + return [ + ...acc, + { value: template, label: template }, + ...(cur.events?.map(({ key, displayName }) => ({ value: key, label: displayName })) ?? []), + ]; + }, []) ?? [], + [cleanCategory, formikValue] + ); + + const objectValue = useMemo(() => options.filter(({ value }) => formikValue.includes(value)), [options, formikValue]); + + const onChange = useCallback( + (e: Event, newValue: SelectOptionProps<string>[]) => { + const values = newValue.map(({ value }) => value); + const cleanValue = cleanCategory?.reduce((acc: string[], cur) => { + const template = `${cur.name}.*`; + if (values.includes(template)) { + return [...acc, template]; + } + const selectedEvents = cur.events?.filter(({ key }) => values.includes(key)).map(({ key }) => key); + if (selectedEvents && selectedEvents.length !== 0) { + return [...acc, ...selectedEvents]; + } + return acc; + }, []); + setValue(cleanValue ?? []); + }, + [setValue, cleanCategory] + ); + + return <Select options={options} value={objectValue} multiselect onChange={onChange} />; +}; diff --git a/packages/connectivity/src/elements/Svgs.tsx b/packages/connectivity/src/elements/Svgs.tsx new file mode 100644 index 000000000..9ae545f1a --- /dev/null +++ b/packages/connectivity/src/elements/Svgs.tsx @@ -0,0 +1,198 @@ +import React, { FC } from 'react'; +import classNames from 'classnames'; + +export const EmailSvg: FC<React.SVGProps<SVGSVGElement>> = (props) => { + const { + xmlns = 'http://www.w3.org/2000/svg', + viewBox = '0 0 24 24', + className, + width = '2rem', + height = '2rem', + ...svgProps + } = props; + return ( + <svg + {...svgProps} + width={width} + height={height} + viewBox={viewBox} + xmlns={xmlns} + className={classNames('fe-icon', className)} + > + <path d='M0 0h24v24H0z' fill='none' /> + <path + fill='#fa6400' + d='M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z' + /> + </svg> + ); +}; + +export const BellSvg: FC<React.SVGProps<SVGSVGElement>> = (props) => { + const { + xmlns = 'http://www.w3.org/2000/svg', + viewBox = '0 0 34 40', + className, + width = '2rem', + height = '2rem', + ...svgProps + } = props; + return ( + <svg + {...svgProps} + width={width} + height={height} + viewBox={viewBox} + xmlns={xmlns} + className={classNames('fe-icon', className)} + > + <g transform='translate(-747 -636) translate(296 292) translate(320 264) translate(131 80)'> + <path + fill='#9381E7' + d='M21.417 35.667c.036 2.3-1.8 4.196-4.1 4.233-2.302.037-4.197-1.799-4.234-4.1.008-.573.127-1.139.35-1.667h7.5c.28.467.445.992.484 1.534zM6.067 15.717c-.115-4.612 2.498-8.858 6.666-10.834-.045-.214-.073-.431-.083-.65.099-2.182 1.878-3.91 4.062-3.945C18.896.252 20.731 1.922 20.9 4.1c.025.21.025.423 0 .633 4.289 1.863 7.124 6.027 7.283 10.7 0 0 .117 8.334 3.25 10.284.1 0 1.884.85 1.884 2.383s-1.767 2.683-3.967 2.7H5.55c-2.183 0-4.1-.917-4-2.383.15-.983.773-1.83 1.667-2.267 3.116-2.117 2.85-10.433 2.85-10.433z' + /> + <circle cx='27.5' cy='11.667' r='5.833' fill='#FFBC07' /> + </g> + </svg> + ); +}; + +export const WebhookSvg: FC<React.SVGProps<SVGSVGElement>> = (props) => { + const { + xmlns = 'http://www.w3.org/2000/svg', + viewBox = '0 0 23 22', + className, + width = '23', + height = '22', + ...svgProps + } = props; + return ( + <svg + {...svgProps} + width={width} + height={height} + viewBox={viewBox} + xmlns={xmlns} + className={classNames('fe-icon', className)} + > + <path + fillRule='evenodd' + clipRule='evenodd' + d='M17.8011 16.5193H6.56484V17.8001C6.34656 19.1131 5.26472 20.1107 3.963 20.1107C2.50386 20.1107 1.321 18.8573 1.321 17.3112C1.321 16.3571 1.7714 15.5145 2.45919 15.0091L1.70158 13.8623C0.673111 14.6209 0 15.8827 0 17.3112C0 19.6303 1.7743 21.5104 3.963 21.5104C5.92582 21.5104 7.55535 19.9984 7.8708 18.0136H17.7477C17.9764 18.4306 18.4026 18.7109 18.8905 18.7109C19.6201 18.7109 20.2115 18.0842 20.2115 17.3112C20.2115 16.5381 19.6201 15.9114 18.8905 15.9114C18.4383 15.9114 18.0392 16.1522 17.8011 16.5193Z' + fill='#9381E7' + /> + <path + fillRule='evenodd' + clipRule='evenodd' + d='M2.87798 16.7378C2.90102 16.6802 2.9283 16.6236 2.9599 16.5682C3.20495 16.1391 3.64859 15.8983 4.10547 15.896L9.10691 7.13783C7.63314 5.90829 7.20962 3.74655 8.19741 2.01681C9.29839 0.0888634 11.7357 -0.572318 13.6414 0.540024C14.8114 1.22294 15.5089 2.41559 15.6184 3.68082L14.2949 3.79813C14.2221 2.95441 13.7571 2.15904 12.9769 1.70365C11.7065 0.962084 10.0816 1.40287 9.34757 2.68817C8.62876 3.94689 9.03097 5.54876 10.2411 6.31098L10.9725 6.73786L5.30773 16.6575C5.4687 16.9974 5.48606 17.4007 5.33261 17.7645C5.14539 18.2769 4.65766 18.6422 4.08547 18.6422C3.35148 18.6422 2.75647 18.0411 2.75647 17.2995C2.75647 17.099 2.79998 16.9087 2.87798 16.7378Z' + fill='#9381E7' + /> + <path + fillRule='evenodd' + clipRule='evenodd' + d='M11.3567 5.15863L16.9714 15.3568L17.64 14.9519L17.6514 14.9725C17.6569 14.9691 17.6624 14.9658 17.668 14.9624C18.9408 14.1918 20.5624 14.6383 21.2899 15.9597C22.0174 17.2811 21.5753 18.977 20.3025 19.7477C19.5179 20.2227 18.6007 20.2352 17.8421 19.8654L17.2757 21.1231C18.4126 21.6753 19.786 21.6555 20.9612 20.944C22.8704 19.788 23.5334 17.2441 22.4422 15.262C21.4569 13.4724 19.3783 12.7526 17.5786 13.4805L12.5967 4.43165C12.8113 4.02128 12.8234 3.50943 12.5856 3.07758C12.2219 2.41687 11.4111 2.19362 10.7747 2.57893C10.1383 2.96424 9.91726 3.81221 10.281 4.47292C10.5122 4.89283 10.9239 5.13604 11.3567 5.15863Z' + fill='#9381E7' + /> + </svg> + ); +}; + +export const SlackSvg: FC<React.SVGProps<SVGSVGElement>> = (props) => { + const { + xmlns = 'http://www.w3.org/2000/svg', + viewBox = '0 0 40 40', + className, + width = '2rem', + height = '2rem', + ...svgProps + } = props; + return ( + <svg + {...svgProps} + width={width} + height={height} + viewBox={viewBox} + xmlns={xmlns} + className={classNames('fe-icon', className)} + > + <g> + <path + fill='#E01E5A' + d='M8.516 4.258c0 2.29-1.87 4.161-4.161 4.161-2.29 0-4.161-1.87-4.161-4.16 0-2.291 1.87-4.162 4.16-4.162h4.162v4.161zM10.613 4.258c0-2.29 1.87-4.161 4.161-4.161 2.29 0 4.161 1.87 4.161 4.161v10.42c0 2.29-1.87 4.16-4.16 4.16-2.291 0-4.162-1.87-4.162-4.16V4.257z' + transform='translate(-424 -372) translate(296 292) translate(128 80) translate(0 20.968)' + /> + <path + fill='#36C5F0' + d='M14.774 8.516c-2.29 0-4.161-1.87-4.161-4.161 0-2.29 1.87-4.161 4.161-4.161 2.29 0 4.161 1.87 4.161 4.16v4.162h-4.16zM14.774 10.613c2.29 0 4.161 1.87 4.161 4.161 0 2.29-1.87 4.161-4.16 4.161H4.354c-2.29 0-4.161-1.87-4.161-4.16 0-2.291 1.87-4.162 4.16-4.162h10.42z' + transform='translate(-424 -372) translate(296 292) translate(128 80)' + /> + <path + fill='#2EB67D' + d='M10.516 14.774c0-2.29 1.871-4.161 4.161-4.161 2.29 0 4.162 1.87 4.162 4.161 0 2.29-1.871 4.161-4.162 4.161h-4.16v-4.16zM8.42 14.774c0 2.29-1.872 4.161-4.162 4.161s-4.161-1.87-4.161-4.16V4.354c0-2.29 1.87-4.161 4.161-4.161 2.29 0 4.161 1.87 4.161 4.16v10.42z' + transform='translate(-424 -372) translate(296 292) translate(128 80) translate(20.968)' + /> + <path + fill='#ECB22E' + d='M4.258 10.516c2.29 0 4.161 1.871 4.161 4.161 0 2.29-1.87 4.162-4.16 4.162-2.291 0-4.162-1.871-4.162-4.162v-4.16h4.161zM4.258 8.42C1.968 8.42.097 6.547.097 4.257S1.967.097 4.258.097h10.42c2.29 0 4.16 1.87 4.16 4.161 0 2.29-1.87 4.161-4.16 4.161H4.257z' + transform='translate(-424 -372) translate(296 292) translate(128 80) translate(20.968 20.968)' + /> + </g> + </svg> + ); +}; + +export const SmsSvg: FC<React.SVGProps<SVGSVGElement>> = (props) => { + const { + xmlns = 'http://www.w3.org/2000/svg', + viewBox = '0 0 448 512', + className, + width = '2rem', + height = '2rem', + ...svgProps + } = props; + return ( + <svg + {...svgProps} + width={width} + height={height} + viewBox={viewBox} + xmlns={xmlns} + className={classNames('fe-icon', className)} + > + <path + fill='#fa6400' + d='M400 32H48A48 48 0 0 0 0 80v352a48 48 0 0 0 48 48h352a48 48 0 0 0 48-48V80a48 48 0 0 0-48-48zm-16.39 307.37l-15 65A15 15 0 0 1 354 416C194 416 64 286.29 64 126a15.7 15.7 0 0 1 11.63-14.61l65-15A18.23 18.23 0 0 1 144 96a16.27 16.27 0 0 1 13.79 9.09l30 70A17.9 17.9 0 0 1 189 181a17 17 0 0 1-5.5 11.61l-37.89 31a231.91 231.91 0 0 0 110.78 110.78l31-37.89A17 17 0 0 1 299 291a17.85 17.85 0 0 1 5.91 1.21l70 30A16.25 16.25 0 0 1 384 336a17.41 17.41 0 0 1-.39 3.37z' + /> + </svg> + ); +}; + +export const CheckSvg: FC<React.SVGProps<SVGSVGElement>> = (props) => { + const { + xmlns = 'http://www.w3.org/2000/svg', + viewBox = '0 0 100 100', + className, + width = '100', + height = '100', + ...svgProps + } = props; + return ( + <svg + {...svgProps} + width={width} + height={height} + viewBox={viewBox} + xmlns={xmlns} + className={classNames('fe-icon', className)} + > + <path d='M74.984 26.078l-3.53 3.563c-9.735 9.755-20.12 20.828-29.845 30.718L28.172 49.297l-3.875-3.188-6.344 7.72 3.844 3.187 17 14 3.531 2.906 3.188-3.219c10.769-10.792 22.435-23.413 33-34l3.53-3.562-7.062-7.063z' /> + </svg> + ); +}; + +export const channelsSvgs = { + email: EmailSvg, + sms: SmsSvg, + slack: SlackSvg, + webhook: WebhookSvg, +}; diff --git a/packages/connectivity/src/elements/makeComponent.tsx b/packages/connectivity/src/elements/makeComponent.tsx new file mode 100644 index 000000000..ea4284fa9 --- /dev/null +++ b/packages/connectivity/src/elements/makeComponent.tsx @@ -0,0 +1,45 @@ +import React, { FC, useEffect, useLayoutEffect } from 'react'; +import classnames from 'classnames'; +import { useHistory, Route } from 'react-router-dom'; +import { RootPathContext, useDispatch } from '@frontegg/react-core'; +import { ConnectivityContentProps } from '../components/ConnectivityContent'; +import { TPlatform } from '../interfaces'; +import { platformForm } from '../consts'; +import { useConnectivityActions } from '@frontegg/react-hooks'; + +export interface IMakeComponent { + type: TPlatform; + defaultPath: string; +} + +export const makeComponent = ({ type, defaultPath }: IMakeComponent): FC<ConnectivityContentProps> => ({ + rootPath = defaultPath, + className, +}) => { + const { loadDataAction, initData } = useConnectivityActions(); + const dispatch = useDispatch(); + const { + replace: historyReplace, + location: { state, ...location }, + } = useHistory(); + + useEffect(() => { + !state && historyReplace({ ...location, state: {} }); + }, [historyReplace, location, state]); + + useLayoutEffect(() => { + loadDataAction([type]); + return () => { + initData(); + }; + }, [dispatch]); + + const Component = platformForm[type]; + return ( + <RootPathContext.Provider value={rootPath}> + <div className={classnames('fe-connectivity-component', className)}> + <Route exact path={`${rootPath}`} component={Component} /> + </div> + </RootPathContext.Provider> + ); +}; diff --git a/packages/connectivity/src/index.scss b/packages/connectivity/src/index.scss new file mode 100644 index 000000000..01974e0ee --- /dev/null +++ b/packages/connectivity/src/index.scss @@ -0,0 +1,4 @@ +@import './styles/content.scss'; +@import './styles/panel.scss'; +@import './styles/webhook.scss'; +@import './styles/connectivity.scss'; diff --git a/packages/connectivity/src/index.ts b/packages/connectivity/src/index.ts new file mode 100644 index 000000000..b4a1660e3 --- /dev/null +++ b/packages/connectivity/src/index.ts @@ -0,0 +1,23 @@ +import { PluginConfig } from '@frontegg/react-core'; +import { makeComponent } from './elements/makeComponent'; +import './index.scss'; +import { ConnectivityListener } from './components/ConnectivityListener'; +import connectivity from '@frontegg/redux-store/connectivity'; +export * from './components/ConnectivityPage'; +export * from './components/ConnectivityHeader'; +export * from './components/ConnectivityContent'; + +export const WebhookComponent = makeComponent({ type: 'webhook', defaultPath: '/webhook' }); +export const SlackComponent = makeComponent({ type: 'slack', defaultPath: '/slack' }); +export const EmailComponent = makeComponent({ type: 'email', defaultPath: '/emails' }); +export const SMSComponent = makeComponent({ type: 'sms', defaultPath: '/sms' }); + +export const ConnectivityPlugin = (): PluginConfig => ({ + storeName: connectivity.storeName, + reducer: connectivity.reducer, + sagas: connectivity.sagas, + preloadedState: { + ...connectivity.initialState, + }, + Listener: ConnectivityListener, +}); diff --git a/packages/connectivity/src/interfaces.ts b/packages/connectivity/src/interfaces.ts new file mode 100644 index 000000000..3ccf6f2a7 --- /dev/null +++ b/packages/connectivity/src/interfaces.ts @@ -0,0 +1,135 @@ +import { + ICategory, + IChannelsMap, + ISlackChannel, + ISlackConfigurations, + IWebhookLogsResponse, + IWebhooksConfigurations, + IEmailSMSConfigResponse, + ISlackEvent, + IEmailSMSSubscriptionResponse, + IWebhooksSaveData, +} from '@frontegg/rest-api'; +import { FC } from 'react'; + +export type TPlatform = 'slack' | 'email' | 'sms' | 'webhook'; + +export type TWebhookStatus = 'success' | 'failed' | undefined; +export interface IPluginState { + connectivity: IConnectivityState; +} + +export interface IConnectivityComponent { + onClose?(): void; +} + +export interface IConnectivityState { + isLoading: boolean; + isSaving: boolean; + isTesting?: boolean; + list: IConnectivityData[]; + sms?: IEmailSMSConfigResponse[]; + email?: IEmailSMSConfigResponse[]; + slack?: ISlackConfigurations; + webhook?: IWebhooksConfigurations[]; + categories?: ICategory[]; + channelMap?: Record<TPlatform, IChannelsMap[]>; + error?: string; + processIds: string[]; + slackChannels: { + isLoading: boolean; + isLoadingScope?: boolean; + data?: ISlackChannel[]; + clientId?: string; + }; + testResult?: IWebhookTestResult; + webhookLogs?: IWebhookLogs; +} + +export interface IConnectivityData { + id: number; + platform: string; + key: TPlatform; + active: boolean; + events: number; + image: FC<React.SVGProps<SVGSVGElement>>; +} + +export interface IRootPath { + rootPath?: string; +} + +export interface IWebhookTestResult { + status: TWebhookStatus; + message?: string; +} + +export interface IWebhookLogs extends Partial<IWebhookLogsResponse> { + isLoading: boolean; +} + +export interface ISlackTableData { + id: string; + name: string; + index: number; + events: ISlackEventData[]; +} +export interface ISlackEventData { + eventId: string; + id?: string; + isActive: boolean; + slackEvents?: Partial<ISlackEvent>[]; + displayName: string; +} + +export interface IFormikEditComponent { + eventIdx: number; + dataIdx: number; +} + +export interface ITableFormData { + id: string; + name: string; + index: number; + events: IEventFormData[]; +} + +export interface IEventFormData { + displayName: string; + id: string; + enabled: boolean; + eventKey: string; + recipients: string[]; + subscriptions: Pick<IEmailSMSSubscriptionResponse, 'id' | 'name'>; +} + +export type TPostData = + | { + platform: 'sms' | 'email'; + data: IEmailSMSConfigResponse[]; + } + | { + platform: 'slack'; + data: ISlackConfigurations; + } + | { + platform: 'webhook'; + data: IWebhooksSaveData; + }; + +export type TPostDataSuccess = + | { + platform: 'sms' | 'email'; + data?: IEmailSMSConfigResponse[]; + id?: string; + } + | { + platform: 'slack'; + data?: ISlackConfigurations; + id?: string; + } + | { + platform: 'webhook'; + data?: IWebhooksSaveData[]; + id?: string; + }; diff --git a/packages/connectivity/src/styles/connectivity.scss b/packages/connectivity/src/styles/connectivity.scss new file mode 100644 index 000000000..92c370f7a --- /dev/null +++ b/packages/connectivity/src/styles/connectivity.scss @@ -0,0 +1,246 @@ +.fe-connectivity-context, +.fe-connectivity-component { + .fe-search { + .MuiInputBase-root, + .fe-semantic-input .input, + .fe-input__inner { + width: 19.14rem; + padding-left: 1.5rem; + position: relative; + &:before { + color: var(--color-gray-4); + position: absolute; + top: 50%; + font-size: 1.25rem; + line-height: 1rem; + left: 0.5rem; + margin-top: -0.5rem; + content: '\260C'; + transform: rotate(100deg); + } + } + .fe-semantic-input .ui.icon.input { + padding-left: 0; + input { + padding-left: 1.5rem; + padding-right: 0.9rem !important; + width: calc(100% + 1.5rem); + } + } + .fe-input__inner { + padding: 0 var(--element-padding) 0 1.5rem; + } + } + .MuiTable-root, + .fe-table { + .MuiTableRow-head > th, + .fe-table__thead-tr-th { + font-weight: bold; + text-transform: none; + &:first-child { + flex: 0 0 19.14rem !important; + width: 19.14rem !important; + + + div { + justify-content: center; + .fe-table__spacer { + flex: 0; + } + } + } + } + &__tbody { + overflow-y: hidden; + } + .MuiTableRow-root > td, + &__tr-td { + &:first-child:not(.fe-table__tr-td-empty) { + flex: 0 0 19.14rem !important; + width: 19.14rem !important; + + div { + justify-content: center; + } + + .fe-button { + background-color: transparent; + font-weight: bold; + color: #3c4a5a; + font-size: 1.125rem; + &:hover { + background-color: lighten(#eaeaeb, 6); + } + &.fe-connectivity-active .fe-connectivity-platform-icon { + border: 2px solid #d4d4d5; + } + } + } + } + .fe-circle { + display: inline-block; + background-color: #f3f5f9; + border-radius: 50%; + min-width: 3rem; + padding: 0 0.57rem; + height: 3rem; + line-height: 3rem; + text-align: center; + } + } + + .fe-connectivity-panel-shown { + display: flex; + + & > .MuiPaper-root, + .fe-table__container { + flex: 0 0 10.64rem; + width: 10.64rem; + overflow: visible; + + .MuiTable-root, + .fe-table { + min-width: 10.64rem !important; + &__thead { + &-tr-th { + display: none; + &:first-child { + display: flex; + flex: 0 0 100% !important; + width: 100% !important; + } + } + } + &__tr-td { + display: none; + &:first-child { + display: flex; + flex: 0 0 100% !important; + width: 100% !important; + } + } + &__table-container { + overflow-y: hidden; + } + } + } + .fe-connectivity-panel { + .fe-table__container { + flex: 0 1 100%; + width: auto; + } + .fe-table { + overflow: visible; + &__thead { + &-tr-th { + display: flex; + justify-content: flex-start; + &:first-child { + flex: 150 0 auto !important; + width: 150px !important; + color: var(--fe-table-header-font-color); + } + } + } + &__tr-td { + display: flex; + justify-content: flex-start; + &:first-child:not(.fe-table__tr-td-empty) { + flex: 150 0 auto !important; + width: 150px !important; + } + } + .fe-table__tr-td-empty { + display: block !important; + margin-top: 0.5rem; + margin-bottom: -1rem; + } + &__tbody { + overflow: visible; + } + } + .fe-switch { + min-width: 3rem; + } + .fe-connectivity-page-fit & { + height: 100%; + overflow: auto; + } + } + } + + .fe-connectivity-table-accordion { + .fe-table__tbody { + display: none; + margin-bottom: var(--element-padding-lg); + } + + &.fe-connectivity-open .fe-table__tbody { + display: block; + } + + .fe-connectivity-accordion-button { + padding: 0; + font-size: inherit; + font-weight: inherit; + text-transform: inherit; + + i.icon, + svg { + width: 1.2rem; + height: 1.2rem; + } + i.icon { + font-size: 1.2rem; + } + } + + .MuiTableCell-body { + border-bottom: 0; + } + } + + .fe-connectivity__content { + &-heading { + border-bottom: 1px solid #edeef0; + min-height: 5rem; + display: flex; + align-items: center; + margin: -1rem 0 0; + font-size: 1.125rem; + } + .fe-semantic-tabs { + padding-top: 1rem; + .ui.menu:last-child { + margin-bottom: -2px; + } + } + } + .fe-accordion { + background: var(--color-white); + margin-bottom: 1.14rem; + padding: 0.57rem 1.71rem 0.57rem 0.57rem; + + .fe-checkbox { + display: block; + margin: 0 -1.71rem 0 -14px; + padding: 1.14rem 1.71rem 1.14rem 1.14rem; + border-bottom: 1px solid #e9eef7; + .fe-connectivity-webhook-accordion-icon { + background-color: #e4f9ef; + } + } + .fe-accordion-content { + margin: 0 -1.71rem 0 -0.57rem; + + div:last-child { + .fe-checkbox { + border-bottom: 0; + } + } + } + } + .multiple.selection.dropdown .dropdown.icon { + top: calc(50% - 0.5rem); + // transform: translateY(-50%); + // height: auto; + } +} diff --git a/packages/connectivity/src/styles/content.scss b/packages/connectivity/src/styles/content.scss new file mode 100644 index 000000000..2696d1511 --- /dev/null +++ b/packages/connectivity/src/styles/content.scss @@ -0,0 +1,158 @@ +.fe-connectivity { + &-page { + display: flex; + flex-direction: column; + height: 100%; + } + &-context { + padding: 2rem; + flex-grow: 1; + order: 1; + overflow: auto; + .fe-connectivity-search { + margin-bottom: 2rem; + } + } + + &-component { + padding: var(--element-padding); + } + + &-platform.ui.button.fluid, + &-platform { + display: flex; + align-items: center; + margin: calc(-1 * var(--fe-table-cell-padding)); + height: auto; + flex-grow: 1; + align-self: stretch; + background-color: var(--color-gray-0); + &.MuiButton-root { + margin: -1.14rem; + } + + &.fe-connectivity-active { + background-color: var(--color-gray-2); + } + + .fe-button > &-right-arrow { + padding: 0 !important; + height: 1.3rem; + width: 1.3rem; + min-width: 1.3rem; + } + + &-icon { + margin: 1rem 2rem 1rem 0; + background-color: #f7f8fa; + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 4.6rem; + width: 4.6rem; + height: 4.6rem; + border-radius: 50%; + } + + &-title { + flex-grow: 1; + text-align: left; + } + } + + &-platform-check { + fill: #d0d3e0; + width: 3rem; + height: 3rem; + &-active { + fill: #0baf60; + } + } + + &-button { + text-transform: uppercase; + width: 8rem; + font-weight: bold; + &.fe-button-secondary { + background-color: rgba(0, 0, 0, 0.1); + color: rgba(0, 0, 0, 0.85); + border: none; + &:hover { + background-color: rgba(0, 0, 0, 0.2); + } + } + } +} + +.fe-slack-auth { + border-radius: 0.36rem; + background-color: var(--color-gray-0); + padding: 1rem; + + &-container { + border-radius: 0.36rem; + text-align: center; + border: 1px solid var(--color-gray-3); + padding: 4rem 8rem; + } + + &__txt { + margin-bottom: 1.3rem; + color: var(--color-gray-5); + } + + &__txt-strong { + font-weight: bold; + color: var(--color-gray-9); + margin-bottom: 1.3rem; + } + + &__btn { + color: white; + text-decoration: none; + background-color: #511557; + border-radius: 0.29rem; + display: inline-flex; + height: 3rem; + align-items: center; + font-weight: bold; + position: relative; + + svg { + width: 1.5rem; + height: 1.5rem; + margin: 0 1rem; + } + + span { + margin: 0 2rem 0 1.5rem; + } + + &:before { + content: ''; + position: absolute; + left: 3.5rem; + display: block; + width: 0; + top: 0; + bottom: 0; + border-left: solid 1px #804386; + } + + &:hover { + cursor: pointer; + } + } +} + +.fe-connectivity-table-input { + &.fe-input-in-form { + margin: 0; + } + + .fe-input__inner { + border-radius: var(--element-border-radius); + border-color: var(--color-gray-1); + background-color: var(--color-gray-1); + } +} diff --git a/packages/connectivity/src/styles/panel.scss b/packages/connectivity/src/styles/panel.scss new file mode 100644 index 000000000..bb91e65dd --- /dev/null +++ b/packages/connectivity/src/styles/panel.scss @@ -0,0 +1,153 @@ +.fe-connectivity-panel { + background-color: var(--color-white); + padding: 0 1.2rem 1rem; + flex: 0 1 100%; + overflow-y: auto; + z-index: 10; + + &-btn { + background-color: var(--color-gray-0); + height: 3rem; + } + + &-close { + width: 100%; + text-align: left; + + &-icon { + display: inline-flex; + width: var(--element-icon-size-sm); + height: var(--element-icon-size-sm); + border-radius: 50%; + justify-content: center; + align-items: center; + background-color: #6d7278; + color: #f9fafc; + font-size: var(--element-font-size); + } + } + + & > div:not(.fe-loader) { + min-height: auto; + height: auto; + margin-bottom: var(--element-padding-lg); + } + + .fe-table__container { + width: auto; + flex: 0 1 100%; + } + + .fe-select { + div[class$='-control'] { + background-color: var(--color-gray-1); + border-color: var(--color-gray-1); + } + + div[class$='-multiValue'] { + height: 2rem; + line-height: 2; + border-radius: 0.5rem; + background-color: #345391; + color: var(--color-white); + + div { + border-radius: 0.5rem; + color: var(--color-white); + background-color: #345391 !important; + } + } + + .fe-inputChip { + width: 100%; + } + } + + .fe-connectivity-webhook-settings { + &__frame { + background-color: var(--color-gray-1); + padding: 2.5rem; + margin: 2rem 0 0; + + .fe-select { + margin-bottom: 2rem !important; + } + .fe-select div[class$='-multiValue'] { + background-color: #cfe9ff; + font-size: 0.86rem; + padding: 0.57rem 1.14rem; + max-width: 90%; + color: #345391; + height: auto; + } + .fe-select div[class$='-multiValue'] div { + color: #345391; + padding: 0; + background-color: transparent !important; + } + } + + .fe-select { + margin-bottom: 1rem; + div[class$='-control'] { + background-color: var(--color-white); + border-color: var(--color-white); + } + } + + h2 { + margin-top: 0.71rem; + margin-bottom: 1.71rem; + font-size: 1.29rem; + font-family: 'Lato', 'Helvetica Neue', Arial, Helvetica, sans-serif; + font-weight: normal; + padding-bottom: 1rem; + } + + h3 { + font-weight: normal; + margin: 0 0 1rem; + } + + .fe-button { + padding: var(--size-padding); + height: var(--size-height); + font-size: var(--size-font-size); + --size-padding: 0 var(--element-padding-lg); + --size-height: var(--element-height-lg); + --size-font-size: var(--element-font-size-lg); + --size-border-radius: var(--element-border-radius-sm); + } + } + + &-menu { + &-button { + padding: 0; + svg { + color: var(--color-gray-5); + width: 1.7rem; + height: 1.7rem; + } + &.ui.button { + background-color: transparent; + } + } + .fe-menu-item { + color: var(--color-gray-6); + width: 10rem; + font-size: 0.9rem; + svg { + width: 1.2rem; + height: 1.2rem; + } + } + &.fe-menu__popup { + z-index: 10; + .fe-menu-item__icon { + font-size: 1.2rem; + top: 50%; + transform: translateY(-50%); + } + } + } +} diff --git a/packages/connectivity/src/styles/webhook.scss b/packages/connectivity/src/styles/webhook.scss new file mode 100644 index 000000000..45ad0c9db --- /dev/null +++ b/packages/connectivity/src/styles/webhook.scss @@ -0,0 +1,250 @@ +.fe-connectivity-webhook { + &-list { + position: relative; + } + + &-logs { + &__retry-icon { + margin-left: 0.85rem; + width: 1rem !important; + } + } + + &-add, + &-add.MuiButtonBase-root { + position: absolute; + right: var(--element-spacing); + top: var(--element-spacing); + } + + &-event { + margin-right: var(--element-spacing); + position: relative; + &::after { + content: ' ,'; + position: absolute; + top: 0; + left: 100%; + display: block; + } + &:nth-last-child(1) { + margin-right: 0; + &::after { + content: none; + } + } + } + + &-cell { + justify-self: stretch; + flex-grow: 1; + + &-link { + cursor: pointer; + } + } + + &-test-payload { + min-height: 10rem; + max-height: 45rem; + overflow: auto; + } + + &-settings { + padding: var(--element-padding); + .fe-semantic-input { + margin-bottom: 24px; + label { + font-size: 1rem !important; + margin-bottom: 7px; + } + .input input { + height: 46px; + } + } + } + + &-check { + margin-bottom: var(--element-padding); + &.fe-check-item { + padding-left: var(--element-padding); + } + + .fe-checkbox__input { + width: 1.5rem; + height: 1.5rem; + } + + input:checked + .fe-checkbox__input .fe-icon { + width: 1.5rem; + height: 1.5rem; + } + } + + &-according { + &-events-name { + flex-grow: 1; + } + } + + &-option { + &-category { + background-color: var(--color-gray-1); + } + } + + &-dialog { + &-success .fe-dialog-content { + border-left: solid 1rem var(--color-success); + } + + &-failed .fe-dialog-content { + border-left: solid 1rem var(--color-danger); + } + + &-message { + width: 35rem; + max-width: 35rem; + } + + &-more { + .fe-dialog-header { + height: auto; + .fe-dialog-title { + width: 95%; + padding: var(--element-padding-lg); + } + } + .fe-dialog-body { + background-color: var(--color-gray-1); + } + &-date { + padding-left: var(--element-padding); + color: var(--color-gray-5); + font-weight: normal; + } + } + + &-action { + border-top: solid 1px var(--element-divider-color); + margin: 0 -2rem -2rem; + padding: 1rem 2rem 2rem; + } + } + + &-status { + border-radius: 2rem; + position: relative; + padding: 0.5rem 2rem; + background-color: var(--color-success-25); + color: var(--color-success); + &::before { + content: ''; + position: absolute; + display: block; + width: 0.5rem; + height: 0.5rem; + top: 50%; + left: 1rem; + background-color: var(--color-success); + border-radius: 50%; + transform: translateY(-50%); + } + + &.fe-status-failed { + background-color: var(--color-danger-25); + color: var(--color-danger); + &:before { + background-color: var(--color-danger); + } + } + } + + &-detail { + text-decoration: underline; + } + + &-accordion { + box-shadow: none; + &-icon.icon, + &-icon { + color: var(--color-primary-darker); + margin-right: var(--element-spacing); + border-radius: var(--element-border-radius-sm); + border: 1px solid var(--color-primary-light); + + .Mui-expanded &, + .fe-accordion-header-expanded &, + .title.active &.icon { + background-color: var(--color-primary-lighter); + border-color: var(--color-primary-lighter); + transform: rotate(90deg); + } + } + .MuiAccordionDetails-root { + flex-direction: column; + } + &-icon.icon { + width: 2.3rem; + height: 2.3rem; + font-size: 2rem; + } + + .ui.checkbox { + label { + line-height: 1.7rem; + padding-left: 2.3rem; + position: relative; + &:before, + &:after { + line-height: 1.7rem; + width: 1.7rem; + height: 1.7rem; + color: white; + } + } + } + input[type='checkbox']:checked ~ label { + &:before { + background-color: #0baf60; + border-color: #0baf60; + } + } + } + + &-description { + color: var(--color-gray-5); + font-weight: normal; + } + + &-help { + color: var(--color-white); + background-color: var(--color-black); + display: inline-flex; + margin-left: var(--element-spacing); + width: 1rem; + height: 1rem; + border-radius: 50%; + align-items: center; + justify-content: center; + font-size: 0.8rem; + cursor: pointer; + + &-block { + width: 16rem; + } + } +} + +.fe-connectivity-webhook-settings { + &__frame { + &-title { + font-weight: 600; + font-size: 1.5rem; + margin: 0.625rem 0; + } + background-color: var(--color-gray-1); + padding: 2.5rem; + margin: 2rem 0 0; + } +} diff --git a/packages/connectivity/src/utils.ts b/packages/connectivity/src/utils.ts new file mode 100644 index 000000000..9597ff41d --- /dev/null +++ b/packages/connectivity/src/utils.ts @@ -0,0 +1,37 @@ +import { useMemo } from 'react'; +import { ICategory, IChannelsMap } from '@frontegg/rest-api'; +import { createSelector } from '@reduxjs/toolkit'; + +export const filterCategories = ( + categories?: ICategory[], + channelMap?: IChannelsMap[] +): (ICategory & { index: number })[] | undefined => + useMemo(() => { + if (categories && channelMap) { + return categories + .map((cat) => ({ + ...cat, + events: cat.events?.filter(({ key }) => channelMap.some(({ key: eventKey }) => eventKey === key)), + })) + .filter(({ events }) => !!events?.length) + .map((cat, index) => ({ ...cat, index })); + } + return undefined; + }, [categories, channelMap]); + +export const selectedEvents = createSelector( + (events?: string[]) => events, + (events?: string[]) => + events?.reduce( + (acc: { names: string[]; eventKeys: string[] }, curr) => { + if (/\.\*$/.test(curr)) { + const val = curr.replace(/\.\*$/, ''); + !acc.names.includes(val) && acc.names.push(val); + } else if (!acc.eventKeys.includes(curr)) { + acc.eventKeys.push(curr); + } + return acc; + }, + { names: [], eventKeys: [] } + ) +); diff --git a/packages/connectivity/tsconfig.json b/packages/connectivity/tsconfig.json new file mode 100644 index 000000000..af25e8686 --- /dev/null +++ b/packages/connectivity/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "declarationDir": "./dist", + "noImplicitAny": false + }, + "include": [ + "./src/**/*.tsx", + "./src/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist", + "src/**/*.stories.tsx", + "src/**/*.test.tsx", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.spec.tsx", + "src/**/*.cy-spec.ts", + "src/**/*.cy-spec.tsx" + ] +} + diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md new file mode 100644 index 000000000..9949f81d0 --- /dev/null +++ b/packages/core/CHANGELOG.md @@ -0,0 +1,914 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [2.8.12](https://github.com/frontegg/frontegg-react/compare/v2.8.11...v2.8.12) (2021-07-20) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +## [2.8.10](https://github.com/frontegg/frontegg-react/compare/v2.8.9...v2.8.10) (2021-07-08) + + +### Bug Fixes + +* **audits:** fix store conflict between old audits and new auditlogs state ([5e493fe](https://github.com/frontegg/frontegg-react/commit/5e493fec79dd73198186a6b2a94e8833e4600102)) + + + + + +## [2.8.9](https://github.com/frontegg/frontegg-react/compare/v2.8.8...v2.8.9) (2021-07-08) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +## [2.8.8](https://github.com/frontegg/frontegg-react/compare/v2.8.7...v2.8.8) (2021-07-06) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +## [2.8.7](https://github.com/frontegg/frontegg-react/compare/v2.8.6...v2.8.7) (2021-07-01) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +## [2.8.6](https://github.com/frontegg/frontegg-react/compare/v2.8.5...v2.8.6) (2021-06-30) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +## [2.8.5](https://github.com/frontegg/frontegg-react/compare/v2.8.4...v2.8.5) (2021-06-30) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +## [2.8.4](https://github.com/frontegg/frontegg-react/compare/v2.8.3...v2.8.4) (2021-06-29) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +## [2.8.3](https://github.com/frontegg/frontegg-react/compare/v2.8.1...v2.8.3) (2021-06-23) + + +### Bug Fixes + +* align all dependencies versions ([f1d5c48](https://github.com/frontegg/frontegg-react/commit/f1d5c48aba34827ea06a554cfb61734f1a40c93b)) +* update libraries version ([77da072](https://github.com/frontegg/frontegg-react/commit/77da072c67cb11e6cccb86b044a3a1a22b09625c)) + + + + + +## [2.8.2](https://github.com/frontegg/frontegg-react/compare/v2.8.1...v2.8.2) (2021-06-23) + + +### Bug Fixes + +* update libraries version ([77da072](https://github.com/frontegg/frontegg-react/commit/77da072c67cb11e6cccb86b044a3a1a22b09625c)) + + + + + +## [2.8.1](https://github.com/frontegg/frontegg-react/compare/v2.8.0...v2.8.1) (2021-06-22) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +# [2.8.0](https://github.com/frontegg/frontegg-react/compare/v2.7.2...v2.8.0) (2021-06-21) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +## [2.7.2](https://github.com/frontegg/frontegg-react/compare/v2.7.1...v2.7.2) (2021-06-14) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +# [2.7.0](https://github.com/frontegg/frontegg-react/compare/v2.6.0...v2.7.0) (2021-06-07) + + +### Bug Fixes + +* **core:** increase domain suffix length ([02af3e9](https://github.com/frontegg/frontegg-react/commit/02af3e9d379831e833adfae85003506146013da3)) + + + + + +# [2.6.0](https://github.com/frontegg/frontegg-react/compare/v2.5.2...v2.6.0) (2021-05-27) + + +### Features + +* **auth:** force terms on social sign up FR-2869 ([#406](https://github.com/frontegg/frontegg-react/issues/406)) ([4462402](https://github.com/frontegg/frontegg-react/commit/4462402c8648a023eb7595c4153a9943c039f995)) + + + + + +## [2.5.2](https://github.com/frontegg/frontegg-react/compare/v2.5.1...v2.5.2) (2021-05-24) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +## [2.5.1](https://github.com/frontegg/frontegg-react/compare/v2.5.0...v2.5.1) (2021-05-23) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +# [2.4.0](https://github.com/frontegg/frontegg-react/compare/v2.3.2...v2.4.0) (2021-05-19) + + +### Features + +* **auth:** [FR-2731] remember MFA devices ([#404](https://github.com/frontegg/frontegg-react/issues/404)) ([7f135d2](https://github.com/frontegg/frontegg-react/commit/7f135d200657ffd19ab54bcf9fd2049c07db43b4)) + + + + + +## [2.3.2](https://github.com/frontegg/frontegg-react/compare/v2.3.1...v2.3.2) (2021-05-10) + + +### Bug Fixes + +* **connectivity:** fix UI glitches ([a78c4f0](https://github.com/frontegg/frontegg-react/commit/a78c4f0587a606cc529909d35a24d98ab3e66f01)) + + + + + +## [2.3.1](https://github.com/frontegg/frontegg-react/compare/v2.3.0...v2.3.1) (2021-05-10) + + +### Bug Fixes + +* **connectivity:** fix changes not saved on swithcing connecticity context ([04758b5](https://github.com/frontegg/frontegg-react/commit/04758b5070da15f20f93010ddd24d9bd9b4f27ab)) + + + + + +# [2.3.0](https://github.com/frontegg/frontegg-react/compare/v2.2.2...v2.3.0) (2021-05-07) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +## [2.2.2](https://github.com/frontegg/frontegg-react/compare/v2.2.1...v2.2.2) (2021-04-29) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +## [2.2.1](https://github.com/frontegg/frontegg-react/compare/v2.2.0...v2.2.1) (2021-04-28) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +# [2.2.0](https://github.com/frontegg/frontegg-react/compare/v2.1.0...v2.2.0) (2021-04-28) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +# [2.1.0](https://github.com/frontegg/frontegg-react/compare/v2.0.0...v2.1.0) (2021-04-13) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +# [2.0.0](https://github.com/frontegg/frontegg-react/compare/v1.28.0...v2.0.0) (2021-04-11) + + +### Bug Fixes + +* **connectivity:** FR-2312 - merge with master ([41b7a2b](https://github.com/frontegg/frontegg-react/commit/41b7a2b7147c249aac61352f2fc70bb068e6ccaf)) +* Fix build for rescript ([58c4b3c](https://github.com/frontegg/frontegg-react/commit/58c4b3c09c45bc42b14614e5012e054615d1de4c)) +* FR-2100 - fix expandable table styles ([9586201](https://github.com/frontegg/frontegg-react/commit/9586201e2f95c43648f5c72b3b353c6a3c9766e2)) +* FR-2312 - fixstyle status button (webhooks); add success for theme' ([1c4ca9d](https://github.com/frontegg/frontegg-react/commit/1c4ca9de4ced5a567740bb2d812c104a51435c3a)) + + +### Features + +* **auth:** enforce users password config on activate/reset/change password ([#342](https://github.com/frontegg/frontegg-react/issues/342)) ([7aeaeb2](https://github.com/frontegg/frontegg-react/commit/7aeaeb2568608dc9f8d6f0f66caf109fa52a6a66)) +* **auth:** login with microsoft account ([8fd8590](https://github.com/frontegg/frontegg-react/commit/8fd8590866bf58c6697f2390930c7a05bb2db220)) +* Extract react hooks to separated sub package ([8ad0333](https://github.com/frontegg/frontegg-react/commit/8ad033332fde18e3f10f7f6f4f5d0d24fc88f0b0)) + + + + + +# [1.28.0](https://github.com/frontegg/frontegg-react/compare/v1.27.0...v1.28.0) (2021-03-22) + + +### Bug Fixes + +* FR-2220 - add loader for MenuItem ([4a3e62e](https://github.com/frontegg/frontegg-react/commit/4a3e62e68f7041e0d376ffc411c57198557c20f1)) +* **core:** FR-2126 - removed list dots for error message ([6a233b0](https://github.com/frontegg/frontegg-react/commit/6a233b0dc1f7650f27c1b14548b59539bb7f9966)) + + +### Features + +* **auth:** request new activation email ([748255f](https://github.com/frontegg/frontegg-react/commit/748255fc924ef5e36764ba264d9a3767a9ea0c59)) + + + + + +# [1.27.0](https://github.com/frontegg/frontegg-react/compare/v1.26.0...v1.27.0) (2021-03-18) + + +### Features + +* **auth:** added option for terms of service in signup page ([f876091](https://github.com/frontegg/frontegg-react/commit/f876091cfde000c7ae003b878bea13ab8271f171)) + + + + + +# [1.26.0](https://github.com/frontegg/frontegg-react/compare/v1.25.0...v1.26.0) (2021-03-17) + + +### Features + +* **auth:** allow getting login/signup redirect url via query param ([ce909fd](https://github.com/frontegg/frontegg-react/commit/ce909fd1a5f430ebdeeeb9182837f837c97f720c)) + + + + + +# [1.25.0](https://github.com/frontegg/frontegg-react/compare/v1.24.0...v1.25.0) (2021-03-07) + + +### Bug Fixes + +* Set fixed version for i18next in @frontegg/react-core ([20f9879](https://github.com/frontegg/frontegg-react/commit/20f98795e88b08e5e98e71d2d062836f47ce1061)) + + + + + +# [1.24.0](https://github.com/frontegg/frontegg-react/compare/v1.23.1...v1.24.0) (2021-03-03) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +## [1.23.1](https://github.com/frontegg/frontegg-react/compare/v1.23.0...v1.23.1) (2021-02-21) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +## [1.22.1](https://github.com/frontegg/frontegg-react/compare/v1.22.0...v1.22.1) (2021-02-16) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +# [1.22.0](https://github.com/frontegg/frontegg-react/compare/v1.21.1...v1.22.0) (2021-02-13) + + +### Bug Fixes + +* **core:** remove unused imported components in the FileInput component ([54a7f29](https://github.com/frontegg/frontegg-react/commit/54a7f29a778eeb0a2f609c3239c85e8244562c90)) + + +### Features + +* **core:** add support ref for the input component ([f47fc79](https://github.com/frontegg/frontegg-react/commit/f47fc79e57738c867d7eb5574caa259f5598633a)) +* **core:** add suppurt ref for the FileInput component ([86b3790](https://github.com/frontegg/frontegg-react/commit/86b3790c35c44cfddb9ceee76b19cd2428b5cccd)) +* **core:** Added tab disabling for FeTabs component; disabled pwd tab in Profile FR-789 ([2354f47](https://github.com/frontegg/frontegg-react/commit/2354f47a5d0fe22e05b3e869b7e963192cd86b45)) + + + + + +## [1.21.1](https://github.com/frontegg/frontegg-react/compare/v1.21.0...v1.21.1) (2021-02-04) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +# [1.21.0](https://github.com/frontegg/frontegg-react/compare/v1.20.1...v1.21.0) (2021-02-04) + + +### Bug Fixes + +* **core:** leave the empty values in the cell instead of text in the FeTable ([8997a53](https://github.com/frontegg/frontegg-react/commit/8997a53a48e53e6a220bc8d709e94674a49b1519)) + + + + + +## [1.20.1](https://github.com/frontegg/frontegg-react/compare/v1.20.0...v1.20.1) (2021-02-02) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +# [1.20.0](https://github.com/frontegg/frontegg-react/compare/v1.19.1...v1.20.0) (2021-02-01) + + +### Bug Fixes + +* **core:** fix close tolltip popup ([f78cc43](https://github.com/frontegg/frontegg-react/commit/f78cc437d812be6d04e0f65c1664da9ce609fae6)) +* **core:** fiz a problem with caching filters data in the FeTable componet ([319d7be](https://github.com/frontegg/frontegg-react/commit/319d7bec60e88c146f98d5825770ed8721ce0637)) + + +### Features + +* **core:** implement horizontal scrolling in the table component ([1315b75](https://github.com/frontegg/frontegg-react/commit/1315b75aa92abeace8b2ea811f62efbb6e8db6c7)) + + + + + +## [1.19.1](https://github.com/frontegg/frontegg-react/compare/v1.19.0...v1.19.1) (2021-01-20) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +# [1.19.0](https://github.com/frontegg/frontegg-react/compare/v1.18.6...v1.19.0) (2021-01-20) + + +### Bug Fixes + +* **core:** add support the data-* attribute for the CheckBox component ([bbcc4d5](https://github.com/frontegg/frontegg-react/commit/bbcc4d59d972254483b83329839e2c29251b8aed)) +* **core:** fix align for the label in the CheckBox ([d094a09](https://github.com/frontegg/frontegg-react/commit/d094a090a95bbf0b3fa61661ea84c1359b10db17)) +* **core:** fix click by form button if process loading is active ([5a9971c](https://github.com/frontegg/frontegg-react/commit/5a9971ce1a4741ac2b73f6fa01c52a9e84fd9905)) + + +### Features + +* **core:** add support the sortType param to the Column values for the FeTable component ([9702cee](https://github.com/frontegg/frontegg-react/commit/9702cee6b9c80c7e2bb9db126a4e93d9935a25d0)) + + +### Performance Improvements + +* **core:** move onChange handler to the useCallback hook ([25c78da](https://github.com/frontegg/frontegg-react/commit/25c78dafe667accee048acbb9a916fd4ad91b0fc)) + + + + + +## [1.18.6](https://github.com/frontegg/frontegg-react/compare/v1.18.5...v1.18.6) (2021-01-19) + + +### Bug Fixes + +* **core:** Fix toggled expandable button svg color FR-1173 ([bcf486d](https://github.com/frontegg/frontegg-react/commit/bcf486d5334a0fade4305bac70fb37b8ec63a53f)) +* Add more space to first column in Table components FR-1171 ([c0b6b38](https://github.com/frontegg/frontegg-react/commit/c0b6b38479b52b3fce66439a640be8e7b4a59809)) +* **core:** Break user full name when no free space available FR-1528 ([da508f4](https://github.com/frontegg/frontegg-react/commit/da508f4bf7e59b8fbb2a232a3f0aec97ff8b2e0c)) + + + + + +## [1.18.5](https://github.com/frontegg/frontegg-react/compare/v1.18.4...v1.18.5) (2021-01-18) + + +### Bug Fixes + +* **core:** fix perfomance for the INputChip component ([cea5e19](https://github.com/frontegg/frontegg-react/commit/cea5e19aef64a6cf42f75d3cbb77cd74fb01f560)) + + + + + +## [1.18.4](https://github.com/frontegg/frontegg-react/compare/v1.18.3...v1.18.4) (2021-01-18) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +## [1.18.3](https://github.com/frontegg/frontegg-react/compare/v1.18.2...v1.18.3) (2021-01-17) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +## [1.18.2](https://github.com/frontegg/frontegg-react/compare/v1.18.1...v1.18.2) (2021-01-15) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +## [1.18.1](https://github.com/frontegg/frontegg-react/compare/v1.18.0...v1.18.1) (2021-01-15) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +# [1.18.0](https://github.com/frontegg/frontegg-react/compare/v1.17.3...v1.18.0) (2021-01-14) + + +### Bug Fixes + +* **core:** fix close property for the FePopup component ([e9f9d85](https://github.com/frontegg/frontegg-react/commit/e9f9d85fbc51c3e83e789fe326acf5117f0d47ca)) + + +### Features + +* **core:** support enter data on blur event in the InputChip element ([6f43239](https://github.com/frontegg/frontegg-react/commit/6f43239fe2ab03f794d8eedc8742eb811f4567de)) + + + + + +## [1.17.3](https://github.com/frontegg/frontegg-react/compare/v1.17.2...v1.17.3) (2021-01-13) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +## [1.17.2](https://github.com/frontegg/frontegg-react/compare/v1.17.1...v1.17.2) (2021-01-12) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +## [1.17.1](https://github.com/frontegg/frontegg-react/compare/v1.17.0...v1.17.1) (2021-01-05) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +# [1.17.0](https://github.com/frontegg/frontegg-react/compare/v1.16.2...v1.17.0) (2021-01-03) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +## [1.16.2](https://github.com/frontegg/frontegg-react/compare/v1.16.0...v1.16.2) (2020-12-27) + + +### Bug Fixes + +* **auth:** fix force mfa recovery code not showing up ([#204](https://github.com/frontegg/frontegg-react/issues/204)) ([f6ce5ac](https://github.com/frontegg/frontegg-react/commit/f6ce5ac2be84c931454051760ae3fcca232b6c12)) +* **auth:** fix some minot texts and css issues on MFA ([#203](https://github.com/frontegg/frontegg-react/issues/203)) ([688cbc7](https://github.com/frontegg/frontegg-react/commit/688cbc75fb1a74730d433d0026841856f666018d)) +* Fix force MFA screen bugs ([#196](https://github.com/frontegg/frontegg-react/issues/196)) ([8d51fb9](https://github.com/frontegg/frontegg-react/commit/8d51fb9794d0d0728bd04a742ce6a3f77845d1fe)) + + + + + +## [1.16.1](https://github.com/frontegg/frontegg-react/compare/v1.16.0...v1.16.1) (2020-12-27) + + +### Bug Fixes + +* **auth:** fix force mfa recovery code not showing up ([#204](https://github.com/frontegg/frontegg-react/issues/204)) ([f6ce5ac](https://github.com/frontegg/frontegg-react/commit/f6ce5ac2be84c931454051760ae3fcca232b6c12)) +* Fix force MFA screen bugs ([#196](https://github.com/frontegg/frontegg-react/issues/196)) ([8d51fb9](https://github.com/frontegg/frontegg-react/commit/8d51fb9794d0d0728bd04a742ce6a3f77845d1fe)) + + + + + +# [1.16.0](https://github.com/frontegg/frontegg-react/compare/v1.15.2...v1.16.0) (2020-12-24) + + +### Bug Fixes + +* **core:** fix clean data in the FeInput component ([6d900c8](https://github.com/frontegg/frontegg-react/commit/6d900c8e04986f21744d69ee741408fd9d66ffb3)) +* **core:** fix style the FeChip component ([1f76479](https://github.com/frontegg/frontegg-react/commit/1f76479817129653b08bac047531d68d2f132fab)) +* **localize:** fix text for secure tooltip ([8761141](https://github.com/frontegg/frontegg-react/commit/87611410fc23b881a11bcc80e9ba489bcf3ccf32)) + + +### Features + +* **core:** add anew property dontDisableSaving to the FInput compoennt ([6ff5648](https://github.com/frontegg/frontegg-react/commit/6ff56488a76816f5b501b616656e0bc97afe03ee)) + + + + + +## [1.15.2](https://github.com/frontegg/frontegg-react/compare/v1.15.1...v1.15.2) (2020-12-24) + + +### Bug Fixes + +* **core:** fix validate the InputChip component ([e363849](https://github.com/frontegg/frontegg-react/commit/e36384952d95edf22541b5a648d6cd09b42b4c95)) + + + + + +## [1.15.1](https://github.com/frontegg/frontegg-react/compare/v1.15.0...v1.15.1) (2020-12-21) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +# [1.15.0](https://github.com/frontegg/frontegg-react/compare/v1.14.1...v1.15.0) (2020-12-20) + + +### Bug Fixes + +* Fix owsp validation exception on undefined value ([54ffaee](https://github.com/frontegg/frontegg-react/commit/54ffaeed42564481bc1f1b9592e02ba4e266f9e6)) + + + + + +## [1.14.1](https://github.com/frontegg/frontegg-react/compare/v1.14.0...v1.14.1) (2020-12-17) + + +### Bug Fixes + +* change primary color to darker blue ([04f94bd](https://github.com/frontegg/frontegg-react/commit/04f94bd5caa1135560e89cf0c886a4b81665956a)) +* Fix create new webhooks button typo ([0be76c3](https://github.com/frontegg/frontegg-react/commit/0be76c38a771996ad849a027b752fb7107b9d3db)) +* Fix input style issues when autocomplete enables ([790e8b4](https://github.com/frontegg/frontegg-react/commit/790e8b41e1e36855baa04b730025ac3246bc6b89)) +* Fix search bar alignments and ui bug fixes ([dd51197](https://github.com/frontegg/frontegg-react/commit/dd5119705cad6e379459171e34a5a3abe4d891ff)) +* prevent dialog from closing if clicking on other portal element ([c6a3b5b](https://github.com/frontegg/frontegg-react/commit/c6a3b5bec8e0f362f9fa816f6dff719d6db23f1a)) +* Re-enable fields in SSO claim domain in validation failed ([4fb385c](https://github.com/frontegg/frontegg-react/commit/4fb385c544d03658964b40285b9ec8041250d269)) + + + + + +# [1.14.0](https://github.com/frontegg/frontegg-react/compare/v1.13.2...v1.14.0) (2020-12-16) + + +### Bug Fixes + +* Fix profile tabs color ([3ab5742](https://github.com/frontegg/frontegg-react/commit/3ab57426355700c05930075c014faa2c10456b9c)) +* UI enhancements for SSO components ([6be3aea](https://github.com/frontegg/frontegg-react/commit/6be3aea9e54aa56e4f28da3d63a81df500435fab)) +* Update components primary color ([ee6d08e](https://github.com/frontegg/frontegg-react/commit/ee6d08ec880fc7ae9427d993a544385db8d3da5b)) + + +### Features + +* **auth:** Api tokens component for users and tenants ([c8b1e17](https://github.com/frontegg/frontegg-react/commit/c8b1e176bee4f4402afbd9625841312428c14b75)) + + + + + +## [1.13.2](https://github.com/frontegg/frontegg-react/compare/v1.13.1...v1.13.2) (2020-12-13) + + +### Bug Fixes + +* **audits:** FR-1001 add 'unknown' when cell value is undefined ([889bc83](https://github.com/frontegg/frontegg-react/commit/889bc83ae9105228b88c32826a81eb7b8de4d0d4)) + + + + + +## [1.13.1](https://github.com/frontegg/frontegg-react/compare/v1.13.0...v1.13.1) (2020-12-10) + + +### Bug Fixes + +* **audits:** FR-996 add closing popup when reference hidden to prevent ui bugs ([b046f95](https://github.com/frontegg/frontegg-react/commit/b046f9503f983401ff26eb2e16edc6954cb101d5)) +* **audits:** FR-999 add x btn for ip popup ([6efc8ea](https://github.com/frontegg/frontegg-react/commit/6efc8ea6e229b6434d75f9af59c0b6f0993ec9cd)) + + + + + +# [1.13.0](https://github.com/frontegg/frontegg-react/compare/v1.12.0...v1.13.0) (2020-12-09) + + +### Features + +* [FR-808] add support in users sign ups ([1a6f7c3](https://github.com/frontegg/frontegg-react/commit/1a6f7c3639ab4c351593d540296e67f65293bbf9)) + + + + + +# [1.12.0](https://github.com/frontegg/frontegg-react/compare/v1.11.1...v1.12.0) (2020-12-09) + + +### Bug Fixes + +* **audits:** FR-1000 fix updating filter value ([00f84d4](https://github.com/frontegg/frontegg-react/commit/00f84d427db5cf1faaedb69d7025debe0513debf)) +* **audits:** FR-1002 add globe icon when location is unknown ([53265fd](https://github.com/frontegg/frontegg-react/commit/53265fd5cdc56d3fc141ad77469d452e2f6dc430)) +* **audits:** FR-1004 prevent call action onPageChange after init render ([bca3bc1](https://github.com/frontegg/frontegg-react/commit/bca3bc14818c01589a5c33ce9f8a1f2c6923a585)) +* **audits:** FR-1004 remove debounce filter, prevent onFilterChange call after init render ([b842cd6](https://github.com/frontegg/frontegg-react/commit/b842cd6186f032750fb6daed3e01e01d5b135498)) +* **audits:** FR-998 change severity attention letters coloring ([efdb0c8](https://github.com/frontegg/frontegg-react/commit/efdb0c8388af715bcdd9657632d5e13c5ca94eb2)) +* **core:** fix the z-index value for the popup component ([6201205](https://github.com/frontegg/frontegg-react/commit/620120501945c9e0a8e89add87e466f397bb7421)) + + + + + +## [1.11.1](https://github.com/frontegg/frontegg-react/compare/v1.11.0...v1.11.1) (2020-12-02) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +# [1.11.0](https://github.com/frontegg/frontegg-react/compare/v1.10.0...v1.11.0) (2020-11-30) + + +### Bug Fixes + +* [FR-815] change password policy to be aligned with backend ([fbc9abf](https://github.com/frontegg/frontegg-react/commit/fbc9abfa776b9f7ac0a8f3c89eaa5c6a39b320b6)) + + + + + +# [1.10.0](https://github.com/frontegg/frontegg-react/compare/v1.9.0...v1.10.0) (2020-11-27) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +# [1.9.0](https://github.com/frontegg/frontegg-react/compare/v1.8.0...v1.9.0) (2020-11-25) + + +### Bug Fixes + +* **core:** fix external finterFunction ([72419cb](https://github.com/frontegg/frontegg-react/commit/72419cb98c279356c30aa3a736f9a0ccd6f285ce)) +* fix console errors ([47a0679](https://github.com/frontegg/frontegg-react/commit/47a0679cb426eeb09bd5d97e0b28fe697c24e3b2)), closes [#119](https://github.com/frontegg/frontegg-react/issues/119) + + +### Features + +* **auth:** New AccountDropdown component added to AuthPlugin ([018f2f8](https://github.com/frontegg/frontegg-react/commit/018f2f8db3ad22981f9270bd2e166b56cf6eb7ff)) +* **core:** add dupport the ClassName property to the Table component ([71a582d](https://github.com/frontegg/frontegg-react/commit/71a582d69e44130cb164a5951e6f1225406cb7e7)) +* **core:** add support the fullWidth property to the InputChip component ([f1c6586](https://github.com/frontegg/frontegg-react/commit/f1c65869939d176aceb9eb8dcd9d8c4d42592caa)) +* New plugin for Audits ([#106](https://github.com/frontegg/frontegg-react/issues/106)) ([921b37a](https://github.com/frontegg/frontegg-react/commit/921b37aca98f23c800c8b5d094ed595d52617679)) + + + + + +# [1.8.0](https://github.com/frontegg/frontegg-react/compare/v1.7.0...v1.8.0) (2020-11-23) + + +### Features + +* Add Connectivity Plugin ([#98](https://github.com/frontegg/frontegg-react/issues/98)) ([db77431](https://github.com/frontegg/frontegg-react/commit/db77431b6d744b93431430543019fedd1c0dbae2)) +* **auth:** Add support in google and github social logins ([#111](https://github.com/frontegg/frontegg-react/issues/111)) ([938b04c](https://github.com/frontegg/frontegg-react/commit/938b04cba618e2029b55ff4c39d5c0fc0d884e6b)) + + + + + +# [1.7.0](https://github.com/frontegg/frontegg-react/compare/v1.6.1...v1.7.0) (2020-11-22) + + +### Bug Fixes + +* remove auth deps from core lib ([1686abd](https://github.com/frontegg/frontegg-react/commit/1686abd9f2dd7a92b997d8a6e649be70aff5b29b)) + + +### Features + +* resolve saga actions outside fronteggprovider ([7878beb](https://github.com/frontegg/frontegg-react/commit/7878bebf49b5131fcdf16bbd21c1bcab03c2d1ae)) + + + + + +## [1.6.2](https://github.com/frontegg/frontegg-react/compare/v1.6.1...v1.6.2) (2020-11-19) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +## [1.6.1](https://github.com/frontegg/frontegg-react/compare/v1.6.0...v1.6.1) (2020-11-15) + + +### Bug Fixes + +* disable store caching in cypress tests ([4f5e5f5](https://github.com/frontegg/frontegg-react/commit/4f5e5f5b0cbbd74794fc8ae36770223a1356bf2a)) +* fix multiple store initialization in strict-mode ([a569f86](https://github.com/frontegg/frontegg-react/commit/a569f86b37292e71b985c3a2e54610121ab419ce)) +* restore test-id to forgot password button ([b8a4ab4](https://github.com/frontegg/frontegg-react/commit/b8a4ab448c5c3fd45e7ad4a1189242d27d3f5822)) + + + + + +# [1.6.0](https://github.com/frontegg/frontegg-react/compare/v1.5.0...v1.6.0) (2020-11-12) + + +### Bug Fixes + +* fix pagination bug in TeamTable ([8ba1c3d](https://github.com/frontegg/frontegg-react/commit/8ba1c3d861257231b1890766c5042cba58998965)) + + +### Features + +* add option to upload profile image ([#96](https://github.com/frontegg/frontegg-react/issues/96)) ([0e4c45c](https://github.com/frontegg/frontegg-react/commit/0e4c45cb08a84519e1f2ebb06295af26cdc05ff7)) + + + + + +# [1.5.0](https://github.com/frontegg/frontegg-react/compare/v1.4.0...v1.5.0) (2020-11-10) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +# [1.4.0](https://github.com/frontegg/frontegg-react/compare/v1.3.0...v1.4.0) (2020-11-09) + + +### Bug Fixes + +* add option to add user without roles ([4d17333](https://github.com/frontegg/frontegg-react/commit/4d17333fc0f157d3c5d4462f20d8f2269b579a65)) +* remove default font-size from root css ([ad774e9](https://github.com/frontegg/frontegg-react/commit/ad774e95b1199efaf53951f3b2a0df52c1bd2900)), closes [#90](https://github.com/frontegg/frontegg-react/issues/90) +* remove memorized store ([b4d2b25](https://github.com/frontegg/frontegg-react/commit/b4d2b2550c3c54220866fbc7540014b279aa12f9)) + + +### Features + +* notifications plugin ([#78](https://github.com/frontegg/frontegg-react/issues/78)) ([0439d17](https://github.com/frontegg/frontegg-react/commit/0439d179ed5c0abae510b7d132dbf03ae907f7f6)) + + + + + +# [1.3.0](https://github.com/frontegg/frontegg-react/compare/v1.2.0...v1.3.0) (2020-11-04) + + +### Bug Fixes + +* **auth:** bug fixes ([#76](https://github.com/frontegg/frontegg-react/issues/76)) ([a758642](https://github.com/frontegg/frontegg-react/commit/a758642458751e930dc4ea6de6acc50b3ede0b77)) +* **auth:** fix ui bugs ([#79](https://github.com/frontegg/frontegg-react/issues/79)) ([0e75d8d](https://github.com/frontegg/frontegg-react/commit/0e75d8dff80937dc2e4308a0ffdd527a12a84a39)) +* calling `response.text()` after `response.json()` fails ([#80](https://github.com/frontegg/frontegg-react/issues/80)) ([3cde90d](https://github.com/frontegg/frontegg-react/commit/3cde90db8a5e9f1850dbf51db492f94e77cce93e)) + + +### Features + +* **elements:** add FeInput element ([f23e439](https://github.com/frontegg/frontegg-react/commit/f23e4392ab849d32c5e0ba67e055f793ecfd5ceb)) +* add support for nextjs and angular ([#82](https://github.com/frontegg/frontegg-react/issues/82)) ([5fa36eb](https://github.com/frontegg/frontegg-react/commit/5fa36ebe7bfa6866c78455a746727ba8b1cafbbc)) + + + + + +# [1.2.0](https://github.com/frontegg/frontegg-react/compare/v1.1.0...v1.2.0) (2020-10-25) + + +### Bug Fixes + +* **elements:** UI elements small fixes ([effb9bd](https://github.com/frontegg/frontegg-react/commit/effb9bd54186133184010c34874af212c040ef90)) +* **packaging:** downgrade typescript to 3.7.5 ([10294fc](https://github.com/frontegg/frontegg-react/commit/10294fc3f6c2f5ade727d2e05070a978fe1c1cc7)) +* restore old react and checkout from release branch ([adbff2e](https://github.com/frontegg/frontegg-react/commit/adbff2e9b28248ae9d292b633fde4233b853a29c)) + + +### Features + +* **auth:** add SSO components ([#64](https://github.com/frontegg/frontegg-react/issues/64)) ([f083762](https://github.com/frontegg/frontegg-react/commit/f0837623073c1f9a636480c6a88d5969fb020a09)) +* **auth:** support all auth functionalities ([#70](https://github.com/frontegg/frontegg-react/issues/70)) ([26d725e](https://github.com/frontegg/frontegg-react/commit/26d725e2f386c6b4a703e4371fac8efef29676d1)) +* **core:** add dynamic elements for Frontegg Components ([#10](https://github.com/frontegg/frontegg-react/issues/10)) ([2bddbb2](https://github.com/frontegg/frontegg-react/commit/2bddbb2794ac0dc4af90c8df0f33b69a312e063a)) +* **core:** add FeSwitchToggle component ([#66](https://github.com/frontegg/frontegg-react/issues/66)) ([5b6c603](https://github.com/frontegg/frontegg-react/commit/5b6c603d912be45b1982c06b7f026badd8e82f1c)) +* **core:** Add FeTabs component ([3699299](https://github.com/frontegg/frontegg-react/commit/36992997e64907345a254a4b44580759705186bb)), closes [#67](https://github.com/frontegg/frontegg-react/issues/67) + + + + + +# [1.1.0](https://github.com/frontegg/frontegg-react/compare/v1.0.89...v1.1.0) (2020-10-14) + + +### Bug Fixes + +* **packaging:** add missing immer dependency ([#52](https://github.com/frontegg/frontegg-react/issues/52)) ([36c6c15](https://github.com/frontegg/frontegg-react/commit/36c6c1583809a532885e65a8c2c375151ad8b9dc)), closes [#51](https://github.com/frontegg/frontegg-react/issues/51) + + +### Features + +* **auth:** add accept invitation component by url ([#50](https://github.com/frontegg/frontegg-react/issues/50)) ([c3a43d6](https://github.com/frontegg/frontegg-react/commit/c3a43d60dad3fc8da9cffc6a81f468b5671d3af9)) +* **auth:** add Team (reducer/saga) to Auth Plugin ([7bed273](https://github.com/frontegg/frontegg-react/commit/7bed27378efe32c9e9091495d0ac4a3f268b206c)) +* **auth:** add TeamAPI to frontegg/react-core api.team collection ([600a8f8](https://github.com/frontegg/frontegg-react/commit/600a8f81a0322702d22dc2abede93d271d1c81f7)) + + + + + +## [1.0.89](https://github.com/frontegg/frontegg-react/compare/v1.0.88...v1.0.89) (2020-10-13) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +## [1.0.88](https://github.com/frontegg/frontegg-react/compare/v1.0.87...v1.0.88) (2020-10-11) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +## [1.0.87](https://github.com/frontegg/frontegg-react/compare/v1.0.86...v1.0.87) (2020-10-09) + + +### Bug Fixes + +* **packaging:** move cjs.js to index.js for main entry point ([0f8f700](https://github.com/frontegg/frontegg-react/commit/0f8f70016566a8f1940d16441a5afa1707dc02a2)) + + + + + +## 1.0.85 (2020-10-08) + +**Note:** Version bump only for package @frontegg/react-core + + + + + +## 1.0.84 (2020-10-08) + +**Note:** Version bump only for package @frontegg/react-core diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 000000000..f26bc1c36 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,94 @@ +{ + "name": "@frontegg/react-core", + "libName": "FronteggCore", + "version": "4.0.23", + "author": "Frontegg LTD", + "main": "dist/index.js", + "module": "dist/index.esm.js", + "es2015": "dist/index.es.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "rollup -c ../../scripts/rollup.config.js && echo DONE", + "build:watch": "rollup -w -c ../../scripts/rollup.config.js", + "test": "jest --runInBand --passWithNoTests -c ../../scripts/jest.config.json --rootDir . && echo DONE" + }, + "dependencies": { + "@frontegg/react-hooks": "5.64.4", + "classnames": "^2.2.6", + "formik": "^2.1.5", + "history": "^4.9.0", + "i18next": "^19.6.3", + "moment": "^2.27.0", + "owasp-password-strength-test": "^1.3.0", + "process": "^0.11.10", + "rc-dialog": "8.5.1", + "rc-util": "^5.2.1", + "react-i18next": "11.7.0", + "react-popper-tooltip": "^3.1.1", + "react-router-dom": "^5.1.2", + "react-select": "^3.1.0", + "react-table": "^7.5.1", + "react-waypoint": "^10.1.0", + "yup": "^0.28.3" + }, + "devDependencies": { + "@types/classnames": "^2.2.10", + "@types/history": "^4.7.7", + "@types/i18next": "^13.0.0", + "@types/moment": "^2.13.0", + "@types/node": "^13.9.1", + "@types/owasp-password-strength-test": "^1.3.0", + "@types/react": "^16.9.19", + "@types/react-dom": "^16.9.8", + "@types/react-i18next": "^8.1.0", + "@types/react-router-dom": "^5.1.2", + "@types/react-select": "^3.0.22", + "@types/react-table": "^7.0.23", + "react": ">16.8.6", + "react-dom": ">16.8.6" + }, + "jest": { + "preset": "ts-jest", + "testEnvironment": "jsdom", + "testPathIgnorePatterns": [ + "dist/" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "prettier": { + "printWidth": 120, + "semi": true, + "singleQuote": true, + "jsxSingleQuote": true, + "trailingComma": "all", + "jsxBracketSameLine": true, + "endOfLine": "lf", + "tabWidth": 2 + }, + "standard": { + "ignore": [ + "node_modules/", + "dist/" + ], + "globals": [ + "describe", + "it", + "test", + "expect", + "afterAll", + "jest" + ] + }, + "gitHead": "00c0bd425a211916eec1170b8359cf302727b338" +} diff --git a/packages/core/src/ElementsFactory/ElementsFactory.tsx b/packages/core/src/ElementsFactory/ElementsFactory.tsx new file mode 100644 index 000000000..b8177b121 --- /dev/null +++ b/packages/core/src/ElementsFactory/ElementsFactory.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { Elements, ElementType } from './interfaces'; + +import { FeButton } from '../elements/Button/FeButton'; +import { FeGrid } from '../elements/Grid/FeGrid'; +import { FeIcon } from '../elements/Icon/FeIcon'; +import { FeLoader } from '../elements/Loader/FeLoader'; +import { FePopup } from '../elements/Popup/FePopup'; +import { FeCheckbox } from '../elements/Checkbox/FeCheckbox'; +import { FeTag } from '../elements/Tag/FeTag'; +import { FeSelect } from '../elements/Select/FeSelect'; +import { FeAccordion } from '../elements/Accordion/FeAccordion'; +import { FeAccordionHeader } from '../elements/Accordion/FeAccordion'; +import { FeAccordionContent } from '../elements/Accordion/FeAccordion'; +import { FeInput } from '../elements/Input/FeInput'; +import { FeSwitchToggle } from '../elements/SwitchToggle/FeSwitchToggle'; +import { FeTable } from '../elements/Table/FeTable'; +import { FeForm } from '../elements/Form/FeForm'; +import { FeTabs } from '../elements/Tabs/FeTabs'; +import { FeMenu } from '../elements/Menu/FeMenu'; +import { FeMenuItem } from '../elements/MenuItem/FeMenuItem'; +import { FeDialog } from '../elements/Dialog/FeDialog'; +import { FeInputChip } from '../elements/InputChip/FeInputChip'; +import { FePagination } from '../elements/Pagination/FePagination'; + +export const fronteggElements: Elements = { + Accordion: FeAccordion, + AccordionHeader: FeAccordionHeader, + AccordionContent: FeAccordionContent, + Loader: FeLoader, + Tag: FeTag, + Button: FeButton, + Input: FeInput, + InputChip: FeInputChip, + Grid: FeGrid, + Icon: FeIcon, + Popup: FePopup, + Pagination: FePagination, + Checkbox: FeCheckbox, + Select: FeSelect, + SwitchToggle: FeSwitchToggle, + Table: FeTable, + Form: FeForm, + Tabs: FeTabs, + Menu: FeMenu, + MenuItem: FeMenuItem, + Dialog: FeDialog, +}; + +export class ElementsFactory { + private static instance: ElementsFactory; + private elements: Elements | null = null; + + private constructor() {} + + private static getInstance(): ElementsFactory { + if (!ElementsFactory.instance) { + ElementsFactory.instance = new ElementsFactory(); + } + return ElementsFactory.instance; + } + + public static setElements = (elements?: Partial<Elements>) => { + ElementsFactory.getInstance().elements = { + ...fronteggElements, + ...elements, + } as any; + }; + + public static getElement = <P extends ElementType>(type: P): Elements[P] => { + const { elements } = ElementsFactory.getInstance(); + if (!elements) { + throw Error('You must pass UI Library to FronteggProvider'); + } + + return elements[type]; + }; +} diff --git a/packages/core/src/ElementsFactory/FileInput.tsx b/packages/core/src/ElementsFactory/FileInput.tsx new file mode 100644 index 000000000..7675ca952 --- /dev/null +++ b/packages/core/src/ElementsFactory/FileInput.tsx @@ -0,0 +1,58 @@ +import React, { ChangeEvent, forwardRef, useCallback } from 'react'; +import { InputProps } from '../elements/Input'; +import { ElementsFactory } from './ElementsFactory'; +import { useField } from 'formik'; + +const toBase64 = (file?: File) => + new Promise<string>((resolve, reject) => { + if (file == null) { + resolve(''); + return; + } + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => resolve(reader.result?.toString() ?? ''); + reader.onerror = (error) => reject(error); + }); + +export type FileInputProps = InputProps & { + validation?: (value: File) => Promise<string | null>; +}; + +export const FileInput = forwardRef<HTMLInputElement, FileInputProps>((props, forwardRef) => + React.createElement(ElementsFactory.getElement('Input'), { ...props, ref: forwardRef } as any) +); +export const FFileInput = forwardRef<HTMLInputElement, FileInputProps & { name: string }>( + ({ validation, ...props }, forwardRef) => { + const { onChange } = props; + const [{ name }, {}, { setValue, setError }] = useField(props.name); + + const handlerOnChange = useCallback( + async (e: ChangeEvent<HTMLInputElement>) => { + e.persist(); + const selectedFile = e.target.files?.[0]; + + if (selectedFile) { + const errorMessage = (await validation?.(selectedFile)) ?? null; + if (errorMessage) { + setError(errorMessage); + return; + } + + const content = await toBase64(selectedFile); + setValue(content); + } else { + setValue(''); + } + onChange && onChange(e); + }, + [onChange, setValue, setError, name] + ); + + return ( + <span style={{ display: 'none' }}> + <FileInput {...props} ref={forwardRef} type='file' onChange={handlerOnChange} /> + </span> + ); + } +); diff --git a/packages/core/src/ElementsFactory/index.ts b/packages/core/src/ElementsFactory/index.ts new file mode 100644 index 000000000..7635cb20d --- /dev/null +++ b/packages/core/src/ElementsFactory/index.ts @@ -0,0 +1,6 @@ +import { ElementsFactory, fronteggElements } from './ElementsFactory'; + +export * from './interfaces'; +export * from './FileInput'; + +export { ElementsFactory, fronteggElements }; diff --git a/packages/core/src/ElementsFactory/interfaces.ts b/packages/core/src/ElementsFactory/interfaces.ts new file mode 100644 index 000000000..fd3bb4fcb --- /dev/null +++ b/packages/core/src/ElementsFactory/interfaces.ts @@ -0,0 +1,60 @@ +import React, { ComponentType, ForwardRefExoticComponent, PropsWithoutRef, RefAttributes } from 'react'; +import { Size } from '../styles'; +import { GridProps } from '../elements/Grid'; +import { ButtonProps } from '../elements/Button'; +import { InputProps } from '../elements/Input'; +import { IInputChip } from '../elements/InputChip'; +import { IconProps } from '../elements/Icon'; +import { PopupProps } from '../elements/Popup'; +import { LoaderProps } from '../elements/Loader'; +import { CheckboxProps } from '../elements/Checkbox'; +import { TagProps } from '../elements/Tag'; +import { TableProps } from '../elements/Table'; +import { SelectProps } from '../elements/Select'; +import { SwitchToggleProps } from '../elements/SwitchToggle'; +import { AccordionContentProps, AccordionHeaderProps, AccordionProps } from '../elements/Accordion'; +import { DialogProps } from '../elements/Dialog'; +import { FormProps } from '../elements/Form'; +import { TabProps } from '../elements/Tabs'; +import { MenuProps } from '../elements/Menu'; +import { MenuItemProps } from '../elements/MenuItem'; +import { PaginationProps } from '../elements/Pagination'; + +export type FormFieldProps = { + inForm?: boolean; // default: false + size?: Size; +}; + +type ComponentTypeOrForwardRef<P, REF> = + | ComponentType<P> + | ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<REF>>; + +export type ElementProps = { + Accordion: ComponentType<AccordionProps>; + AccordionHeader: ComponentType<AccordionHeaderProps>; + AccordionContent: ComponentType<AccordionContentProps>; + Button: ComponentTypeOrForwardRef<ButtonProps, HTMLButtonElement>; + Tag: ComponentType<TagProps>; + Input: ComponentType<InputProps>; + InputChip: ComponentType<IInputChip>; + Form: ComponentType<FormProps>; + Loader: ComponentType<LoaderProps>; + SwitchToggle: ComponentType<SwitchToggleProps>; + Icon: ComponentType<IconProps>; + Tabs: ComponentType<TabProps>; + Dialog: ComponentType<DialogProps>; + Checkbox: ComponentTypeOrForwardRef<CheckboxProps, HTMLInputElement>; + Grid: ComponentTypeOrForwardRef<GridProps, HTMLDivElement>; + Table: ComponentTypeOrForwardRef<TableProps, HTMLTableElement>; + Popup: ComponentTypeOrForwardRef<PopupProps, HTMLDivElement>; + Select: ComponentType<SelectProps>; + Menu: ComponentType<MenuProps>; + MenuItem: ComponentType<MenuItemProps>; + Pagination: ComponentType<PaginationProps>; +}; + +export type ElementType = keyof ElementProps; + +export type Elements = { + [type in ElementType]: ElementProps[type]; +}; diff --git a/packages/core/src/FronteggProvider.tsx b/packages/core/src/FronteggProvider.tsx new file mode 100644 index 000000000..0d9eec344 --- /dev/null +++ b/packages/core/src/FronteggProvider.tsx @@ -0,0 +1,143 @@ +import React, { FC, useEffect, useMemo, useRef } from 'react'; +import { Middleware, Reducer, EnhancedStore } from '@frontegg/redux-store/toolkit'; +import { Provider, FronteggStoreContext } from '@frontegg/react-hooks'; +import { I18nextProvider } from 'react-i18next'; +import { ContextOptions, ListenerProps, LogLevel } from './interfaces'; +import { i18n } from './I18nInitializer'; +import { BrowserRouter, useHistory, useLocation } from 'react-router-dom'; +import { Elements, ElementsFactory } from './ElementsFactory'; +import { ContextHolder, RedirectOptions } from '@frontegg/rest-api'; +import { createFronteggStore } from '@frontegg/redux-store'; +import { authStoreName } from '@frontegg/redux-store/auth'; + +const isSSR = typeof window === 'undefined'; + +export interface PluginConfig { + storeName: string; + reducer: Reducer; + sagas: () => void; + preloadedState: any; + Listener?: React.ComponentType<ListenerProps<any>>; + WrapperComponent?: React.ComponentType<any>; +} + +export interface FeProviderProps { + context: ContextOptions; + plugins: PluginConfig[]; + uiLibrary?: Partial<Elements>; + onRedirectTo?: (path: string, opts?: RedirectOptions) => void; + debugMode?: boolean; + storeMiddlewares?: Middleware[]; + store?: EnhancedStore; +} + +const FePlugins: FC<FeProviderProps> = (props) => { + const listeners = useMemo(() => { + return props.plugins + .filter((p) => p.Listener) + .map((p) => ({ storeName: p.storeName, Listener: p.Listener! })) + .map(({ storeName, Listener }, i) => <Listener key={storeName} />); + }, [props.plugins]); + + const children = useMemo(() => { + let combinedWrapper: any = props.children; + const wrappers = props.plugins.filter((p) => p.WrapperComponent).map((p) => p.WrapperComponent!); + wrappers.forEach((Wrapper) => (combinedWrapper = <Wrapper>{combinedWrapper}</Wrapper>)); + return combinedWrapper; + }, []); + + return ( + <> + {listeners} + {children} + </> + ); +}; + +const FeState: FC<FeProviderProps> = (props) => { + const history = useHistory(); + const storeRef = useRef<any>({}); + const location = useLocation(); + const baseName = isSSR + ? '' + : window.location.pathname.substring(0, window.location.pathname.lastIndexOf(location.pathname)); + + const onRedirectTo = + props.onRedirectTo ?? + ((_path: string, opts?: RedirectOptions) => { + let path = _path; + if (path.startsWith(baseName)) { + path = path.substring(baseName.length); + } + if (opts?.preserveQueryParams) { + path = `${path}${window.location.search}`; + } + if (opts?.refresh && !isSSR) { + window.Cypress ? history.push(path) : (window.location.href = path); + } else { + opts?.replace ? history.replace(path) : history.push(path); + } + }); + ContextHolder.setOnRedirectTo(onRedirectTo); + + const store = useMemo( + () => + props.store ?? + createFronteggStore( + { context: props.context }, + storeRef.current, + false, + { + ...(props.plugins?.find((n) => n.storeName === authStoreName)?.preloadedState ?? {}), + onRedirectTo, + }, + { + audits: { + context: props.context, + ...props.context.auditsOptions, + ...(props.plugins?.find((n) => n.storeName === 'audits')?.preloadedState ?? {}), + } as any, + } + ), + [props.store] + ); + + useEffect( + () => () => { + try { + (storeRef.current as any)?.store.destroy?.(); + } catch (e) {} + }, + [] + ); + + /* for Cypress tests */ + if (!isSSR && window.Cypress) { + window.cypressHistory = history; + } + + return ( + <Provider context={FronteggStoreContext} store={store}> + <I18nextProvider i18n={i18n}> + <FePlugins {...props} /> + </I18nextProvider> + </Provider> + ); +}; + +const defaultLogLevel: LogLevel = 'error'; + +export const FronteggProvider: FC<FeProviderProps> = (props) => { + ContextHolder.setContext({ ...props.context, logLevel: props.context.logLevel || defaultLogLevel }); + ElementsFactory.setElements(props.uiLibrary); + + const withRouter = !useHistory(); + if (withRouter) { + return ( + <BrowserRouter> + <FeState {...props} /> + </BrowserRouter> + ); + } + return <FeState {...props} />; +}; diff --git a/packages/core/src/HOCs.ts b/packages/core/src/HOCs.ts new file mode 100644 index 000000000..fde79e77a --- /dev/null +++ b/packages/core/src/HOCs.ts @@ -0,0 +1,4 @@ +import { withTranslation, WithTranslation } from 'react-i18next'; + +export type WithT = WithTranslation; +export { withTranslation as withT }; diff --git a/packages/core/src/I18nInitializer/en.ts b/packages/core/src/I18nInitializer/en.ts new file mode 100644 index 000000000..2b903b9b5 --- /dev/null +++ b/packages/core/src/I18nInitializer/en.ts @@ -0,0 +1,381 @@ +export default { + translation: { + common: { + switchTenant: 'Switch Tenant', + 'click-here': 'Click here', + 'date-of-birth': 'Date of Birth', + 'empty-items': 'No Items', + 'enter-email': 'Enter email', + 'enter-name': 'Enter name', + 'enter-descripiton': 'Your description', + 'instruction-for': 'Instruction for', + 'last-updated': 'Last updated', + 'not-configured': 'Not Configured', + continue: 'Continue', + active: 'Active', + automatic: 'Automatic', + back: 'Back', + cancel: 'Cancel', + channels: 'Channels', + clear: 'Clear', + close: 'Close', + configure: 'Configure', + configured: 'Configured', + copied: 'Copied!', + country: 'Country', + createdAt: 'Created At', + createdBy: 'Created By', + delete: 'Delete', + description: 'Description', + detail: 'Detail', + disable: 'Disable', + disabled: 'Disabled', + displayName: 'Display Name', + domain: 'Domain', + done: 'Done', + edit: 'Edit', + email: 'Email', + emails: 'Emails', + enable: 'Enable', + enabled: 'Enabled', + events: 'Events', + failed: 'Failed', + filter: 'Filter', + finish: 'Finish', + install: 'Install', + instruction: 'Instruction', + invite: 'Invite', + invocations: 'Invocations', + joinedTeam: 'Joined Team', + lastLogin: 'Last Login', + loading: 'Loading', + logDetails: 'Log details', + logs: 'Logs', + manual: 'Manual', + me: '(Me)', + message: 'Message', + name: 'Name', + next: 'Next', + notFound: 'Data not found', + notFoundUser: 'User not found', + noResults: 'No Results Found', + password: 'Password', + pending: 'Pending', + pendingApproval: 'Pending Approval', + permissions: 'Permissions', + phones: 'Phones', + platform: 'Platform', + proceed: 'Proceed', + remove: 'Remove', + roles: 'Roles', + save: 'Save', + search: 'Search', + clientId: 'Client Id', + secretKey: 'Secret Key', + select: 'Select', + selected: 'Selected', + sms: 'SMS', + status: 'Status', + step: 'Step {{num}}', + success: 'Success', + title: 'Title', + validate: 'Validate', + validated: 'Validated', + verify: 'Verify', + yourself: 'yourself', + optional: 'Optional', + more: 'more', + payload: 'payload', + }, + auth: { + login: { + login: 'Login', + continue: 'Continue', + email: 'Email', + password: 'Password', + 'enter-your-password': 'Enter Your Password', + 'forgot-password': 'Forgot Password?', + 'authentication-succeeded': 'Authentication Succeeded', + 'recover-multi-factor': 'Recover Multi-Factor', + 'please-enter-the-6-digit-code': 'Please enter the 6 digit code from your authenticator app', + 'please-enter-the-recovery-code': 'Please enter your MFA recovery code', + 'disable-mfa': 'Disable MFA', + 'redirect-to-sso-message': 'Being redirected to your SSO provider...', + 'disable-two-factor-title': 'Having trouble?', + 'disable-two-factor-description': 'to disable Multi-Factor with recovery code', + 'back-to-login': 'Back to login', + 'login-with-sso-failed': 'Failed to Login with SSO, try again later.', + 'suggest-sign-up': { + message: 'Dont have an account? ', + 'sign-up-link': 'Sign up.', + }, + }, + 'forgot-password': { + 'email-label': 'Enter your email', + 'remind-me': 'Remind Me', + 'password-has-been-changed': 'Your password has been changed', + 'reset-password-failed-title': 'Reset Password Failed', + 'reset-password-failed-description': 'Please double check your reset url', + 'back-to-login': 'Back to login', + 'reset-email-sent': 'A password reset email has been sent to your registered email address', + + 'new-password': 'New password', + 'enter-your-password': 'Enter your password', + 'confirm-new-password': 'Confirm New password', + 'enter-your-password-again': 'Enter your password again', + 'reset-password-button': 'Reset Password', + }, + 'activate-account': { + 'failed-title': 'Activation failed', + 'failed-description': 'Please double check your activation url', + 'back-to-login': 'Back to login', + 'activation-succeeded': 'Activation Succeeded', + 'new-password': 'New password', + 'enter-your-password': 'Enter your password', + 'confirm-new-password': 'Confirm New password', + 'enter-your-password-again': 'Enter your password again', + 'activate-account-button': 'Activate', + 'ask-for-new-activation-link': 'Ask for new activation link', + 'request-sent': 'Request sent ', + send: 'send', + }, + account: { + 'invalid-title': 'Invalid link', + 'invalid-description': 'Please double check your link', + 'failed-title': 'Authorization Failed', + 'failed-description': 'We were unable to authorize you', + 'success-title': 'Authorize successfully!', + 'pending-title': 'Please wait while we authorize you...', + }, + sso: { + title: 'Single Sign On', + subtitle: 'Configure single-sign-on with your own Identity Provider', + overview: { + 'enable-sso-message': 'Enable SSO and configure the settings to quickly use this functionality', + 'claim-domain': 'Claim Domain', + 'configure-your-idp': 'Configure Your IDP', + 'manage-authorization': 'Manage Authorization', + }, + 'go-to-idp': 'Configure IDP', + 'claim-domain': { + guide: { + title: 'Bullets', + description: 'Helpful information explaining the process', + 'steps-0': 'Enter your domain name', + 'steps-1': 'Click proceed', + 'steps-2': 'Copy the TXT value to your DNS', + 'steps-3': 'Click "Validate', + }, + form: { + title: 'Claim domain', + 'enter-your-domain': 'Enter your Domain', + 'copy-info-to-txt-record': 'Copy this info into a new TXT record in your DNS file:', + 'record-name': 'Record Name', + 'record-value': 'Record Value', + 'validate-error': + 'Validation did not succeed, please notice that DNS records might take some time to update. Please try again.', + }, + }, + idp: { + 'error-ask-your-vendor': 'Ask your vendor to configure SSO before!', + guide: { + title: 'Bullets', + description: 'Helpful information explaining the process', + 'steps-0': 'Create an entry for the application on the IDP', + 'steps-1': 'Download the IDP federation metadata XML', + 'steps-2': 'Click on next to step 2', + 'steps-3': 'Upload the IDP metadata XML', + 'steps-4': 'Click "Configure"', + 'step-by-step': 'Detailed step by step', + oidc: { + 'steps-0': 'Create an entry for the application on the IDP', + 'steps-1': 'Enter ASC URL', + 'steps-2': 'Click on next to step 2', + 'steps-3': 'Enter Client Id', + 'steps-4': 'Enter Secret Key', + 'steps-5': 'Click "Configure"', + 'step-by-step': 'Detailed step by step', + }, + }, + select: { + title: 'Select your IDP', + }, + form: { + title: 'Configure Your IDP', + 'acs-url': 'ACS URL', + 'entity-id': 'Entity ID', + 'metadata-file': 'Metadata File', + endpoint: 'SSO Endpoint', + certificate: 'Public Certificate', + 'endpoint-desc': 'URL from the SSO', + 'certificate-desc': 'Provide Public Certificate', + }, + }, + authorization: { + title: 'Manage Authorization', + subtitle: 'Select default roles which will be assigned to each user who authenticates using SSO.', + }, + }, + dropzone: { + title: 'Metadata File', + dnd: 'Drag & Drop', + description: 'Click or drop an XML file with your configurations', + }, + profile: { + title: 'My Profile', + info: { + title: 'Basic Info', + title2: 'Basic Information', + 'upload-photo': 'Upload Photo', + 'upload-photo-note': 'At least 512x512px PNG or JPEG file', + 'invalid-profile-photo': 'Profile Photo must be at least 512x512px', + 'user-title': 'Title', + 'user-name': 'Display Name', + }, + 'password-settings': { + title: 'Change Password', + button: 'Change Password', + 'success-message': 'Your password have been changed!', + }, + }, + mfa: { + title: 'Multi-factor Authentication', + 'two-factor': 'Two-factor authentication', + 'enable-message': 'Enable two-factor authentication to get an extra layer of security', + 'disable-title': 'Disable two-factor authentication', + 'enroll-button': 'Enroll MFA', + 'remember-this-device': `Don't ask again on this device for {{count}} day`, + 'remember-this-device_plural': `Don't ask again on this device for {{count}} days`, + 'disable-button': 'Disable MFA', + verify: { + message: 'Enable two-factor to get an extra layer of security.', + forceMfaMessage: 'Multi-Factor authentication is required in order to access the account.', + 'scan-qr-description-1': `Use your phone to scan the following QR code with `, + 'scan-qr-description-2': ` or other authenticator apps.`, + 'enter-generated-code': 'Enter the generated 6-digit code below.', + }, + 'recovery-code': { + message: 'Recovery code can be used to disable two-factor authentication in case you lose your phone.', + 'your-code': 'Your recovery code', + 'copy-and-save-code': `Copy and save the code, because we won't show it again.`, + }, + disable: { + message: 'Disable two-factor will remove an extra layer of security.', + 'enter-generated-code': 'Enter the generated 6-digit code', + }, + }, + apiTokens: { + title: 'API Tokens', + subtitle: 'Connect your own APIs to important event notifications', + addNewToken: 'Add new token', + modal: { + title: 'New API token', + successDescription: 'Your API key was succesfully generated!', + subtitleTenant: 'Create a new API token and assign relevant roles', + subtitleUser: 'Create a new API token', + description: 'Description', + permissions: 'Permissions', + create: 'Create', + tip: `Copy and save the ID and Secret below because we won't show it again.`, + }, + deleteModal: { + title: 'Delete Api Token', + message: 'Are you sure you want to permanently delete the selected API token?', + }, + }, + team: { + title: 'Team Management', + subtitle: 'Total of {{totalItems}} team members', + 'invite-user': 'Invite User', + 'search-users': 'Search by any text', + resendActivation: 'Resend activation email', + deleteUser: 'Delete user', + leaveTeam: 'Leave Team', + 'add-dialog': { + title: 'Invite New Teammate', + }, + deleteDialog: { + title: 'Delete team member', + message: "You are about to remove '{{email}}' from this account. Are you sure?", + }, + }, + 'social-logins': { + login: { + 'button-text': 'Login with {{providerName}}', + }, + signup: { + 'button-text': 'Sign up with {{providerName}}', + }, + error: { + 'invalid-callback-url': 'Invalid callback url', + }, + }, + 'sign-up': { + form: { + name: 'Name', + 'submit-button': 'Sign Up', + email: 'Email', + 'company-name': 'Company Name', + 'terms-error': 'You must accept our Terms of Service to proceed', + 'marketing-error': 'You must allow Marketing Material to proceed', + }, + 'suggest-login': { + message: 'Already have an account? ', + 'login-link': 'Log in.', + }, + success: { + title: 'Thanks for signing up!', + 'activate-message': 'Please check your inbox in order to activate your account.', + 'go-to-login-message': 'You have signed up successfully. Click on the button below to continue.', + 'go-to-login': 'Go to login', + }, + }, + }, + validation: { + 'must-be-a-valid-email': 'Must be a valid email', + 'must-be-a-valid-domain': 'Must be a valid domain', + 'must-be-a-valid-json': 'Must be a valid JSON object. {"key": "value"}', + 'must-be-a-valid-url': 'Must be a valid URL', + 'passwords-must-match': 'Passwords must match', + 'required-field': 'The {{name}} is required', + 'min-length': '{{name}} must be at least {{limit}} characters', + 'max-length': '{{name}} must be up to {{limit}} characters', + length: '{{name}} must be {{limit}} characters', + 'invalid-phone': 'Invalid phone number', + }, + reports: { + 'list-page': { + title: 'Reports', + subtitle: 'Generate insights on your account usage', + }, + }, + connectivity: { + addHook: 'Create Hook', + addNewHook: 'Add new hook', + deleteWebhook: 'Delete webhook', + enterEmail: 'Enter an email', + enterPhone: 'Enter a phone number', + eventSettings: 'Event settings', + generalSettings: 'General settings', + headerSubTitle: 'Connect your own APIs to important event notifications', + headerTitle: 'Connectivity', + inputName: 'Input name...', + manageCategories: 'Manage categories', + queryDeleteWebhook: 'Are you sure what you want delete the "{{name}}" webhook?', + secretKeyHelp: 'JWT signed with this secret will be sent in the header "x-webhook-secret" of the webhook request', + selectAll: 'SELECT ALL IN THE {{name}} CATEGORY', + selectEvents: 'Select events', + shortDescription: 'Add short description', + slack: 'Slack', + sms: 'SMS', + testHook: 'Test hook', + updateHook: 'Update Hook', + webhook: 'Webhooks', + json: 'JSON payload', + recipients: { + wrongEmail: 'One of the email addresses is wrong.', + wrongPhone: 'One of the phone numbers is wrong.', + }, + }, + }, +}; diff --git a/packages/core/src/I18nInitializer/index.ts b/packages/core/src/I18nInitializer/index.ts new file mode 100644 index 000000000..4347237ea --- /dev/null +++ b/packages/core/src/I18nInitializer/index.ts @@ -0,0 +1,18 @@ +import i18n, { Resource } from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import en from './en'; + +const resources: Resource = { en }; + +i18n + .use(initReactI18next) // passes i18n down to react-i18next + .init({ + resources, + lng: 'en', + + interpolation: { + escapeValue: false, // react already safes from xss + }, + }); + +export { i18n }; diff --git a/packages/core/src/components/PageHeader/PageHeader.tsx b/packages/core/src/components/PageHeader/PageHeader.tsx new file mode 100644 index 000000000..8039bd23b --- /dev/null +++ b/packages/core/src/components/PageHeader/PageHeader.tsx @@ -0,0 +1,52 @@ +import React, { FC, ReactElement, ReactNode, useState } from 'react'; +import classNames from 'classnames'; +import { Icon } from '../../elements/Icon'; +import { ProxyComponent, useProxyComponent } from '../../ngSupport'; + +export interface PageHeaderProps extends ProxyComponent { + className?: string; + title?: ReactNode | string; + titleClassName?: string; + subTitle?: string | ReactElement; + childClassName?: string; + onBackButtonClick?: (e: React.MouseEvent) => void; + centerChildren?: ReactNode; +} + +export const PageHeader: FC<PageHeaderProps> = (props) => { + const { + className, + onBackButtonClick, + title, + titleClassName, + subTitle, + children, + childClassName, + centerChildren, + } = props; + + const proxyPortals = useProxyComponent(props); + return ( + <div className={classNames('fe-page-header', className)}> + <div className='fe-left'> + <div className={classNames(titleClassName, 'fe-title', { 'fe-title__back-button': onBackButtonClick })}> + <span + onClick={onBackButtonClick} + className={classNames('fe-back-button', { + 'mt-2': subTitle, + visible: onBackButtonClick, + })} + > + <Icon name='back' /> + </span> + {title} + {subTitle && <div className='fe-subtitle'>{subTitle}</div>} + </div> + </div> + {centerChildren} + {children && <div className={classNames('fe-right', childClassName)}>{children}</div>} + + {proxyPortals} + </div> + ); +}; diff --git a/packages/core/src/components/PageHeader/index.tsx b/packages/core/src/components/PageHeader/index.tsx new file mode 100644 index 000000000..a4d3e95d1 --- /dev/null +++ b/packages/core/src/components/PageHeader/index.tsx @@ -0,0 +1,3 @@ +import './style.scss'; + +export * from './PageHeader'; diff --git a/packages/core/src/components/PageHeader/style.scss b/packages/core/src/components/PageHeader/style.scss new file mode 100644 index 000000000..c01bfd0d1 --- /dev/null +++ b/packages/core/src/components/PageHeader/style.scss @@ -0,0 +1,144 @@ +:root { + --fe-header-padding: 2rem; + --fe-header-font-size: 1.5rem; + --fe-header-font-color: #323233; + --fe-header-font-weight: bold; + --fe-header-sub-title-color: #a0a0a0; + --fe-header-sub-title-font-weight: normal; + --fe-header-back-button-color: #a0a0a0; + --fe-header-hover-bg: rgba(75, 75, 75, 0.1); + --fe-header-tab-font-size: 0.9rem; + --fe-header-tab-font-color: #99a6b9; + --fe-header-tab-font-weight: 400; +} + +.fe-page-header { + width: 100%; + padding: var(--fe-header-padding); + font-size: var(--fe-header-font-size); + color: var(--fe-header-font-color); + display: flex; + flex-direction: row; + box-sizing: border-box; + border-bottom: 1px solid #eee; + line-height: 1; + min-height: 90px; + //margin-bottom: 10px; + + .fe-left { + display: flex; + flex-direction: column; + justify-content: space-around; + + .fe-title { + text-align: left; + position: relative; + font-size: 1em; + line-height: 1em; + font-weight: var(--fe-header-font-weight); + transition: all 0.3s ease-out; + } + + .fe-title__back-button { + padding-left: 2.8rem; + } + + .fe-subtitle { + font-size: 0.6em; + margin-top: 0.5rem; + color: var(--fe-header-sub-title-color); + font-weight: var(--fe-header-sub-title-font-weight); + display: block; + line-height: 1; + } + } + + .fe-right { + margin: -0.5rem 0; + display: flex; + flex-direction: row; + flex: 1; + justify-content: flex-end; + } + + &.fe-page-header__with-tabs { + padding-bottom: 0; + + .ui.menu { + margin-bottom: 1px; + margin-top: 1rem; + + .item { + border-bottom-width: 1px; + padding: 0.8rem 0; + margin: -1px 1rem; + font-size: var(--fe-header-tab-font-size); + color: var(--fe-header-tab-font-color); + font-weight: var(--fe-header-tab-font-weight); + + &:first-child { + margin-left: 0; + } + + &.active { + color: #0baf60; + border-bottom: 1px solid #0baf60; + + &:hover { + color: #0a9c56; + border-bottom: 1px solid #0a9c56; + } + } + } + } + } +} +.fe-back-button { + position: absolute; + left: 0; + top: 0; + bottom: 0; + margin: auto; + opacity: 0; + width: 0; + overflow: hidden; + height: 2rem; + text-align: center; + border-radius: 0.3rem; + box-sizing: border-box; + cursor: pointer; + transition: all 0.3s ease-out; + + &.visible { + width: 2rem; + opacity: 1; + overflow: initial; + } + + .fe-icon { + color: var(--fe-header-back-button-color); + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + margin: auto; + transition: fill 300ms ease-out; + } + + &.fe-block { + display: inline-block; + width: 2rem; + position: relative; + opacity: 1; + margin: 0 1rem; + } + + &:hover { + background: var(--fe-header-hover-bg); + + svg { + fill: var(--fe-header-font-color); + } + } +} diff --git a/packages/core/src/components/PageTabs/PageTabs.tsx b/packages/core/src/components/PageTabs/PageTabs.tsx new file mode 100644 index 000000000..a38a5f287 --- /dev/null +++ b/packages/core/src/components/PageTabs/PageTabs.tsx @@ -0,0 +1,44 @@ +import React, { ComponentType, FC, useMemo, useState } from 'react'; +import { matchPath, useLocation } from 'react-router-dom'; +import { ContextHolder } from '@frontegg/rest-api'; +import { Tabs } from '../../elements/Tabs'; + +export type PageTabProps = { + Title: ComponentType; + route: string; + comp?: ComponentType; + disabled?: boolean; +}; + +export type PageProps<T = {}> = FC<T> & Omit<PageTabProps, 'comp'>; + +export type TabsProps = { + tabs: PageTabProps[]; +}; + +const findMatchPath = (pathname: string, tabs: { route: string }[]) => { + const activeTab = tabs.findIndex(({ route }) => matchPath(pathname, { path: route, exact: true })); + return activeTab === -1 ? 0 : activeTab; +}; +export const PageTabs: FC<TabsProps> = (props) => { + const location = useLocation(); + const firstMatch = useMemo(() => findMatchPath(location.pathname, props.tabs), []); + const [activeTab, setActiveTab] = useState(firstMatch); + const items = useMemo( + () => props.tabs.map(({ Title, disabled }) => ({ Title: React.createElement(Title), disabled })), + [props.tabs] + ); + + return ( + <div className='fe-tabs'> + <Tabs + items={items} + activeTab={activeTab} + onTabChange={(_event, activeTab) => { + ContextHolder.onRedirectTo(props.tabs[activeTab].route, { replace: true }); + setActiveTab(activeTab); + }} + /> + </div> + ); +}; diff --git a/packages/core/src/components/PageTabs/index.ts b/packages/core/src/components/PageTabs/index.ts new file mode 100644 index 000000000..f92ec840b --- /dev/null +++ b/packages/core/src/components/PageTabs/index.ts @@ -0,0 +1,39 @@ +import { Children, ReactNode } from 'react'; +import * as ReactIs from 'react-is'; +import { PageTabProps } from './PageTabs'; + +export * from './PageTabs'; + +const joinPaths = (root: string, path: string) => { + const p1 = root.endsWith('/') ? root.substring(0, root.length - 1) : root; + const p2 = path.startsWith('/') ? path.substring(1) : path; + return `${p1}/${p2}`; +}; + +export const buildTabsFromChildren = (rootPath: string, children?: ReactNode): [PageTabProps[], string[]] => { + const invalidTabs: string[] = []; + let components: any = children; + if (!children) { + return [[], invalidTabs]; + } + if (ReactIs.isFragment(children)) { + components = components.props.children; + return buildTabsFromChildren(rootPath, components); + } + const tabs: PageTabProps[] = []; + + Children.forEach(components, (child: any, i) => { + if (typeof child !== 'object') { + return null; + } + if (!child.type.Title || !child.type.route) { + invalidTabs.push(`${i}`); + } + tabs.push({ + Title: child.type.Title ?? (() => `Tab ${tabs.length + 1}`), + route: joinPaths(rootPath, child.type.route ?? (i === 0 ? rootPath : `/tab-${tabs.length + 1}`)), + comp: child, + }); + }); + return [tabs, invalidTabs]; +}; diff --git a/packages/core/src/components/TableCells.tsx b/packages/core/src/components/TableCells.tsx new file mode 100644 index 000000000..2348dfa57 --- /dev/null +++ b/packages/core/src/components/TableCells.tsx @@ -0,0 +1,27 @@ +import React, { FC } from 'react'; +import moment from 'moment'; +import { CellProps } from 'react-table'; + +type TableCellsType = { + [key in string]: FC<CellProps<any>>; +}; + +export const TableCells: TableCellsType = { + Avatar: ({ value }) => { + return <img className='fe-table-cell__avatar-img' src={value} alt='Image' />; + }, + Title: ({ value }) => { + return <div className='fe-table-cell__title'>{value ?? ''}</div>; + }, + Description: ({ value }) => { + return <div className='fe-table-cell__description'>{value ?? ''}</div>; + }, + DateAgo: ({ value }) => { + return ( + <div className='fe-table-cell__date-ago'> + <div>{value ? moment.utc(value).local().format('dddd, LL H:mm') : 'N/A'}</div> + {value && <div>{moment.utc(value).local().fromNow()}</div>} + </div> + ); + }, +}; diff --git a/packages/core/src/components/index.ts b/packages/core/src/components/index.ts new file mode 100644 index 000000000..d0551cd7e --- /dev/null +++ b/packages/core/src/components/index.ts @@ -0,0 +1,3 @@ +export * from './PageHeader'; +export * from './PageTabs'; +export * from './TableCells'; diff --git a/packages/core/src/elements/Accordion/Accordion.tsx b/packages/core/src/elements/Accordion/Accordion.tsx new file mode 100644 index 000000000..554463ec2 --- /dev/null +++ b/packages/core/src/elements/Accordion/Accordion.tsx @@ -0,0 +1,15 @@ +import React, { forwardRef } from 'react'; +import { ElementsFactory } from '../../ElementsFactory'; +import { AccordionContentProps, AccordionHeaderProps, AccordionProps } from './interfaces'; + +export const Accordion = forwardRef<HTMLDivElement, AccordionProps>((props, ref) => + React.createElement(ElementsFactory.getElement('Accordion'), { ...props, ref } as any) +); + +export const AccordionHeader = forwardRef<HTMLDivElement, AccordionHeaderProps>((props, ref) => + React.createElement(ElementsFactory.getElement('AccordionHeader'), { ...props, ref } as any) +); + +export const AccordionContent = forwardRef<HTMLDivElement, AccordionContentProps>((props, ref) => + React.createElement(ElementsFactory.getElement('AccordionContent'), { ...props, ref } as any) +); diff --git a/packages/core/src/elements/Accordion/FeAccordion/FeAccordion.scss b/packages/core/src/elements/Accordion/FeAccordion/FeAccordion.scss new file mode 100644 index 000000000..8259327de --- /dev/null +++ b/packages/core/src/elements/Accordion/FeAccordion/FeAccordion.scss @@ -0,0 +1,16 @@ +@import '../../../styles/mixin.scss'; + +.fe-accordion { + width: 100%; + box-shadow: var(--shadow-1); + display: flex; + flex-direction: column; + margin: 0; + transition: margin 0.3s ease; + + @include with-theme; + + &-expanded { + margin: var(--element-spacing) 0; + } +} diff --git a/packages/core/src/elements/Accordion/FeAccordion/FeAccordion.tsx b/packages/core/src/elements/Accordion/FeAccordion/FeAccordion.tsx new file mode 100644 index 000000000..3c5445a11 --- /dev/null +++ b/packages/core/src/elements/Accordion/FeAccordion/FeAccordion.tsx @@ -0,0 +1,30 @@ +import React, { forwardRef, useState } from 'react'; +import { ClassNameGenerator } from '../../../styles'; +import { AccordionProps } from '../interfaces'; +import { FeAccordionContext } from './FeAccordionContext'; +import './FeAccordion.scss'; + +const prefixCls = 'fe-accordion'; +export const FeAccordion = forwardRef<HTMLDivElement, AccordionProps>((props, ref) => { + const [innerExpanded, setInnerExpanded] = useState(false); + const { className, children, disabled, expanded: expandedFromProps, onChange, ...rest } = props; + + const expanded = expandedFromProps ?? innerExpanded; + + const classes = ClassNameGenerator.generate( + { + prefixCls, + className, + theme: props.disabled ? 'disabled' : undefined, + }, + expanded && 'expanded' + ); + + return ( + <FeAccordionContext.Provider value={{ expanded, setExpanded: onChange ?? setInnerExpanded }}> + <div ref={ref} className={classes} {...rest}> + {children} + </div> + </FeAccordionContext.Provider> + ); +}); diff --git a/packages/core/src/elements/Accordion/FeAccordion/FeAccordionContent.scss b/packages/core/src/elements/Accordion/FeAccordion/FeAccordionContent.scss new file mode 100644 index 000000000..71cb30a68 --- /dev/null +++ b/packages/core/src/elements/Accordion/FeAccordion/FeAccordionContent.scss @@ -0,0 +1,6 @@ +.fe-accordion-content { + overflow: hidden; + padding: 0 var(--element-padding); + max-height: 0; + transition: all 0.3s ease; +} diff --git a/packages/core/src/elements/Accordion/FeAccordion/FeAccordionContent.tsx b/packages/core/src/elements/Accordion/FeAccordion/FeAccordionContent.tsx new file mode 100644 index 000000000..c1127861f --- /dev/null +++ b/packages/core/src/elements/Accordion/FeAccordion/FeAccordionContent.tsx @@ -0,0 +1,30 @@ +import React, { FC, useContext, useLayoutEffect, useRef } from 'react'; +import { ClassNameGenerator } from '../../../styles'; +import { AccordionContentProps } from '../interfaces'; +import { FeAccordionContext } from './FeAccordionContext'; +import './FeAccordionContent.scss'; + +const prefixCls = 'fe-accordion-content'; +export const FeAccordionContent: FC<AccordionContentProps> = (props) => { + const ref = useRef<HTMLDivElement>(null); + + const { expanded } = useContext(FeAccordionContext); + const { className, children, ...rest } = props; + + const classes = ClassNameGenerator.generate({ + prefixCls, + className, + }); + + useLayoutEffect(() => { + if (!ref.current) return; + + ref.current.style.maxHeight = expanded ? `${ref.current.scrollHeight}px` : ''; + }, [expanded]); + + return ( + <div ref={ref} className={classes} {...rest}> + {children} + </div> + ); +}; diff --git a/packages/core/src/elements/Accordion/FeAccordion/FeAccordionContext.ts b/packages/core/src/elements/Accordion/FeAccordion/FeAccordionContext.ts new file mode 100644 index 000000000..4571412dd --- /dev/null +++ b/packages/core/src/elements/Accordion/FeAccordion/FeAccordionContext.ts @@ -0,0 +1,8 @@ +import { createContext } from 'react'; + +const defaultContext = { + expanded: false, + setExpanded: (expanded: boolean) => {}, +}; + +export const FeAccordionContext = createContext(defaultContext); diff --git a/packages/core/src/elements/Accordion/FeAccordion/FeAccordionHeader.scss b/packages/core/src/elements/Accordion/FeAccordion/FeAccordionHeader.scss new file mode 100644 index 000000000..259fc4cab --- /dev/null +++ b/packages/core/src/elements/Accordion/FeAccordion/FeAccordionHeader.scss @@ -0,0 +1,25 @@ +.fe-accordion-header { + $wrapper-class: &; + padding: var(--element-spacing); + display: flex; + cursor: pointer; + transition: padding 0.3s ease; + + &__icon { + display: flex; + align-items: center; + transition: transform 0.3s ease; + + svg { + height: 1rem; + } + } + + &-expanded { + padding: var(--element-padding) var(--element-spacing) var(--element-spacing); + + & #{$wrapper-class}__icon { + transform: rotate(180deg); + } + } +} diff --git a/packages/core/src/elements/Accordion/FeAccordion/FeAccordionHeader.tsx b/packages/core/src/elements/Accordion/FeAccordion/FeAccordionHeader.tsx new file mode 100644 index 000000000..948666040 --- /dev/null +++ b/packages/core/src/elements/Accordion/FeAccordion/FeAccordionHeader.tsx @@ -0,0 +1,28 @@ +import React, { forwardRef, useCallback, useContext } from 'react'; +import { ClassNameGenerator } from '../../../styles'; +import { AccordionHeaderProps } from '../interfaces'; +import { FeAccordionContext } from './FeAccordionContext'; +import './FeAccordionHeader.scss'; + +const prefixCls = 'fe-accordion-header'; +export const FeAccordionHeader = forwardRef<HTMLDivElement, AccordionHeaderProps>((props, ref) => { + const { expanded, setExpanded } = useContext(FeAccordionContext); + const { className, children, expandIcon, ...rest } = props; + + const classes = ClassNameGenerator.generate( + { + prefixCls, + className, + }, + expanded && 'expanded' + ); + + const toggleExpanded = useCallback(() => setExpanded(!expanded), [expanded, setExpanded]); + + return ( + <div ref={ref} className={classes} onClick={toggleExpanded} {...rest}> + {children} + <div className={`${prefixCls}__icon`}>{expandIcon}</div> + </div> + ); +}); diff --git a/packages/core/src/elements/Accordion/FeAccordion/index.ts b/packages/core/src/elements/Accordion/FeAccordion/index.ts new file mode 100644 index 000000000..d2bb77d95 --- /dev/null +++ b/packages/core/src/elements/Accordion/FeAccordion/index.ts @@ -0,0 +1,3 @@ +export * from './FeAccordion'; +export * from './FeAccordionHeader'; +export * from './FeAccordionContent'; diff --git a/packages/core/src/elements/Accordion/index.ts b/packages/core/src/elements/Accordion/index.ts new file mode 100644 index 000000000..3e524a8dd --- /dev/null +++ b/packages/core/src/elements/Accordion/index.ts @@ -0,0 +1,2 @@ +export * from './interfaces'; +export * from './Accordion'; diff --git a/packages/core/src/elements/Accordion/interfaces.ts b/packages/core/src/elements/Accordion/interfaces.ts new file mode 100644 index 000000000..0b3dfbc0a --- /dev/null +++ b/packages/core/src/elements/Accordion/interfaces.ts @@ -0,0 +1,14 @@ +import React, { ReactNode } from 'react'; + +export interface AccordionProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> { + children: NonNullable<ReactNode>; + disabled?: boolean; + expanded?: boolean; + onChange?: (expended: boolean) => void; +} + +export interface AccordionHeaderProps extends React.HTMLAttributes<HTMLDivElement> { + expandIcon?: ReactNode; +} + +export interface AccordionContentProps extends React.HTMLAttributes<HTMLDivElement> {} diff --git a/packages/core/src/elements/Button/Button.tsx b/packages/core/src/elements/Button/Button.tsx new file mode 100644 index 000000000..4c61c0897 --- /dev/null +++ b/packages/core/src/elements/Button/Button.tsx @@ -0,0 +1,25 @@ +import React, { forwardRef } from 'react'; +import { useFormikContext } from 'formik'; +import { ElementsFactory } from '../../ElementsFactory'; +import { ButtonProps } from './interfaces'; + +export const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => + React.createElement(ElementsFactory.getElement('Button'), { ...props, ref } as any) +); + +export const FButton = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => { + const { isValid, dirty } = useFormikContext(); + const { formikDisableIfNotDirty, disabled, ...restProps } = props; + const _disabled = disabled || !isValid || ((formikDisableIfNotDirty ?? true) && !dirty) || props.loading; + return ( + <Button + data-test-id='disabled-btn' + ref={ref} + inForm + {...restProps} + disabled={_disabled} + size={props.size ?? props.type === 'submit' ? 'large' : undefined} + fullWidth={props.fullWidth ?? true} + /> + ); +}); diff --git a/packages/core/src/elements/Button/FeButton.scss b/packages/core/src/elements/Button/FeButton.scss new file mode 100644 index 000000000..9a60ad846 --- /dev/null +++ b/packages/core/src/elements/Button/FeButton.scss @@ -0,0 +1,80 @@ +@import '../../styles/mixin.scss'; + +.fe-icon-button { + border: unset !important; +} + +.fe-button { + position: relative; + background: var(--color-white); + height: var(--element-height); + cursor: pointer; + font-family: var(--element-font-family); + font-size: var(--element-font-size); + font-stretch: normal; + font-style: normal; + line-height: normal; + padding: 0 var(--element-padding); + vertical-align: middle; + border-radius: var(--element-border-radius-sm); + box-shadow: none; + border: 1px solid var(--element-border-color); + background-color: var(--color-white); + text-decoration: none; + transition: all 0.1s ease-out; + outline: none; + overflow: hidden; + color: var(--color-gray-8); + + @include with-clickable; + @include with-theme; + @include with-full-width; + + @include with-size { + border-radius: var(--element-border-radius-sm); + } + + &-transparent { + background-color: transparent; + border-color: transparent; + } + + &-loader { + color: transparent !important; + text-shadow: none !important; + + .fe-loader { + position: absolute; + width: var(--element-height-sm); + height: var(--element-height-sm); + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + visibility: visible; + } + + & > *:not(.fe-loader) { + visibility: hidden; + } + } + + & > svg { + padding: 0 var(--element-padding) / 2; + vertical-align: bottom; + width: 1rem; + height: 1rem; + box-sizing: initial; + + &:first-child { + padding: 0 var(--element-padding) 0 0; + } + + &:last-child { + padding: 0 0 0 var(--element-padding); + } + + &:first-child:last-child { + padding: 0; + } + } +} diff --git a/packages/core/src/elements/Button/FeButton.tsx b/packages/core/src/elements/Button/FeButton.tsx new file mode 100644 index 000000000..c9e1c1daf --- /dev/null +++ b/packages/core/src/elements/Button/FeButton.tsx @@ -0,0 +1,51 @@ +import React, { forwardRef } from 'react'; +import { ButtonProps } from './interfaces'; +import { FeLoader } from '../Loader/FeLoader'; +import { ClassNameGenerator } from '../../styles'; +import classNames from 'classnames'; +import './FeButton.scss'; + +const prefixCls = 'fe-button'; +export const FeButton = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => { + const { + className, + children, + variant, + size, + loading, + iconButton, + fullWidth, + isCancel, + inForm, + asLink, + type = 'button', + transparent, + testId, + ...restProps + } = props; + + const disabled = props.disabled || loading; + + const classes = ClassNameGenerator.generate({ + className, + prefixCls, + size, + theme: disabled ? 'disabled' : variant, + isClickable: true, + isFullWidth: fullWidth, + isLoading: loading, + }); + + return ( + <button + ref={ref} + className={classNames(classes, { ['fe-icon-button']: iconButton, ['fe-button-transparent']: transparent })} + type={type} + test-id={testId} + {...restProps} + > + {children} + {loading && <FeLoader size={size === 'small' ? 18 : 24} />} + </button> + ); +}); diff --git a/packages/core/src/elements/Button/index.ts b/packages/core/src/elements/Button/index.ts new file mode 100644 index 000000000..c63192a54 --- /dev/null +++ b/packages/core/src/elements/Button/index.ts @@ -0,0 +1,2 @@ +export * from './Button'; +export * from './interfaces'; diff --git a/packages/core/src/elements/Button/interfaces.ts b/packages/core/src/elements/Button/interfaces.ts new file mode 100644 index 000000000..b7dea7ae7 --- /dev/null +++ b/packages/core/src/elements/Button/interfaces.ts @@ -0,0 +1,20 @@ +import React from 'react'; +import { FormFieldProps } from '../../ElementsFactory'; +import { Theme } from '../../styles'; + +export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, FormFieldProps { + fullWidth?: boolean; + variant?: Theme; + transparent?: boolean; + isCancel?: boolean; + asLink?: boolean; + loading?: boolean; + iconButton?: boolean; + + // @deprecated + submit?: boolean; + testId?: string; + + // internal use + formikDisableIfNotDirty?: boolean; // default true +} diff --git a/packages/core/src/elements/Checkbox/Checkbox.tsx b/packages/core/src/elements/Checkbox/Checkbox.tsx new file mode 100644 index 000000000..f142189d8 --- /dev/null +++ b/packages/core/src/elements/Checkbox/Checkbox.tsx @@ -0,0 +1,34 @@ +import React, { ChangeEvent, forwardRef, useCallback } from 'react'; +import { ElementsFactory } from '../../ElementsFactory'; +import { useField } from 'formik'; +import { CheckboxProps } from './interfaces'; + +export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>((props, ref) => + React.createElement(ElementsFactory.getElement('Checkbox'), { ...props, ref } as any) +); + +export const FCheckbox = forwardRef<HTMLInputElement, CheckboxProps & { name: string }>((props, ref) => { + const [inputProps, { touched, error }, { setValue }] = useField(props.name); + const { onChange } = props; + + const handleChange = useCallback( + (e: ChangeEvent<HTMLInputElement>) => { + onChange?.(e); + // inputProps.onChange(e); + setValue(inputProps.value === undefined ? true : inputProps.value === true ? false : true); + }, + [setValue, onChange] + ); + + return ( + <Checkbox + ref={ref} + inForm + {...inputProps} + {...props} + fullWidth={props.fullWidth ?? true} + onChange={handleChange} + error={touched && error ? error : undefined} + /> + ); +}); diff --git a/packages/core/src/elements/Checkbox/FeCheckbox.scss b/packages/core/src/elements/Checkbox/FeCheckbox.scss new file mode 100644 index 000000000..5bf281a46 --- /dev/null +++ b/packages/core/src/elements/Checkbox/FeCheckbox.scss @@ -0,0 +1,108 @@ +.fe-checkbox { + position: relative; + outline: none; + display: inline-block; + + &__in-form { + margin-bottom: var(--element-spacing); + margin-right: calc(var(--element-spacing) * 2); + } + + &__full-width { + display: block; + margin-right: 0; + } + + &__content { + display: inline-block; + position: relative; + } + + input { + top: 0; + left: 0; + width: 100%; + height: 100%; + margin: 0; + opacity: 0; + padding: 0; + z-index: 1; + position: absolute; + cursor: pointer; + } + + .fe-checkbox__label { + vertical-align: middle; + margin: 0; + } + + .fe-checkbox__customLabel { + line-height: 100%; + } + + .fe-checkbox__input { + width: 1rem; + height: 1rem; + flex-shrink: 0; + border: 2px solid var(--fe-checkbox-border-color, var(--color-gray-4)); + border-radius: var(--fe-checkbox-border-radius, var(--element-border-radius-sm)); + display: inline-block; + cursor: pointer; + color: var(--fe-checkbox-icon-color, var(--color-text)); + transition: all 200ms; + vertical-align: middle; + margin-right: 0.5rem; + position: relative; + opacity: 1; + } + + input:hover:not(:disabled) + .fe-checkbox__input, + input:active:not(:disabled) + .fe-checkbox__input, + input:focus:not(:disabled) + .fe-checkbox__input { + border-color: var(--fe-checkbox-icon-color, var(--color-primary)); + } + + .fe-icon { + position: absolute; + top: 0; + left: 0; + color: var(--color-white); + margin: 0; + width: 1rem; + height: 1rem; + max-width: unset; + opacity: 0; + transform: scale(0.1); + transition: all 200ms; + } + + input:checked + .fe-checkbox__input { + border-color: var(--fe-checkbox-active-color, var(--color-primary)); + background-color: var(--fe-checkbox-active-color, var(--color-primary)); + color: var(--fe-checkbox-icon-color, var(--color-primary)); + border-width: 0.5rem; + + .fe-icon { + transform: scale(1); + opacity: 1; + top: -0.5rem; + left: -0.5rem; + height: 1rem; + width: 1rem; + fill: var(--color-white); + margin: 0; + } + } + + input:checked:hover:not(:disabled) + .fe-checkbox__input, + input:checked:active:not(:disabled) + .fe-checkbox__input, + input:checked:focus:not(:disabled) + .fe-checkbox__input { + filter: brightness(0.9); + } + + &__disabled { + input { + cursor: default; + } + } +} diff --git a/packages/core/src/elements/Checkbox/FeCheckbox.tsx b/packages/core/src/elements/Checkbox/FeCheckbox.tsx new file mode 100644 index 000000000..597849df1 --- /dev/null +++ b/packages/core/src/elements/Checkbox/FeCheckbox.tsx @@ -0,0 +1,66 @@ +import React, { forwardRef, useCallback, useState } from 'react'; +import { CheckboxProps } from './interfaces'; +import classNames from 'classnames'; +import './FeCheckbox.scss'; +import { FeIcon } from '../Icon/FeIcon'; + +const prefixCls = 'fe-checkbox'; +export const FeCheckbox = forwardRef<HTMLInputElement, CheckboxProps>((props, ref) => { + const { + className, + label, + renderLabel, + disabled, + fullWidth, + indeterminate, + inForm, + name, + onBlur, + onChange, + size, + ...rest + } = props; + const [_checked, _setChecked] = useState(props.defaultChecked); + + const checked = props.hasOwnProperty('checked') ? props.checked : _checked; + + const toggleCheck = useCallback( + (e) => { + onChange?.(e); + _setChecked(e.target.checked); + }, + [checked, indeterminate, onChange] + ); + + return ( + <div + className={classNames(prefixCls, className, { + [`${prefixCls}__in-form`]: inForm, + [`${prefixCls}__checked`]: checked, + [`${prefixCls}__disabled`]: disabled, + [`${prefixCls}__full-width`]: fullWidth, + [`${prefixCls}__with-label`]: !!label, + })} + > + <div className={`${prefixCls}__content`}> + <input + {...rest} + type='checkbox' + ref={ref} + checked={indeterminate || checked} + onChange={toggleCheck} + name={name} + onBlur={onBlur} + /> + <span className={`${prefixCls}__input`}> + <FeIcon name={indeterminate ? 'indeterminate' : 'checkmark'} /> + </span> + {label ? ( + <label className={`${prefixCls}__label`}>{label}</label> + ) : ( + <div className={`${prefixCls}__customLabel`}>{renderLabel?.()}</div> + )} + </div> + </div> + ); +}); diff --git a/packages/core/src/elements/Checkbox/index.ts b/packages/core/src/elements/Checkbox/index.ts new file mode 100644 index 000000000..9470b1773 --- /dev/null +++ b/packages/core/src/elements/Checkbox/index.ts @@ -0,0 +1,2 @@ +export * from './Checkbox'; +export * from './interfaces'; diff --git a/packages/core/src/elements/Checkbox/interfaces.ts b/packages/core/src/elements/Checkbox/interfaces.ts new file mode 100644 index 000000000..cc7075692 --- /dev/null +++ b/packages/core/src/elements/Checkbox/interfaces.ts @@ -0,0 +1,10 @@ +import { InputHTMLAttributes } from 'react'; +import { FormFieldProps } from '../../ElementsFactory'; + +export interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'value' | 'size'>, FormFieldProps { + label?: string; + renderLabel?: () => JSX.Element; + indeterminate?: boolean; + fullWidth?: boolean; + error?: string; +} diff --git a/packages/core/src/elements/Chip/FeChip.scss b/packages/core/src/elements/Chip/FeChip.scss new file mode 100644 index 000000000..2ee097f2f --- /dev/null +++ b/packages/core/src/elements/Chip/FeChip.scss @@ -0,0 +1,24 @@ +.fe-chip { + display: inline-flex; + background-color: var(--color-primary-light); + height: var(--element-height-sm); + white-space: nowrap; + justify-content: center; + align-items: center; + vertical-align: middle; + border-radius: var(--element-height-sm); + padding-left: var(--element-spacing); + + & > *:nth-child(1) { + margin-left: var(--element-spacing); + } + + & > *:nth-last-child(1) { + margin-right: var(--element-spacing); + } + + &-delete { + padding: 0; + margin-left: var(--element-spacing); + } +} diff --git a/packages/core/src/elements/Chip/FeChip.tsx b/packages/core/src/elements/Chip/FeChip.tsx new file mode 100644 index 000000000..2c27b9379 --- /dev/null +++ b/packages/core/src/elements/Chip/FeChip.tsx @@ -0,0 +1,17 @@ +import React, { FC } from 'react'; +import classnames from 'classnames'; +import { FeButton } from '../Button/FeButton'; +import { IChip } from './interfaces'; +import { FeIcon } from '../Icon/FeIcon'; +import './FeChip.scss'; + +export const FeChip: FC<IChip> = ({ className, onDelete, label }) => ( + <div className={classnames('fe-chip', className)}> + <span>{label}</span> + {!!onDelete && ( + <FeButton iconButton className='fe-chip-delete' onClick={onDelete} transparent> + <FeIcon name='delete' /> + </FeButton> + )} + </div> +); diff --git a/packages/core/src/elements/Chip/index.ts b/packages/core/src/elements/Chip/index.ts new file mode 100644 index 000000000..957860982 --- /dev/null +++ b/packages/core/src/elements/Chip/index.ts @@ -0,0 +1 @@ +export * from './interfaces'; diff --git a/packages/core/src/elements/Chip/interfaces.ts b/packages/core/src/elements/Chip/interfaces.ts new file mode 100644 index 000000000..7f2b64c26 --- /dev/null +++ b/packages/core/src/elements/Chip/interfaces.ts @@ -0,0 +1,5 @@ +export interface IChip { + label: string; + onDelete?(): void; + className?: string; +} diff --git a/packages/core/src/elements/Dialog/FeDialog.scss b/packages/core/src/elements/Dialog/FeDialog.scss new file mode 100644 index 000000000..92e667d57 --- /dev/null +++ b/packages/core/src/elements/Dialog/FeDialog.scss @@ -0,0 +1,210 @@ +@import '~rc-dialog/assets/index.css'; + +.fe-dialog { + position: relative; + width: auto; + margin: 10px; + + &-wrap { + position: fixed; + overflow: auto; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1050; + -webkit-overflow-scrolling: touch; + outline: 0; + } + + &-header { + display: flex; + flex-direction: row; + align-items: center; + height: 4.5rem; + border-radius: var(--element-border-radius-sm) var(--element-border-radius-sm) 0 0; + background: var(--color-white); + color: var(--color-gray-8); + border-bottom: 1px solid var(--element-divider-color); + padding: 0 2rem; + } + + &-title { + margin: 0; + font-size: 1.1rem; + line-height: 1.5rem; + font-weight: bold; + } + + &-content { + overflow: hidden; + position: relative; + background-color: var(--color-white); + border: none; + border-radius: var(--element-border-radius-sm); + background-clip: padding-box; + box-shadow: 0 2rem 3rem -1rem rgba(0, 0, 0, 0.1); + } + + &-body { + padding: 2rem; + } + + &__footer { + width: calc(100% + 4rem); + margin: 2rem -2rem -2rem; + padding: 1.5rem 2rem; + background: var(--color-gray-0); + } + + &-mask { + position: fixed; + top: 0; + right: 0; + left: 0; + bottom: 0; + background-color: var(--color-black-10); + height: 100%; + filter: alpha(opacity=50); + z-index: 1050; + + &-hidden { + display: none; + } + } +} + +.fe-dialog-close { + cursor: pointer; + border: 0; + background: transparent; + font-size: 21px; + position: absolute; + right: 20px; + font-weight: 700; + line-height: 1; + color: #000; + text-shadow: 0 1px 0 #fff; + filter: alpha(opacity=20); + opacity: 0.2; + text-decoration: none; + top: 1.5rem; + outline: none; +} + +.fe-dialog-close-x:after { + content: '×'; +} + +.fe-dialog-close:hover { + opacity: 1; + filter: alpha(opacity=100); + text-decoration: none; +} + +.fe-dialog-footer { + border-top: 1px solid #e9e9e9; + padding: 10px 20px; + text-align: right; + border-radius: 0 0 5px 5px; +} + +.fe-dialog-zoom-enter, +.fe-dialog-zoom-appear { + opacity: 0; + animation-duration: 0.2s; + animation-fill-mode: both; + animation-timing-function: cubic-bezier(0.08, 0.82, 0.17, 1); + animation-play-state: paused; +} + +.fe-dialog-zoom-leave { + animation-duration: 0.2s; + animation-fill-mode: both; + animation-timing-function: cubic-bezier(0.6, 0.04, 0.98, 0.34); + animation-play-state: paused; +} + +.fe-dialog-zoom-enter.fe-dialog-zoom-enter-active, +.fe-dialog-zoom-appear.fe-dialog-zoom-appear-active { + animation-name: rcDialogZoomIn; + animation-play-state: running; +} + +.fe-dialog-zoom-leave.fe-dialog-zoom-leave-active { + animation-name: rcDialogZoomOut; + animation-play-state: running; +} + +.fe-dialog-fade-enter, +.fe-dialog-fade-appear { + opacity: 0; + animation-duration: 0.2s; + animation-fill-mode: both; + animation-timing-function: cubic-bezier(0.55, 0, 0.55, 0.2); + animation-play-state: paused; +} + +.fe-dialog-fade-leave { + animation-duration: 0.2s; + animation-fill-mode: both; + animation-timing-function: cubic-bezier(0.55, 0, 0.55, 0.2); + animation-play-state: paused; +} + +.fe-dialog-fade-enter.fe-dialog-fade-enter-active, +.fe-dialog-fade-appear.fe-dialog-fade-appear-active { + animation-name: rcDialogFadeIn; + animation-play-state: running; +} + +.fe-dialog-fade-leave.fe-dialog-fade-leave-active { + animation-name: rcDialogFadeOut; + animation-play-state: running; +} + +@keyframes rcDialogFadeIn { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +@keyframes rcDialogFadeOut { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } +} + +@keyframes rcDialogZoomIn { + 0% { + opacity: 0; + transform: scale(0, 0); + } + 100% { + opacity: 1; + transform: scale(1, 1); + } +} + +@keyframes rcDialogZoomOut { + 0% { + transform: scale(1, 1); + } + 100% { + opacity: 0; + transform: scale(0, 0); + } +} + +@media (min-width: 768px) { + .fe-dialog { + width: 600px; + margin: 30px auto; + } +} diff --git a/packages/core/src/elements/Dialog/FeDialog.tsx b/packages/core/src/elements/Dialog/FeDialog.tsx new file mode 100644 index 000000000..7da5d90ee --- /dev/null +++ b/packages/core/src/elements/Dialog/FeDialog.tsx @@ -0,0 +1,24 @@ +import React, { FC } from 'react'; +import { DialogProps } from './interfaces'; +import Dialog from 'rc-dialog'; +import './FeDialog.scss'; + +export const FeDialog: FC<DialogProps> = ({ children, open, header, onClose, ...props }) => { + return ( + <Dialog + {...props} + visible={open} + maskClosable={false} + destroyOnClose={true} + title={header} + closable={true} + onClose={() => onClose?.()} + width={600} + prefixCls='fe-dialog' + animation='zoom' + maskAnimation='fade' + > + {children} + </Dialog> + ); +}; diff --git a/packages/core/src/elements/Dialog/index.ts b/packages/core/src/elements/Dialog/index.ts new file mode 100644 index 000000000..8ca3fcfbf --- /dev/null +++ b/packages/core/src/elements/Dialog/index.ts @@ -0,0 +1,6 @@ +import React, { FC } from 'react'; +import { ElementsFactory } from '../../ElementsFactory'; +import { DialogProps } from './interfaces'; + +export * from './interfaces'; +export const Dialog: FC<DialogProps> = (props) => React.createElement(ElementsFactory.getElement('Dialog'), props); diff --git a/packages/core/src/elements/Dialog/interfaces.ts b/packages/core/src/elements/Dialog/interfaces.ts new file mode 100644 index 000000000..5ef03e397 --- /dev/null +++ b/packages/core/src/elements/Dialog/interfaces.ts @@ -0,0 +1,12 @@ +import { ReactNode } from 'react'; + +export type DialogProps = { + open?: boolean; + onOpen?: () => void; + onClose?: () => void; + closeOnEscape?: boolean; // default: true + closeOnDimmerClick?: boolean; // default: true + header?: ReactNode; + className?: string; + size?: 'mini' | 'tiny' | 'small' | 'large' | 'fullscreen'; +}; diff --git a/packages/core/src/elements/ErrorMessage.tsx b/packages/core/src/elements/ErrorMessage.tsx new file mode 100644 index 000000000..5ff55a830 --- /dev/null +++ b/packages/core/src/elements/ErrorMessage.tsx @@ -0,0 +1,38 @@ +import React, { FC } from 'react'; + +export type OnError = { + // triggered if change password failed. return true to override the default behavior + onError?: (error: any) => boolean; +}; +type ErrorMessageProps = OnError & { + style?: object; + error?: any; + separator?: boolean; +}; +export const ErrorMessage: FC<ErrorMessageProps> = ({ error, separator, onError, style }) => { + if (!error) { + return null; + } + if (onError) { + if (onError(error)) { + return null; + } + } + + if (separator && error.indexOf(', ') !== -1) { + return ( + <div className='fe-error-message' style={style}> + <ul> + {error.split(', ').map((err: string) => ( + <li>{err}</li> + ))} + </ul> + </div> + ); + } + return ( + <div className='fe-error-message' style={style}> + {error} + </div> + ); +}; diff --git a/packages/core/src/elements/Form/FeForm.scss b/packages/core/src/elements/Form/FeForm.scss new file mode 100644 index 000000000..e69de29bb diff --git a/packages/core/src/elements/Form/FeForm.tsx b/packages/core/src/elements/Form/FeForm.tsx new file mode 100644 index 000000000..cc8b13bdf --- /dev/null +++ b/packages/core/src/elements/Form/FeForm.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import './FeForm.scss'; +import { FormProps } from './interfaces'; + +export class FeForm extends React.Component<FormProps> { + render() { + return <div {...(this.props as any)} />; + } +} diff --git a/packages/core/src/elements/Form/Form.tsx b/packages/core/src/elements/Form/Form.tsx new file mode 100644 index 000000000..e122e7f93 --- /dev/null +++ b/packages/core/src/elements/Form/Form.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { Form as FormikForm } from 'formik'; +import classNames from 'classnames'; +import { FormProps } from './interfaces'; +import { ElementsFactory } from '../../ElementsFactory'; + +export const Form = (props: FormProps) => React.createElement(ElementsFactory.getElement('Form'), props); + +export const FForm = ({ children, className, ...restProps }: FormProps) => { + return ( + <Form as='div' className={classNames('fe-form', className)} {...restProps}> + <FormikForm>{children}</FormikForm> + </Form> + ); +}; diff --git a/packages/core/src/elements/Form/index.ts b/packages/core/src/elements/Form/index.ts new file mode 100644 index 000000000..2d81491d1 --- /dev/null +++ b/packages/core/src/elements/Form/index.ts @@ -0,0 +1,2 @@ +export * from './interfaces'; +export * from './Form'; diff --git a/packages/core/src/elements/Form/interfaces.ts b/packages/core/src/elements/Form/interfaces.ts new file mode 100644 index 000000000..e4b7559af --- /dev/null +++ b/packages/core/src/elements/Form/interfaces.ts @@ -0,0 +1,5 @@ +import React from 'react'; + +export interface FormProps extends React.HTMLAttributes<HTMLFormElement> { + as?: string; // default is form +} diff --git a/packages/core/src/elements/Grid/FeGrid.tsx b/packages/core/src/elements/Grid/FeGrid.tsx new file mode 100644 index 000000000..2a118880f --- /dev/null +++ b/packages/core/src/elements/Grid/FeGrid.tsx @@ -0,0 +1,51 @@ +import React, { forwardRef, ForwardRefRenderFunction, RefForwardingComponent } from 'react'; +import classNames from 'classnames'; +import { GridProps } from './inteterfaces'; + +export const FeGrid = forwardRef<HTMLDivElement, GridProps>((props, ref) => { + const { + alignContent = 'stretch', + alignItems = 'stretch', + className: classNameProp, + container = false, + direction = 'row', + item = false, + justifyContent = 'flex-start', + lg = false, + md = false, + sm = false, + spacing = 0, + wrap = 'wrap', + xl = false, + xs = false, + zeroMinWidth = false, + ...other + } = props; + + return ( + <div + className={classNames( + 'fe-grid', + { + [`fe-container`]: container, + [`fe-item`]: item, + [`fe-zeroMinWidth`]: zeroMinWidth, + [`fe-spacing-xs-${String(spacing)}`]: container && spacing !== 0, + [`fe-direction-xs-${String(direction)}`]: direction !== 'row', + [`fe-wrap-xs-${String(wrap)}`]: wrap !== 'wrap', + [`fe-align-items-xs-${String(alignItems)}`]: alignItems !== 'stretch', + [`fe-align-content-xs-${String(alignContent)}`]: alignContent !== 'stretch', + [`fe-justify-content-xs-${String(justifyContent)}`]: justifyContent !== 'flex-start', + [`fe-grid-xs-${String(xs)}`]: xs, + [`fe-grid-sm-${String(sm)}`]: sm, + [`fe-grid-md-${String(md)}`]: md, + [`fe-grid-lg-${String(lg)}`]: lg, + [`fe-grid-xl-${String(xl)}`]: xl, + }, + classNameProp + )} + ref={ref} + {...other} + /> + ); +}); diff --git a/packages/core/src/elements/Grid/Grid.tsx b/packages/core/src/elements/Grid/Grid.tsx new file mode 100644 index 000000000..b167d6ffc --- /dev/null +++ b/packages/core/src/elements/Grid/Grid.tsx @@ -0,0 +1,8 @@ +import React, { forwardRef } from 'react'; +import { ElementsFactory } from '../../ElementsFactory'; +import { GridProps } from './inteterfaces'; +import './grid.scss'; + +export const Grid = forwardRef<HTMLDivElement, GridProps>((props, ref) => + React.createElement(ElementsFactory.getElement('Grid'), { ...props, ref } as any) +); diff --git a/packages/core/src/elements/Grid/grid.scss b/packages/core/src/elements/Grid/grid.scss new file mode 100644 index 000000000..a29f87907 --- /dev/null +++ b/packages/core/src/elements/Grid/grid.scss @@ -0,0 +1,619 @@ +.fe-grid { + &.fe-container { + width: 100%; + display: flex; + flex-wrap: wrap; + box-sizing: border-box; + } + + &.fe-item { + margin: 0; + box-sizing: border-box; + } + + &.fe-zeroMinWidth { + min-width: 0; + } + + &.fe-direction-xs-column { + flex-direction: column; + } + + &.fe-direction-xs-column-reverse { + flex-direction: column-reverse; + } + + &.fe-direction-xs-row-reverse { + flex-direction: row-reverse; + } + + &.fe-wrap-xs-nowrap { + flex-wrap: nowrap; + } + + &.fe-wrap-xs-wrap-reverse { + flex-wrap: wrap-reverse; + } + + &.fe-align-items-xs-center { + align-items: center; + } + + &.fe-align-items-xs-flex-start { + align-items: flex-start; + } + + &.fe-align-items-xs-flex-end { + align-items: flex-end; + } + + &.fe-align-items-xs-baseline { + align-items: baseline; + } + + &.fe-align-content-xs-center { + align-content: center; + } + + &.fe-align-content-xs-flex-start { + align-content: flex-start; + } + + &.fe-align-content-xs-flex-end { + align-content: flex-end; + } + + &.fe-align-content-xs-space-between { + align-content: space-between; + } + + &.fe-align-content-xs-space-around { + align-content: space-around; + } + + &.fe-justify-content-xs-center { + justify-content: center; + } + + &.fe-justify-content-xs-flex-end { + justify-content: flex-end; + } + + &.fe-justify-content-xs-space-between { + justify-content: space-between; + } + + &.fe-justify-content-xs-space-around { + justify-content: space-around; + } + + &.fe-justify-content-xs-space-evenly { + justify-content: space-evenly; + } + + &.fe-spacing-xs-1 { + width: calc(100% + 8px); + margin: -4px; + + > .fe-grid.fe-item { + padding: 4px; + } + } + + &.fe-spacing-xs-2 { + width: calc(100% + 16px); + margin: -8px; + + > .fe-grid.fe-item { + padding: 8px; + } + } + + &.fe-spacing-xs-3 { + width: calc(100% + 24px); + margin: -12px; + + > .fe-grid.fe-item { + padding: 12px; + } + } + + &.fe-spacing-xs-4 { + width: calc(100% + 32px); + margin: -16px; + + > .fe-grid.fe-item { + padding: 16px; + } + } + + &.fe-spacing-xs-5 { + width: calc(100% + 40px); + margin: -20px; + + > .fe-grid.fe-item { + padding: 20px; + } + } + + &.fe-spacing-xs-6 { + width: calc(100% + 48px); + margin: -24px; + + > .fe-grid.fe-item { + padding: 24px; + } + } + + &.fe-spacing-xs-7 { + width: calc(100% + 56px); + margin: -28px; + + > .fe-grid.fe-item { + padding: 28px; + } + } + + &.fe-spacing-xs-8 { + width: calc(100% + 64px); + margin: -32px; + + > .fe-grid.fe-item { + padding: 32px; + } + } + + &.fe-spacing-xs-9 { + width: calc(100% + 72px); + margin: -36px; + + > .fe-grid.fe-item { + padding: 36px; + } + } + + &.fe-spacing-xs-10 { + width: calc(100% + 80px); + margin: -40px; + + > .fe-grid.fe-item { + padding: 40px; + } + } + + &.fe-grid-xs-auto { + flex-grow: 0; + max-width: none; + flex-basis: auto; + } + + &.fe-grid-xs-true { + flex-grow: 1; + max-width: 100%; + flex-basis: 0; + } + + &.fe-grid-xs-1 { + flex-grow: 0; + max-width: 8.333333%; + flex-basis: 8.333333%; + } + + &.fe-grid-xs-2 { + flex-grow: 0; + max-width: 16.666667%; + flex-basis: 16.666667%; + } + + &.fe-grid-xs-3 { + flex-grow: 0; + max-width: 25%; + flex-basis: 25%; + } + + &.fe-grid-xs-4 { + flex-grow: 0; + max-width: 33.333333%; + flex-basis: 33.333333%; + } + + &.fe-grid-xs-5 { + flex-grow: 0; + max-width: 41.666667%; + flex-basis: 41.666667%; + } + + &.fe-grid-xs-6 { + flex-grow: 0; + max-width: 50%; + flex-basis: 50%; + } + + &.fe-grid-xs-7 { + flex-grow: 0; + max-width: 58.333333%; + flex-basis: 58.333333%; + } + + &.fe-grid-xs-8 { + flex-grow: 0; + max-width: 66.666667%; + flex-basis: 66.666667%; + } + + &.fe-grid-xs-9 { + flex-grow: 0; + max-width: 75%; + flex-basis: 75%; + } + + &.fe-grid-xs-10 { + flex-grow: 0; + max-width: 83.333333%; + flex-basis: 83.333333%; + } + + &.fe-grid-xs-11 { + flex-grow: 0; + max-width: 91.666667%; + flex-basis: 91.666667%; + } + + &.fe-grid-xs-12 { + flex-grow: 0; + max-width: 100%; + flex-basis: 100%; + } +} + +@media (min-width: 600px) { + .fe-grid { + &.fe-grid-sm-auto { + flex-grow: 0; + max-width: none; + flex-basis: auto; + } + + &.fe-grid-sm-true { + flex-grow: 1; + max-width: 100%; + flex-basis: 0; + } + + &.fe-grid-sm-1 { + flex-grow: 0; + max-width: 8.333333%; + flex-basis: 8.333333%; + } + + &.fe-grid-sm-2 { + flex-grow: 0; + max-width: 16.666667%; + flex-basis: 16.666667%; + } + + &.fe-grid-sm-3 { + flex-grow: 0; + max-width: 25%; + flex-basis: 25%; + } + + &.fe-grid-sm-4 { + flex-grow: 0; + max-width: 33.333333%; + flex-basis: 33.333333%; + } + + &.fe-grid-sm-5 { + flex-grow: 0; + max-width: 41.666667%; + flex-basis: 41.666667%; + } + + &.fe-grid-sm-6 { + flex-grow: 0; + max-width: 50%; + flex-basis: 50%; + } + + &.fe-grid-sm-7 { + flex-grow: 0; + max-width: 58.333333%; + flex-basis: 58.333333%; + } + + &.fe-grid-sm-8 { + flex-grow: 0; + max-width: 66.666667%; + flex-basis: 66.666667%; + } + + &.fe-grid-sm-9 { + flex-grow: 0; + max-width: 75%; + flex-basis: 75%; + } + + &.fe-grid-sm-10 { + flex-grow: 0; + max-width: 83.333333%; + flex-basis: 83.333333%; + } + + &.fe-grid-sm-11 { + flex-grow: 0; + max-width: 91.666667%; + flex-basis: 91.666667%; + } + + &.fe-grid-sm-12 { + flex-grow: 0; + max-width: 100%; + flex-basis: 100%; + } + } +} + +@media (min-width: 960px) { + .fe-grid { + &.fe-grid-md-auto { + flex-grow: 0; + max-width: none; + flex-basis: auto; + } + + &.fe-grid-md-true { + flex-grow: 1; + max-width: 100%; + flex-basis: 0; + } + + &.fe-grid-md-1 { + flex-grow: 0; + max-width: 8.333333%; + flex-basis: 8.333333%; + } + + &.fe-grid-md-2 { + flex-grow: 0; + max-width: 16.666667%; + flex-basis: 16.666667%; + } + + &.fe-grid-md-3 { + flex-grow: 0; + max-width: 25%; + flex-basis: 25%; + } + + &.fe-grid-md-4 { + flex-grow: 0; + max-width: 33.333333%; + flex-basis: 33.333333%; + } + + &.fe-grid-md-5 { + flex-grow: 0; + max-width: 41.666667%; + flex-basis: 41.666667%; + } + + &.fe-grid-md-6 { + flex-grow: 0; + max-width: 50%; + flex-basis: 50%; + } + + &.fe-grid-md-7 { + flex-grow: 0; + max-width: 58.333333%; + flex-basis: 58.333333%; + } + + &.fe-grid-md-8 { + flex-grow: 0; + max-width: 66.666667%; + flex-basis: 66.666667%; + } + + &.fe-grid-md-9 { + flex-grow: 0; + max-width: 75%; + flex-basis: 75%; + } + + &.fe-grid-md-10 { + flex-grow: 0; + max-width: 83.333333%; + flex-basis: 83.333333%; + } + + &.fe-grid-md-11 { + flex-grow: 0; + max-width: 91.666667%; + flex-basis: 91.666667%; + } + + &.fe-grid-md-12 { + flex-grow: 0; + max-width: 100%; + flex-basis: 100%; + } + } +} + +@media (min-width: 1280px) { + .fe-grid { + &.fe-grid-lg-auto { + flex-grow: 0; + max-width: none; + flex-basis: auto; + } + + &.fe-grid-lg-true { + flex-grow: 1; + max-width: 100%; + flex-basis: 0; + } + + &.fe-grid-lg-1 { + flex-grow: 0; + max-width: 8.333333%; + flex-basis: 8.333333%; + } + + &.fe-grid-lg-2 { + flex-grow: 0; + max-width: 16.666667%; + flex-basis: 16.666667%; + } + + &.fe-grid-lg-3 { + flex-grow: 0; + max-width: 25%; + flex-basis: 25%; + } + + &.fe-grid-lg-4 { + flex-grow: 0; + max-width: 33.333333%; + flex-basis: 33.333333%; + } + + &.fe-grid-lg-5 { + flex-grow: 0; + max-width: 41.666667%; + flex-basis: 41.666667%; + } + + &.fe-grid-lg-6 { + flex-grow: 0; + max-width: 50%; + flex-basis: 50%; + } + + &.fe-grid-lg-7 { + flex-grow: 0; + max-width: 58.333333%; + flex-basis: 58.333333%; + } + + &.fe-grid-lg-8 { + flex-grow: 0; + max-width: 66.666667%; + flex-basis: 66.666667%; + } + + &.fe-grid-lg-9 { + flex-grow: 0; + max-width: 75%; + flex-basis: 75%; + } + + &.fe-grid-lg-10 { + flex-grow: 0; + max-width: 83.333333%; + flex-basis: 83.333333%; + } + + &.fe-grid-lg-11 { + flex-grow: 0; + max-width: 91.666667%; + flex-basis: 91.666667%; + } + + &.fe-grid-lg-12 { + flex-grow: 0; + max-width: 100%; + flex-basis: 100%; + } + } +} + +@media (min-width: 1920px) { + .fe-grid { + &.fe-grid-xl-auto { + flex-grow: 0; + max-width: none; + flex-basis: auto; + } + + &.fe-grid-xl-true { + flex-grow: 1; + max-width: 100%; + flex-basis: 0; + } + + &.fe-grid-xl-1 { + flex-grow: 0; + max-width: 8.333333%; + flex-basis: 8.333333%; + } + + &.fe-grid-xl-2 { + flex-grow: 0; + max-width: 16.666667%; + flex-basis: 16.666667%; + } + + &.fe-grid-xl-3 { + flex-grow: 0; + max-width: 25%; + flex-basis: 25%; + } + + &.fe-grid-xl-4 { + flex-grow: 0; + max-width: 33.333333%; + flex-basis: 33.333333%; + } + + &.fe-grid-xl-5 { + flex-grow: 0; + max-width: 41.666667%; + flex-basis: 41.666667%; + } + + &.fe-grid-xl-6 { + flex-grow: 0; + max-width: 50%; + flex-basis: 50%; + } + + &.fe-grid-xl-7 { + flex-grow: 0; + max-width: 58.333333%; + flex-basis: 58.333333%; + } + + &.fe-grid-xl-8 { + flex-grow: 0; + max-width: 66.666667%; + flex-basis: 66.666667%; + } + + &.fe-grid-xl-9 { + flex-grow: 0; + max-width: 75%; + flex-basis: 75%; + } + + &.fe-grid-xl-10 { + flex-grow: 0; + max-width: 83.333333%; + flex-basis: 83.333333%; + } + + &.fe-grid-xl-11 { + flex-grow: 0; + max-width: 91.666667%; + flex-basis: 91.666667%; + } + + &.fe-grid-xl-12 { + flex-grow: 0; + max-width: 100%; + flex-basis: 100%; + } + } +} diff --git a/packages/core/src/elements/Grid/index.ts b/packages/core/src/elements/Grid/index.ts new file mode 100644 index 000000000..3d6ff7a33 --- /dev/null +++ b/packages/core/src/elements/Grid/index.ts @@ -0,0 +1,2 @@ +export * from './Grid'; +export * from './inteterfaces'; diff --git a/packages/core/src/elements/Grid/inteterfaces.ts b/packages/core/src/elements/Grid/inteterfaces.ts new file mode 100644 index 000000000..4a6488aac --- /dev/null +++ b/packages/core/src/elements/Grid/inteterfaces.ts @@ -0,0 +1,102 @@ +import { HTMLAttributes } from 'react'; + +export type GridItemsAlignment = 'flex-start' | 'center' | 'flex-end' | 'stretch' | 'baseline'; +export type GridContentAlignment = 'stretch' | 'center' | 'flex-start' | 'flex-end' | 'space-between' | 'space-around'; +export type GridDirection = 'row' | 'row-reverse' | 'column' | 'column-reverse'; +export type GridSpacing = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10; +export type GridJustification = + | 'flex-start' + | 'center' + | 'flex-end' + | 'space-between' + | 'space-around' + | 'space-evenly'; +export type GridWrap = 'nowrap' | 'wrap' | 'wrap-reverse'; +export type GridSize = 'auto' | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; + +export interface GridProps extends HTMLAttributes<HTMLDivElement> { + /** + * Defines the `align-content` style property. + * It's applied for all screen sizes. + * @default 'stretch' + */ + alignContent?: GridContentAlignment; + /** + * Defines the `align-items` style property. + * It's applied for all screen sizes. + * @default 'stretch' + */ + alignItems?: GridItemsAlignment; + /** + * If `true`, the component will have the flex *container* behavior. + * You should be wrapping *items* with a *container*. + * @default false + */ + container?: boolean; + /** + * Defines the `flex-direction` style property. + * It is applied for all screen sizes. + * @default 'row' + */ + direction?: GridDirection; + /** + * If `true`, the component will have the flex *item* behavior. + * You should be wrapping *items* with a *container*. + * @default false + */ + item?: boolean; + /** + * Defines the `justify-content` style property. + * It is applied for all screen sizes. + * @default 'flex-start' + */ + justifyContent?: GridJustification; + /** + * Defines the number of grids the component is going to use. + * It's applied for the `lg` breakpoint and wider screens if not overridden. + * @default false + */ + lg?: boolean | GridSize; + /** + * Defines the number of grids the component is going to use. + * It's applied for the `md` breakpoint and wider screens if not overridden. + * @default false + */ + md?: boolean | GridSize; + /** + * Defines the number of grids the component is going to use. + * It's applied for the `sm` breakpoint and wider screens if not overridden. + * @default false + */ + sm?: boolean | GridSize; + /** + * Defines the space between the type `item` component. + * It can only be used on a type `container` component. + * @default 0 + */ + spacing?: GridSpacing; + /** + * Defines the `flex-wrap` style property. + * It's applied for all screen sizes. + * @default 'wrap' + */ + wrap?: GridWrap; + /** + * Defines the number of grids the component is going to use. + * It's applied for the `xl` breakpoint and wider screens. + * @default false + */ + xl?: boolean | GridSize; + /** + * Defines the number of grids the component is going to use. + * It's applied for all the screen sizes with the lowest priority. + * @default false + */ + xs?: boolean | GridSize; + /** + * If `true`, it sets `min-width: 0` on the item. + * Refer to the limitations section of the documentation to better understand the use case. + * @default false + */ + zeroMinWidth?: boolean; +} diff --git a/packages/core/src/elements/Icon/FeIcon.tsx b/packages/core/src/elements/Icon/FeIcon.tsx new file mode 100644 index 000000000..591bb077f --- /dev/null +++ b/packages/core/src/elements/Icon/FeIcon.tsx @@ -0,0 +1,63 @@ +import React, { FC, forwardRef } from 'react'; +import { IconNames, IconProps } from './interfaces'; +import { omitProps } from '../../helpers'; +import { SortArrows, SortArrowsAsc, SortArrowsDesc } from './svgs/SortArrows'; +import { Visibility, VisibilityOff } from './svgs/Visibility'; +import { Filters } from './svgs/Filters'; +import { UpArrow, DownArrow, RightArrow, LeftArrow } from './svgs/Arrows'; +import { Checkmark, Indeterminate } from './svgs/Checkmark'; +import { Delete, Search, Send, Refresh, Edit, Exit, Swap } from './svgs/Actions'; +import { PersonAdd, Profile } from './svgs/PersonAdd'; +import { VerticalDots } from './svgs/VerticalDots'; +import { CalendarToday } from './svgs/CalendarToday'; +import { Flash } from './svgs/Flash'; +import { Csv } from './svgs/Csv'; +import { Pdf } from './svgs/Pdf'; +import { List } from './svgs/List'; +import { Globe } from './svgs/Globe'; +import { Close } from './svgs/Close'; +import { Copy } from './svgs/Copy'; +import { Warning } from './svgs/Warning'; + +const mapIcons: Partial<{ [key in IconNames]: FC }> = { + 'down-arrow': DownArrow, + 'left-arrow': LeftArrow, + 'person-add': PersonAdd, + 'right-arrow': RightArrow, + 'sort-arrows-asc': SortArrowsAsc, + 'sort-arrows-desc': SortArrowsDesc, + 'sort-arrows': SortArrows, + 'up-arrow': UpArrow, + 'vertical-dots': VerticalDots, + 'visibility-off': VisibilityOff, + back: LeftArrow, + checkmark: Checkmark, + delete: Delete, + edit: Edit, + filters: Filters, + indeterminate: Indeterminate, + search: Search, + send: Send, + refresh: Refresh, + 'calendar-today': CalendarToday, + flash: Flash, + csv: Csv, + pdf: Pdf, + visibility: Visibility, + list: List, + exit: Exit, + swap: Swap, + profile: Profile, + globe: Globe, + close: Close, + copy: Copy, + warning: Warning, +}; + +export const FeIcon = forwardRef<HTMLElement, IconProps>((props, ref) => { + const SelectedIcon: any = mapIcons[props.name] ?? (() => null); + if (!SelectedIcon) { + return null; + } + return <SelectedIcon ref={ref} {...omitProps(props, ['name'])} />; +}); diff --git a/packages/core/src/elements/Icon/Icon.tsx b/packages/core/src/elements/Icon/Icon.tsx new file mode 100644 index 000000000..df4ad9c27 --- /dev/null +++ b/packages/core/src/elements/Icon/Icon.tsx @@ -0,0 +1,7 @@ +import { ElementsFactory } from '../../ElementsFactory'; +import React, { forwardRef } from 'react'; +import { IconProps } from './interfaces'; + +export const Icon = forwardRef<HTMLElement, IconProps>((props, ref) => + React.createElement(ElementsFactory.getElement('Icon'), { ...props, ref } as any) +); diff --git a/packages/core/src/elements/Icon/index.ts b/packages/core/src/elements/Icon/index.ts new file mode 100644 index 000000000..99a4834c0 --- /dev/null +++ b/packages/core/src/elements/Icon/index.ts @@ -0,0 +1,2 @@ +export * from './Icon'; +export * from './interfaces'; diff --git a/packages/core/src/elements/Icon/interfaces.ts b/packages/core/src/elements/Icon/interfaces.ts new file mode 100644 index 000000000..718fbc71b --- /dev/null +++ b/packages/core/src/elements/Icon/interfaces.ts @@ -0,0 +1,42 @@ +import React from 'react'; +import { Size } from '../../styles'; + +export type IconNames = + | 'back' + | 'checkmark' + | 'copy' + | 'search' + | 'warning' + | 'refresh' + | 'calendar-today' + | 'flash' + | 'image' + | 'delete' + | 'down-arrow' + | 'edit' + | 'filters' + | 'indeterminate' + | 'left-arrow' + | 'person-add' + | 'right-arrow' + | 'send' + | 'sort-arrows-asc' + | 'sort-arrows-desc' + | 'sort-arrows' + | 'up-arrow' + | 'vertical-dots' + | 'pdf' + | 'csv' + | 'visibility-off' + | 'visibility' + | 'list' + | 'exit' + | 'swap' + | 'profile' + | 'globe' + | 'close'; + +export interface IconProps extends React.HTMLAttributes<HTMLElement> { + name: IconNames; + size?: Size; +} diff --git a/packages/core/src/elements/Icon/svgs/Actions.tsx b/packages/core/src/elements/Icon/svgs/Actions.tsx new file mode 100644 index 000000000..484f8bb0e --- /dev/null +++ b/packages/core/src/elements/Icon/svgs/Actions.tsx @@ -0,0 +1,181 @@ +import React, { FC } from 'react'; +import classNames from 'classnames'; + +export const Delete: FC<React.SVGProps<SVGSVGElement>> = (props) => { + const { + xmlns = 'http://www.w3.org/2000/svg', + viewBox = '0 0 24 24', + className, + width = '2rem', + height = '2rem', + ...svgProps + } = props; + return ( + <svg + {...svgProps} + width={width} + height={height} + viewBox={viewBox} + xmlns={xmlns} + className={classNames('fe-icon', className)} + > + <path + fill='currentColor' + d='M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z' + /> + </svg> + ); +}; + +export const Search: FC<React.SVGProps<SVGSVGElement>> = (props) => { + const { + xmlns = 'http://www.w3.org/2000/svg', + viewBox = '0 0 24 24', + className, + width = '2rem', + height = '2rem', + ...svgProps + } = props; + return ( + <svg + {...svgProps} + width={width} + height={height} + viewBox={viewBox} + xmlns={xmlns} + className={classNames('fe-icon', className)} + > + <path + fill='currentColor' + d='M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z' + /> + </svg> + ); +}; + +export const Send: FC<React.SVGProps<SVGSVGElement>> = (props) => { + const { + xmlns = 'http://www.w3.org/2000/svg', + viewBox = '0 0 24 24', + className, + width = '2rem', + height = '2rem', + ...svgProps + } = props; + return ( + <svg + {...svgProps} + width={width} + height={height} + viewBox={viewBox} + xmlns={xmlns} + className={classNames('fe-icon', className)} + > + <path fill='currentColor' d='M2.01 21L23 12 2.01 3 2 10l15 2-15 2z' /> + </svg> + ); +}; + +export const Refresh: FC<React.SVGProps<SVGSVGElement>> = (props) => { + const { + xmlns = 'http://www.w3.org/2000/svg', + viewBox = '0 0 24 24', + className, + width = '2rem', + height = '2rem', + ...svgProps + } = props; + return ( + <svg + {...svgProps} + width={width} + height={height} + viewBox={viewBox} + xmlns={xmlns} + className={classNames('fe-icon', className)} + > + <path + fill='currentColor' + d='M19 8l-4 4h3c0 3.31-2.69 6-6 6-1.01 0-1.97-.25-2.8-.7l-1.46 1.46C8.97 19.54 10.43 20 12 20c4.42 0 8-3.58 8-8h3l-4-4zM6 12c0-3.31 2.69-6 6-6 1.01 0 1.97.25 2.8.7l1.46-1.46C15.03 4.46 13.57 4 12 4c-4.42 0-8 3.58-8 8H1l4 4 4-4H6z' + /> + </svg> + ); +}; + +export const Edit: FC<React.SVGProps<SVGSVGElement>> = (props) => { + const { + xmlns = 'http://www.w3.org/2000/svg', + viewBox = '0 0 576 512', + className, + width = '2rem', + height = '2rem', + ...svgProps + } = props; + return ( + <svg + {...svgProps} + width={width} + height={height} + viewBox={viewBox} + xmlns={xmlns} + className={classNames('fe-icon', className)} + > + <path + fill='currentColor' + d='M402.3 344.9l32-32c5-5 13.7-1.5 13.7 5.7V464c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V112c0-26.5 21.5-48 48-48h273.5c7.1 0 10.7 8.6 5.7 13.7l-32 32c-1.5 1.5-3.5 2.3-5.7 2.3H48v352h352V350.5c0-2.1.8-4.1 2.3-5.6zm156.6-201.8L296.3 405.7l-90.4 10c-26.2 2.9-48.5-19.2-45.6-45.6l10-90.4L432.9 17.1c22.9-22.9 59.9-22.9 82.7 0l43.2 43.2c22.9 22.9 22.9 60 .1 82.8zM460.1 174L402 115.9 216.2 301.8l-7.3 65.3 65.3-7.3L460.1 174zm64.8-79.7l-43.2-43.2c-4.1-4.1-10.8-4.1-14.8 0L436 82l58.1 58.1 30.9-30.9c4-4.2 4-10.8-.1-14.9z' + /> + </svg> + ); +}; + +export const Exit: FC<React.SVGProps<SVGSVGElement>> = (props) => { + const { + xmlns = 'http://www.w3.org/2000/svg', + viewBox = '0 0 24 24', + className, + width = '2rem', + height = '2rem', + ...svgProps + } = props; + return ( + <svg + {...svgProps} + width={width} + height={height} + viewBox={viewBox} + xmlns={xmlns} + className={classNames('fe-icon', className)} + > + <path + fill='currentColor' + d='M10.09 15.59L11.5 17l5-5-5-5-1.41 1.41L12.67 11H3v2h9.67l-2.58 2.59zM19 3H5c-1.11 0-2 .9-2 2v4h2V5h14v14H5v-4H3v4c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z' + /> + </svg> + ); +}; + +export const Swap: FC<React.SVGProps<SVGSVGElement>> = (props) => { + const { + xmlns = 'http://www.w3.org/2000/svg', + viewBox = '0 0 24 24', + className, + width = '2rem', + height = '2rem', + ...svgProps + } = props; + return ( + <svg + {...svgProps} + width={width} + height={height} + viewBox={viewBox} + xmlns={xmlns} + className={classNames('fe-icon', className)} + > + <path + fill='currentColor' + d='M19 8l-4 4h3c0 3.31-2.69 6-6 6-1.01 0-1.97-.25-2.8-.7l-1.46 1.46C8.97 19.54 10.43 20 12 20c4.42 0 8-3.58 8-8h3l-4-4zM6 12c0-3.31 2.69-6 6-6 1.01 0 1.97.25 2.8.7l1.46-1.46C15.03 4.46 13.57 4 12 4c-4.42 0-8 3.58-8 8H1l4 4 4-4H6z' + /> + </svg> + ); +}; diff --git a/packages/core/src/elements/Icon/svgs/Arrows.tsx b/packages/core/src/elements/Icon/svgs/Arrows.tsx new file mode 100644 index 000000000..d372fc6a2 --- /dev/null +++ b/packages/core/src/elements/Icon/svgs/Arrows.tsx @@ -0,0 +1,68 @@ +import React, { FC } from 'react'; +import classNames from 'classnames'; + +export const UpArrow: FC<React.SVGProps<SVGSVGElement>> = (props) => { + const { + xmlns = 'http://www.w3.org/2000/svg', + viewBox = '0 0 16 16', + className, + width = '2rem', + height = '2rem', + ...svgProps + } = props; + + const children = props.children ?? ( + <path + d='M 6 13.4 L 4.6 12 L 8.6 8 L 4.6 4 L 6 2.6 L 11.4 8 Z' + fill='currentColor' + fillRule='evenodd' + transform='matrix(0, -1, 1, 0, 0, 16)' + /> + ); + + return ( + <svg + {...svgProps} + width={width} + height={height} + viewBox={viewBox} + xmlns={xmlns} + className={classNames('fe-icon', className)} + > + {children} + </svg> + ); +}; + +export const DownArrow: FC<React.SVGProps<SVGSVGElement>> = (props) => { + return ( + <UpArrow {...props}> + <path + d='M 6 13.4 L 4.6 12 L 8.6 8 L 4.6 4 L 6 2.6 L 11.4 8 Z' + fill='currentColor' + fillRule='evenodd' + transform='matrix(0, 1, -1, 0, 16, 0)' + /> + </UpArrow> + ); +}; + +export const RightArrow: FC<React.SVGProps<SVGSVGElement>> = (props) => { + return ( + <UpArrow {...props}> + <path d='M 6 13.4 L 4.6 12 L 8.6 8 L 4.6 4 L 6 2.6 L 11.4 8 Z' fill='currentColor' fillRule='evenodd' /> + </UpArrow> + ); +}; +export const LeftArrow: FC<React.SVGProps<SVGSVGElement>> = (props) => { + return ( + <UpArrow {...props}> + <path + d='M 6 13.4 L 4.6 12 L 8.6 8 L 4.6 4 L 6 2.6 L 11.4 8 Z' + fill='currentColor' + fillRule='evenodd' + transform='matrix(-1, 0, 0, -1, 16, 16)' + /> + </UpArrow> + ); +}; diff --git a/packages/core/src/elements/Icon/svgs/CalendarToday.tsx b/packages/core/src/elements/Icon/svgs/CalendarToday.tsx new file mode 100644 index 000000000..027bc3983 --- /dev/null +++ b/packages/core/src/elements/Icon/svgs/CalendarToday.tsx @@ -0,0 +1,28 @@ +import React, { FC } from 'react'; +import classNames from 'classnames'; + +export const CalendarToday: FC<React.SVGProps<SVGSVGElement>> = (props) => { + const { + xmlns = 'http://www.w3.org/2000/svg', + viewBox = '0 0 24 24', + className, + width = '2rem', + height = '2rem', + ...svgProps + } = props; + return ( + <svg + {...svgProps} + width={width} + height={height} + viewBox={viewBox} + xmlns={xmlns} + className={classNames('fe-icon', className)} + > + <path + fill='currentColor' + d='M20 3h-1V1h-2v2H7V1H5v2H4c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 18H4V8h16v13z' + /> + </svg> + ); +}; diff --git a/packages/core/src/elements/Icon/svgs/Checkmark.tsx b/packages/core/src/elements/Icon/svgs/Checkmark.tsx new file mode 100644 index 000000000..14812d4d5 --- /dev/null +++ b/packages/core/src/elements/Icon/svgs/Checkmark.tsx @@ -0,0 +1,50 @@ +import React, { forwardRef } from 'react'; +import classNames from 'classnames'; + +export const Checkmark = forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>((props, ref) => { + const { + xmlns = 'http://www.w3.org/2000/svg', + viewBox = '0 0 24 24', + className, + width = '2rem', + height = '2rem', + ...svgProps + } = props; + return ( + <svg + {...svgProps} + ref={ref} + width={width} + height={height} + viewBox={viewBox} + xmlns={xmlns} + className={classNames('fe-icon', className)} + > + <path d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z' /> + </svg> + ); +}); + +export const Indeterminate = forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>((props, ref) => { + const { + xmlns = 'http://www.w3.org/2000/svg', + viewBox = '0 0 24 24', + className, + width = '2rem', + height = '2rem', + ...svgProps + } = props; + return ( + <svg + {...svgProps} + ref={ref} + width={width} + height={height} + viewBox={viewBox} + xmlns={xmlns} + className={classNames('fe-icon', className)} + > + <path d='M18 13H6c-.55 0-1-.45-1-1s.45-1 1-1h12c.55 0 1 .45 1 1s-.45 1-1 1z' /> + </svg> + ); +}); diff --git a/packages/core/src/elements/Icon/svgs/Close.tsx b/packages/core/src/elements/Icon/svgs/Close.tsx new file mode 100644 index 000000000..b35118f3a --- /dev/null +++ b/packages/core/src/elements/Icon/svgs/Close.tsx @@ -0,0 +1,28 @@ +import React, { FC } from 'react'; +import classNames from 'classnames'; + +export const Close: FC<React.SVGProps<SVGSVGElement>> = (props) => { + const { + xmlns = 'http://www.w3.org/2000/svg', + viewBox = '0 0 24 24', + className, + width = '2rem', + height = '2rem', + ...svgProps + } = props; + return ( + <svg + {...svgProps} + width={width} + height={height} + viewBox={viewBox} + xmlns={xmlns} + className={classNames('fe-icon', className)} + > + <path + d='M1.613.21l.094.083L8 6.585 14.293.293c.39-.39 1.024-.39 1.414 0 .36.36.388.928.083 1.32l-.083.094L9.415 8l6.292 6.293c.39.39.39 1.024 0 1.414-.36.36-.928.388-1.32.083l-.094-.083L8 9.415l-6.293 6.292c-.39.39-1.024.39-1.414 0-.36-.36-.388-.928-.083-1.32l.083-.094L6.585 8 .293 1.707c-.39-.39-.39-1.024 0-1.414.36-.36.928-.388 1.32-.083z' + transform='translate(-1142 -286) translate(730 262) translate(412 24) translate(4 4)' + /> + </svg> + ); +}; diff --git a/packages/core/src/elements/Icon/svgs/Copy.tsx b/packages/core/src/elements/Icon/svgs/Copy.tsx new file mode 100644 index 000000000..1a35e8233 --- /dev/null +++ b/packages/core/src/elements/Icon/svgs/Copy.tsx @@ -0,0 +1,32 @@ +import React, { FC } from 'react'; +import classNames from 'classnames'; + +export const Copy: FC<React.SVGProps<SVGSVGElement>> = (props) => { + const { + xmlns = 'http://www.w3.org/2000/svg', + viewBox = '0 0 16 16', + className, + width = '2rem', + height = '2rem', + ...svgProps + } = props; + return ( + <svg + {...svgProps} + width={width} + height={height} + viewBox={viewBox} + xmlns={xmlns} + className={classNames('fe-icon', className)} + > + <path + d='M9 4H1c-.6 0-1 .4-1 1v10c0 .6.4 1 1 1h8c.6 0 1-.4 1-1V5c0-.6-.4-1-1-1z' + transform='translate(-1307 -522) translate(745 184) translate(562 338) translate(1)' + /> + <path + d='M13 0H3v2h9v11h2V1c0-.6-.4-1-1-1z' + transform='translate(-1307 -522) translate(745 184) translate(562 338) translate(1)' + /> + </svg> + ); +}; diff --git a/packages/core/src/elements/Icon/svgs/Csv.tsx b/packages/core/src/elements/Icon/svgs/Csv.tsx new file mode 100644 index 000000000..2daddc9a3 --- /dev/null +++ b/packages/core/src/elements/Icon/svgs/Csv.tsx @@ -0,0 +1,46 @@ +import React, { FC } from 'react'; +import classNames from 'classnames'; + +export const Csv: FC<React.SVGProps<SVGSVGElement>> = (props) => { + const { + xmlns = 'http://www.w3.org/2000/svg', + viewBox = '0 0 21 21', + className, + width = '21px', + height = '21px', + ...svgProps + } = props; + return ( + <svg + className={classNames('fe-icon', className)} + {...svgProps} + width={width} + height={height} + viewBox={viewBox} + xmlns={xmlns} + > + <g transform='translate(0 0)'> + <path + d='M22.219,14.575a.906.906,0,0,0-.893.893V26.082a1.606,1.606,0,0,1-1.592,1.592H7.31a1.606,1.606,0,0,1-1.592-1.592V13.658A1.606,1.606,0,0,1,7.31,12.066h10.4a.893.893,0,0,0,0-1.785H7.31a3.387,3.387,0,0,0-3.377,3.377V26.082A3.387,3.387,0,0,0,7.31,29.46H19.734a3.387,3.387,0,0,0,3.377-3.377V15.468A.891.891,0,0,0,22.219,14.575Z' + transform='translate(-3.933 -8.037)' + ></path> + <path + d='M37.1,3.864a.893.893,0,1,0,0,1.785h3.04l-8.323,8.323a.9.9,0,0,0,0,1.254.874.874,0,0,0,1.254,0l8.419-8.419v3.04a.893.893,0,0,0,1.785,0V3.864Z' + transform='translate(-21.906 -3.864)' + ></path> + <path + d='M15.939,41.554a.91.91,0,0,0-.386-.289,1.227,1.227,0,0,0-.531-.1,1.431,1.431,0,0,0-.555.121,1.5,1.5,0,0,0-.458.338,1.324,1.324,0,0,0-.289.507,1.8,1.8,0,0,0-.1.627,1.714,1.714,0,0,0,.1.627,1.735,1.735,0,0,0,.289.507,1.5,1.5,0,0,0,.434.338,1.431,1.431,0,0,0,.555.121,1.308,1.308,0,0,0,.6-.145,1.322,1.322,0,0,0,.434-.386l.893.675a2.069,2.069,0,0,1-.8.651,2.36,2.36,0,0,1-.989.217,2.971,2.971,0,0,1-1.061-.169,2.716,2.716,0,0,1-.844-.507,2.464,2.464,0,0,1-.555-.82,3.016,3.016,0,0,1,0-2.123,2.152,2.152,0,0,1,.555-.82,2.464,2.464,0,0,1,.844-.507,2.969,2.969,0,0,1,1.061-.193,2.658,2.658,0,0,1,.434.048,2.917,2.917,0,0,1,.434.121,1.341,1.341,0,0,1,.41.217,1.624,1.624,0,0,1,.362.338Z' + transform='translate(-9.498 -27.514)' + ></path> + <path + d='M28.535,41.3a.741.741,0,0,0-.362-.241,1.366,1.366,0,0,0-.434-.072.821.821,0,0,0-.241.024,1.008,1.008,0,0,0-.241.1.523.523,0,0,0-.169.145.408.408,0,0,0-.072.241.365.365,0,0,0,.169.338,1.619,1.619,0,0,0,.41.193c.169.048.362.121.555.169a2.275,2.275,0,0,1,.555.241,1.115,1.115,0,0,1,.41.41,1.2,1.2,0,0,1,.169.7,1.763,1.763,0,0,1-.145.724,1.651,1.651,0,0,1-.41.507,1.4,1.4,0,0,1-.6.289,2.49,2.49,0,0,1-.724.1,2.664,2.664,0,0,1-.893-.145,2.419,2.419,0,0,1-.772-.482l.772-.844a1.1,1.1,0,0,0,.41.338,1.315,1.315,0,0,0,.531.121.99.99,0,0,0,.265-.024.722.722,0,0,0,.241-.1.564.564,0,0,0,.169-.169.408.408,0,0,0,.072-.241.434.434,0,0,0-.169-.362,1.224,1.224,0,0,0-.434-.217c-.169-.048-.362-.121-.555-.169a2.275,2.275,0,0,1-.555-.241,1.4,1.4,0,0,1-.434-.41,1.09,1.09,0,0,1-.145-.651,1.575,1.575,0,0,1,.145-.7,1.651,1.651,0,0,1,.41-.507,1.8,1.8,0,0,1,.6-.314,2.23,2.23,0,0,1,.7-.1,2.755,2.755,0,0,1,.8.121,2.23,2.23,0,0,1,.7.386Z' + transform='translate(-18.114 -27.334)' + ></path> + <path + d='M36.846,40.365h1.23L39.283,43.6h.024l1.23-3.233h1.158l-2.026,4.921H38.8Z' + transform='translate(-25.339 -27.603)' + ></path> + </g> + </svg> + ); +}; diff --git a/packages/core/src/elements/Icon/svgs/Filters.tsx b/packages/core/src/elements/Icon/svgs/Filters.tsx new file mode 100644 index 000000000..36bf821f2 --- /dev/null +++ b/packages/core/src/elements/Icon/svgs/Filters.tsx @@ -0,0 +1,29 @@ +import React, { FC } from 'react'; +import classNames from 'classnames'; + +export const Filters: FC<React.SVGProps<SVGSVGElement>> = (props) => { + const { + xmlns = 'http://www.w3.org/2000/svg', + viewBox = '0 0 12 12', + className, + width = '2rem', + height = '2rem', + ...svgProps + } = props; + return ( + <svg + {...svgProps} + width={width} + height={height} + viewBox={viewBox} + xmlns={xmlns} + className={classNames('fe-icon', className)} + > + <path + fill='currentColor' + d='M7 8c.552 0 1 .448 1 1s-.448 1-1 1H5c-.552 0-1-.448-1-1s.448-1 1-1zm2-4c.552 0 1 .448 1 1s-.448 1-1 1H3c-.552 0-1-.448-1-1s.448-1 1-1zm2-4c.552 0 1 .448 1 1s-.448 1-1 1H1c-.552 0-1-.448-1-1s.448-1 1-1z' + transform='translate(-580 -322) translate(296 304) translate(96 16) translate(188 2) translate(0 1)' + /> + </svg> + ); +}; diff --git a/packages/core/src/elements/Icon/svgs/Flash.tsx b/packages/core/src/elements/Icon/svgs/Flash.tsx new file mode 100644 index 000000000..243d4f407 --- /dev/null +++ b/packages/core/src/elements/Icon/svgs/Flash.tsx @@ -0,0 +1,25 @@ +import React, { FC } from 'react'; +import classNames from 'classnames'; + +export const Flash: FC<React.SVGProps<SVGSVGElement>> = (props) => { + const { + xmlns = 'http://www.w3.org/2000/svg', + viewBox = '0 0 24 24', + className, + width = '2rem', + height = '2rem', + ...svgProps + } = props; + return ( + <svg + {...svgProps} + width={width} + height={height} + viewBox={viewBox} + xmlns={xmlns} + className={classNames('fe-icon', className)} + > + <path fill='currentColor' d='M7 2v11h3v9l7-12h-4l4-8z' /> + </svg> + ); +}; diff --git a/packages/core/src/elements/Icon/svgs/Globe.tsx b/packages/core/src/elements/Icon/svgs/Globe.tsx new file mode 100644 index 000000000..c7a1b82f3 --- /dev/null +++ b/packages/core/src/elements/Icon/svgs/Globe.tsx @@ -0,0 +1,28 @@ +import React, { FC } from 'react'; +import classNames from 'classnames'; + +export const Globe: FC<React.SVGProps<SVGSVGElement>> = (props) => { + const { + xmlns = 'http://www.w3.org/2000/svg', + viewBox = '0 0 32 32', + className, + width = '2rem', + height = '2rem', + ...svgProps + } = props; + return ( + <svg + {...svgProps} + width={width} + height={height} + viewBox={viewBox} + xmlns={xmlns} + className={classNames('fe-icon', className)} + > + <path + d='M15.5,0C6.939,0,0,6.939,0,15.5S6.939,31,15.5,31S31,24.061,31,15.5S24.061,0,15.5,0z M28.975,15 h-5.996c-0.055-2.215-0.449-4.331-1.148-6.268c1.35-0.563,2.598-1.312,3.736-2.197C27.59,8.806,28.855,11.755,28.975,15z M14.947,28.972c-1.662-1.384-3.056-3.181-4.076-5.268c1.309-0.409,2.696-0.633,4.129-0.679v5.949 C14.982,28.974,14.965,28.973,14.947,28.972z M16.051,2.028c1.886,1.57,3.425,3.673,4.466,6.126c-1.426,0.487-2.941,0.77-4.518,0.82 V2.025C16.018,2.026,16.034,2.027,16.051,2.028z M17.717,2.198c2.76,0.459,5.236,1.748,7.164,3.614 c-1.047,0.803-2.191,1.483-3.428,1.998C20.543,5.653,19.266,3.746,17.717,2.198z M15,2.025v6.949 c-1.577-0.051-3.093-0.333-4.518-0.82c1.041-2.453,2.58-4.556,4.466-6.126C14.966,2.027,14.982,2.026,15,2.025z M9.546,7.811 c-1.235-0.515-2.38-1.195-3.427-1.998c1.927-1.866,4.404-3.155,7.164-3.614C11.733,3.746,10.457,5.653,9.546,7.811z M10.105,9.076 C11.647,9.611,13.29,9.923,15,9.975V15H9.021C9.075,12.906,9.446,10.905,10.105,9.076z M15,16v6.025 c-1.577,0.048-3.102,0.306-4.539,0.769C9.595,20.732,9.084,18.435,9.021,16H15z M13.283,28.802 c-2.473-0.411-4.719-1.488-6.545-3.052c0.985-0.683,2.05-1.261,3.188-1.7C10.798,25.858,11.939,27.463,13.283,28.802z M16,28.975 v-5.949c1.432,0.046,2.82,0.27,4.129,0.679c-1.021,2.087-2.414,3.884-4.076,5.268C16.036,28.973,16.018,28.974,16,28.975z M21.074,24.05c1.137,0.439,2.201,1.018,3.188,1.7c-1.826,1.563-4.072,2.641-6.545,3.052C19.061,27.463,20.201,25.858,21.074,24.05z M20.539,22.794c-1.438-0.463-2.963-0.721-4.539-0.769V16h5.979C21.916,18.435,21.404,20.732,20.539,22.794z M16,15V9.975 c1.709-0.052,3.352-0.363,4.895-0.898c0.658,1.829,1.029,3.83,1.084,5.924H16z M5.434,6.535C6.572,7.42,7.82,8.169,9.169,8.732 C8.47,10.669,8.076,12.785,8.021,15H2.025C2.145,11.755,3.41,8.806,5.434,6.535z M2.025,16h5.996 c0.062,2.555,0.596,4.968,1.503,7.137c-1.267,0.494-2.448,1.152-3.538,1.931C3.638,22.731,2.156,19.536,2.025,16z M25.014,25.067 c-1.09-0.778-2.271-1.437-3.539-1.931c0.908-2.169,1.441-4.582,1.504-7.137h5.996C28.844,19.536,27.361,22.731,25.014,25.067z' + fill='#333332' + /> + </svg> + ); +}; diff --git a/packages/core/src/elements/Icon/svgs/List.tsx b/packages/core/src/elements/Icon/svgs/List.tsx new file mode 100644 index 000000000..94cb6b1bf --- /dev/null +++ b/packages/core/src/elements/Icon/svgs/List.tsx @@ -0,0 +1,43 @@ +import React, { FC } from 'react'; +import classNames from 'classnames'; + +export const List: FC<React.SVGProps<SVGSVGElement>> = (props) => { + const { + xmlns = 'http://www.w3.org/2000/svg', + viewBox = '0 0 12 12', + className, + width = '2rem', + height = '2rem', + ...svgProps + } = props; + return ( + <svg + {...svgProps} + width={width} + height={height} + viewBox={viewBox} + xmlns={xmlns} + className={classNames('fe-icon', className)} + > + <g transform='translate(-1.502 -5.866)'> + <path + id='Path_1573' + d='M1.922 6.707h7.16a.42.42 0 0 0 0-.841h-7.16a.42.42 0 1 0 0 .841z' + data-name='Path 1573' + /> + <path + id='Path_1574' + d='M9.082 16.267h-7.16a.42.42 0 0 0 0 .841h7.16a.42.42 0 0 0 0-.841z' + data-name='Path 1574' + transform='translate(0 -7.879)' + /> + <path + id='Path_1575' + d='M7.683 26.667H1.922a.42.42 0 1 0 0 .841h5.76a.42.42 0 1 0 0-.841z' + data-name='Path 1575' + transform='translate(0 -15.757)' + /> + </g> + </svg> + ); +}; diff --git a/packages/core/src/elements/Icon/svgs/Pdf.tsx b/packages/core/src/elements/Icon/svgs/Pdf.tsx new file mode 100644 index 000000000..3f4e3265b --- /dev/null +++ b/packages/core/src/elements/Icon/svgs/Pdf.tsx @@ -0,0 +1,46 @@ +import React, { FC } from 'react'; +import classNames from 'classnames'; + +export const Pdf: FC<React.SVGProps<SVGSVGElement>> = (props) => { + const { + xmlns = 'http://www.w3.org/2000/svg', + viewBox = '0 0 21 21', + className, + width = '21px', + height = '21px', + ...svgProps + } = props; + return ( + <svg + className={classNames('fe-icon', className)} + {...svgProps} + width={width} + height={height} + viewBox={viewBox} + xmlns={xmlns} + > + <g transform='translate(0 0)'> + <path + d='M22.267,14.586a.909.909,0,0,0-.895.895V26.124a1.611,1.611,0,0,1-1.6,1.6H7.319a1.611,1.611,0,0,1-1.6-1.6V13.667a1.611,1.611,0,0,1,1.6-1.6H17.744a.895.895,0,0,0,0-1.79H7.319a3.4,3.4,0,0,0-3.386,3.386V26.124A3.4,3.4,0,0,0,7.319,29.51H19.776a3.4,3.4,0,0,0,3.386-3.386V15.481A.893.893,0,0,0,22.267,14.586Z' + transform='translate(-3.933 -8.032)' + ></path> + <path + d='M37.119,3.864a.895.895,0,0,0,0,1.79h3.048L31.821,14a.906.906,0,0,0,0,1.258.876.876,0,0,0,1.258,0l8.441-8.441V9.863a.895.895,0,1,0,1.79,0v-6Z' + transform='translate(-21.88 -3.864)' + ></path> + <path + d='M12.834,40.572H14.7a3.351,3.351,0,0,1,.726.073,1.513,1.513,0,0,1,.6.242,1.256,1.256,0,0,1,.411.46,1.887,1.887,0,0,1,0,1.451,1.273,1.273,0,0,1-.387.484,1.556,1.556,0,0,1-.581.242,3.352,3.352,0,0,1-.726.073h-.8V45.53H12.858V40.572Zm1.088,2.1h.726a1.178,1.178,0,0,0,.29-.024.724.724,0,0,0,.242-.1.448.448,0,0,0,.169-.194.516.516,0,0,0,.073-.29.5.5,0,0,0-.314-.484.67.67,0,0,0-.314-.073c-.121,0-.218-.024-.314-.024h-.556v1.185Z' + transform='translate(-9.714 -27.704)' + ></path> + <path + d='M25.461,40.572h1.645a4.439,4.439,0,0,1,1.137.145,2.823,2.823,0,0,1,.943.435,1.98,1.98,0,0,1,.629.774,2.426,2.426,0,0,1,.242,1.161,2.46,2.46,0,0,1-.218,1.064,2.393,2.393,0,0,1-.6.774,2.906,2.906,0,0,1-.895.484,3.915,3.915,0,0,1-1.064.169h-1.79V40.572Zm1.088,3.967h.581a3.283,3.283,0,0,0,.7-.073,1.326,1.326,0,0,0,.556-.266,1.113,1.113,0,0,0,.387-.484,1.615,1.615,0,0,0,.145-.726,1.272,1.272,0,0,0-.145-.629,1.3,1.3,0,0,0-.363-.435,1.7,1.7,0,0,0-.556-.266,2.7,2.7,0,0,0-.653-.073h-.653Z' + transform='translate(-17.914 -27.704)' + ></path> + <path + d='M40.848,40.572h3.338v1.016H41.936V42.6h2.08V43.62h-2.08v1.935H40.848Z' + transform='translate(-27.908 -27.704)' + ></path> + </g> + </svg> + ); +}; diff --git a/packages/core/src/elements/Icon/svgs/PersonAdd.tsx b/packages/core/src/elements/Icon/svgs/PersonAdd.tsx new file mode 100644 index 000000000..ba69e1831 --- /dev/null +++ b/packages/core/src/elements/Icon/svgs/PersonAdd.tsx @@ -0,0 +1,54 @@ +import React, { FC } from 'react'; +import classNames from 'classnames'; + +export const PersonAdd: FC<React.SVGProps<SVGSVGElement>> = (props) => { + const { + xmlns = 'http://www.w3.org/2000/svg', + viewBox = '0 0 24 24', + className, + width = '2rem', + height = '2rem', + ...svgProps + } = props; + return ( + <svg + {...svgProps} + width={width} + height={height} + viewBox={viewBox} + xmlns={xmlns} + className={classNames('fe-icon', className)} + > + <path + fill='currentColor' + d='M 12 3 A 4 4 0 0 0 8 7 A 4 4 0 0 0 12 11 A 4 4 0 0 0 16 7 A 4 4 0 0 0 12 3 z M 18 12 C 14.7 12 12 14.7 12 18 C 12 21.3 14.7 24 18 24 C 21.3 24 24 21.3 24 18 C 24 14.7 21.3 12 18 12 z M 11.050781 14.046875 C 7.8907812 14.308875 3 15.796 3 18.5 L 3 21 L 10.587891 21 C 10.211891 20.073 10 19.062 10 18 C 10 16.56 10.384781 15.213875 11.050781 14.046875 z M 17 15 L 19 15 L 19 17 L 21 17 L 21 19 L 19 19 L 19 21 L 17 21 L 17 19 L 15 19 L 15 17 L 17 17 L 17 15 z' + /> + </svg> + ); +}; + +export const Profile: FC<React.SVGProps<SVGSVGElement>> = (props) => { + const { + xmlns = 'http://www.w3.org/2000/svg', + viewBox = '0 0 24 24', + className, + width = '2rem', + height = '2rem', + ...svgProps + } = props; + return ( + <svg + {...svgProps} + width={width} + height={height} + viewBox={viewBox} + xmlns={xmlns} + className={classNames('fe-icon', className)} + > + <path + fill='currentColor' + d='M9 11.75c-.69 0-1.25.56-1.25 1.25s.56 1.25 1.25 1.25 1.25-.56 1.25-1.25-.56-1.25-1.25-1.25zm6 0c-.69 0-1.25.56-1.25 1.25s.56 1.25 1.25 1.25 1.25-.56 1.25-1.25-.56-1.25-1.25-1.25zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8 0-.29.02-.58.05-.86 2.36-1.05 4.23-2.98 5.21-5.37C11.07 8.33 14.05 10 17.42 10c.78 0 1.53-.09 2.25-.26.21.71.33 1.47.33 2.26 0 4.41-3.59 8-8 8z' + /> + </svg> + ); +}; diff --git a/packages/core/src/elements/Icon/svgs/SortArrows.tsx b/packages/core/src/elements/Icon/svgs/SortArrows.tsx new file mode 100644 index 000000000..3e7a65dac --- /dev/null +++ b/packages/core/src/elements/Icon/svgs/SortArrows.tsx @@ -0,0 +1,49 @@ +import React, { FC } from 'react'; +import classNames from 'classnames'; + +export const SortArrows: FC<React.SVGProps<SVGSVGElement>> = (props) => { + const { + xmlns = 'http://www.w3.org/2000/svg', + viewBox = '0 0 12 12', + className, + width = '2rem', + height = '2rem', + ...svgProps + } = props; + const children = props.children ?? ( + <> + <path fill='currentColor' d='M 2.497 3.989 L 9.497 3.989 L 5.997 -0.011 L 2.497 3.989 Z' /> + <path fill='currentColor' d='M 5.997 11.989 L 9.497 7.989 L 2.497 7.989 L 5.997 11.989 Z' /> + </> + ); + return ( + <svg + {...svgProps} + width={width} + height={height} + viewBox={viewBox} + xmlns={xmlns} + className={classNames('fe-icon', className)} + > + {children} + </svg> + ); +}; + +export const SortArrowsAsc: FC<React.SVGProps<SVGSVGElement>> = (props) => { + return ( + <SortArrows {...props}> + <path fill='var(--color-primary-dark)' d='M 2.497 3.989 L 9.497 3.989 L 5.997 -0.011 L 2.497 3.989 Z' /> + <path fill='currentColor' d='M 5.997 11.989 L 9.497 7.989 L 2.497 7.989 L 5.997 11.989 Z' /> + </SortArrows> + ); +}; + +export const SortArrowsDesc: FC<React.SVGProps<SVGSVGElement>> = (props) => { + return ( + <SortArrows {...props}> + <path fill='currentColor' d='M 2.497 3.989 L 9.497 3.989 L 5.997 -0.011 L 2.497 3.989 Z' /> + <path fill='var(--color-primary-dark)' d='M 5.997 11.989 L 9.497 7.989 L 2.497 7.989 L 5.997 11.989 Z' /> + </SortArrows> + ); +}; diff --git a/packages/core/src/elements/Icon/svgs/VerticalDots.tsx b/packages/core/src/elements/Icon/svgs/VerticalDots.tsx new file mode 100644 index 000000000..9764c02e7 --- /dev/null +++ b/packages/core/src/elements/Icon/svgs/VerticalDots.tsx @@ -0,0 +1,28 @@ +import React, { FC } from 'react'; +import classNames from 'classnames'; + +export const VerticalDots: FC<React.SVGProps<SVGSVGElement>> = (props) => { + const { + xmlns = 'http://www.w3.org/2000/svg', + viewBox = '0 0 24 24', + className, + width = '2rem', + height = '2rem', + ...svgProps + } = props; + return ( + <svg + {...svgProps} + width={width} + height={height} + viewBox={viewBox} + xmlns={xmlns} + className={classNames('fe-icon', className)} + > + <path + fill='currentColor' + d='M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z' + /> + </svg> + ); +}; diff --git a/packages/core/src/elements/Icon/svgs/Visibility.tsx b/packages/core/src/elements/Icon/svgs/Visibility.tsx new file mode 100644 index 000000000..1ea703a14 --- /dev/null +++ b/packages/core/src/elements/Icon/svgs/Visibility.tsx @@ -0,0 +1,59 @@ +import React, { forwardRef } from 'react'; +import classNames from 'classnames'; + +export const Visibility = forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>((props, ref) => { + const { + xmlns = 'http://www.w3.org/2000/svg', + viewBox = '0 0 24 24', + className, + width = '2rem', + height = '2rem', + ...svgProps + } = props; + return ( + <svg + {...svgProps} + ref={ref} + width={width} + height={height} + viewBox={viewBox} + xmlns={xmlns} + className={classNames('fe-icon', className)} + > + <path + d='M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5z + M12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5z + m0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z' + /> + </svg> + ); +}); + +export const VisibilityOff = forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>((props, ref) => { + const { + xmlns = 'http://www.w3.org/2000/svg', + viewBox = '0 0 24 24', + className, + width = '2rem', + height = '2rem', + ...svgProps + } = props; + return ( + <svg + {...svgProps} + ref={ref} + width={width} + height={height} + viewBox={viewBox} + xmlns={xmlns} + className={classNames('fe-icon', className)} + > + <path + d='M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7z + M2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27z + M7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2z + m4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z' + /> + </svg> + ); +}); diff --git a/packages/core/src/elements/Icon/svgs/Warning.tsx b/packages/core/src/elements/Icon/svgs/Warning.tsx new file mode 100644 index 000000000..792806d4d --- /dev/null +++ b/packages/core/src/elements/Icon/svgs/Warning.tsx @@ -0,0 +1,28 @@ +import React, { FC } from 'react'; +import classNames from 'classnames'; + +export const Warning: FC<React.SVGProps<SVGSVGElement>> = (props) => { + const { + xmlns = 'http://www.w3.org/2000/svg', + viewBox = '0 0 24 24', + className, + width = '2rem', + height = '2rem', + ...svgProps + } = props; + return ( + <svg + {...svgProps} + width={width} + height={height} + viewBox={viewBox} + xmlns={xmlns} + className={classNames('fe-icon', className)} + > + <path + d='M22.824 18.794L13.323.98c-.564-1.059-2.082-1.059-2.648 0L1.176 18.794C.644 19.792 1.368 21 2.501 21h19c1.131 0 1.855-1.207 1.323-2.206zM12 18c-.828 0-1.5-.672-1.5-1.5S11.172 15 12 15s1.5.672 1.5 1.5S12.828 18 12 18zm1.5-4.5h-3v-6h3v6z' + transform='translate(-809 -382) translate(745 184) translate(32 146) translate(32 52) translate(0 1.5)' + /> + </svg> + ); +}; diff --git a/packages/core/src/elements/Input/FeInput.scss b/packages/core/src/elements/Input/FeInput.scss new file mode 100644 index 000000000..6e694352b --- /dev/null +++ b/packages/core/src/elements/Input/FeInput.scss @@ -0,0 +1,144 @@ +@import '../../styles/mixin.scss'; + +.fe-input { + $class: &; + + display: inline-flex; + position: relative; + flex-direction: column; + vertical-align: middle; + + @include with-full-width; + + &__header { + display: flex; + margin-bottom: var(--element-spacing); + font-size: var(--element-font-size); + + & * > { + background-color: inherit; + color: inherit; + font-size: var(--element-font-size); + font-weight: 600; + } + + #{$class}__label-button { + margin-left: auto; + border: none; + padding: 0; + height: auto; + filter: none; + text-decoration: underline; + } + } + + &-in-form { + margin: 0.5rem 0 1rem; + } + + &__inner { + overflow: hidden; + display: inline-flex; + position: relative; + vertical-align: middle; + align-items: center; + font-family: var(--element-font-family); + font-size: var(--element-font-size); + height: var(--element-height); + width: 100%; + border-radius: var(--element-border-radius-sm); + background-color: var(--color-white); + color: var(--color-gray-8); + border: 1px solid var(--element-border-color); + + @include with-size { + border-radius: var(--element-border-radius-sm); + padding: 0; + } + + @include with-theme { + background-color: var(--color-white); + color: var(--color-gray-8); + border: 1px solid var(--element-border-color); + } + + @include with-theme(disabled) { + background-color: var(--color-gray-0); + color: var(--color-text-disabled); + + border: 1px solid var(--background-disabled); + opacity: 0.85; + } + + &:hover { + border-color: var(--color-gray-5); + } + + &:focus-within { + border-color: var(--theme-color); + } + + #{$class}__input { + padding: 0 var(--element-padding); + width: 100%; + height: 100%; + border: none; + outline: none; + background-color: inherit; + font-family: inherit; + font-size: inherit; + color: inherit; + } + + &-error { + border-color: var(--color-danger); + color: var(--color-danger); + } + + &-multi { + vertical-align: middle; + min-height: var(--element-height); + height: auto; + padding: 0; + overflow: hidden; + + #{$class}__input { + padding: var(--element-padding); + } + } + + .fe-icon { + cursor: pointer; + user-select: none; + @include with-clickable { + border-radius: 50%; + width: var(--size-icon); + height: var(--size-icon); + } + } + } + &-with-prefix-icon { + #{$class}__input { + padding-left: 2rem; + } + .fe-icon { + margin-right: -1.5rem; + z-index: 1; + margin-left: 0.5rem; + } + } + &-with-suffix-icon { + #{$class}__input { + padding-right: 2rem; + } + .fe-icon { + margin-left: -2rem; + margin-right: 0.5rem; + } + } + + &__error { + color: var(--color-danger); + font-size: var(--element-font-size-sm); + } +} diff --git a/packages/core/src/elements/Input/FeInput.tsx b/packages/core/src/elements/Input/FeInput.tsx new file mode 100644 index 000000000..bea251765 --- /dev/null +++ b/packages/core/src/elements/Input/FeInput.tsx @@ -0,0 +1,124 @@ +import React, { + useState, + forwardRef, + ChangeEvent, + useCallback, + InputHTMLAttributes, + TextareaHTMLAttributes, +} from 'react'; +import classNames from 'classnames'; +import { FeIcon } from '../Icon/FeIcon'; +import { InputProps } from './interfaces'; +import { FeButton } from '../Button/FeButton'; +import { ClassNameGenerator } from '../../styles'; +import './FeInput.scss'; + +const prefixCls = 'fe-input'; +export const FeInput = forwardRef<HTMLInputElement, InputProps>((props, forwardRef) => { + const { + size, + label, + error, + inForm, + variant, + className, + multiline, + fullWidth, + prefixIcon, + suffixIcon, + labelButton, + onSearch: propsOnSearch, + type: propsType = 'text', + ...restProps + } = props; + const { iconAction, ...propsWithoutJunk } = restProps; + + const [showPassword, setShowPassword] = useState(false); + const togglePassword = useCallback(() => setShowPassword((_) => !_), []); + + const [text, setText] = useState(props.value); + const changeText = useCallback((e: ChangeEvent<HTMLInputElement>) => setText(e.target.value), []); + + const onSearch = useCallback(() => propsOnSearch?.(text as string), [text]); + + const withPrefixIcon = !!prefixIcon; + const withSuffixIcon = propsType === 'password' || propsType === 'search' || !!suffixIcon; + + const classes = ClassNameGenerator.generate( + { + prefixCls, + className, + isFullWidth: fullWidth, + }, + inForm && 'in-form', + withPrefixIcon && `with-prefix-icon`, + withSuffixIcon && `with-suffix-icon` + ); + + const innerClasses = ClassNameGenerator.generate( + { + prefixCls: `${prefixCls}__inner`, + className, + size, + theme: props.disabled ? 'disabled' : variant, + }, + multiline && 'multi', + error && 'error' + ); + + const clickableIconClasses = ClassNameGenerator.generate({ + prefixCls: 'fe-icon', + isClickable: true, + }); + + const element = multiline ? 'textarea' : 'input'; + const type = propsType === 'password' && showPassword ? 'text' : propsType === 'search' ? 'text' : propsType; + + return ( + <div className={classes}> + {(label || labelButton) && ( + <div className={`${prefixCls}__header`}> + {label && <div className={`${prefixCls}__label`}>{label}</div>} + + {labelButton && ( + <FeButton + tabIndex={-1} + className={classNames(`${prefixCls}__label-button`, labelButton.className)} + {...labelButton} + /> + )} + </div> + )} + + <div className={innerClasses}> + {prefixIcon} + + {React.createElement<InputHTMLAttributes<HTMLInputElement> | TextareaHTMLAttributes<HTMLTextAreaElement>>( + element, + { + ...propsWithoutJunk, + className: `${prefixCls}__input`, + value: props.hasOwnProperty('value') ? props.value : text, + onChange: props.onChange || changeText, + type, + ref: forwardRef, + } as any + )} + + {propsType === 'password' && ( + <FeIcon + className={clickableIconClasses} + name={showPassword ? 'visibility' : 'visibility-off'} + onClick={togglePassword} + /> + )} + + {propsType === 'search' && <FeIcon className={clickableIconClasses} name='search' onClick={onSearch} />} + + {suffixIcon} + </div> + + {error && <div className={`${prefixCls}__error`}>{error}</div>} + </div> + ); +}); diff --git a/packages/core/src/elements/Input/Input.tsx b/packages/core/src/elements/Input/Input.tsx new file mode 100644 index 000000000..af2bed427 --- /dev/null +++ b/packages/core/src/elements/Input/Input.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { ElementsFactory } from '../../ElementsFactory/ElementsFactory'; +import { useField, useFormikContext } from 'formik'; +import { InputProps } from './interfaces'; + +export const Input = (props: InputProps) => React.createElement(ElementsFactory.getElement('Input'), props); + +export const FInput = ({ dontDisableSaving, ...props }: InputProps & { name: string; dontDisableSaving?: boolean }) => { + const [inputProps, { touched, error }] = useField(props.name); + const { isSubmitting } = useFormikContext(); + const { onChange } = props; + + return ( + <Input + inForm + {...inputProps} + {...props} + disabled={(!dontDisableSaving && isSubmitting) || props.disabled} + fullWidth={props.fullWidth ?? true} + onChange={(e) => { + onChange?.(e); + inputProps.onChange(e); + }} + error={touched && error ? error : undefined} + /> + ); +}; diff --git a/packages/core/src/elements/Input/index.ts b/packages/core/src/elements/Input/index.ts new file mode 100644 index 000000000..f1654dba9 --- /dev/null +++ b/packages/core/src/elements/Input/index.ts @@ -0,0 +1,2 @@ +export * from './Input'; +export * from './interfaces'; diff --git a/packages/core/src/elements/Input/interfaces.ts b/packages/core/src/elements/Input/interfaces.ts new file mode 100644 index 000000000..18d994122 --- /dev/null +++ b/packages/core/src/elements/Input/interfaces.ts @@ -0,0 +1,20 @@ +import React, { ReactElement } from 'react'; +import { FormFieldProps } from '../../ElementsFactory'; +import { Theme } from '../../styles'; +import { ButtonProps } from '../Button'; + +export type InputType = 'text' | 'password' | 'search' | 'file' | 'email'; + +export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size' | 'type'>, FormFieldProps { + label?: JSX.Element | string; + type?: InputType; + variant?: Theme; + labelButton?: ButtonProps; + fullWidth?: boolean; + multiline?: boolean; + error?: string; + prefixIcon?: ReactElement; + suffixIcon?: ReactElement; + onSearch?: (text: InputProps['value']) => void; + iconAction?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void; +} diff --git a/packages/core/src/elements/InputChip/FeInputChip.scss b/packages/core/src/elements/InputChip/FeInputChip.scss new file mode 100644 index 000000000..fb8e42518 --- /dev/null +++ b/packages/core/src/elements/InputChip/FeInputChip.scss @@ -0,0 +1,9 @@ +.fe-inputChip { + .fe-chip { + margin-right: var(--element-spacing); + } + .fe-input__inner { + height: auto; + min-height: var(--element-height); + } +} diff --git a/packages/core/src/elements/InputChip/FeInputChip.tsx b/packages/core/src/elements/InputChip/FeInputChip.tsx new file mode 100644 index 000000000..a34a5b2c3 --- /dev/null +++ b/packages/core/src/elements/InputChip/FeInputChip.tsx @@ -0,0 +1,34 @@ +import React, { forwardRef, useRef } from 'react'; +import classNames from 'classnames'; +import { IInputChip } from './interfaces'; +import { FeGrid } from '../Grid/FeGrid'; +import { FeChip } from '../Chip/FeChip'; +import './FeInputChip.scss'; +import { useCombinedRefs } from '../../hooks'; + +export const FeInputChip = forwardRef<HTMLInputElement, IInputChip>( + ({ chips, label, onDelete, fullWidth, className, error, ...inputProps }, ref) => { + const inputRef = useRef<HTMLInputElement>(null); + const refCallback = useCombinedRefs<HTMLInputElement>([ref, inputRef]); + + return ( + <div + className={classNames('fe-input fe-inputChip', className, { 'fe-input-full-width': fullWidth })} + onClick={() => inputRef.current?.focus()} + > + {!!label && <div>{label}</div>} + <FeGrid container className='fe-input__inner'> + {chips.map((val, idx) => ( + <FeGrid item key={idx} className='fe-mt-1 fe-mb-1'> + <FeChip label={val} onDelete={() => onDelete(idx)} /> + </FeGrid> + ))} + <FeGrid item> + <input {...inputProps} className='fe-input__input' ref={refCallback} /> + </FeGrid> + </FeGrid> + {error && <div className='fe-error'>{error}</div>} + </div> + ); + } +); diff --git a/packages/core/src/elements/InputChip/InputChip.tsx b/packages/core/src/elements/InputChip/InputChip.tsx new file mode 100644 index 000000000..433950bcc --- /dev/null +++ b/packages/core/src/elements/InputChip/InputChip.tsx @@ -0,0 +1,93 @@ +import React, { FC, ChangeEvent, KeyboardEvent, useCallback, useEffect, useRef } from 'react'; +import { useField, useFormikContext, setIn, getIn } from 'formik'; +import { ElementsFactory } from '../../ElementsFactory'; +import { InputChipProps } from './interfaces'; +import { useDebounce } from '../../hooks'; + +export const InputChip: FC<InputChipProps> = ({ onChange, validate, value = [], ...props }) => { + const inputRef = useRef<HTMLInputElement>(null); + const onSave = useCallback(async () => { + const inputValue = inputRef.current?.value ?? ''; + if (!inputValue) return; + if (validate && !(await validate([...value, inputValue]))) { + return; + } + if (inputRef.current) { + inputRef.current.value = ''; + } + const ev2 = new Event('input', { bubbles: true }); + inputRef.current?.dispatchEvent(ev2); + + onChange && onChange([...value, inputValue]); + return; + }, [inputRef, value, validate]); + + const onKeyPress = useCallback( + async (e: KeyboardEvent<HTMLInputElement>) => { + if (e.key === 'Enter') { + await onSave(); + } + return; + }, + [onSave] + ); + + const onDelete = useCallback( + (idx: number) => { + onChange && onChange([...value.slice(0, idx), ...value.slice(idx + 1)]); + }, + [onChange, value] + ); + + const onBlur = useCallback( + async (e: ChangeEvent<HTMLInputElement>) => { + if (!e.currentTarget.value.trim()) { + e.currentTarget.value = ''; + return; + } + await onSave(); + return; + }, + [onSave] + ); + + return React.createElement(ElementsFactory.getElement('InputChip'), { + ...props, + onBlur, + onKeyPress, + onDelete, + chips: value, + ref: inputRef, + }); +}; + +export const FInputChip: FC<InputChipProps & { name: string }> = ({ name, disabled, onChange, ...props }) => { + const [inputProps, { touched, error }, { setValue, setTouched }] = useField(name); + const { values, isSubmitting, validateForm } = useFormikContext(); + + const debounceError = useDebounce(error, 2000); + + useEffect(() => { + !!debounceError && validateForm(values); + }, [validateForm, debounceError, values]); + + const onValidate = useCallback( + async (value: string[]) => { + !touched && setTouched(true); + const errors = await validateForm(setIn(values, name, value)); + return !getIn(errors, name); + }, + [setTouched, name, values, validateForm] + ); + + return ( + <InputChip + {...inputProps} + {...props} + validate={onValidate} + disabled={isSubmitting || disabled} + error={touched && error ? error : undefined} + onChange={onChange ?? ((val) => setValue(val))} + /> + ); +}; diff --git a/packages/core/src/elements/InputChip/index.ts b/packages/core/src/elements/InputChip/index.ts new file mode 100644 index 000000000..2b78fb68e --- /dev/null +++ b/packages/core/src/elements/InputChip/index.ts @@ -0,0 +1,2 @@ +export * from './InputChip'; +export * from './interfaces'; diff --git a/packages/core/src/elements/InputChip/interfaces.ts b/packages/core/src/elements/InputChip/interfaces.ts new file mode 100644 index 000000000..6ce9fbe06 --- /dev/null +++ b/packages/core/src/elements/InputChip/interfaces.ts @@ -0,0 +1,21 @@ +import { Ref } from 'react'; + +export interface InputChipProps { + value?: string[]; + disabled?: boolean; + fullWidth?: boolean; + placeholder?: string; + error?: string; + onChange?(value: string[]): void; + className?: string; + label?: JSX.Element; + validate?(newValue: string[]): Promise<boolean>; +} + +export interface IInputChip extends Omit<InputChipProps, 'value' | 'onChange'> { + ref?: Ref<HTMLInputElement> | null; + chips: string[]; + onBlur: React.ChangeEventHandler<HTMLInputElement>; + onDelete(idx: number): void; + onKeyPress: React.KeyboardEventHandler<HTMLInputElement>; +} diff --git a/packages/core/src/elements/Loader/FeLoader.scss b/packages/core/src/elements/Loader/FeLoader.scss new file mode 100644 index 000000000..1182da5bd --- /dev/null +++ b/packages/core/src/elements/Loader/FeLoader.scss @@ -0,0 +1,53 @@ +@import '../../styles/mixin.scss'; + +.fe-loader { + position: relative; + width: var(--element-height); + height: var(--element-height); + display: inline-flex; + + &-center { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + margin: auto; + } + + &__inner { + @include with-theme() { + background-color: transparent; + border: 2px solid var(--color-black-20); + border-bottom: 2px solid; + } + @include with-theme(primary) { + --theme-color: var(--color-primary); + --theme-background: var(--color-primary-lighter); + } + + position: absolute; + width: 100%; + height: 100%; + border-radius: 50%; + animation: spin 0.8s infinite linear; + animation-direction: reverse; + left: 0; + top: 0; + bottom: 0; + right: 0; + margin: auto; + } +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 50% { + transform: rotate(180deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/packages/core/src/elements/Loader/FeLoader.tsx b/packages/core/src/elements/Loader/FeLoader.tsx new file mode 100644 index 000000000..4fb253df3 --- /dev/null +++ b/packages/core/src/elements/Loader/FeLoader.tsx @@ -0,0 +1,38 @@ +import React, { forwardRef } from 'react'; +import { LoaderProps } from './interfaces'; +import { ClassNameGenerator } from '../../styles'; +import classnames from 'classnames'; +import './FeLoader.scss'; + +const prefixCls = 'fe-loader'; +export const FeLoader = forwardRef<HTMLDivElement, LoaderProps>((props, ref) => { + const { className, center, children, variant = 'primary', size, ...restProps } = props; + + const classes = ClassNameGenerator.generate( + { + prefixCls, + className, + }, + center && 'center' + ); + + const innerClasses = ClassNameGenerator.generate({ + prefixCls: `${prefixCls}__inner`, + className, + theme: variant, + }); + + return ( + <div + ref={ref} + className={classes} + {...restProps} + style={{ + width: size ?? 24, + height: size ?? 24, + }} + > + <span className={innerClasses} /> + </div> + ); +}); diff --git a/packages/core/src/elements/Loader/Loader.tsx b/packages/core/src/elements/Loader/Loader.tsx new file mode 100644 index 000000000..4ea022614 --- /dev/null +++ b/packages/core/src/elements/Loader/Loader.tsx @@ -0,0 +1,5 @@ +import React from 'react'; +import { ElementsFactory } from '../../ElementsFactory'; +import { LoaderProps } from './interfaces'; + +export const Loader = (props: LoaderProps) => React.createElement(ElementsFactory.getElement('Loader'), props); diff --git a/packages/core/src/elements/Loader/index.ts b/packages/core/src/elements/Loader/index.ts new file mode 100644 index 000000000..8777efeca --- /dev/null +++ b/packages/core/src/elements/Loader/index.ts @@ -0,0 +1,2 @@ +export * from './Loader'; +export * from './interfaces'; diff --git a/packages/core/src/elements/Loader/interfaces.ts b/packages/core/src/elements/Loader/interfaces.ts new file mode 100644 index 000000000..e375068ed --- /dev/null +++ b/packages/core/src/elements/Loader/interfaces.ts @@ -0,0 +1,7 @@ +import { Size, Theme } from '../../styles'; + +export interface LoaderProps extends React.HTMLAttributes<HTMLElement> { + center?: boolean; + variant?: Theme; + size?: number; +} diff --git a/packages/core/src/elements/Menu/FeMenu.scss b/packages/core/src/elements/Menu/FeMenu.scss new file mode 100644 index 000000000..62f8d1620 --- /dev/null +++ b/packages/core/src/elements/Menu/FeMenu.scss @@ -0,0 +1,4 @@ +.fe-menu__popup { + padding: 0; + overflow: hidden; +} diff --git a/packages/core/src/elements/Menu/FeMenu.tsx b/packages/core/src/elements/Menu/FeMenu.tsx new file mode 100644 index 000000000..726008544 --- /dev/null +++ b/packages/core/src/elements/Menu/FeMenu.tsx @@ -0,0 +1,24 @@ +import React, { FC } from 'react'; +import { MenuProps } from './interfaces'; +import { FePopup } from '../Popup/FePopup'; +import './FeMenu.scss'; +import { FeMenuItem } from '../MenuItem/FeMenuItem'; +import classNames from 'classnames'; + +export const FeMenu: FC<MenuProps> = (props) => { + const withIcons = props.items.reduce((p, n) => p || !!n.icon, false); + return ( + <FePopup + className={classNames('fe-menu__popup', props.className)} + content={ + <div className='fe-menu'> + {props.items.map((item, idx) => ( + <FeMenuItem key={idx} withIcons={withIcons} {...item} /> + ))} + </div> + } + action={'click'} + trigger={props.trigger} + /> + ); +}; diff --git a/packages/core/src/elements/Menu/index.tsx b/packages/core/src/elements/Menu/index.tsx new file mode 100644 index 000000000..07304ca04 --- /dev/null +++ b/packages/core/src/elements/Menu/index.tsx @@ -0,0 +1,9 @@ +import React, { forwardRef } from 'react'; +import { MenuProps } from './interfaces'; +import { ElementsFactory } from '../../ElementsFactory'; + +export * from './interfaces'; + +export const Menu = forwardRef<HTMLDivElement, MenuProps>((props, ref) => + React.createElement(ElementsFactory.getElement('Menu'), { ...props, ref } as any) +); diff --git a/packages/core/src/elements/Menu/interfaces.ts b/packages/core/src/elements/Menu/interfaces.ts new file mode 100644 index 000000000..b7917d041 --- /dev/null +++ b/packages/core/src/elements/Menu/interfaces.ts @@ -0,0 +1,8 @@ +import { ReactElement } from 'react'; +import { MenuItemProps } from '../MenuItem'; + +export interface MenuProps { + trigger: ReactElement; + items: MenuItemProps[]; + className?: string; +} diff --git a/packages/core/src/elements/MenuItem/FeMenuItem.scss b/packages/core/src/elements/MenuItem/FeMenuItem.scss new file mode 100644 index 000000000..d5efda3c0 --- /dev/null +++ b/packages/core/src/elements/MenuItem/FeMenuItem.scss @@ -0,0 +1,36 @@ +.fe-menu-item { + display: flex; + align-items: center; + position: relative; + padding: var(--element-padding); + background: var(--color-white); + cursor: pointer; + + &__with-icons { + padding-left: 3.5rem; + } + + &__loader { + position: absolute; + top: 0; + bottom: 0; + left: 0.63rem; + margin: auto 0; + height: 1.25rem; + width: 1.25rem; + } + + &__icon { + position: absolute; + left: 1.25rem; + top: 0; + bottom: 0; + margin: auto 0; + height: 1.25rem; + width: 1.25rem; + } + + &:hover { + background: var(--color-gray-2); + } +} diff --git a/packages/core/src/elements/MenuItem/FeMenuItem.tsx b/packages/core/src/elements/MenuItem/FeMenuItem.tsx new file mode 100644 index 000000000..4f29f521b --- /dev/null +++ b/packages/core/src/elements/MenuItem/FeMenuItem.tsx @@ -0,0 +1,26 @@ +import React, { FC, useCallback } from 'react'; +import classNames from 'classnames'; +import { MenuItemProps } from './interfaces'; +import { Loader } from '../Loader'; +import './FeMenuItem.scss'; + +export const FeMenuItem: FC<MenuItemProps> = (props) => { + const { withIcons, onClick, className, iconClassName, text, icon, loading } = props; + + const renderIcon = useCallback(() => { + if (loading) return <Loader className='fe-menu-item__loader' />; + if (icon) return React.cloneElement(icon, { className: classNames('fe-menu-item__icon', iconClassName) }); + return null; + }, [icon, iconClassName, loading]); + + return ( + <div + {...props} + onClick={(e: any) => onClick?.(e, props)} + className={classNames('fe-menu-item', className, { 'fe-menu-item__with-icons': withIcons })} + > + {renderIcon()} + {text} + </div> + ); +}; diff --git a/packages/core/src/elements/MenuItem/index.tsx b/packages/core/src/elements/MenuItem/index.tsx new file mode 100644 index 000000000..195be1e65 --- /dev/null +++ b/packages/core/src/elements/MenuItem/index.tsx @@ -0,0 +1,9 @@ +import React, { forwardRef } from 'react'; +import { MenuItemProps } from './interfaces'; +import { ElementsFactory } from '../../ElementsFactory'; + +export * from './interfaces'; + +export const MenuItem = forwardRef<HTMLDivElement, MenuItemProps>((props, ref) => + React.createElement(ElementsFactory.getElement('MenuItem'), { ...props, ref } as any) +); diff --git a/packages/core/src/elements/MenuItem/interfaces.ts b/packages/core/src/elements/MenuItem/interfaces.ts new file mode 100644 index 000000000..ff064f80c --- /dev/null +++ b/packages/core/src/elements/MenuItem/interfaces.ts @@ -0,0 +1,12 @@ +import { ReactElement, MouseEvent } from 'react'; + +export interface MenuItemProps { + withIcons?: boolean; + icon?: ReactElement; + loading?: boolean; + selected?: boolean; + text?: ReactElement | string; + onClick?: (e: MouseEvent<HTMLElement>, item: MenuItemProps) => void; + className?: string; + iconClassName?: string; +} diff --git a/packages/core/src/elements/Pagination/FePagination.scss b/packages/core/src/elements/Pagination/FePagination.scss new file mode 100644 index 000000000..20d1c51ad --- /dev/null +++ b/packages/core/src/elements/Pagination/FePagination.scss @@ -0,0 +1,64 @@ +.fe-pagination { + height: 3rem; + min-height: 3rem; + text-align: center; + justify-content: center; + align-items: center; + display: flex; + flex-direction: row; + background: var(--color-gray-0); + + .fe-button { + border-radius: 50%; + width: 2rem; + height: 2rem; + padding: 0; + color: var(--color-gray-5); + background: var(--color-gray-0); + border: 0; + margin: 0 0.5rem; + justify-content: center; + align-items: center; + display: flex; + + &.fe-button-disabled { + opacity: 0.45; + background: transparent; + } + + svg { + width: 1.2rem; + height: 1.2rem; + } + } + + &-option { + margin: 0.25rem; + font-size: 0.9rem; + width: 2rem; + height: 2rem; + text-align: center; + line-height: 2rem; + cursor: pointer; + border-radius: 50%; + + &:hover { + background: var(--color-gray-2); + } + + &.selected-option { + font-weight: bold; + background: var(--color-primary-lighter); + color: var(--color-primary); + } + } + + .page-separator { + font-size: 0.8rem; + height: 0.5rem; + line-height: 1px; + color: var(--color-gray-5); + letter-spacing: 1.2px; + font-weight: bold; + } +} diff --git a/packages/core/src/elements/Pagination/FePagination.tsx b/packages/core/src/elements/Pagination/FePagination.tsx new file mode 100644 index 000000000..52b7970fa --- /dev/null +++ b/packages/core/src/elements/Pagination/FePagination.tsx @@ -0,0 +1,107 @@ +import React, { FC } from 'react'; +import { FeIcon } from '../Icon/FeIcon'; +import { FeButton } from '../Button/FeButton'; +import classNames from 'classnames'; +import { PaginationProps } from './interfaces'; +import './FePagination.scss'; + +const generatePageButtons = (pageOptions: number[]) => { + return pageOptions + .filter((value, index, self) => self.indexOf(value) === index) + .sort((a, b) => (a > b ? 1 : -1)) + .reduce( + (memo: number[][], item: number) => { + const lastArray = memo[memo.length - 1]; + const lastItem = lastArray[lastArray.length - 1]; + if (item - lastItem === 1) { + lastArray.push(item); + } else { + memo.push([item]); + } + return memo; + }, + [[]] + ) + .filter((arr) => arr.length > 0); +}; + +export const FePagination: FC<PaginationProps> = (props) => { + const { count, onChange, page } = props; + const canPreviousPage = page >= 1; + const canNextPage = page < count; + const pageOptions = Array.from(new Array(count), (x, index) => index + 1); + + if (count < 5) { + return ( + <div className='fe-table__pagination'> + <FeButton disabled={!canPreviousPage} onClick={(e) => onChange(e, page - 1)} data-test-id='leftArrow-btn'> + <FeIcon name='left-arrow' /> + </FeButton> + {pageOptions.map((p) => { + return ( + <div + key={p} + className={classNames('fe-table__pagination-option', { + 'selected-option': p === page, + })} + onClick={(e) => onChange(e, p)} + > + {p} + </div> + ); + })} + <FeButton disabled={!canNextPage} onClick={(e) => onChange(e, page + 1)} data-test-id='RightArrow-btn'> + <FeIcon name='right-arrow' /> + </FeButton> + </div> + ); + } + + const pageButtons: number[] = [page]; + if (page < 5) { + pageButtons.push(0, 1, 2, 3); + } else { + pageButtons.push(0, page - 2, page - 1); + } + if (page > count - 5) { + pageButtons.push(count - 4, count - 3, count - 2, count - 1); + } else { + pageButtons.push(count - 1, page + 1, page + 2); + } + const pageChunks = generatePageButtons(pageButtons); + + return ( + <div className='fe-pagination'> + <FeButton disabled={canPreviousPage} onClick={(e) => onChange(e, page - 1)} data-test-id='leftArrow-btn'> + <FeIcon name='left-arrow' /> + </FeButton> + {pageChunks.map((pageChunk, index) => { + return ( + <React.Fragment key={index}> + {pageChunk.map((p) => { + return ( + <div + key={p} + className={classNames('fe-pagination-option', { + 'selected-option': page === pageOptions[p], + })} + onClick={(e) => onChange(e, pageOptions[p])} + > + {pageOptions[p]} + </div> + ); + })} + {index !== pageChunks.length - 1 && ( + <div key='page-separator' className='page-separator'> + ... + </div> + )} + </React.Fragment> + ); + })} + <FeButton disabled={canNextPage} onClick={(e) => onChange(e, page + 1)} data-test-id='rightArrow-btn'> + <FeIcon name='right-arrow' /> + </FeButton> + </div> + ); +}; diff --git a/packages/core/src/elements/Pagination/index.tsx b/packages/core/src/elements/Pagination/index.tsx new file mode 100644 index 000000000..f1c9d13e7 --- /dev/null +++ b/packages/core/src/elements/Pagination/index.tsx @@ -0,0 +1,9 @@ +import React, { forwardRef } from 'react'; +import { PaginationProps } from './interfaces'; +import { ElementsFactory } from '../../ElementsFactory'; + +export * from './interfaces'; + +export const Pagination = forwardRef<HTMLDivElement, PaginationProps>((props, ref) => + React.createElement(ElementsFactory.getElement('Pagination'), { ...props, ref } as any) +); diff --git a/packages/core/src/elements/Pagination/interfaces.ts b/packages/core/src/elements/Pagination/interfaces.ts new file mode 100644 index 000000000..0eea9d071 --- /dev/null +++ b/packages/core/src/elements/Pagination/interfaces.ts @@ -0,0 +1,7 @@ +import { MouseEvent } from 'react'; + +export interface PaginationProps { + count: number; + page: number; + onChange: (e: MouseEvent<HTMLElement>, value: number) => void; +} diff --git a/packages/core/src/elements/Popup/FePopup.scss b/packages/core/src/elements/Popup/FePopup.scss new file mode 100644 index 000000000..77c34db89 --- /dev/null +++ b/packages/core/src/elements/Popup/FePopup.scss @@ -0,0 +1,11 @@ +.fe-popup { + &__container { + z-index: 10; + border-radius: var(--fe-popup-border-radius, var(--element-border-radius-sm)); + padding: var(--fe-popup-padding, var(--element-padding-lg)); + box-shadow: var(--fe-popup-shadow, var(--popup-shadow)); + background-color: var(--fe-popup-bg, var(--color-white)); + + color: var(--color-gray-9); + } +} diff --git a/packages/core/src/elements/Popup/FePopup.tsx b/packages/core/src/elements/Popup/FePopup.tsx new file mode 100644 index 000000000..47f717883 --- /dev/null +++ b/packages/core/src/elements/Popup/FePopup.tsx @@ -0,0 +1,84 @@ +import { PopupPosition, PopupProps } from './interfaces'; +import React, { forwardRef, useMemo, MouseEvent, useEffect, useRef, useImperativeHandle } from 'react'; +import Popup from 'react-popper-tooltip'; +import './FePopup.scss'; +import classNames from 'classnames'; + +const preparePosition = (p?: PopupPosition): any => { + if (!p) { + return 'bottom'; + } + + if (p.vertical === 'center') { + return p.horizontal === 'center' ? 'auto' : p.horizontal; + } + + if (p.horizontal === 'center') { + return p.vertical; + } + + return `${p.vertical}-${p.horizontal === 'left' ? 'start' : 'end'}`; +}; + +export const FePopup = forwardRef<Popup, PopupProps>((props, ref) => { + const { position, trigger, action, content, className, mountNode, open } = props; + const placement = useMemo(() => preparePosition(position), [position]); + const popupRef = useRef<Popup | null>(null); + + useImperativeHandle<any, any>(ref, () => ({ + ...popupRef.current, + closePopup: () => popupRef.current?.setState({ tooltipShown: false }), + })); + + useEffect(() => { + if (open != null) { + popupRef.current?.setState({ tooltipShown: open }); + } + }, [open]); + + return ( + <Popup + data-test-id='popup-btn' + ref={(node) => { + popupRef.current = node; + if (ref && typeof ref === 'function') { + ref?.(node); + } else if (ref && typeof ref === 'object') { + ref.current = node; + } + }} + trigger={action} + closeOnReferenceHidden={true} + placement={placement} + onVisibilityChange={(visible) => (visible ? props.onOpen?.() : props.onClose?.())} + portalContainer={mountNode} + tooltip={({ tooltipRef, getTooltipProps }) => { + return ( + <div + {...getTooltipProps({ + ref: tooltipRef, + className: classNames('fe-popup__container', className), + onClick: (e: MouseEvent) => e.stopPropagation(), + })} + > + {typeof content === 'function' ? content() : content} + </div> + ); + }} + > + {({ getTriggerProps, triggerRef: ref }) => { + return ( + <span + ref={ref} + {...getTriggerProps({ + ref, + onClick: (e: MouseEvent) => e.stopPropagation(), + })} + > + {trigger} + </span> + ); + }} + </Popup> + ); +}); diff --git a/packages/core/src/elements/Popup/Popup.tsx b/packages/core/src/elements/Popup/Popup.tsx new file mode 100644 index 000000000..5231af382 --- /dev/null +++ b/packages/core/src/elements/Popup/Popup.tsx @@ -0,0 +1,7 @@ +import { PopupProps } from './interfaces'; +import { ElementsFactory } from '../../ElementsFactory'; +import React, { forwardRef } from 'react'; + +export const Popup = forwardRef<HTMLDivElement, PopupProps>((props, ref) => + React.createElement(ElementsFactory.getElement('Popup'), { ...props, ref } as any) +); diff --git a/packages/core/src/elements/Popup/index.ts b/packages/core/src/elements/Popup/index.ts new file mode 100644 index 000000000..f53d889f3 --- /dev/null +++ b/packages/core/src/elements/Popup/index.ts @@ -0,0 +1,2 @@ +export * from './Popup'; +export * from './interfaces'; diff --git a/packages/core/src/elements/Popup/interfaces.ts b/packages/core/src/elements/Popup/interfaces.ts new file mode 100644 index 000000000..072d58519 --- /dev/null +++ b/packages/core/src/elements/Popup/interfaces.ts @@ -0,0 +1,19 @@ +import { ReactElement, ReactNode } from 'react'; + +export type PopupPosition = { + vertical: 'top' | 'bottom' | 'center'; + horizontal: 'left' | 'right' | 'center'; +}; +export type PopupAction = 'hover' | 'click' | 'focus'; + +export interface PopupProps { + className?: string; + open?: boolean; + position?: PopupPosition; + content: ReactNode; + onOpen?: () => void; + onClose?: () => void; + action: PopupAction; + trigger: ReactElement; + mountNode?: HTMLElement; +} diff --git a/packages/core/src/elements/Select/FeSelect.tsx b/packages/core/src/elements/Select/FeSelect.tsx new file mode 100644 index 000000000..e9061151c --- /dev/null +++ b/packages/core/src/elements/Select/FeSelect.tsx @@ -0,0 +1,101 @@ +import React, { useCallback, useState } from 'react'; +import { SelectProps, SelectOptionProps } from './interfaces'; +import Select, { components, MultiValueProps } from 'react-select'; +import { useT } from '../../hooks'; +import { ClassNameGenerator } from '../../styles'; +import classNames from 'classnames'; + +export const FeSelect = (props: SelectProps) => { + const [open, setOpen] = useState(false); + const { t } = useT(); + const { + label, + value, + onChange, + onClose, + options, + onOpen, + name, + open: openProps, + loading, + noOptionsText, + loadingText, + multiselect, + getOptionLabel, + renderOption, + fullWidth, + onBlur, + disableMenuPortalTarget, + } = props; + + const getState = useCallback( + (option: MultiValueProps<any> | any) => ({ + selected: option.selectProps.isSelected, + disabled: option.selectProps.isDisabled, + index: option.selectProps.options?.findIndex( + (o: SelectOptionProps<string>) => o.value === option.selectProps.value + ), + }), + [] + ); + + const MultiValueLabel = useCallback( + (props) => ( + <components.MultiValueLabel {...props}>{renderOption?.(props.data, getState(props))}</components.MultiValueLabel> + ), + [renderOption] + ); + + const customStyles = { + container: (provided: any, { selectProps: { width } }: any) => ({ + ...provided, + minWidth: '14em', + width: typeof fullWidth === 'boolean' ? width : '100%', + maxWidth: '100%', + }), + menuPortal: (provided: any) => { + const { zIndex, ...rest } = provided; + return { ...rest, zIndex: 1051 }; + }, + }; + + const className = classNames( + ClassNameGenerator.generate({ + prefixCls: 'fe-select', + className: props.className, + isFullWidth: props.fullWidth, + }), + { + 'fe-input__in-form ': props.inForm, + } + ); + + return ( + <Select + isDisabled={props.disabled} + classNamePrefix={'fe-select'} + name={name} + className={className} + styles={customStyles} + isMulti={multiselect ?? false} + placeholder={label} + value={value} + width={fullWidth ? '100%' : 'max-content'} + components={renderOption ? { MultiValueLabel } : {}} + options={options} + menuPortalTarget={disableMenuPortalTarget ? undefined : document.body} + isLoading={loading ?? false} + {...(multiselect && { closeMenuOnSelect: false })} + onBlur={(e) => { + onBlur && onBlur({ ...e, target: { ...e.target, name } }); + }} + menuIsOpen={openProps ?? open} + loadingMessage={() => loadingText ?? `${t('common.loading')}...`} + noOptionsMessage={() => noOptionsText ?? t('common.empty-items')} + onMenuOpen={() => (onOpen ? onOpen : setOpen(true))} + onMenuClose={() => (onClose ? onClose : setOpen(false))} + onChange={(newValues, e: any) => onChange?.(e, newValues ?? [])} + getOptionLabel={(option) => (getOptionLabel ? getOptionLabel(option) : option.label)} + /> + ); +}; diff --git a/packages/core/src/elements/Select/Select.tsx b/packages/core/src/elements/Select/Select.tsx new file mode 100644 index 000000000..0a777dd92 --- /dev/null +++ b/packages/core/src/elements/Select/Select.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { ElementsFactory } from '../../ElementsFactory'; +import { SelectProps } from './interfaces'; +import { useField, useFormikContext } from 'formik'; + +export const Select = (props: SelectProps) => React.createElement(ElementsFactory.getElement('Select'), props); + +export const FSelect = (props: SelectProps & { name: string }) => { + const [inputProps, { touched, error }] = useField(props.name); + const { isSubmitting, setFieldValue } = useFormikContext(); + const { onChange, name } = props; + + return ( + <Select + {...props} + inForm + name={name} + value={inputProps.value} + onBlur={inputProps.onBlur} + disabled={isSubmitting || props.disabled} + fullWidth={props.fullWidth ?? true} + onChange={(e, newValues) => { + onChange?.(e, newValues); + setFieldValue(name, newValues, true); + }} + error={touched && error ? error : undefined} + /> + ); +}; diff --git a/packages/core/src/elements/Select/index.ts b/packages/core/src/elements/Select/index.ts new file mode 100644 index 000000000..d49d0b14f --- /dev/null +++ b/packages/core/src/elements/Select/index.ts @@ -0,0 +1,2 @@ +export * from './Select'; +export * from './interfaces'; diff --git a/packages/core/src/elements/Select/interfaces.tsx b/packages/core/src/elements/Select/interfaces.tsx new file mode 100644 index 000000000..ef1607aad --- /dev/null +++ b/packages/core/src/elements/Select/interfaces.tsx @@ -0,0 +1,42 @@ +import { ReactNode, FocusEventHandler } from 'react'; +import { Size, Theme } from '../../styles'; +import { FormFieldProps } from '../../ElementsFactory'; + +export interface SelectOptionProps<T = any> { + label: string; + value: T; +} + +export interface StateProps { + selected: boolean; + index: number; + disabled: boolean; +} + +export interface SelectProps<T = any> extends FormFieldProps { + className?: string; + name?: string; + value?: T[] | SelectOptionProps; + label?: string; + error?: string; + disabled?: boolean; + placeholder?: string; + fullWidth?: boolean; + onChange?: (e: Event, newValues: T[]) => void; + options: SelectOptionProps<T>[]; + multiselect?: boolean; + loading?: boolean; + getOptionLabel?: (option: SelectOptionProps<T>) => string; + renderOption?: (option: SelectOptionProps<T>, state: StateProps) => ReactNode; + open?: boolean; + onOpen?: () => void; + onClose?: () => void; + onBlur?: FocusEventHandler<HTMLElement & { name?: string }>; + + noOptionsText?: string; + loadingText?: string; + + theme?: Theme; + + disableMenuPortalTarget?: boolean; +} diff --git a/packages/core/src/elements/SwitchToggle/FeSwitchToggle.scss b/packages/core/src/elements/SwitchToggle/FeSwitchToggle.scss new file mode 100644 index 000000000..9432b3b4b --- /dev/null +++ b/packages/core/src/elements/SwitchToggle/FeSwitchToggle.scss @@ -0,0 +1,107 @@ +.fe-switch { + position: relative; + display: inline-block; + width: 3rem; + height: 1.4375rem; + border-radius: 16px; + border: solid 1px var(--element-border-color); +} + +.fe-switch input { + opacity: 0; + width: 0; + height: 0; +} + +.fe-slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 16px; + background-color: var(--color-gray-1); + -webkit-transition: 0.4s; + transition: 0.4s; +} + +.fe-slider:before { + position: absolute; + content: ''; + height: 1.4375rem; + width: 1.4375rem; + border-radius: 50%; + border: solid 1px var(--element-border-color); + left: -1px; + bottom: -1px; + background-color: white; + box-shadow: 0 1px 1px 0 rgba(61, 70, 107, 0.2); + -webkit-transition: 0.4s; + transition: 0.4s; +} + +input:checked + .fe-slider { + background-color: var(--color-primary); +} + +input:focus + .fe-slider { + box-shadow: 0 0 1px var(--color-primary); +} + +input:checked + .fe-slider:before { + transform: translateX(1.625rem); +} + +.fe-switch-toggle { + &__with_labels { + font-size: 0.9rem; + text-transform: uppercase; + color: var(--color-text-disabled); + display: flex; + align-items: center; + justify-content: center; + } + + &__label { + margin: 0 1rem; + transition: color 0.3s ease-in; + cursor: pointer; + user-select: none; + + &:hover { + color: var(--color-text); + } + } + + &__active-left .fe-switch-toggle__label:first-child, + &__active-right .fe-switch-toggle__label:last-child { + color: var(--color-text); + font-weight: bold; + } + + &__disabled { + label { + box-shadow: none; + opacity: 0.5 !important; + } + } + + &__loading { + cursor: progress !important; + + * { + cursor: progress !important; + } + + .fe-slider:before, + input:checked + .fe-slider:before { + transform: translateX(0.8125rem); + } + + label { + box-shadow: none; + opacity: 0.5 !important; + } + } +} diff --git a/packages/core/src/elements/SwitchToggle/FeSwitchToggle.tsx b/packages/core/src/elements/SwitchToggle/FeSwitchToggle.tsx new file mode 100644 index 000000000..2a1c19171 --- /dev/null +++ b/packages/core/src/elements/SwitchToggle/FeSwitchToggle.tsx @@ -0,0 +1,49 @@ +import React, { useRef } from 'react'; +import { SwitchToggleProps } from './interfaces'; +import classNames from 'classnames'; + +import './FeSwitchToggle.scss'; +export const FeSwitchToggle = (props: SwitchToggleProps) => { + const ref = useRef<HTMLInputElement>(null); + const { loading, disabled, value, labels, readOnly, name } = props; + const onChange = disabled || readOnly ? undefined : props.onChange; + + const toggle = ( + <label className='fe-switch'> + <input ref={ref} checked={value} onChange={(e) => onChange?.(e.target.checked)} name={name} type='checkbox' /> + <span className='fe-slider'></span> + </label> + ); + + if (labels) { + return ( + <div + className={classNames('fe-switch-toggle__with_labels', { + 'fe-switch-toggle__active-left': !value, + 'fe-switch-toggle__active-right': value, + 'fe-switch-toggle__disabled': disabled || loading, + 'fe-switch-toggle__loading': loading, + })} + > + <span + className='fe-switch-toggle__label' + onClick={() => { + props.value && ref?.current?.click(); + }} + > + {labels[0]} + </span> + {toggle} + <span + className='fe-switch-toggle__label' + onClick={() => { + !props.value && ref?.current?.click(); + }} + > + {labels[1]} + </span> + </div> + ); + } + return toggle; +}; diff --git a/packages/core/src/elements/SwitchToggle/SwitchToggle.tsx b/packages/core/src/elements/SwitchToggle/SwitchToggle.tsx new file mode 100644 index 000000000..6a7ef6e30 --- /dev/null +++ b/packages/core/src/elements/SwitchToggle/SwitchToggle.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +import { ElementsFactory } from '../../ElementsFactory'; +import { SwitchToggleProps } from './interfaces'; + +export const SwitchToggle = (props: SwitchToggleProps) => + React.createElement(ElementsFactory.getElement('SwitchToggle'), props); diff --git a/packages/core/src/elements/SwitchToggle/index.ts b/packages/core/src/elements/SwitchToggle/index.ts new file mode 100644 index 000000000..0cb0d930f --- /dev/null +++ b/packages/core/src/elements/SwitchToggle/index.ts @@ -0,0 +1,2 @@ +export * from './SwitchToggle'; +export * from './interfaces'; diff --git a/packages/core/src/elements/SwitchToggle/interfaces.tsx b/packages/core/src/elements/SwitchToggle/interfaces.tsx new file mode 100644 index 000000000..674e7b559 --- /dev/null +++ b/packages/core/src/elements/SwitchToggle/interfaces.tsx @@ -0,0 +1,9 @@ +export interface SwitchToggleProps { + name?: string; + loading?: boolean; + disabled?: boolean; + readOnly?: boolean; + value?: boolean; + labels?: [string, string]; + onChange?: (toggled: boolean) => void; +} diff --git a/packages/core/src/elements/Table/FeTable.scss b/packages/core/src/elements/Table/FeTable.scss new file mode 100644 index 000000000..82c6772d3 --- /dev/null +++ b/packages/core/src/elements/Table/FeTable.scss @@ -0,0 +1,265 @@ +.fe-table { + --fe-table-header-bg: var(--color-gray-0); + --fe-table-header-font-color: var(--color-blue-gray-8); + --fe-table-header-icon-color: var(--color-gray-5); + --fe-table-header-font-size: 0.85rem; + --fe-table-header-align: left; + --fe-table-header-height: 3rem; + --fe-table-header-padding: 0 1rem; + + --fe-table-row-divider-width: 1px; + --fe-table-row-divider-color: var(--color-gray-2); + + --fe-table-cell-padding: 0.5rem; + --fe-table-cell-height: 3.5rem; + --fe-table-cell-font-size: 0.85rem; + --fe-table-cell-font-color: var(--color-blue-gray-8); + + --fe-table-elements-border-radius: 0.5rem; + + border-spacing: 0; + width: 100%; + max-height: 100%; + min-height: 100%; + height: 100%; + overflow: hidden; + flex: 1; + display: flex; + flex-direction: column; + + &__container { + position: relative; + display: flex; + height: 100%; + width: 100%; + flex: 1; + flex-direction: column; + max-height: 100vh; + overflow: auto; + } + + &__table-container { + flex: 1; + overflow: auto; + position: relative; + display: flex; + flex-direction: column; + -webkit-overflow-scrolling: touch; + + &-loading { + overflow: hidden; + } + } + + &__thead { + position: sticky; + top: 0; + min-height: var(--fe-table-header-height); + align-items: stretch; + display: flex; + } + + &__thead-tr { + background: var(--fe-table-header-bg); + align-items: stretch; + border-radius: var(--element-border-radius-sm); + } + + &__thead-tr-th { + color: var(--fe-table-header-font-color); + padding: var(--fe-table-header-padding); + text-align: var(--fe-table-header-align); + font-size: var(--fe-table-header-font-size); + line-height: var(--fe-table-header-font-size); + text-transform: uppercase; + position: sticky; + top: 0; + display: flex; + align-items: center; + border-top: 3px solid transparent; + border-bottom: 3px solid transparent; + transition: border 300ms; + + > * { + vertical-align: baseline; + } + + &__first-cell { + padding-left: 2rem; + &__expander { + padding-left: 0; + width: 0; + } + } + + .fe-icon { + height: var(--fe-table-header-font-size); + margin: 0 0.5rem; + width: auto; + color: var(--fe-table-header-icon-color); + } + + &.fe-table__thead-sortable { + &-asc { + border-top: 2px solid var(--color-gray-3); + } + + &-desc { + border-bottom: 2px solid var(--color-gray-3); + } + } + } + + &__spacer { + flex: 1; + } + + &__filter-button { + padding: 0.5rem; + box-sizing: content-box; + transition: all 0.3s; + border-radius: var(--fe-table-elements-border-radius); + cursor: pointer; + + &:hover { + color: var(--fe-table-header-font-color); + background: var(--color-gray-2); + } + } + + &__active-filter { + color: var(--color-primary-dark); + background: var(--color-gray-2); + } + + &__tbody { + order: 1; + flex: 1; + + &__loading { + opacity: 0.8; + } + } + + &__tr { + min-height: var(--fe-table-cell-height); + border-bottom: var(--fe-table-row-divider-width) solid var(--fe-table-row-divider-color); + + &.is-expanded { + border-bottom: var(--fe-table-row-divider-width) solid transparent; + } + } + + &__tr-td { + color: var(--fe-table-header-font-color); + padding: var(--fe-table-cell-padding); + font-size: var(--fe-table-cell-font-size); + display: flex; + align-items: center; + min-height: 5rem; + + &__first-cell { + padding-left: 2rem; + } + + &-empty, + &-loader { + text-align: center; + justify-content: center; + } + } + + &__expand-button.fe-button { + height: 2rem; + width: 2rem; + padding: 0; + + svg { + transition: transform 0.3s; + } + + &.is-expanded > svg { + transform: rotate(90deg); + } + } + + &__tr-expanded-content { + transition: max-height 300ms ease-in-out, padding 200ms ease-in-out, opacity 300ms 200ms ease-in-out; + overflow: auto; + max-height: 0; + opacity: 0; + padding: 0 2rem 0 5rem; + border-bottom: none; + + &.is-expanded { + border-bottom: var(--fe-table-row-divider-width) solid var(--fe-table-row-divider-color); + opacity: 1; + max-height: 400px; + } + } + + &__pagination { + height: 3rem; + min-height: 3rem; + text-align: center; + justify-content: center; + align-items: center; + display: flex; + flex-direction: row; + background: var(--color-gray-0); + + .fe-button { + border-radius: 50%; + width: 2rem; + height: 2rem; + padding: 0; + color: var(--color-gray-5); + background: var(--color-gray-0); + border: 0; + margin: 0 0.5rem; + justify-content: center; + align-items: center; + display: flex; + + &.fe-button-disabled { + opacity: 0.45; + background: transparent; + } + + svg { + width: 1.2rem; + height: 1.2rem; + } + } + + &-option { + margin: 0.25rem; + font-size: 0.9rem; + width: 2rem; + height: 2rem; + text-align: center; + line-height: 2rem; + cursor: pointer; + border-radius: 50%; + + &:hover { + background: var(--color-gray-2); + } + + &.selected-option { + font-weight: bold; + background: var(--color-primary-lighter); + color: var(--color-primary); + } + } + + .page-separator { + font-size: 0.8rem; + height: 0.5rem; + line-height: 1px; + color: var(--color-gray-5); + letter-spacing: 1.2px; + font-weight: bold; + } + } +} diff --git a/packages/core/src/elements/Table/FeTable.tsx b/packages/core/src/elements/Table/FeTable.tsx new file mode 100644 index 000000000..c9049c80a --- /dev/null +++ b/packages/core/src/elements/Table/FeTable.tsx @@ -0,0 +1,307 @@ +import React, { FC, useCallback, useEffect, useMemo, useRef } from 'react'; +import { + FeTableColumnOptions, + FeTableColumnProps, + FeTableInstance, + FeTableState, + FeUseTable, + TableProps, +} from './interfaces'; +import { + useTable, + useFilters, + useSortBy, + UseFiltersState, + UseSortByState, + useExpanded, + Cell, + UseExpandedRowProps, + Row, + Column, + useFlexLayout, + usePagination, + UsePaginationState, + useRowSelect, + UseRowSelectRowProps, + UseRowSelectState, +} from 'react-table'; + +import './FeTable.scss'; +import classNames from 'classnames'; +import { FeButton } from '../Button/FeButton'; +import { FeIcon } from '../Icon/FeIcon'; +import { FeTableTHead, FeTableTHeadProps } from './FeTableTHead'; +import { FeTableTBody, FeTableTBodyProps } from './FeTableTBody'; +import { FeTablePagination, FeTablePaginationProps } from './FeTablePagination'; +import { FeTableToolbar } from './FeTableToolbar'; +import { FeCheckbox } from '../Checkbox/FeCheckbox'; +import { checkTableProps } from './TableUtils'; +import { FeLoader } from '../Loader/FeLoader'; + +const prefixCls = 'fe-table'; +export const FeTable: FC<TableProps> = <T extends object>(props: TableProps<T>) => { + const tableRef = useRef<HTMLDivElement>(null); + const firstRender = useRef<boolean>(true); + const columns = useMemo(() => { + const columns = props.columns.map( + ({ sortable, Filter, Header, ...rest }) => + ({ + ...rest, + disableSortBy: !sortable, + disableFilters: !Filter, + Filter, + Header: Header ?? <div style={{ minWidth: rest.minWidth, maxWidth: rest.maxWidth }} />, + } as FeTableColumnOptions<T>) + ); + if (props.expandable) { + columns.unshift({ + id: 'fe-expander', + minWidth: 60, + maxWidth: '60px' as any, + Header: <div style={{ minWidth: '2rem', maxWidth: '2rem' }} />, + Cell: (cell: Cell<T>) => { + const row = cell.row as Row<T> & UseExpandedRowProps<T>; + return ( + <FeButton + className={classNames('fe-table__expand-button', { 'is-expanded': row.isExpanded })} + {...row.getToggleRowExpandedProps()} + variant={row.isExpanded ? 'primary' : undefined} + > + <FeIcon name='right-arrow' /> + </FeButton> + ); + }, + }); + } + if (props.selection) { + columns.unshift({ + id: 'fe-selection', + minWidth: 60, + maxWidth: '60px' as any, + Cell: (cell: Cell<T>) => { + const row = cell.row as Row<T> & UseRowSelectRowProps<T>; + return ( + <FeCheckbox + {...row.getToggleRowSelectedProps()} + checked={row.isSelected} + onChange={(e) => onRowSelected(row.original, e.target.checked)} + /> + ); + }, + }); + } + return columns as Column<T>[]; + }, [props.columns, props.expandable]); + + const { + getTableProps, + getTableBodyProps, + headerGroups, + rows, + prepareRow, + state, + + // The page controls ;) + page, + canPreviousPage, + canNextPage, + pageOptions, + pageCount, + gotoPage, + nextPage, + previousPage, + setPageSize, + + // select props + toggleAllRowsSelected, + isAllRowsSelected, + selectedFlatRows, + toggleRowSelected, + } = useTable( + { + columns, + data: props.data, + getRowId: (row: any) => row[props.rowKey], + manualSortBy: !!props.onSortChange, + manualFilters: !!props.onFilterChange, + manualPagination: !!props.onPageChange, + manualRowSelectedKey: props.rowKey, + pageCount: !!props.onPageChange ? props.pageCount : undefined, + autoResetPage: !props.onPageChange, + useControlledState: (state1: any, meta) => + ({ + ...state1, + sortBy: props.sortBy ?? state1.sortBy, + filters: props.filters ?? state1.filters, + selectedRowIds: props.selectedRowIds ?? state1.selectedRowIds, + } as FeTableState<T>), + expandSubRows: false, + autoResetExpanded: false, + initialState: { + pageIndex: 0, + pageSize: props.pageSize, + selectedRowIds: props.selectedRowIds || {}, + }, + } as FeUseTable<T>, + useFilters, + useSortBy, + useExpanded, + usePagination, + useRowSelect, + useFlexLayout + ) as FeTableInstance<T>; + + checkTableProps(props); + + const tableState = state as UseSortByState<T> & UseFiltersState<T> & UsePaginationState<T> & UseRowSelectState<T>; + + const onSortChange = useCallback( + (column: FeTableColumnProps<T>) => { + if (props.hasOwnProperty('sortBy')) { + const sortBy = props.isMultiSort ? tableState.sortBy.filter(({ id }) => id !== column.id) : []; + if (!column.isSorted) { + sortBy.push({ id: column.id, desc: false }); + } else if (!column.isSortedDesc) { + sortBy.push({ id: column.id, desc: true }); + } + props.onSortChange?.(sortBy); + } else { + if (column.isSorted && column.isSortedDesc) { + column.clearSortBy(); + } else { + column.toggleSortBy(column.isSorted, props.isMultiSort ?? false); + } + } + }, + [props.onSortChange] + ); + + const onFilterChange = useCallback( + (column: FeTableColumnProps<T>, filterValue?: any) => { + if (props.hasOwnProperty('filters')) { + const filters = tableState.filters.filter(({ id }) => id !== column.id); + if (filterValue != null) { + filters.push({ id: column.id, value: filterValue }); + } + props.onFilterChange?.(filters); + } else { + column.setFilter(filterValue); + } + }, + [props.onFilterChange, tableState] + ); + + const onToggleAllRowsSelected = useCallback( + (value: boolean) => { + if (props.hasOwnProperty('selectedRowIds')) { + const selectedIds = props.data.reduce((p, n: any) => ({ ...p, [n[props.rowKey]]: true }), {}); + props.onRowSelected?.(value ? selectedIds : {}); + } else { + toggleAllRowsSelected(value); + } + }, + [props.onRowSelected] + ); + + const onRowSelected = useCallback( + (row: any, value: boolean) => { + const id = row[props.rowKey]; + if (props.hasOwnProperty('selectedRowIds')) { + const newSelectedRows: any = { ...props.selectedRowIds }; + if (value) { + newSelectedRows[id] = true; + } else { + delete newSelectedRows[id]; + } + props.onRowSelected?.(newSelectedRows); + } else { + toggleRowSelected(id, value); + } + }, + [props.onRowSelected] + ); + + const handleOnPageChange = useCallback(() => { + if (pagination === 'pages') { + tableRef.current?.querySelector(`.${prefixCls}__tbody`)?.scroll?.({ top: 0, left: 0, behavior: 'smooth' }); + } + props.onPageChange?.(tableState.pageSize, tableState.pageIndex); + }, [tableState.pageIndex]); + + useEffect(() => { + !props.hasOwnProperty('sortBy') && props.onSortChange?.(tableState.sortBy); + }, [props.sortBy, tableState.sortBy]); + + useEffect(() => { + !props.hasOwnProperty('filters') && props.onFilterChange?.(tableState.filters); + }, [props.filters, tableState.filters]); + + useEffect(() => { + firstRender.current ? (firstRender.current = false) : handleOnPageChange(); + }, [tableState.pageIndex]); + + useEffect(() => { + !props.hasOwnProperty('selectedRowIds') && props.onRowSelected?.(tableState.selectedRowIds as any); + }, [tableState.selectedRowIds]); + + const tableHeadProps: FeTableTHeadProps<T> = { + prefixCls, + headerGroups, + onSortChange, + onFilterChange, + toggleAllRowsSelected, + isAllRowsSelected, + selectedFlatRows, + }; + + const tableRows: (Row<T> & UseExpandedRowProps<T>)[] = useMemo( + () => (props.pagination ? page : rows) as (Row<T> & UseExpandedRowProps<T>)[], + [page, rows, props.pagination] + ); + + const tablePaginationProps: FeTablePaginationProps<T> = { + pageIndex: tableState.pageIndex, + pageSize: tableState.pageSize, + canPreviousPage, + canNextPage, + pageOptions, + pageCount, + gotoPage, + nextPage, + previousPage, + setPageSize, + }; + + const { className, toolbar, loading, pagination, pageSize } = props; + + return ( + <div className='fe-table__container'> + <div ref={tableRef} className={classNames(prefixCls, className)} {...getTableProps()}> + {toolbar && <FeTableToolbar />} + + <div + className={classNames( + `${prefixCls}__table-container`, + loading && pagination === 'pages' && `${prefixCls}__table-container-loading` + )} + > + <FeTableTBody + pageSize={pageSize} + pagination={pagination} + onInfiniteScroll={handleOnPageChange} + loading={props.loading} + prefixCls={prefixCls} + prepareRow={prepareRow} + getTableBodyProps={getTableBodyProps} + renderExpandedComponent={props.renderExpandedComponent} + rows={tableRows} + /> + <FeTableTHead {...tableHeadProps} /> + </div> + + {loading && pagination === 'pages' && rows.length > 0 && <FeLoader center size={24} />} + {pagination === 'pages' && <FeTablePagination {...tablePaginationProps} />} + </div> + </div> + ); +}; diff --git a/packages/core/src/elements/Table/FeTableExpandable.tsx b/packages/core/src/elements/Table/FeTableExpandable.tsx new file mode 100644 index 000000000..f019d5c06 --- /dev/null +++ b/packages/core/src/elements/Table/FeTableExpandable.tsx @@ -0,0 +1,29 @@ +import React, { FC, useEffect, useRef } from 'react'; +import { Row } from 'react-table'; + +type FeTableExpandableProps<T extends object> = { + isExpanded: boolean; + renderExpandedComponent?: (data: T, index: number) => React.ReactNode; + row: Row<T>; +}; +export const FeTableExpandable: FC<FeTableExpandableProps<any>> = <T extends object>( + props: FeTableExpandableProps<T> +) => { + const { isExpanded, renderExpandedComponent, row } = props; + const ref = useRef<HTMLDivElement | null>(); + useEffect(() => { + if (!ref.current) { + return; + } + if (isExpanded) { + ref.current?.classList?.add?.('is-expanded'); + } else { + ref.current?.classList?.remove?.('is-expanded'); + } + }, [isExpanded]); + return ( + <div ref={(node) => (ref.current = node)} className='fe-table__tr-expanded-content'> + {isExpanded && renderExpandedComponent?.(row.original, row.index)} + </div> + ); +}; diff --git a/packages/core/src/elements/Table/FeTableFilterColumn.tsx b/packages/core/src/elements/Table/FeTableFilterColumn.tsx new file mode 100644 index 000000000..3f0750764 --- /dev/null +++ b/packages/core/src/elements/Table/FeTableFilterColumn.tsx @@ -0,0 +1,56 @@ +import React, { FC, useEffect, useState, useRef, useCallback } from 'react'; +import classNames from 'classnames'; +import { FeIcon } from '../Icon/FeIcon'; +import { FeTableColumnProps } from './interfaces'; +import { FePopup } from '../Popup/FePopup'; + +type FeTableFilterColumnProps<T extends object = any> = { + prefixCls: string; + column: FeTableColumnProps<T>; + onFilterChange?: (column: FeTableColumnProps<T>, value: any) => void; +}; + +export const FeTableFilterColumn: FC<FeTableFilterColumnProps> = <T extends object>({ + prefixCls, + column, + onFilterChange, +}: FeTableFilterColumnProps<T>) => { + const [filterValue, setFilterValue] = useState(column.filterValue); + const popupRef = useRef<any>(null); + + useEffect(() => { + setFilterValue(column.filterValue); + }, [column.filterValue]); + + useEffect(() => setFilterValue(column.filterValue), [column.filterValue]); + + const closePopup = useCallback(() => { + popupRef?.current?.hideTooltip?.(); + }, [popupRef]); + + const FilterComponent = column.Filter; + return ( + <FePopup + ref={popupRef} + content={ + <FilterComponent + closePopup={closePopup} + value={filterValue} + setFilterValue={(value) => onFilterChange?.(column, value)} + /> + } + action={'click'} + trigger={ + <span> + <FeIcon + key={1} + name='filters' + className={classNames(`${prefixCls}__filter-button`, { + [`${prefixCls}__active-filter`]: column.filterValue != null, + })} + /> + </span> + } + /> + ); +}; diff --git a/packages/core/src/elements/Table/FeTablePagination.tsx b/packages/core/src/elements/Table/FeTablePagination.tsx new file mode 100644 index 000000000..96e7d0f76 --- /dev/null +++ b/packages/core/src/elements/Table/FeTablePagination.tsx @@ -0,0 +1,126 @@ +import React, { FC } from 'react'; +import { FeIcon } from '../Icon/FeIcon'; +import { FeButton } from '../Button/FeButton'; +import classNames from 'classnames'; + +export type FeTablePaginationProps<T extends object> = { + pageIndex: number; + pageSize?: number; + pageCount: number; + pageOptions: number[]; + canPreviousPage: boolean; + canNextPage: boolean; + gotoPage: (updater: ((pageIndex: number) => number) | number) => void; + previousPage: () => void; + nextPage: () => void; + setPageSize?: (pageSize: number) => void; +}; + +const generatePageButtons = (pageOptions: number[]) => { + return pageOptions + .filter((value, index, self) => self.indexOf(value) === index) + .sort((a, b) => (a > b ? 1 : -1)) + .reduce( + (memo: number[][], item: number) => { + const lastArray = memo[memo.length - 1]; + const lastItem = lastArray[lastArray.length - 1]; + if (item - lastItem === 1) { + lastArray.push(item); + } else { + memo.push([item]); + } + return memo; + }, + [[]] + ) + .filter((arr) => arr.length > 0); +}; +export const FeTablePagination: FC<FeTablePaginationProps<any>> = <T extends object>( + props: FeTablePaginationProps<T> +) => { + const { pageIndex, pageCount, canPreviousPage, canNextPage, pageOptions, nextPage, previousPage } = props; + // if total pages more less 10 + // - display all numbers + // if more than 10 + // - if -3 < 1 + // - display 1 2 [3] 4 5 ... {max-page} + // - if +3 > max-page + // - display 0 ... x-1 x-2 [x] {max-page}-2 {max-page}-1 {max-page} + + if (pageOptions.length < 2) { + return null; + } + + if (pageCount < 10) { + return ( + <div className='fe-table__pagination'> + <FeButton disabled={!canPreviousPage} onClick={previousPage}> + <FeIcon name='left-arrow' /> + </FeButton> + {pageOptions.map((page) => { + return ( + <div + key={page} + className={classNames('fe-table__pagination-option', { + 'selected-option': page === pageIndex, + })} + onClick={() => props.gotoPage(page)} + > + {page + 1} + </div> + ); + })} + <FeButton disabled={!canNextPage} onClick={nextPage}> + <FeIcon name='right-arrow' /> + </FeButton> + </div> + ); + } + const pageButtons: number[] = [pageIndex]; + if (pageIndex < 5) { + pageButtons.push(0, 1, 2, 3); + } else { + pageButtons.push(0, pageIndex - 2, pageIndex - 1); + } + if (pageIndex > pageCount - 5) { + pageButtons.push(pageCount - 4, pageCount - 3, pageCount - 2, pageCount - 1); + } else { + pageButtons.push(pageCount - 1, pageIndex + 1, pageIndex + 2); + } + const pageChunks = generatePageButtons(pageButtons); + + return ( + <div className='fe-table__pagination'> + <FeButton disabled={!canPreviousPage} onClick={previousPage}> + <FeIcon name='left-arrow' /> + </FeButton> + {pageChunks.map((pageChunk, index) => { + return ( + <React.Fragment key={index}> + {pageChunk.map((page) => { + return ( + <div + key={page} + className={classNames('fe-table__pagination-option', { + 'selected-option': page === pageIndex, + })} + onClick={() => props.gotoPage(page)} + > + {page + 1} + </div> + ); + })} + {index !== pageChunks.length - 1 && ( + <div key='page-separator' className='page-separator'> + ... + </div> + )} + </React.Fragment> + ); + })} + <FeButton disabled={!canNextPage} onClick={nextPage}> + <FeIcon name='right-arrow' /> + </FeButton> + </div> + ); +}; diff --git a/packages/core/src/elements/Table/FeTableSortColumn.tsx b/packages/core/src/elements/Table/FeTableSortColumn.tsx new file mode 100644 index 000000000..583087f73 --- /dev/null +++ b/packages/core/src/elements/Table/FeTableSortColumn.tsx @@ -0,0 +1,23 @@ +import React, { FC, useCallback } from 'react'; +import { FeIcon } from '../Icon/FeIcon'; +import { FeTableColumnProps } from './interfaces'; + +type FeTableSortColumnProps<T extends object = any> = { + column: FeTableColumnProps<T>; +}; + +export const FeTableSortColumn: FC<FeTableSortColumnProps> = ({ column }: FeTableSortColumnProps) => { + if (!column.canSort) { + return null; + } + + if (!column.isSorted) { + return <FeIcon name='sort-arrows' />; + } + + if (column.isSortedDesc) { + return <FeIcon name='sort-arrows-desc' />; + } + + return <FeIcon name='sort-arrows-asc' />; +}; diff --git a/packages/core/src/elements/Table/FeTableTBody.tsx b/packages/core/src/elements/Table/FeTableTBody.tsx new file mode 100644 index 000000000..43aa1b21c --- /dev/null +++ b/packages/core/src/elements/Table/FeTableTBody.tsx @@ -0,0 +1,122 @@ +import React, { FC, useMemo } from 'react'; +import classNames from 'classnames'; +import { Row, TableBodyPropGetter, TableBodyProps, UseExpandedRowProps } from 'react-table'; +import { FeTableExpandable } from './FeTableExpandable'; +import { FeLoader } from '../Loader/FeLoader'; +import { useT } from '../../hooks'; +import { Waypoint } from 'react-waypoint'; +import { TableProps } from './interfaces'; + +export type FeTableTBodyProps<T extends object> = { + pagination?: TableProps['pagination']; + loading?: boolean; + prefixCls: string; + getTableBodyProps: (propGetter?: TableBodyPropGetter<T>) => TableBodyProps; + prepareRow: (row: Row<T>) => void; + rows: (Row<T> & UseExpandedRowProps<T>)[]; + renderExpandedComponent?: (data: T, index: number) => React.ReactNode; + pageSize?: number; + onInfiniteScroll?: () => void; +}; +export type FeTableTBodyRowProps<T extends object> = { + prepareRow: (row: Row<T>) => void; + row: Row<T> & UseExpandedRowProps<T>; + renderExpandedComponent?: (data: T, index: number) => React.ReactNode; +}; + +export const FeTableTBodyRow: FC<FeTableTBodyRowProps<any>> = (props: FeTableTBodyRowProps<any>) => { + const { prepareRow, row, renderExpandedComponent } = props; + + useMemo(() => { + prepareRow(row); + }, [row]); + + return ( + <> + <div className={classNames('fe-table__tr', { 'is-expanded': row.isExpanded })} {...row.getRowProps()}> + {row.cells.map((cell, index) => { + const cellProps = cell.getCellProps(); + cellProps.className = classNames('fe-table__tr-td', { + 'fe-table__tr-td__first-cell': index === 0, + }); + return <div {...cellProps}>{cell.render('Cell')}</div>; + })} + </div> + <FeTableExpandable isExpanded={row.isExpanded} row={row} renderExpandedComponent={renderExpandedComponent} /> + </> + ); +}; + +export const FeTableTBody: FC<FeTableTBodyProps<any>> = <T extends object>(props: FeTableTBodyProps<T>) => { + const { + getTableBodyProps, + prepareRow, + rows, + renderExpandedComponent, + loading, + pagination, + onInfiniteScroll, + pageSize, + } = props; + const { t } = useT(); + + const isInfiniteScroll = pagination === 'infinite-scroll'; + const isFirstWaypoint = useMemo(() => rows.length <= (pageSize ?? 20), [pageSize, rows.length]); + + const renderWaypoint = (index: number) => { + const itemsAfterWaypoint = 15; + const itemsAfterWaypointOnFirstRender = 4; + const waypoint = ( + <Waypoint + onEnter={({ previousPosition }) => { + if (!loading && previousPosition !== 'above') { + onInfiniteScroll?.(); + } + }} + /> + ); + if (isFirstWaypoint && index === rows.length - itemsAfterWaypointOnFirstRender) { + return waypoint; + } + if (!isFirstWaypoint && index === rows.length - itemsAfterWaypoint) { + return waypoint; + } + }; + + return ( + <div + className={classNames('fe-table__tbody', { + 'fe-table__tbody__loading': pagination === 'pages' && props.loading, + })} + {...getTableBodyProps()} + > + {rows.map((row, index) => ( + <React.Fragment key={row.id}> + <FeTableTBodyRow prepareRow={prepareRow} row={row} renderExpandedComponent={renderExpandedComponent} /> + {isInfiniteScroll && renderWaypoint(index)} + </React.Fragment> + ))} + + {pagination === 'infinite-scroll' && loading && rows.length !== 0 && ( + <div className={classNames('fe-table__tr')}> + <div className={classNames('fe-table__tr-td fe-table__tr-td-loader')}> + <FeLoader size={20} /> + </div> + </div> + )} + + {loading && rows.length === 0 && ( + <div className={classNames('fe-table__tr')}> + <div className={classNames('fe-table__tr-td fe-table__tr-td-loader')}> + <FeLoader size={24} /> + </div> + </div> + )} + {!loading && rows.length === 0 && ( + <div className={classNames('fe-table__tr')}> + <div className={classNames('fe-table__tr-td fe-table__tr-td-empty')}>{t('common.noResults')}</div> + </div> + )} + </div> + ); +}; diff --git a/packages/core/src/elements/Table/FeTableTHead.tsx b/packages/core/src/elements/Table/FeTableTHead.tsx new file mode 100644 index 000000000..57af6dfd6 --- /dev/null +++ b/packages/core/src/elements/Table/FeTableTHead.tsx @@ -0,0 +1,89 @@ +import React, { FC } from 'react'; +import { FeTableColumnProps } from './interfaces'; +import classNames from 'classnames'; +import { HeaderGroup, Row, TableSortByToggleProps } from 'react-table'; +import { FeTableSortColumn } from './FeTableSortColumn'; +import { FeTableFilterColumn } from './FeTableFilterColumn'; +import { FeCheckbox } from '../Checkbox/FeCheckbox'; + +export type FeTableTHeadProps<T extends object> = { + prefixCls: string; + headerGroups: HeaderGroup<T>[]; + onSortChange?: (column: FeTableColumnProps<T>) => void; + onFilterChange?: (column: FeTableColumnProps<T>, filterValue?: any) => void; + toggleAllRowsSelected?: (value: boolean) => void; + isAllRowsSelected?: boolean; + selectedFlatRows?: Row<T>[]; +}; +export const FeTableTHead: FC<FeTableTHeadProps<any>> = <T extends object>(props: FeTableTHeadProps<T>) => { + const { + prefixCls, + headerGroups, + onSortChange, + onFilterChange, + toggleAllRowsSelected, + selectedFlatRows, + isAllRowsSelected, + } = props; + return ( + <div className='fe-table__thead'> + {headerGroups.map((headerGroup) => ( + <div className='fe-table__thead-tr' {...headerGroup.getHeaderGroupProps()}> + {headerGroup.headers.map((c, index) => { + const column = c as FeTableColumnProps<T>; + if (column.id === 'fe-selection') { + return ( + <div + className={classNames('fe-table__thead-tr-th', { + 'fe-table__thead-tr-th__first-cell': index === 0, + })} + {...column.getHeaderProps()} + > + <FeCheckbox + indeterminate={!isAllRowsSelected && (selectedFlatRows ?? []).length > 0} + checked={isAllRowsSelected} + onChange={() => toggleAllRowsSelected?.(!isAllRowsSelected)} + /> + </div> + ); + } + const withExpander = headerGroup.headers[0].id === 'fe-expander'; + const minWidth = headerGroup.headers[0].minWidth || 0; + const ownWidth = column.width || 0; + const width = index === 1 && withExpander ? { width: Number(ownWidth) + minWidth } : {}; + + const { style, ...headerProps } = { + ...column.getHeaderProps( + column.getSortByToggleProps((p: Partial<TableSortByToggleProps>) => ({ + ...p, + onClick: column.canSort ? () => onSortChange?.(column) : undefined, + })) + ), + }; + + return ( + <div + className={classNames('fe-table__thead-tr-th', { + 'fe-table__thead-tr-th__first-cell': index === 0, + 'fe-table__thead-tr-th__first-cell__expander': index === 0 && withExpander, + 'fe-table__thead-sortable-asc': column.isSorted && !column.isSortedDesc, + 'fe-table__thead-sortable-desc': column.isSorted && column.isSortedDesc, + })} + {...headerProps} + style={{ ...style, ...width }} + > + {column.render('Header')} + <FeTableSortColumn column={column} /> + + <div className='fe-table__spacer' /> + {column.canFilter && ( + <FeTableFilterColumn prefixCls={prefixCls} column={column} onFilterChange={onFilterChange} /> + )} + </div> + ); + })} + </div> + ))} + </div> + ); +}; diff --git a/packages/core/src/elements/Table/FeTableToolbar.tsx b/packages/core/src/elements/Table/FeTableToolbar.tsx new file mode 100644 index 000000000..ba7645cda --- /dev/null +++ b/packages/core/src/elements/Table/FeTableToolbar.tsx @@ -0,0 +1,6 @@ +import React, { FC } from 'react'; + +type FeTableToolbarProps<T extends object> = {}; +export const FeTableToolbar: FC<FeTableToolbarProps<any>> = <T extends object>(props: FeTableToolbarProps<T>) => { + return <div className='fe-table__toolbar'></div>; +}; diff --git a/packages/core/src/elements/Table/Table.tsx b/packages/core/src/elements/Table/Table.tsx new file mode 100644 index 000000000..cb57435ce --- /dev/null +++ b/packages/core/src/elements/Table/Table.tsx @@ -0,0 +1,7 @@ +import React, { forwardRef, ReactElement, Ref } from 'react'; +import { TableProps } from './interfaces'; +import { ElementsFactory } from '../../ElementsFactory'; + +export const Table = forwardRef((props, ref) => + React.createElement(ElementsFactory.getElement('Table'), { ...props, ref } as any) +) as <T extends object = {}>(props: TableProps<T> & { ref?: Ref<HTMLTableElement> }) => ReactElement; diff --git a/packages/core/src/elements/Table/TableUtils.ts b/packages/core/src/elements/Table/TableUtils.ts new file mode 100644 index 000000000..30c027877 --- /dev/null +++ b/packages/core/src/elements/Table/TableUtils.ts @@ -0,0 +1,19 @@ +import { TableProps } from './interfaces'; + +export const checkTableProps = <T extends {}>(props: TableProps<T>) => { + if (props.expandable && !props.renderExpandedComponent) { + throw Error('FeTable: you must provide renderExpandedComponent property if the table is expandable'); + } + if (props.hasOwnProperty('sortBy') && !props.onSortChange) { + throw Error('FeTable: you must provide onSortChange property if sortBy is controlled'); + } + if (props.hasOwnProperty('filters') && !props.onFilterChange) { + throw Error('FeTable: you must provide onFilterChange property if filters is controlled'); + } + if (props.hasOwnProperty('pagination') && props.pagination === 'pages' && !props.pageSize) { + throw Error('FeTable: you must provide pageSize property if pagination enabled'); + } + if (props.hasOwnProperty('onPageChange') && !props.pageCount) { + throw Error('FeTable: you must provide pageCount property if onPageChange is controlled'); + } +}; diff --git a/packages/core/src/elements/Table/index.ts b/packages/core/src/elements/Table/index.ts new file mode 100644 index 000000000..7a4e17426 --- /dev/null +++ b/packages/core/src/elements/Table/index.ts @@ -0,0 +1,3 @@ +export * from './interfaces'; +export * from './Table'; +export * as TableUtils from './TableUtils'; diff --git a/packages/core/src/elements/Table/interfaces.ts b/packages/core/src/elements/Table/interfaces.ts new file mode 100644 index 000000000..143c559c6 --- /dev/null +++ b/packages/core/src/elements/Table/interfaces.ts @@ -0,0 +1,160 @@ +import React, { ComponentType, ReactNode } from 'react'; +import { + Cell, + CellProps, + Column, + DefaultSortTypes, + HeaderGroup, + HeaderProps, + IdType, + Renderer, + SortByFn, + TableInstance, + TableState, + UseExpandedOptions, + UseFiltersColumnOptions, + UseFiltersColumnProps, + UseFiltersOptions, + UseFiltersState, + UsePaginationInstanceProps, + UsePaginationOptions, + UseRowSelectInstanceProps, + UseRowSelectOptions, + UseRowSelectState, + UseSortByColumnOptions, + UseSortByColumnProps, + UseSortByOptions, + UseSortByState, + UseTableColumnOptions, + UseTableInstanceProps, + UseTableOptions, +} from 'react-table'; + +export interface TableProps<T extends object = {}> { + /** + * Common Props + */ + className?: string; + /* column array to be displayed in the table */ + columns: TableColumnProps<T>[]; + pagination?: 'pages' | 'infinite-scroll'; + onPageChange?: (pageSize: number, page: number) => void; + pageCount?: number; + pageSize?: number; + + toolbar?: boolean; + loading?: boolean; + emptyRowsPlaceholder?: ReactNode; + + selection?: 'single' | 'multi'; + onRowSelected?: (rowIds: Record<string | number, boolean>) => void; + selectedRowIds?: Record<string | number, boolean>; + + expandable?: boolean; + renderExpandedComponent?: (data: T, index: number) => React.ReactNode; + + tableHeader?: boolean; + + data: T[]; + totalData: number; + rowKey: keyof T | string; + + isMultiSort?: boolean; + sortBy?: TableSort[]; + onSortChange?: (sortBy: TableSort[]) => void; + + filters?: TableFilter[]; + onFilterChange?: (filters: TableFilter[]) => void; +} + +export interface TableColumnProps<T extends object = any> { + /** + * Required + * This string/function is used to build the data model for your column. + * The data returned by an accessor should be primitive and sortable. + */ + accessor?: string | IdType<T> | never; + /** + * Required if accessor is a function + * This is the unique ID for the column. It is used by reference in things like sorting, grouping, filtering etc. + * If a string accessor is used, it defaults as the column ID, but can be overridden if necessary. + */ + id?: string; + + /** + * Optional + * Defaults to () => null + */ + Header?: Renderer<HeaderProps<T>>; + + /** + * Optional + * Defaults to ({ value }) => String(value) + * Must return valid JSX + */ + Cell?: CellComponent<T>; + + sortable?: boolean; + Filter?: FilterComponent; + + minWidth?: string | number; + maxWidth?: string | number; + + /** + * Optional + * String options: basic, datetime, alphanumeric. Defaults to alphanumeric. + * If a function is passed, it must be memoized. The sortType function should return -1 if rowA is larger, and 1 if rowB is larger. react-table will take care of the rest. + * more information about this parameter on the github page https://react-table-omega.vercel.app/docs/api/useSortBy#column-options + */ + sortType?: SortByFn<T> | DefaultSortTypes; +} + +export type CellComponent<T extends {} = any> = Renderer<CellProps<T>>; +export type FilterComponent<T = any> = ComponentType<{ + value: T | null; + setFilterValue: (value: T | null) => void; + closePopup?: () => void; +}>; + +export interface TableSort { + id: string; + desc?: boolean; +} + +export interface TableFilter { + id: string; + value: any; +} + +export interface TableColumnFilterProps<T = {}> { + type: 'text' | 'select'; + filterBy: string; +} + +export type FeTableColumnProps<T extends object> = HeaderGroup<T> & + UseSortByColumnProps<T> & + UseFiltersColumnProps<T> & + UseRowSelectInstanceProps<T> & { Filter: FilterComponent }; + +export type FeTableColumnOptions<T extends object> = Column<T> & + UseTableColumnOptions<T> & + UseSortByColumnOptions<T> & + UseFiltersColumnOptions<T> & + UseRowSelectOptions<T>; + +export type FeUseTable<T extends object> = UseTableOptions<T> & + UseFiltersOptions<T> & + UseSortByOptions<T> & + UseExpandedOptions<T> & + UseRowSelectOptions<T> & + UsePaginationOptions<T>; + +export type FeTableInstance<T extends object> = TableInstance<T> & + UseTableInstanceProps<T> & + UsePaginationInstanceProps<T> & + UseRowSelectInstanceProps<T>; + +export type FeTableState<T extends object> = TableState<T> & + UseFiltersState<T> & + UseSortByState<T> & + UseRowSelectState<T>; diff --git a/packages/core/src/elements/Tabs/FeTabs.scss b/packages/core/src/elements/Tabs/FeTabs.scss new file mode 100644 index 000000000..9ee1312c4 --- /dev/null +++ b/packages/core/src/elements/Tabs/FeTabs.scss @@ -0,0 +1,46 @@ +.fe-core-tabs { + border-bottom: 2px solid var(--color-gray-2); + padding: 0 2rem; + &-menu { + font-size: 1rem; + display: flex; + margin-top: 1rem; + font-weight: 400; + min-height: 3rem; + align-items: center; + } + + &-item { + line-height: 1; + padding: 0.8rem 0.2rem; + margin: 0 1rem -0.5rem; + font-size: 1rem; + letter-spacing: normal; + transition: background 0.1s ease, box-shadow 0.1s ease, color 0.1s ease; + color: var(--fe-header-tab-font-color); + border-bottom: 2px solid transparent; + font-weight: var(--fe-header-tab-font-weight); + &.disabled { + pointer-events: none; + cursor: default; + color: lightgrey; + } + &:first-child { + margin-left: 0; + } + &:hover { + color: var(--fe-header-tab-hover-color, var(--color-primary-75)); + cursor: pointer; + } + &-active { + color: var(--fe-header-tab-active-color, var(--color-primary)); + border-bottom: 2px solid var(--fe-header-tab-active-color, var(--color-primary)); + box-shadow: none; + + &:hover { + color: var(--fe-header-tab-active-hover-color, var(--color-primary-75)); + border-bottom: 2px solid var(--fe-header-tab-active-hover-color, var(--color-primary-75)); + } + } + } +} diff --git a/packages/core/src/elements/Tabs/FeTabs.tsx b/packages/core/src/elements/Tabs/FeTabs.tsx new file mode 100644 index 000000000..675e55078 --- /dev/null +++ b/packages/core/src/elements/Tabs/FeTabs.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { TabProps } from './interfaces'; +import classNames from 'classnames'; +import './FeTabs.scss'; + +const clsPrefix = 'fe-core-tabs'; + +export const FeTabs = (props: TabProps) => { + const { activeTab, items, onTabChange } = props; + + return ( + <div className='fe-core-tabs'> + <div className={`${clsPrefix}-menu`}> + {items.map(({ disabled, Title }, idx) => ( + <Tab + className={classNames(`${clsPrefix}-item`, { + [`${clsPrefix}-item-active`]: activeTab === idx, + })} + idx={idx} + onClick={onTabChange} + disabled={disabled} + key={idx} + > + {Title ?? ''} + </Tab> + ))} + </div> + </div> + ); +}; + +type TTab = { + children: JSX.Element | string; + className?: string; + idx: number; + disabled?: boolean; + onClick: (e: any, activeTab: number) => void; +}; + +export const Tab = (props: TTab) => { + const { children, className, onClick, idx, disabled } = props; + return ( + <a onClick={(e) => onClick(e, idx)} className={classNames(className, { disabled })}> + {children} + </a> + ); +}; diff --git a/packages/core/src/elements/Tabs/Tabs.tsx b/packages/core/src/elements/Tabs/Tabs.tsx new file mode 100644 index 000000000..0d6499e16 --- /dev/null +++ b/packages/core/src/elements/Tabs/Tabs.tsx @@ -0,0 +1,5 @@ +import React from 'react'; +import { TabProps } from './interfaces'; +import { ElementsFactory } from '../../ElementsFactory'; + +export const Tabs = (props: TabProps) => React.createElement(ElementsFactory.getElement('Tabs'), props); diff --git a/packages/core/src/elements/Tabs/index.ts b/packages/core/src/elements/Tabs/index.ts new file mode 100644 index 000000000..fa252fd20 --- /dev/null +++ b/packages/core/src/elements/Tabs/index.ts @@ -0,0 +1,2 @@ +export * from './Tabs'; +export * from './interfaces'; diff --git a/packages/core/src/elements/Tabs/interfaces.ts b/packages/core/src/elements/Tabs/interfaces.ts new file mode 100644 index 000000000..97b573451 --- /dev/null +++ b/packages/core/src/elements/Tabs/interfaces.ts @@ -0,0 +1,11 @@ +export interface TabProps { + className?: string; + items: TabItem[]; + activeTab: number; + onTabChange: (event: React.MouseEvent<HTMLDivElement>, activeIndex: number) => void; +} + +export interface TabItem { + disabled?: boolean; + Title?: JSX.Element | string; +} diff --git a/packages/core/src/elements/Tag/FeTag.scss b/packages/core/src/elements/Tag/FeTag.scss new file mode 100644 index 000000000..1d0c17145 --- /dev/null +++ b/packages/core/src/elements/Tag/FeTag.scss @@ -0,0 +1,32 @@ +@import '../../styles/mixin.scss'; + +.fe-tag { + height: var(--element-height); + padding: 0 var(--element-padding); + font-size: var(--element-font-size); + color: var(--color-text); + border: none; + cursor: default; + display: inline-flex; + outline: 0; + box-sizing: border-box; + align-items: center; + white-space: nowrap; + border-radius: var(--element-border-radius); + vertical-align: middle; + justify-content: center; + text-decoration: none; + background-color: var(--color-gray-3); + @include with-theme; + @include with-clickable; + @include with-size; + + &__icon { + @include with-clickable; + } + + .fe-icon { + margin-right: calc(-1 * var(--element-padding-sm)); + margin-left: var(--element-spacing); + } +} diff --git a/packages/core/src/elements/Tag/FeTag.tsx b/packages/core/src/elements/Tag/FeTag.tsx new file mode 100644 index 000000000..f77120caf --- /dev/null +++ b/packages/core/src/elements/Tag/FeTag.tsx @@ -0,0 +1,30 @@ +import React, { forwardRef } from 'react'; +import { TagProps } from './interfaces'; +import './FeTag.scss'; +import { FeIcon } from '../Icon/FeIcon'; +import { ClassNameGenerator } from '../../styles'; + +const prefixCls = 'fe-tag'; +export const FeTag = forwardRef<HTMLDivElement, TagProps>((props, ref) => { + const { className, children, size, variant = 'default', onDelete, ...rest } = props; + + const classes = ClassNameGenerator.generate({ + prefixCls, + className, + size, + theme: props.disabled ? 'disabled' : variant, + isClickable: !!props.onClick, + }); + + const deleteIconClasses = ClassNameGenerator.generate({ + prefixCls: `${prefixCls}__icon`, + isClickable: true, + }); + + return ( + <div ref={ref} className={classes} {...rest}> + {children} + {onDelete && <FeIcon className={deleteIconClasses} name='delete' onClick={onDelete} />} + </div> + ); +}); diff --git a/packages/core/src/elements/Tag/Tag.tsx b/packages/core/src/elements/Tag/Tag.tsx new file mode 100644 index 000000000..bcf9d07fc --- /dev/null +++ b/packages/core/src/elements/Tag/Tag.tsx @@ -0,0 +1,7 @@ +import React, { forwardRef } from 'react'; +import { ElementsFactory } from '../../ElementsFactory'; +import { TagProps } from './interfaces'; + +export const Tag = forwardRef<HTMLDivElement, TagProps>((props, ref) => + React.createElement(ElementsFactory.getElement('Tag'), { ...props, ref } as any) +); diff --git a/packages/core/src/elements/Tag/index.ts b/packages/core/src/elements/Tag/index.ts new file mode 100644 index 000000000..53ea2d446 --- /dev/null +++ b/packages/core/src/elements/Tag/index.ts @@ -0,0 +1,2 @@ +export * from './Tag'; +export * from './interfaces'; diff --git a/packages/core/src/elements/Tag/interfaces.ts b/packages/core/src/elements/Tag/interfaces.ts new file mode 100644 index 000000000..991a888ff --- /dev/null +++ b/packages/core/src/elements/Tag/interfaces.ts @@ -0,0 +1,9 @@ +import { HTMLAttributes } from 'react'; +import { Size, Theme } from '../../styles'; + +export interface TagProps extends HTMLAttributes<HTMLElement> { + variant?: Theme; + size?: Size; + disabled?: boolean; + onDelete?: () => void; +} diff --git a/packages/core/src/elements/index.ts b/packages/core/src/elements/index.ts new file mode 100644 index 000000000..0c62b0991 --- /dev/null +++ b/packages/core/src/elements/index.ts @@ -0,0 +1,20 @@ +export * from './ErrorMessage'; +export * from './Button'; +export * from './Input'; +export * from './Loader'; +export * from './Tag'; +export * from './Grid'; +export * from './Table'; +export * from './Icon'; +export * from './Popup'; +export * from './Checkbox'; +export * from './Select'; +export * from './Accordion'; +export * from './SwitchToggle'; +export * from './Form'; +export * from './Tabs'; +export * from './Menu'; +export * from './MenuItem'; +export * from './Dialog'; +export * from './InputChip'; +export * from './Pagination'; diff --git a/packages/core/src/helpers/DialogContext.ts b/packages/core/src/helpers/DialogContext.ts new file mode 100644 index 000000000..b46a9e079 --- /dev/null +++ b/packages/core/src/helpers/DialogContext.ts @@ -0,0 +1,8 @@ +import React, { useContext } from 'react'; + +type DialogContextState = { + onClose?: () => void; +}; +export const DialogContext = React.createContext<DialogContextState>({ onClose: () => {} }); + +export const useDialog = () => useContext(DialogContext); diff --git a/packages/core/src/helpers/DynamicComponent.tsx b/packages/core/src/helpers/DynamicComponent.tsx new file mode 100644 index 000000000..8ebf0601d --- /dev/null +++ b/packages/core/src/helpers/DynamicComponent.tsx @@ -0,0 +1,213 @@ +import React, { ComponentType, FC, ReactElement, ReactNode, useMemo } from 'react'; + +export class EmptyRender extends React.Component<any, any> { + render() { + return null; + } +} + +export type ComponentsTypesWithProps<E> = { + [P in keyof Partial<E>]: ComponentType<E[P]> | Partial<E[P]> | null; +}; + +export type ComponentsTypeProps<E> = { + [P in keyof Partial<E>]: E[P] | null; +}; +export type ComponentsTypes<E> = { + [P in keyof E]: ComponentType<E[P]>; +}; + +export type PartialInnerTypes<E> = { + [P in keyof E]: Partial<E[P]>; +}; + +export type RendererFunction<P, C = P, R = ReactNode> = ( + props: Omit<P, 'renderer' | 'components'>, + components?: ComponentsTypes<C> +) => R | null; +export type RendererFunctionFC<P, R = ReactElement> = (props: Omit<P, 'renderer'>) => R | null; + +export type ComponentRenderer<P, M = {}, A = {}> = ( + props: Omit<P, 'renderer' | 'components'> & M & A +) => ReactElement | null; + +export function memoEqual(prevProps: any, nextProps: any) { + const equal = Object.keys(nextProps).reduce((p: boolean, next: any) => { + if (typeof prevProps[next] === 'function' && typeof nextProps[next] === 'function') { + return p; + } + if (prevProps[next] !== nextProps[next]) { + return p && false; + } else { + return p; + } + }, true); + return equal; +} + +export function memoEqualNoChildren(prevProps: any, nextProps: any) { + return Object.keys(nextProps).reduce((p: boolean, next: any) => { + if (next === 'children') { + return p; + } + if (typeof prevProps[next] === 'function' && typeof nextProps[next] === 'function') { + return p; + } + if (prevProps[next] !== nextProps[next]) { + return p && false; + } else { + return p; + } + }, true); +} + +export const buildDynamicComponent = <T extends {}, P>( + components: Partial<P> | undefined | null, + component: keyof P, + DefaultComponent: ComponentType<any> +): any => { + if (components?.hasOwnProperty(component)) { + const comp = components?.[component]; + if (comp == null) { + return EmptyRender; + } + if (typeof comp === 'function') { + return comp; + } + return React.memo((props) => { + return React.createElement(DefaultComponent, { ...comp, ...props }); + }, memoEqual); + } + return React.memo(DefaultComponent as any, memoEqual); +}; + +export const buildPropsComponents = <P extends {}>(components: any, defaultComponents: P): P => { + if (!components) { + return defaultComponents; + } + return Object.keys(defaultComponents as any) + .map((compName: any) => ({ + [compName as string]: buildDynamicComponent(components, compName as string, (defaultComponents as any)[compName]), + })) + .reduce((p: any, comp: any) => ({ ...p, ...comp }), {}); +}; + +export const generateComponent = <T extends {}, P>( + components: Partial<P> | undefined | null, + component: keyof P, + defaultComponent: ComponentType<any> +): any => { + if (components?.hasOwnProperty(component)) { + const comp = components?.[component]; + if (comp == null) { + return EmptyRender; + } + if (typeof comp === 'function') { + return comp; + } + return React.memo((props) => { + return React.createElement(defaultComponent, { ...comp, ...props }); + }, memoEqual); + } + return defaultComponent; +}; + +export const buildComponents = <P extends {}>(components: any, defaultComponents: P): P => { + if (!components) { + return defaultComponents; + } + return Object.keys(defaultComponents as any) + .map((compName: any) => ({ + [compName as string]: generateComponent(components, compName as string, (defaultComponents as any)[compName]), + })) + .reduce((p: any, comp: any) => ({ ...p, ...comp }), {}); +}; + +export const useDynamicComponents = <COMPS, A, P extends { components?: ComponentsTypesWithProps<COMPS> }>( + defaultComponents: A, + props: P +) => { + return useMemo(() => buildComponents(props.components, defaultComponents), [props.components]); +}; + +export const buildComponentsProps = <P extends {}>(configComponents: P, propsComponents: P) => { + const props: any = {}; + + const merger = (comps: any) => { + Object.keys(comps || {}).map((key) => { + if (comps[key] === null) { + props[key] = null; + return; + } + if (props.hasOwnProperty(key)) { + props[key] = { + ...(props[key] || {}), + ...comps[key], + }; + } else { + props[key] = comps[key]; + } + }); + }; + merger(configComponents); + merger(propsComponents); + + return props; +}; + +export const cloneComponentsWithProps = <P extends {}>( + components: ComponentsTypes<P>, + configProps: PartialInnerTypes<P> +): ComponentsTypes<P> => { + const cloned: any = components; + Object.keys(configProps).forEach((key) => { + if ((cloned as any)[key] != null) { + cloned[key] = (props: any) => React.createElement(cloned[key], { ...configProps, ...props }); + } + }); + return cloned as ComponentsTypes<P>; +}; + +export class FronteggClass< + COMPS, + P extends { components?: ComponentsTypesWithProps<COMPS> } = {}, + S = {} +> extends React.Component<P, S> { + compsProps: PartialInnerTypes<COMPS>; + comps: ComponentsTypes<COMPS>; + + constructor(props: P, defaultComponents: ComponentsTypes<COMPS>) { + super(props); + this.compsProps = buildComponentsProps({}, this.props.components ?? {}); + this.comps = buildComponents(this.compsProps, defaultComponents); + } +} + +export const checkValidChildren = <T extends {}>( + wrapperName: string, + hostName: string, + children: ReactNode, + requiredComponents: Partial<T>, + depth: number = 0 +) => { + if (children == null) { + return true; + } + let _keys = Object.keys(requiredComponents); + const _values = _keys.map((key) => (requiredComponents as any)[key]); + React.Children.map(children, (child: any, index) => { + const childIndex = _values.indexOf(child?.type); + if (childIndex !== -1) { + // @ts-ignore + delete requiredComponents[_keys[childIndex]]; + } + checkValidChildren(wrapperName, hostName, child?.props?.children, requiredComponents, depth + 1); + }); + _keys = Object.keys(requiredComponents); + if (_keys.length > 0 && depth === 0) { + const warn = _keys.map((k) => `${hostName}.${k}`).join(', '); + throw Error( + `Missing required components inside ${wrapperName} => [${warn}].\nDid you mean to hide these components? just pass 'hide' property to it, Example:\n\n\t<${wrapperName}>\n\t\t<${hostName}.${_keys[0]} hide />\n\t</${hostName}>\n\n` + ); + } +}; diff --git a/packages/core/src/helpers/FormikAutoSave.tsx b/packages/core/src/helpers/FormikAutoSave.tsx new file mode 100644 index 000000000..462f804c9 --- /dev/null +++ b/packages/core/src/helpers/FormikAutoSave.tsx @@ -0,0 +1,29 @@ +import { FC, useEffect } from 'react'; +import { FFormik, useDebounce } from '../hooks'; + +export interface IFormikAutoSave { + debounceMs?: number; + isSaving: boolean; +} + +export const FormikAutoSave: FC<IFormikAutoSave> = ({ debounceMs = 500, isSaving }) => { + const { values, submitForm, initialValues, setSubmitting, isValid } = FFormik.useFormikContext(); + + const saveData = useDebounce(values, debounceMs); + + useEffect((): (() => void) => { + return () => { + submitForm(); + }; + }, []); + + useEffect(() => { + isValid && JSON.stringify(initialValues) !== JSON.stringify(saveData) && submitForm(); + }, [saveData, submitForm, isValid, initialValues]); + + useEffect(() => { + !isSaving && setSubmitting(false); + }, [isSaving]); + + return null; +}; diff --git a/packages/core/src/helpers/Logger.ts b/packages/core/src/helpers/Logger.ts new file mode 100644 index 000000000..5df91fd61 --- /dev/null +++ b/packages/core/src/helpers/Logger.ts @@ -0,0 +1,24 @@ +// @ts-ignore +const debugging = process.env.NODE_ENV === 'development'; +// tslint:disable-next-line:no-empty +const emptyFunction = () => {}; + +export default class Logger { + private constructor(private readonly module: string) {} + + static from = (module: string) => new Logger(module); + private _log = (l: string, prefix: string) => { + // tslint:disable-next-line:no-console + if (console.log.bind === undefined) { + // @ts-ignore + return Function.prototype.bind.call(console[l], console, prefix || '', this.module, ':'); + } else { + // @ts-ignore + return console[l].bind(console, prefix || '', this.module, ':'); + } + }; + debug = debugging ? this._log('log', 'DEBUG |') : emptyFunction; + info = debugging ? this._log('log', 'INFO |') : emptyFunction; + warn = this._log('warn', 'WARN |'); + error = this._log('error', 'ERROR |'); +} diff --git a/packages/core/src/helpers/RootPathContext.ts b/packages/core/src/helpers/RootPathContext.ts new file mode 100644 index 000000000..03f81087f --- /dev/null +++ b/packages/core/src/helpers/RootPathContext.ts @@ -0,0 +1,21 @@ +import React, { useContext } from 'react'; +import { useRouteMatch } from 'react-router'; + +export const RootPathContext = React.createContext<string | null>(null); + +export const useRootPath = (props: any, defaultRoute: string = '/'): [string, boolean] => { + const rootPathFromContext = useContext(RootPathContext); + const routeMatch = useRouteMatch(); + const rootPath = rootPathFromContext ?? props.rootPath ?? routeMatch?.url ?? defaultRoute; + const isRootPathContext = rootPathFromContext != null; + + return [rootPath, isRootPathContext]; +}; + +export const checkRootPath = (error: string): string => { + const path = useContext(RootPathContext); + if (path != null) { + return path; + } + throw Error(error); +}; diff --git a/packages/core/src/helpers/index.tsx b/packages/core/src/helpers/index.tsx new file mode 100644 index 000000000..7c8dfee03 --- /dev/null +++ b/packages/core/src/helpers/index.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import moment from 'moment'; + +export * from './sagaHelpers'; +export * from './validates'; +export * from './DynamicComponent'; +export * from './RootPathContext'; +export * from './DialogContext'; +export { default as Logger } from './Logger'; + +export function omitProps<T>(props: any, keys: string[]): T { + const newProps = { ...props }; + keys.forEach((key) => { + delete newProps[key]; + }); + return newProps as T; +} + +export const formatDate = (date: string) => { + const mDate = moment(date); + return ( + <> + {mDate.fromNow()} <small>{mDate.format('L LT')} </small> + </> + ); +}; diff --git a/packages/core/src/helpers/sagaHelpers.ts b/packages/core/src/helpers/sagaHelpers.ts new file mode 100644 index 000000000..11a357877 --- /dev/null +++ b/packages/core/src/helpers/sagaHelpers.ts @@ -0,0 +1,41 @@ +import { delay, select, PayloadAction } from '@frontegg/redux-store/toolkit'; +import { ContextOptions } from '../interfaces'; + +export function* getContext() { + let result; + do { + const availableContext = yield select(({ root: { context } }) => context); + if (!availableContext) { + yield delay(50); + } else { + result = availableContext; + } + } while (!result); + return result as ContextOptions; +} + +export const reducerActionOnly = <S, T>() => ({ + prepare: (payload: T) => ({ payload }), + reducer: (state: S) => state, +}); + +export const reducerResetByState = <S>(key: keyof S, preloadedState: S) => () => ({ ...preloadedState }); +export const reducerResetByKey = <S, T>(key: keyof S, preloadedState: S) => (state: S) => ({ + ...state, + [key]: preloadedState[key], +}); + +export const reducerByState = <S, T>(key: keyof S) => (state: S, { payload }: PayloadAction<T>) => ({ + ...state, + [key]: payload, +}); +export const reducerBySubState = <S, T>(key: keyof S) => ({ + prepare: (payload: Partial<T>) => ({ payload }), + reducer: (state: S, { payload }: PayloadAction<Partial<T>>) => ({ + ...state, + [key]: { + ...state[key], + ...payload, + }, + }), +}); diff --git a/packages/core/src/helpers/useSearch.tsx b/packages/core/src/helpers/useSearch.tsx new file mode 100644 index 000000000..eaf74e795 --- /dev/null +++ b/packages/core/src/helpers/useSearch.tsx @@ -0,0 +1,53 @@ +import React, { useMemo, useRef, useState } from 'react'; +import classnames from 'classnames'; +import { Input } from '../elements/Input'; +import { useDebounce } from '../hooks'; + +export interface IUseSearchProps<T> { + data?: T[]; + filteredBy: keyof T; + placeholder?: string; // the custom placeholder + className?: string; // className for the root Element + inputClassName?: string; // className for the input element + debounce?: number; // the debounce before filter data + filterFunction?(value: T[], reg: RegExp, isEmpty: boolean): T[] | undefined | null; // If need filtered more than by one field +} + +export function useSearch<T extends {}>({ + filteredBy, + data, + placeholder, + className, + inputClassName, + debounce = 500, + filterFunction, +}: IUseSearchProps<T>): [T[], JSX.Element] { + const [filter, setFilter] = useState(''); + + const filterDebounce = useDebounce(filter, debounce); + + const filteredData = useMemo(() => { + const reg = new RegExp(filterDebounce.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i'); + return ( + (filterFunction + ? filterFunction(data || [], reg, filterDebounce.trim() === '') + : data?.filter((el) => reg.test(`${el[filteredBy]}`))) ?? [] + ); + }, [filterDebounce, data]); + + const Search = useMemo( + () => ( + <div className={classnames(className, 'fe-search')}> + <Input + className={inputClassName} + placeholder={placeholder ?? 'Search by title...'} + onChange={(e) => setFilter(e.target.value)} + value={filter} + /> + </div> + ), + [filter] + ); + + return [filteredData, Search]; +} diff --git a/packages/core/src/helpers/validates.ts b/packages/core/src/helpers/validates.ts new file mode 100644 index 000000000..2cd178ab1 --- /dev/null +++ b/packages/core/src/helpers/validates.ts @@ -0,0 +1,107 @@ +import * as Yup from 'yup'; +import { TFunction } from 'i18next'; +import { ValidationError } from 'yup'; +import owasp, { TestConfig } from 'owasp-password-strength-test'; + +export const validatePassword = (t: TFunction) => + Yup.string() + .min(6, t('validation.min-length', { name: t('common.password'), limit: 6 })) + .required(t('validation.required-field', { name: t('common.password') })); + +export const validateEmail = (t: TFunction) => + Yup.string() + .email(t('validation.must-be-a-valid-email', 'Must be a valid email')) + .required(t('validation.required-field', { name: t('common.email') })); + +export const validateTwoFactorCode = (t: TFunction) => + Yup.string() + .length(6, t('validation.min-length', { name: 'Code', limit: 6 })) + .required(t('validation.required-field', { name: 'code' })); + +export const validateTwoFactorRecoveryCode = (t: TFunction) => + Yup.string() + .min(8, t('validation.max-length', { name: 'code', limit: 8 })) + .required(t('validation.required-field', { name: 'code' })); + +export const validatePasswordConfirmation = (t: TFunction, field: string = 'password') => + Yup.string() + .required(t('validation.required-field', { name: 'confirmation of the password' })) + .when(field, { + is: (val) => !!(val && val.length > 0), + then: Yup.string().oneOf([Yup.ref(field)], t('validation.passwords-must-match', 'Passwords must match')), + }); + +export const validatePasswordUsingOWASP = (testConfig: Partial<TestConfig> | null | undefined) => + Yup.string() + .label('password') + .required() + .test('validate_owasp', 'Invalid Password', async function (value) { + // Use function to access Yup 'this' context + + if (value == null) { + return true; + } + testConfig && owasp.config(testConfig); + const { errors } = owasp.test(value); + // validate using owasp + + if (errors?.length) { + return this.createError({ message: errors[0] }); + } + return true; + }); + +export const validateDomain = (t: TFunction) => + Yup.string() + .matches( + /(?=.{4,253}$)^((([A-Za-z0-9]{1,63})|([0-9]{1}))([\.]{1}|[\-]{1,})){1,}((?=.*[a-zA-Z])([a-zA-Z0-9]+){2,25}){1}/, + t('validation.must-be-a-valid-domain', 'Must be a valid domain') + ) + .required(t('validation.required-field', { name: 'domain' })); + +export const validateUrl = (name: string, t: TFunction) => + Yup.string() + .url(t('validation.must-be-a-valid-url', 'Must be a valid URL')) + .required(t('validation.required-field', { name })); + +export const validateLength = (name: string, limit: number, t: TFunction) => + Yup.string() + .min(limit, t('validation.min-length', { name, limit })) + .required(t('validation.required-field', { name })); + +export const validateRequired = (name: string, t: TFunction) => + Yup.string().required(t('validation.required-field', { name })); + +export const validateArrayLength = (t: TFunction, name: string) => + Yup.array().required(t('validation.required-field', { name })); + +export const validateSchema = (props: any) => Yup.object(props); + +export const validateObject = (name: string, t: TFunction) => + Yup.object() + .required(t('validation.required-field', { name })) + .typeError(t('validation.must-be-a-valid-json', { name })); + +export const validationPhone = (t: TFunction) => + Yup.string() + .matches( + /^(?!\b(0)\1+\b)(\+?\d{1,3}[. -]?)?\(?\d{3}\)?([. -]?)\d{3}\3\d{4}$/, + t('validation.invalid-phone', 'Invalid phone number') + ) + .required(t('validation.required-field', { name: 'phone' })); + +export const validateCheckbox = () => Yup.boolean().required().oneOf([true]); + +export const validateSchemaSync = (props: any, values: any) => + new Promise((resolve) => { + validateSchema(props) + .validate(values, { abortEarly: false }) + .then(() => resolve({})) + .catch((errors) => { + resolve( + errors.inner + .map((error: ValidationError) => ({ [error.path]: error.message })) + .reduce((p: object, n: object) => ({ ...p, ...n }), {}) + ); + }); + }); diff --git a/packages/core/src/hooks.ts b/packages/core/src/hooks.ts new file mode 100644 index 000000000..d7d2987d7 --- /dev/null +++ b/packages/core/src/hooks.ts @@ -0,0 +1,47 @@ +import { Ref, useCallback, useEffect, useRef, useState } from 'react'; +import * as FFormik from 'formik'; +import { useTranslation, UseTranslationResponse } from 'react-i18next'; +import { useSelector, useDispatch, shallowEqual } from '@frontegg/react-hooks'; + +export { useSelector, useDispatch, shallowEqual }; +export { FFormik }; +export const useT = (): UseTranslationResponse => useTranslation(); + +export function useDebounce<T>(value: T, delay: number) { + const [debouncedValue, setDebouncedValue] = useState<T>(value); + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value]); + return debouncedValue; +} + +export function usePrevious<T>(value: T) { + const ref = useRef<T>(); + useEffect(() => { + ref.current = value; + }); + return ref.current as T; +} + +export function useCombinedRefs<T extends any>(refs: Ref<T>[]): Ref<T> { + return useCallback( + (node: T) => { + refs.forEach((ref) => { + if (!ref) return; + if (typeof ref === 'function') { + return ref(node); + } + if (typeof ref === 'object') { + (ref as any).current = node; + } + }); + }, + [refs] + ); +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 000000000..58359e4ad --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,16 @@ +export * from './FronteggProvider'; + +export * from './interfaces'; +export * from './HOCs'; +export * from './hooks'; +export * from './helpers'; +export { default as Logger } from './helpers/Logger'; +export * from './helpers/FormikAutoSave'; +export * from './helpers/useSearch'; +export * from './elements'; +export * from './components'; +export * from './styles'; +export * from './ngSupport'; +export * from './ElementsFactory'; + +export { memoEqual } from './helpers/DynamicComponent'; diff --git a/packages/core/src/interfaces/index.ts b/packages/core/src/interfaces/index.ts new file mode 100644 index 000000000..a83e907f5 --- /dev/null +++ b/packages/core/src/interfaces/index.ts @@ -0,0 +1,114 @@ +import { ReactElement } from 'react'; + +declare global { + interface Window { + Cypress: any; + cypressStore: any; + cypressHistory: any; + } +} + +export interface KeyValuePair { + key: string; + value: string; +} + +export interface ColorOptions { + foreground?: string; + background?: string; +} + +export interface ThemeOptions { + tableMaxWidth?: string; + filterBoxColor?: ColorOptions; + tableHeaderColor?: ColorOptions; + tableRowColor?: ColorOptions; + tableRowTimeStatusColor?: string; + addButtonColor?: ColorOptions; + deleteButtonColor?: ColorOptions; + tableFontSizeNames?: string; + tableFontSizeDescriptions?: string; + tableRowHeight?: string; + tableTextLineHeight?: string; + tableTextFontFamily?: string; + paginationControlsColor?: ColorOptions; + modalDialogHeaderColor?: ColorOptions; + modalCancelButtonColor?: ColorOptions; + modalAcceptButtonColor?: ColorOptions; + addBellBackgroundColor?: string; + addUnreadIndicatorColor?: ColorOptions; + addPinBackgroundColor?: string; + notificationsBoxShowTitle?: boolean; + notificationsBoxShadow?: boolean; + notificationsBoxBorderRadius?: string; + notificationsBoxWidth?: string; + notificationsBoxFontSize?: string; + notificationsBoxForeColor?: string; + notificationsBoxBackColor?: string; + notificationsBoxBorderColor?: string; + notificationsRowHeight?: string; + notificationsRowPadding?: string; + notificationsRowBackgroundColor?: string; + notificationsRowTimePresentation?: 'absolute' | 'conditional'; + notificationsRowTitleColor?: string; + notificationsRowTitleFontSize?: string; + notificationsRowDescriptionColor?: string; + notificationsRowTitleFontWeight?: string; + notificationsRowDescriptionFontSize?: string; + notificationsRowDescriptionFontWeight?: string; + notificationsRowPresentationColor?: string; + notificationsRowPresentationFontSize?: string; + notificationsRowPresentationFontWeight?: string; + notificationsBellIcon?: string; + notificationsBellIconSize?: string; + notificationsBellForeColor?: string; + notificationsBellBackColor?: string; + notificationsAlertLocation?: string; + notificationsAlertIconSize?: string; + notificationsAlertForeColor?: string; + notificationsAlertBackColor?: string; + notificationsAlertBadgeCount?: string; + notificationsOptionsPin?: string; + notificationsOptionsDelete?: string; + notificationsOptionsIconType?: string; + notificationsOptionsLocation?: string; + notificationsOptionsColor?: string; + notificationsUnreadRowBackgroundColor?: string; + notificationsUnreadRowTitleColor?: string; + notificationsUnreadRowTitleFontSize?: string; + notificationsUnreadRowTitleFontWeight?: string; + notificationsUnreadRowDescriptionColor?: string; + notificationsUnreadRowDescriptionFontSzie?: string; + notificationsUnreadRowTimePresentationColor?: string; + notificationsUnreadRowTimePresentationFontSize?: string; + notificationsPaginationType?: string; + notificationsPaginationColor?: string; + notificationsPaginationSize?: string; + notificationsPaginationWeight?: string; +} + +export type LogLevel = 'warn' | 'error'; + +//TODO: DOUBLE ContextOptions IN PACKAGESS +export interface ContextOptions { + baseUrl: string; + tokenResolver?: () => Promise<string> | string; // custom resolve Authorization Header value + additionalQueryParamsResolver?: () => Promise<KeyValuePair[]> | KeyValuePair[]; + additionalHeadersResolver?: () => Promise<KeyValuePair[]> | KeyValuePair[]; + requestCredentials?: RequestCredentials; + theme?: ThemeOptions | any; + isDemonstration?: boolean; + errorComponent?: ReactElement | string; + currentUserId?: string; + currentUserRoles?: string[]; + currentUserPermissions?: string[]; + urlPrefix?: string; + logLevel?: LogLevel; + auditsOptions?: { + virtualScroll?: boolean; + }; +} + +export interface ListenerProps<T> { + resolveActions?: (storeName: string, actions: T) => void; +} diff --git a/packages/core/src/ngSupport.ts b/packages/core/src/ngSupport.ts new file mode 100644 index 000000000..15d7f082f --- /dev/null +++ b/packages/core/src/ngSupport.ts @@ -0,0 +1,24 @@ +import { useState, createElement } from 'react'; +import { createPortal, render } from 'react-dom'; +import { createBrowserHistory } from 'history'; + +export interface ProxyComponent { + // internal use + _history?: any; + _resolvePortals?: (setPortals: any) => void; + _resolveActions?: (storeName: string, actions: any) => void; +} + +export const useProxyComponent = <T extends ProxyComponent>(props: T) => { + const [rcPortals, setRcPortals] = useState([]); + props._resolvePortals?.(setRcPortals); + return rcPortals; +}; + +export const DOMProxy = { + createElement, + createPortal, + render, +}; + +export { createBrowserHistory }; diff --git a/packages/core/src/reducer.ts b/packages/core/src/reducer.ts new file mode 100644 index 000000000..0e8b3a788 --- /dev/null +++ b/packages/core/src/reducer.ts @@ -0,0 +1,26 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { ContextOptions } from './interfaces'; + +export interface RootState { + context?: ContextOptions; +} + +const initialState: RootState = { + context: undefined, +}; + +const { reducer, actions } = createSlice({ + name: 'root', + initialState, + reducers: { + setContext: { + prepare: (context: ContextOptions) => ({ payload: context }), + reducer: (state: RootState, { payload }: PayloadAction<ContextOptions>) => ({ + ...state, + context: payload, + }), + }, + }, +}); + +export { reducer as rootReducer, actions as rootActions, initialState as rootInitialState }; diff --git a/packages/core/src/styles/ClassNameGenerator.ts b/packages/core/src/styles/ClassNameGenerator.ts new file mode 100644 index 000000000..8ad162877 --- /dev/null +++ b/packages/core/src/styles/ClassNameGenerator.ts @@ -0,0 +1,39 @@ +import classNames from 'classnames'; +import { Size, Theme } from './Styles'; + +export interface ClassNameGeneratorOptions { + prefixCls: string; + className?: string; + isClickable?: boolean; + theme?: Theme; + size?: Size; + isFullWidth?: boolean; + isLoading?: boolean; +} + +export class ClassNameGenerator { + public static generate = ( + { + prefixCls, + className, + theme, + size, + isClickable = false, + isFullWidth = false, + isLoading = false, + }: ClassNameGeneratorOptions, + ...extraClasses: (string | boolean | null | undefined)[] + ) => { + return classNames(prefixCls, className, { + [`${prefixCls}-${theme}`]: theme, + [`${prefixCls}-${size}`]: size, + [`${prefixCls}-clickable`]: isClickable, + [`${prefixCls}-full-width`]: isFullWidth, + [`${prefixCls}-loader`]: isLoading, + ...extraClasses + .filter((_) => !!_) + .map((className) => `${prefixCls}-${className}`) + .reduce((acc, curr) => ({ ...acc, [curr]: true }), {}), + }); + }; +} diff --git a/packages/core/src/styles/Styles.ts b/packages/core/src/styles/Styles.ts new file mode 100644 index 000000000..5bd7c664a --- /dev/null +++ b/packages/core/src/styles/Styles.ts @@ -0,0 +1,2 @@ +export type Theme = 'primary' | 'secondary' | 'danger' | 'success' | 'disabled' | 'default'; +export type Size = 'small' | 'medium' | 'large'; diff --git a/packages/core/src/styles/colors.scss b/packages/core/src/styles/colors.scss new file mode 100644 index 000000000..1442c7a7d --- /dev/null +++ b/packages/core/src/styles/colors.scss @@ -0,0 +1,373 @@ +:root { + --color-primary: var(--color-blue-6); + --color-primary-25: var(--color-blue-6-25); + --color-primary-50: var(--color-blue-6-50); + --color-primary-75: var(--color-blue-6-75); + + --color-primary-lighter: var(--color-blue-0); + --color-primary-light: var(--color-blue-3); + --color-primary-dark: var(--color-blue-7); + --color-primary-darker: var(--color-blue-8); + --color-primary-bg: var(--color-blue-6); + + --color-secondary: var(--color-deep-purple-3); + --color-secondary-75: var(--color-deep-purple-a1-75); + --color-secondary-50: var(--color-deep-purple-a1-50); + --color-secondary-25: var(--color-deep-purple-a1-25); + + --color-secondary-light: var(--color-deep-purple-1); + --color-secondary-lightest: var(--color-deep-purple-0); + --color-secondary-dark: var(--color-deep-purple-5); + --color-secondary-darker: var(--color-deep-purple-7); + --color-secondary-bg: var(--color-deep-purple-a1); + + /* feedback */ + --color-success: var(--color-light-green-a3); + --color-success-75: var(--color-light-green-a3-75); + --color-success-50: var(--color-light-green-a3-50); + --color-success-25: var(--color-light-green-a3-25); + --color-sucess-light: var(--color-light-green-0); + + --color-danger: var(--color-red-5); + --color-danger-75: var(--color-red-8-75); + --color-danger-50: var(--color-red-8-50); + --color-danger-25: var(--color-red-8-25); + + --color-danger-dark: var(--color-red-9); + --color-danger-darker: var(--color-red-9); + --color-danger-light: var(--color-red-0); + --color-warning: var(--color-yellow-8); + --background-warning: var(--color-yellow-0); + --color-info: var(--color-blue-5); + --background-info: #d6f2fd; + --background-disabled: var(--color-gray-2); + + /* text */ + --color-text-on-primary: var(--color-gray-9); + --color-text-on-secondary: var(--color-gray-0); + --color-text-on-success: var(--color-gray-9); + --color-text-on-danger: var(--color-red-5); + --color-text-on-warning: var(--color-gray-9); + --color-text-on-info: var(--color-gray-0); + + /* typography */ + --color-text: var(--color-gray-9); + --color-text-heading: var(--color-black); + --color-text-subtitle: var(--color-gray-6); + --color-text-disabled: var(--color-gray-6); + --color-link: var(--color-primary); + --color-link-visited: var(--color-primary-dark); + --color-blockquote-border: var(--color-gray-2); + + /* social colors */ + --color-facebook: var(--color-indigo-5); + --color-google: var(--color-red-6); + --color-github: var(--color-gray-8); + --color-microsoft: var(--color-blue-4); + + /* select component */ + --select-color-primary: var(--color-secondary); + --select-color-primary-75: var(--color-secondary-75); + --select-color-primary-50: var(--color-secondary-50); + --select-color-primary-25: var(--color-secondary-25); + + --color-white: #fff; + --color-black: #000; + --color-black-10: rgba(0, 0, 0, 0.1); + --color-black-20: rgba(0, 0, 0, 0.2); + --color-black-30: rgba(0, 0, 0, 0.3); + --color-black-40: rgba(0, 0, 0, 0.4); + --color-black-50: rgba(0, 0, 0, 0.5); + --color-black-60: rgba(0, 0, 0, 0.6); + --color-black-70: rgba(0, 0, 0, 0.7); + --color-black-80: rgba(0, 0, 0, 0.8); + --color-black-90: rgba(0, 0, 0, 0.9); + + --color-red-0: #fdecec; + --color-red-1: #ffcdd2; + --color-red-2: #ef9a9a; + --color-red-3: #e57373; + --color-red-4: #ef5350; + --color-red-5: #f52424; + --color-red-6: #e53935; + --color-red-7: #d32f2f; + --color-red-7-75: rgba(211, 47, 47, 0.75); + --color-red-7-50: rgba(211, 47, 47, 0.5); + --color-red-7-25: rgba(211, 47, 47, 0.25); + --color-red-7-10: rgba(211, 47, 47, 0.1); + --color-red-8: #c62828; + --color-red-8-75: rgba(198, 40, 40, 0.75); + --color-red-8-50: rgba(198, 40, 40, 0.5); + --color-red-8-25: rgba(198, 40, 40, 0.25); + --color-red-9: #b71c1c; + --color-red-a0: #ff8a80; + --color-red-a1: #ff5252; + --color-red-a2: #ff1744; + --color-red-a3: #d50000; + --color-pink-0: #fce4ec; + --color-pink-1: #f8bbd0; + --color-pink-2: #f48fb1; + --color-pink-3: #f06292; + --color-pink-4: #ec407a; + --color-pink-5: #e91e63; + --color-pink-6: #d81b60; + --color-pink-7: #c2185b; + --color-pink-8: #ad1457; + --color-pink-9: #880e4f; + --color-pink-a0: #ff80ab; + --color-pink-a1: #ff4081; + --color-pink-a2: #f50057; + --color-pink-a3: #c51162; + --color-purple-0: #f3e5f5; + --color-purple-1: #e1bee7; + --color-purple-2: #ce93d8; + --color-purple-3: #ba68c8; + --color-purple-4: #ab47bc; + --color-purple-5: #9c27b0; + --color-purple-6: #8e24aa; + --color-purple-7: #7b1fa2; + --color-purple-8: #6a1b9a; + --color-purple-9: #4a148c; + --color-purple-a0: #ea80fc; + --color-purple-a1: #e040fb; + --color-purple-a2: #d500f9; + --color-purple-a3: #aa00ff; + --color-deep-purple-0: #ede7f6; + --color-deep-purple-1: #d1c4e9; + --color-deep-purple-2: #b39ddb; + --color-deep-purple-3: #9575cd; + --color-deep-purple-4: #7e57c2; + --color-deep-purple-5: #673ab7; + --color-deep-purple-6: #5e35b1; + --color-deep-purple-7: #512da8; + --color-deep-purple-8: #4527a0; + --color-deep-purple-9: #311b92; + --color-deep-purple-a0: #b388ff; + + --color-deep-purple-a1: #6e5bd8; + --color-deep-purple-a1-75: rgba(110, 81, 216, 0.75); + --color-deep-purple-a1-50: rgba(110, 81, 216, 0.5); + --color-deep-purple-a1-25: rgba(110, 81, 216, 0.25); + + --color-deep-purple-a2: #651fff; + --color-deep-purple-a3: #6200ea; + + --color-indigo-0: #e8eaf6; + --color-indigo-1: #c5cae9; + --color-indigo-2: #9fa8da; + --color-indigo-3: #7986cb; + --color-indigo-4: #5c6bc0; + --color-indigo-5: #3b5998; + --color-indigo-6: #3949ab; + --color-indigo-7: #303f9f; + --color-indigo-8: #283593; + --color-indigo-9: #1a237e; + --color-indigo-a0: #8c9eff; + --color-indigo-a1: #536dfe; + --color-indigo-a2: #3d5afe; + --color-indigo-a3: #304ffe; + --color-blue-0: #e3f2fd; + --color-blue-1: #bbdefb; + --color-blue-2: #90caf9; + --color-blue-3: #64b5f6; + --color-blue-4: #42a5f5; + --color-blue-5: #2196f3; + --color-blue-5-25: rgba(33, 150, 243, 0.25); + --color-blue-5-50: rgba(33, 150, 243, 0.5); + --color-blue-5-75: rgba(33, 150, 243, 0.75); + --color-blue-6: #1e88e5; + --color-blue-6-25: rgb(30, 136, 229, 0.25); + --color-blue-6-50: rgb(30, 136, 229, 0.5); + --color-blue-6-75: rgb(30, 136, 229, 0.75); + --color-blue-7: #1976d2; + --color-blue-8: #1565c0; + --color-blue-9: #0d47a1; + --color-blue-a0: #82b1ff; + --color-blue-a1: #448aff; + --color-blue-a2: #2979ff; + --color-blue-a3: #2962ff; + --color-light-blue-0: #e1f5fe; + --color-light-blue-1: #b3e5fc; + --color-light-blue-2: #81d4fa; + --color-light-blue-3: #4fc3f7; + --color-light-blue-4: #29b6f6; + --color-light-blue-5: #03a9f4; + --color-light-blue-6: #039be5; + --color-light-blue-7: #0288d1; + --color-light-blue-8: #0277bd; + --color-light-blue-9: #01579b; + --color-light-blue-a0: #80d8ff; + --color-light-blue-a1: #40c4ff; + --color-light-blue-a2: #00b0ff; + --color-light-blue-a3: #0091ea; + --color-cyan-0: #e0f7fa; + --color-cyan-1: #b2ebf2; + --color-cyan-2: #80deea; + --color-cyan-3: #4dd0e1; + --color-cyan-4: #26c6da; + --color-cyan-5: #00bcd4; + --color-cyan-6: #00acc1; + --color-cyan-7: #0097a7; + --color-cyan-8: #00838f; + --color-cyan-9: #006064; + --color-cyan-a0: #84ffff; + --color-cyan-a1: #18ffff; + --color-cyan-a2: #00e5ff; + --color-cyan-a3: #00b8d4; + --color-teal-0: #e0f2f1; + --color-teal-1: #b2dfdb; + --color-teal-2: #80cbc4; + --color-teal-3: #4db6ac; + --color-teal-4: #26a69a; + --color-teal-5: #009688; + --color-teal-6: #00897b; + --color-teal-7: #00796b; + --color-teal-8: #00695c; + --color-teal-9: #004d40; + --color-teal-a0: #a7ffeb; + --color-teal-a1: #64ffda; + --color-teal-a2: #1de9b6; + --color-teal-a3: #00bfa5; + --color-green-0: #e8f5e9; + --color-green-1: #c8e6c9; + --color-green-2: #a5d6a7; + --color-green-3: #81c784; + --color-green-4: #66bb6a; + --color-green-5: #0baf60; + --color-green-5-75: rgba(11, 175, 96, 0.75); + --color-green-5-50: rgba(11, 175, 96, 0.5); + --color-green-5-25: rgba(11, 175, 96, 0.25); + --color-green-6: #43a047; + --color-green-7: #388e3c; + --color-green-8: #2e7d32; + --color-green-9: #1b5e20; + --color-green-a0: #b9f6ca; + --color-green-a1: #69f0ae; + --color-green-a2: #00e676; + --color-green-a3: #00c853; + --color-light-green-0: #f1f8e9; + --color-light-green-1: #dcedc8; + --color-light-green-2: #c5e1a5; + --color-light-green-3: #aed581; + --color-light-green-4: #9ccc65; + --color-light-green-5: #8bc34a; + --color-light-green-6: #7cb342; + --color-light-green-7: #689f38; + --color-light-green-8: #558b2f; + --color-light-green-9: #33691e; + --color-light-green-a0: #ccff90; + --color-light-green-a1: #b2ff59; + --color-light-green-a2: #76ff03; + --color-light-green-a3: rgba(49, 196, 52, 1); + --color-light-green-a3-25: rgba(49, 196, 52, 0.25); + --color-light-green-a3-50: rgba(49, 196, 52, 0.5); + --color-light-green-a3-75: rgba(49, 196, 52, 0.75); + --color-lime-0: #f9fbe7; + --color-lime-1: #f0f4c3; + --color-lime-2: #e6ee9c; + --color-lime-3: #dce775; + --color-lime-4: #d4e157; + --color-lime-5: #cddc39; + --color-lime-6: #c0ca33; + --color-lime-7: #afb42b; + --color-lime-8: #9e9d24; + --color-lime-9: #827717; + --color-lime-a0: #f4ff81; + --color-lime-a1: #eeff41; + --color-lime-a2: #c6ff00; + --color-lime-a3: #aeea00; + --color-yellow-0: #fffde7; + --color-yellow-1: #fff9c4; + --color-yellow-2: #fff59d; + --color-yellow-3: #fff176; + --color-yellow-4: #ffee58; + --color-yellow-5: #ffeb3b; + --color-yellow-6: #fdd835; + --color-yellow-7: #fbc02d; + --color-yellow-8: #f9a825; + --color-yellow-9: #f57f17; + --color-yellow-a0: #ffff8d; + --color-yellow-a1: #ffff00; + --color-yellow-a2: #ffea00; + --color-yellow-a3: #ffd600; + --color-amber-0: #fff8e1; + --color-amber-1: #ffecb3; + --color-amber-2: #ffe082; + --color-amber-3: #ffd54f; + --color-amber-4: #ffca28; + --color-amber-5: #ffc107; + --color-amber-5-75: rgba(255, 193, 7, 0.75); + --color-amber-5-50: rgba(255, 193, 7, 0.5); + --color-amber-5-25: rgba(255, 193, 7, 0.25); + + --color-amber-6: #f2b607; + --color-amber-7: #dca706; + --color-amber-8: #ff8f00; + --color-amber-9: #ff6f00; + --color-amber-a0: #ffe57f; + --color-amber-a1: #ffd740; + --color-amber-a2: #ffc400; + --color-amber-a3: #ffab00; + --color-orange-0: #fff3e0; + --color-orange-1: #ffe0b2; + --color-orange-2: #ffcc80; + --color-orange-3: #ffb74d; + --color-orange-4: #ffa726; + --color-orange-5: #ff9800; + --color-orange-6: #fb8c00; + --color-orange-7: #f57c00; + --color-orange-8: #ef6c00; + --color-orange-9: #e65100; + --color-orange-a0: #ffd180; + --color-orange-a1: #ffab40; + --color-orange-a2: #ff9100; + --color-orange-a3: #ff6d00; + --color-deep-orange-0: #fbe9e7; + --color-deep-orange-1: #ffccbc; + --color-deep-orange-2: #ffab91; + --color-deep-orange-3: #ff8a65; + --color-deep-orange-4: #ff7043; + --color-deep-orange-5: #ff5722; + --color-deep-orange-6: #f4511e; + --color-deep-orange-7: #e64a19; + --color-deep-orange-8: #d84315; + --color-deep-orange-9: #bf360c; + --color-deep-orange-a0: #ff9e80; + --color-deep-orange-a1: #ff6e40; + --color-deep-orange-a2: #ff3d00; + --color-deep-orange-a3: #dd2c00; + --color-brown-0: #efebe9; + --color-brown-1: #d7ccc8; + --color-brown-2: #bcaaa4; + --color-brown-3: #a1887f; + --color-brown-4: #8d6e63; + --color-brown-5: #795548; + --color-brown-6: #6d4c41; + --color-brown-7: #5d4037; + --color-brown-8: #4e342e; + --color-brown-9: #3e2723; + --color-gray-0: #f9fafc; + --color-gray-1: #f7f8fa; + --color-gray-2: #eeeeee; + --color-gray-3: #e0e0e0; + --color-gray-4: #cccdd2; + --color-gray-5: #9e9e9e; + --color-gray-6: #757575; + --color-gray-7: #616161; + --color-gray-8: #424242; + --color-gray-9: #212121; + --color-blue-gray-0: #eceff1; + --color-blue-gray-1: #cfd8dc; + --color-blue-gray-2: #b0bec5; + --color-blue-gray-3: #90a4ae; + --color-blue-gray-4: #78909c; + --color-blue-gray-5: #607d8b; + --color-blue-gray-6: #546e7a; + --color-blue-gray-7: #455a64; + --color-blue-gray-8: #37474f; + --color-blue-gray-9: #263238; +} + +.fe-color-danger { + color: var(--color-danger) !important; +} diff --git a/packages/core/src/styles/common.scss b/packages/core/src/styles/common.scss new file mode 100644 index 000000000..f6adbd21e --- /dev/null +++ b/packages/core/src/styles/common.scss @@ -0,0 +1,25 @@ +@import './colors.scss'; +@import './shadows.scss'; +@import './semantic-overrides.scss'; +@import './elements.scss'; +@import './helpers.scss'; + +* { + box-sizing: border-box; +} + +.frontegg { + box-sizing: border-box; + margin: 0; + padding: 0; + background: #fff; + font-family: 'Nunito Sans', Helvetica Neue, Arial, Helvetica, sans-serif; + font-size: 14px; + line-height: 1.4285em; + color: rgba(0, 0, 0, 0.87); + -webkit-font-smoothing: antialiased; + height: 100%; + flex: 1 1; + display: flex; + flex-direction: column; +} diff --git a/packages/core/src/styles/elements.scss b/packages/core/src/styles/elements.scss new file mode 100644 index 000000000..83d097c1d --- /dev/null +++ b/packages/core/src/styles/elements.scss @@ -0,0 +1,176 @@ +:root { + --element-font-family: Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif; + + --element-font-size: 0.9375rem; + --element-font-size-lg: 1rem; + --element-font-size-sm: 0.875rem; + + --element-padding: 0.9375rem; + --element-padding-lg: 1rem; + --element-padding-sm: 0.875rem; + + --element-height: 2.4375rem; + --element-height-lg: 2.875rem; + --element-height-sm: 2rem; + + --element-icon-size: 1.5rem; + --element-icon-size-sm: 1.5rem; + + --element-spacing: 0.5rem; + --element-default-width: 300px; + + --element-border-radius: 0.5rem; + --element-border-radius-lg: 1rem; + --element-border-radius-sm: 0.25rem; + --element-border-radius-tiny: 0.125rem; + + --element-border-color: var(--color-gray-3); + --element-divider-color: var(--color-gray-2); + + --body-bg: var(--color-gray-1); +} + +.fe-note { + margin: 1rem 0; + font-size: 0.8rem; + + &-title { + font-weight: bold; + line-height: 2; + } + + &-description { + font-weight: normal; + } +} + +.fe-link-button { + padding: 0; + background: transparent !important; + display: inline-block; + font-size: inherit; + color: inherit; + font-weight: bold; + + &:hover { + text-decoration: underline; + } +} + +.fe-placeholder-box { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + max-width: 40rem; + margin: 3rem auto; + min-height: 14rem; + border-radius: 0.5rem; + background-color: #f9fafc; + position: relative; + + &__inner { + display: flex; + align-items: center; + width: calc(100% - 2rem); + height: calc(100% - 2rem); + border-radius: 0.5rem; + border: 1.5px solid #d4dde9; + position: absolute; + + span { + margin: 0 auto; + max-width: 60%; + text-align: center; + color: #99a6b9; + font-size: 16px; + font-weight: 700; + } + } +} + +.fe-card { + &-container { + margin: 2rem auto; + max-width: 90%; + width: 40rem; + padding: 1rem; + background: var(--color-gray-0); + border-radius: 0.25rem; + } + + &-content { + height: 100%; + border: 1px solid var(--color-blue-gray-0); + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 3.5rem 0; + border-radius: 0.25rem; + } +} + +.fe-form { + width: 100%; + box-sizing: border-box; +} + +.fe-row { + display: flex; + flex-direction: row; + flex-flow: wrap; +} + +.fe-input__inline { + width: 300px !important; + display: inline-block; + margin: 0 1rem 1rem 0 !important; + + &:last-child { + margin-right: 0 !important; + } +} + +.fe-table-cell { + &__title { + font-size: 1rem; + color: var(--color-black); + word-break: break-word; + } + + &__description { + font-size: 0.9rem; + color: var(--color-gray-6); + overflow: hidden; + white-space: pre-wrap; + word-break: break-word; + } + &__date-ago { + > div:first-child { + font-size: 0.9rem; + color: var(--color-black); + } + + > div:last-child { + font-size: 0.85rem; + color: var(--color-gray-6); + } + } + + &__avatar-img { + height: 2.25rem; + width: 2.25rem; + border-radius: 50%; + } +} + +.fe-search { + margin-bottom: var(--element-padding-lg); +} + +@media screen and (max-width: 600px) { + .fe-input__inline { + width: 100% !important; + } +} diff --git a/packages/core/src/styles/helpers.scss b/packages/core/src/styles/helpers.scss new file mode 100644 index 000000000..2bff4d852 --- /dev/null +++ b/packages/core/src/styles/helpers.scss @@ -0,0 +1,99 @@ +@mixin fe-space-helper($cls, $property) { + #{$cls}-0 { + #{$property}: 0; + } + #{$cls}-1 { + #{$property}: 0.5rem !important; + } + #{$cls}-2 { + #{$property}: 1rem !important; + } + #{$cls}-3 { + #{$property}: 1.5rem !important; + } + #{$cls}-4 { + #{$property}: 2rem !important; + } +} + +@include fe-space-helper('.fe-mt', margin-top); +@include fe-space-helper('.fe-mb', margin-bottom); +@include fe-space-helper('.fe-ml', margin-left); +@include fe-space-helper('.fe-mr', margin-right); +@include fe-space-helper('.fe-pt', padding-top); +@include fe-space-helper('.fe-pb', padding-bottom); +@include fe-space-helper('.fe-pl', padding-left); +@include fe-space-helper('.fe-pr', padding-right); + +.fe-center { + text-align: center; + align-items: center; +} + +.fe-relative { + position: relative; +} + +.fe-bold { + font-weight: bold; +} + +.fe-text-align-end { + text-align: end; +} + +.fe-text-align-start { + text-align: start; +} + +.fe-text-align-center { + text-align: center; +} + +.fe-error-message, +.fe-success-message { + flex: 1; + font-weight: bold; + color: #9a0000; + margin-top: 1rem; + margin-bottom: 0.5rem; + text-align: center; + + ul { + padding-left: 1.5rem; + text-align: left; + font-size: 0.8rem; + list-style-type: none; + } +} + +.fe-success-message { + color: initial; +} + +.fe-self-flex-start { + align-self: flex-start; +} + +.fe-self-flex-end { + align-self: flex-end; +} + +.fe-dflex { + display: flex; +} + +.fe-dflex-space-between { + justify-content: space-between; +} + +.fe-dflex-column { + flex-direction: column; +} + +.fe-mb-0 { + margin-bottom: 0; +} +.fe-mt-2 { + margin-top: 8px; +} diff --git a/packages/core/src/styles/index.ts b/packages/core/src/styles/index.ts new file mode 100644 index 000000000..1b1525acc --- /dev/null +++ b/packages/core/src/styles/index.ts @@ -0,0 +1,4 @@ +import './common.scss'; + +export * from './ClassNameGenerator'; +export * from './Styles'; diff --git a/packages/core/src/styles/mixin.scss b/packages/core/src/styles/mixin.scss new file mode 100644 index 000000000..a77c4ce35 --- /dev/null +++ b/packages/core/src/styles/mixin.scss @@ -0,0 +1,111 @@ +@use "sass:list"; + +@mixin with-theme($apply-to: all) { + @if $apply-to == all { + &-primary, + &-secondary, + &-danger, + &-success, + &-default, + &-disabled { + background-color: var(--theme-background); + color: var(--theme-color); + border: 1px solid var(--theme-background); + + @content; + } + + &-primary { + --theme-color: var(--color-primary-lighter); + --theme-background: var(--color-primary); + } + + &-secondary { + --theme-color: var(--color-secondary); + --theme-background: var(--color-secondary-light); + } + + &-danger { + --theme-color: var(--color-danger); + --theme-background: var(--color-danger-light); + } + + &-success { + --theme-color: var(--color-success); + --theme-background: var(--color-sucess-light); + } + + &-default { + --theme-color: var(--color-gray-8); + --theme-background: var(--color-gray-1); + } + + &-disabled { + --theme-color: var(--color-text-disabled); + --theme-background: var(--background-disabled); + border: none; + pointer-events: none; + cursor: default; + } + } @else { + &-#{$apply-to} { + @content; + } + } +} + +@mixin with-full-width { + &-full-width { + width: 100%; + } +} + +@mixin with-clickable { + &-clickable { + cursor: pointer; + transition: all 0.1s ease-out; + user-select: none; + + &:focus { + filter: brightness(0.95); + } + + &:hover { + filter: brightness(0.9); + } + + &:active { + filter: brightness(0.8); + } + + @content; + } +} + +@mixin with-size { + --size-icon: var(--element-icon-size); + + &-small, + &-large { + padding: var(--size-padding); + height: var(--size-height); + font-size: var(--size-font-size); + border-radius: var(--size-border-radius); + @content; + } + + &-small { + --size-padding: 0 var(--element-padding-sm); + --size-height: var(--element-height-sm); + --size-font-size: var(--element-font-size-sm); + --size-border-radius: var(--element-border-radius-lg); + --size-icon: var(--element-icon-size-sm); + } + + &-large { + --size-padding: 0 var(--element-padding-lg); + --size-height: var(--element-height-lg); + --size-font-size: var(--element-font-size-lg); + --size-border-radius: var(--element-border-radius-sm); + } +} diff --git a/packages/core/src/styles/semantic-overrides.scss b/packages/core/src/styles/semantic-overrides.scss new file mode 100644 index 000000000..9d64ff667 --- /dev/null +++ b/packages/core/src/styles/semantic-overrides.scss @@ -0,0 +1,66 @@ +:root { + //font-smoothing: none; + //-webkit-font-smoothing: none; +} + +/******************************* + Highlighting +*******************************/ + +::-webkit-selection { + background-color: highlight; + color: highlighttext; +} + +::-moz-selection { + background-color: highlight; + color: highlighttext; +} + +::selection { + background-color: highlight; + color: highlighttext; +} + +/*-------------------- + Autofilled +---------------------*/ + +.ui.form .field.field input:-webkit-autofill { + -webkit-box-shadow: inherit !important; + box-shadow: inherit !important; + border-color: inherit; +} + +/* Focus */ + +.ui.form .field.field input:-webkit-autofill:focus { + -webkit-box-shadow: inherit !important; + box-shadow: inherit !important; + border-color: inherit; +} + +/* Error */ +.ui.form .error.error input:-webkit-autofill { + -webkit-box-shadow: 0px 0px 0px 100px #fffaf0 inset !important; + box-shadow: 0px 0px 0px 100px #fffaf0 inset !important; + border-color: #e0b4b4 !important; +} + +.ui.form .disabled.field, +.ui.form .disabled.fields .field, +.ui.form .field :disabled { + opacity: inherit; +} + +.fe-dimmer { + .ui.active.loader { + &:after { + border-color: var(--color-gray-6) transparent transparent; + } + + &:before { + border-color: var(--color-black-10); + } + } +} diff --git a/packages/core/src/styles/shadows.scss b/packages/core/src/styles/shadows.scss new file mode 100644 index 000000000..474573b8f --- /dev/null +++ b/packages/core/src/styles/shadows.scss @@ -0,0 +1,29 @@ +:root { + --shadow-1: 0px 2px 1px -1px rgba(0, 0, 0, 0.1), 0px 1px 1px 0px rgba(0, 0, 0, 0.1), + 0px 2px 3px 0px rgba(0, 0, 0, 0.12); + --shadow-1-active: 0px 2px 1px -1px rgba(0, 0, 0, 0.1), 0px 1px 1px 0px rgba(0, 0, 0, 0.1), + 0px 2px 3px -2px rgba(0, 0, 0, 0.12); + --shadow-2: 0px 3px 1px -2px rgba(0, 0, 0, 0.1), 0px 2px 2px 0px rgba(0, 0, 0, 0.1), + 0px 1px 5px 0px rgba(0, 0, 0, 0.12); + --shadow-3: 0px 3px 3px -2px rgba(0, 0, 0, 0.1), 0px 3px 4px 0px rgba(0, 0, 0, 0.1), + 0px 1px 8px 0px rgba(0, 0, 0, 0.12); + --shadow-light: 0 1.5rem 2.5rem 0.2rem rgba(0, 0, 0, 0.1); + --button-shadow: 0px 1px 1px -1px rgba(0, 0, 0, 0.1), 0px 2px 2px 0px rgba(0, 0, 0, 0.12); + --popup-shadow: 0 0.85rem 2rem 0 rgba(105, 114, 133, 0.3); +} + +@media (prefers-color-scheme: dark) { + :root { + --shadow-1: 0px 2px 1px -1px rgba(255, 255, 255, 0.1), 0px 1px 1px 0px rgba(255, 255, 255, 0.1), + 0px 2px 3px 0px rgba(255, 255, 255, 0.12); + --shadow-1-active: 0px 2px 1px -1px rgba(255, 255, 255, 0.1), 0px 1px 1px 0px rgba(255, 255, 255, 0.1), + 0px 2px 3px -2px rgba(255, 255, 255, 0.12); + --shadow-2: 0px 3px 1px -2px rgba(255, 255, 255, 0.1), 0px 2px 2px 0px rgba(255, 255, 255, 0.1), + 0px 1px 5px 0px rgba(255, 255, 255, 0.12); + --shadow-3: 0px 3px 3px -2px rgba(255, 255, 255, 0.1), 0px 3px 4px 0px rgba(255, 255, 255, 0.1), + 0px 1px 8px 0px rgba(255, 255, 255, 0.12); + --shadow-light: 0 1.5rem 2.5rem 0.2rem rgba(255, 255, 255, 0.1); + --button-shadow: 0px 1px 1px -1px rgba(255, 255, 255, 0.1), 0px 2px 2px 0px rgba(255, 255, 255, 0.12); + --popup-shadow: 0 0.85rem 2rem 0 rgba(255, 255, 255, 0.1); + } +} diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 000000000..a9380e94f --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "isolatedModules": false, + "outDir": "./dist" + }, + "include": [ + "./src/**/*.tsx", + "./src/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist", + "src/**/*.stories.tsx", + "src/**/*.test.tsx", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.spec.tsx" + ] +} + diff --git a/packages/demo-saas/CHANGELOG.md b/packages/demo-saas/CHANGELOG.md new file mode 100644 index 000000000..db39d6074 --- /dev/null +++ b/packages/demo-saas/CHANGELOG.md @@ -0,0 +1,758 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [2.8.13](https://github.com/frontegg/frontegg-react/compare/v2.8.12...v2.8.13) (2021-07-22) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [2.8.12](https://github.com/frontegg/frontegg-react/compare/v2.8.11...v2.8.12) (2021-07-20) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [2.8.10](https://github.com/frontegg/frontegg-react/compare/v2.8.9...v2.8.10) (2021-07-08) + + +### Bug Fixes + +* **audits:** fix store conflict between old audits and new auditlogs state ([5e493fe](https://github.com/frontegg/frontegg-react/commit/5e493fec79dd73198186a6b2a94e8833e4600102)) + + + + + +## [2.8.9](https://github.com/frontegg/frontegg-react/compare/v2.8.8...v2.8.9) (2021-07-08) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [2.8.8](https://github.com/frontegg/frontegg-react/compare/v2.8.7...v2.8.8) (2021-07-06) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [2.8.7](https://github.com/frontegg/frontegg-react/compare/v2.8.6...v2.8.7) (2021-07-01) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [2.8.6](https://github.com/frontegg/frontegg-react/compare/v2.8.5...v2.8.6) (2021-06-30) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [2.8.5](https://github.com/frontegg/frontegg-react/compare/v2.8.4...v2.8.5) (2021-06-30) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [2.8.4](https://github.com/frontegg/frontegg-react/compare/v2.8.3...v2.8.4) (2021-06-29) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [2.8.3](https://github.com/frontegg/frontegg-react/compare/v2.8.1...v2.8.3) (2021-06-23) + + +### Bug Fixes + +* align all dependencies versions ([f1d5c48](https://github.com/frontegg/frontegg-react/commit/f1d5c48aba34827ea06a554cfb61734f1a40c93b)) + + + + + +## [2.8.2](https://github.com/frontegg/frontegg-react/compare/v2.8.1...v2.8.2) (2021-06-23) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [2.8.1](https://github.com/frontegg/frontegg-react/compare/v2.8.0...v2.8.1) (2021-06-22) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +# [2.8.0](https://github.com/frontegg/frontegg-react/compare/v2.7.2...v2.8.0) (2021-06-21) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [2.7.2](https://github.com/frontegg/frontegg-react/compare/v2.7.1...v2.7.2) (2021-06-14) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +# [2.7.0](https://github.com/frontegg/frontegg-react/compare/v2.6.0...v2.7.0) (2021-06-07) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +# [2.6.0](https://github.com/frontegg/frontegg-react/compare/v2.5.2...v2.6.0) (2021-05-27) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [2.5.2](https://github.com/frontegg/frontegg-react/compare/v2.5.1...v2.5.2) (2021-05-24) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [2.5.1](https://github.com/frontegg/frontegg-react/compare/v2.5.0...v2.5.1) (2021-05-23) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +# [2.4.0](https://github.com/frontegg/frontegg-react/compare/v2.3.2...v2.4.0) (2021-05-19) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [2.3.2](https://github.com/frontegg/frontegg-react/compare/v2.3.1...v2.3.2) (2021-05-10) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [2.3.1](https://github.com/frontegg/frontegg-react/compare/v2.3.0...v2.3.1) (2021-05-10) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +# [2.3.0](https://github.com/frontegg/frontegg-react/compare/v2.2.2...v2.3.0) (2021-05-07) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [2.2.2](https://github.com/frontegg/frontegg-react/compare/v2.2.1...v2.2.2) (2021-04-29) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [2.2.1](https://github.com/frontegg/frontegg-react/compare/v2.2.0...v2.2.1) (2021-04-28) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +# [2.2.0](https://github.com/frontegg/frontegg-react/compare/v2.1.0...v2.2.0) (2021-04-28) + + +### Features + +* add frontegg react library to support routing and sharing store ([2fed55f](https://github.com/frontegg/frontegg-react/commit/2fed55f61832c785d4ec99d7193226b9cf4f3a16)), closes [#FR-2761](https://github.com/frontegg/frontegg-react/issues/FR-2761) + + + + + +# [2.1.0](https://github.com/frontegg/frontegg-react/compare/v2.0.0...v2.1.0) (2021-04-13) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +# [2.0.0](https://github.com/frontegg/frontegg-react/compare/v1.28.0...v2.0.0) (2021-04-11) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +# [1.28.0](https://github.com/frontegg/frontegg-react/compare/v1.27.0...v1.28.0) (2021-03-22) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +# [1.27.0](https://github.com/frontegg/frontegg-react/compare/v1.26.0...v1.27.0) (2021-03-18) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +# [1.26.0](https://github.com/frontegg/frontegg-react/compare/v1.25.0...v1.26.0) (2021-03-17) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +# [1.25.0](https://github.com/frontegg/frontegg-react/compare/v1.24.0...v1.25.0) (2021-03-07) + + +### Bug Fixes + +* FR-1932 - merge with master; removed unused Captcha ref; ([a088a21](https://github.com/frontegg/frontegg-react/commit/a088a21f673a843d35511cfdaa4fb5b2283bfb09)) + + +### Features + +* FR-1932 - added captcha for login/sign up; removed unused components demosaas ([e5e75c8](https://github.com/frontegg/frontegg-react/commit/e5e75c82524bfffe158924e75128fa84d5224b14)) + + + + + +# [1.24.0](https://github.com/frontegg/frontegg-react/compare/v1.23.1...v1.24.0) (2021-03-03) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [1.23.1](https://github.com/frontegg/frontegg-react/compare/v1.23.0...v1.23.1) (2021-02-21) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +# [1.23.0](https://github.com/frontegg/frontegg-react/compare/v1.22.1...v1.23.0) (2021-02-18) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [1.22.1](https://github.com/frontegg/frontegg-react/compare/v1.22.0...v1.22.1) (2021-02-16) + + +### Bug Fixes + +* Fix ActivateAccount test ([bbf3781](https://github.com/frontegg/frontegg-react/commit/bbf37817feccdc1331e92b829b39d33d0461052e)) + + + + + +# [1.22.0](https://github.com/frontegg/frontegg-react/compare/v1.21.1...v1.22.0) (2021-02-13) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [1.21.1](https://github.com/frontegg/frontegg-react/compare/v1.21.0...v1.21.1) (2021-02-04) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +# [1.21.0](https://github.com/frontegg/frontegg-react/compare/v1.20.1...v1.21.0) (2021-02-04) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [1.20.1](https://github.com/frontegg/frontegg-react/compare/v1.20.0...v1.20.1) (2021-02-02) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +# [1.20.0](https://github.com/frontegg/frontegg-react/compare/v1.19.1...v1.20.0) (2021-02-01) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [1.19.1](https://github.com/frontegg/frontegg-react/compare/v1.19.0...v1.19.1) (2021-01-20) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +# [1.19.0](https://github.com/frontegg/frontegg-react/compare/v1.18.6...v1.19.0) (2021-01-20) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [1.18.6](https://github.com/frontegg/frontegg-react/compare/v1.18.5...v1.18.6) (2021-01-19) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [1.18.5](https://github.com/frontegg/frontegg-react/compare/v1.18.4...v1.18.5) (2021-01-18) + + +### Bug Fixes + +* **core:** fix perfomance for the INputChip component ([cea5e19](https://github.com/frontegg/frontegg-react/commit/cea5e19aef64a6cf42f75d3cbb77cd74fb01f560)) + + + + + +## [1.18.4](https://github.com/frontegg/frontegg-react/compare/v1.18.3...v1.18.4) (2021-01-18) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [1.18.3](https://github.com/frontegg/frontegg-react/compare/v1.18.2...v1.18.3) (2021-01-17) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [1.18.2](https://github.com/frontegg/frontegg-react/compare/v1.18.1...v1.18.2) (2021-01-15) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [1.18.1](https://github.com/frontegg/frontegg-react/compare/v1.18.0...v1.18.1) (2021-01-15) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +# [1.18.0](https://github.com/frontegg/frontegg-react/compare/v1.17.3...v1.18.0) (2021-01-14) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [1.17.3](https://github.com/frontegg/frontegg-react/compare/v1.17.2...v1.17.3) (2021-01-13) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [1.17.2](https://github.com/frontegg/frontegg-react/compare/v1.17.1...v1.17.2) (2021-01-12) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [1.17.1](https://github.com/frontegg/frontegg-react/compare/v1.17.0...v1.17.1) (2021-01-05) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +# [1.17.0](https://github.com/frontegg/frontegg-react/compare/v1.16.2...v1.17.0) (2021-01-03) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [1.16.2](https://github.com/frontegg/frontegg-react/compare/v1.16.0...v1.16.2) (2020-12-27) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [1.16.1](https://github.com/frontegg/frontegg-react/compare/v1.16.0...v1.16.1) (2020-12-27) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +# [1.16.0](https://github.com/frontegg/frontegg-react/compare/v1.15.2...v1.16.0) (2020-12-24) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [1.15.2](https://github.com/frontegg/frontegg-react/compare/v1.15.1...v1.15.2) (2020-12-24) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [1.15.1](https://github.com/frontegg/frontegg-react/compare/v1.15.0...v1.15.1) (2020-12-21) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +# [1.15.0](https://github.com/frontegg/frontegg-react/compare/v1.14.1...v1.15.0) (2020-12-20) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [1.14.1](https://github.com/frontegg/frontegg-react/compare/v1.14.0...v1.14.1) (2020-12-17) + + +### Bug Fixes + +* Fix create new webhooks button typo ([0be76c3](https://github.com/frontegg/frontegg-react/commit/0be76c38a771996ad849a027b752fb7107b9d3db)) + + + + + +# [1.14.0](https://github.com/frontegg/frontegg-react/compare/v1.13.2...v1.14.0) (2020-12-16) + + +### Bug Fixes + +* Fix profile tabs color ([3ab5742](https://github.com/frontegg/frontegg-react/commit/3ab57426355700c05930075c014faa2c10456b9c)) + + +### Features + +* **auth:** Api tokens component for users and tenants ([c8b1e17](https://github.com/frontegg/frontegg-react/commit/c8b1e176bee4f4402afbd9625841312428c14b75)) + + + + + +## [1.13.2](https://github.com/frontegg/frontegg-react/compare/v1.13.1...v1.13.2) (2020-12-13) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [1.13.1](https://github.com/frontegg/frontegg-react/compare/v1.13.0...v1.13.1) (2020-12-10) + + +### Bug Fixes + +* **audits:** FR-999 add x btn for ip popup ([6efc8ea](https://github.com/frontegg/frontegg-react/commit/6efc8ea6e229b6434d75f9af59c0b6f0993ec9cd)) + + + + + +# [1.13.0](https://github.com/frontegg/frontegg-react/compare/v1.12.0...v1.13.0) (2020-12-09) + + +### Bug Fixes + +* fix testId error in material components ([0e3d2a6](https://github.com/frontegg/frontegg-react/commit/0e3d2a610f762d9065eee261dd996ecea77e1c8d)), closes [#119](https://github.com/frontegg/frontegg-react/issues/119) + + + + + +# [1.12.0](https://github.com/frontegg/frontegg-react/compare/v1.11.1...v1.12.0) (2020-12-09) + + +### Bug Fixes + +* **audits:** FR-1002 add globe icon when location is unknown ([53265fd](https://github.com/frontegg/frontegg-react/commit/53265fd5cdc56d3fc141ad77469d452e2f6dc430)) + + + + + +## [1.11.1](https://github.com/frontegg/frontegg-react/compare/v1.11.0...v1.11.1) (2020-12-02) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +# [1.11.0](https://github.com/frontegg/frontegg-react/compare/v1.10.0...v1.11.0) (2020-11-30) + + +### Features + +* **ci:** add option to publish prerelease version ([0ff0c67](https://github.com/frontegg/frontegg-react/commit/0ff0c672f86eacf175790b89173f1c6e34789b7e)) + + + + + +# [1.10.0](https://github.com/frontegg/frontegg-react/compare/v1.9.0...v1.10.0) (2020-11-27) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +# [1.9.0](https://github.com/frontegg/frontegg-react/compare/v1.8.0...v1.9.0) (2020-11-25) + + +### Features + +* **auth:** New AccountDropdown component added to AuthPlugin ([018f2f8](https://github.com/frontegg/frontegg-react/commit/018f2f8db3ad22981f9270bd2e166b56cf6eb7ff)) +* New plugin for Audits ([#106](https://github.com/frontegg/frontegg-react/issues/106)) ([921b37a](https://github.com/frontegg/frontegg-react/commit/921b37aca98f23c800c8b5d094ed595d52617679)) + + + + + +# [1.8.0](https://github.com/frontegg/frontegg-react/compare/v1.7.0...v1.8.0) (2020-11-23) + + +### Features + +* Add Connectivity Plugin ([#98](https://github.com/frontegg/frontegg-react/issues/98)) ([db77431](https://github.com/frontegg/frontegg-react/commit/db77431b6d744b93431430543019fedd1c0dbae2)) + + + + + +# [1.7.0](https://github.com/frontegg/frontegg-react/compare/v1.6.1...v1.7.0) (2020-11-22) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [1.6.2](https://github.com/frontegg/frontegg-react/compare/v1.6.1...v1.6.2) (2020-11-19) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [1.6.1](https://github.com/frontegg/frontegg-react/compare/v1.6.0...v1.6.1) (2020-11-15) + + +### Bug Fixes + +* fix multiple store initialization in strict-mode ([a569f86](https://github.com/frontegg/frontegg-react/commit/a569f86b37292e71b985c3a2e54610121ab419ce)) + + + + + +# [1.6.0](https://github.com/frontegg/frontegg-react/compare/v1.5.0...v1.6.0) (2020-11-12) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +# [1.5.0](https://github.com/frontegg/frontegg-react/compare/v1.4.0...v1.5.0) (2020-11-10) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +# [1.4.0](https://github.com/frontegg/frontegg-react/compare/v1.3.0...v1.4.0) (2020-11-09) + + +### Bug Fixes + +* remove logs ([16b0976](https://github.com/frontegg/frontegg-react/commit/16b09762f77e8c4491e1570b954a1c04511ba53f)) + + +### Features + +* notifications plugin ([#78](https://github.com/frontegg/frontegg-react/issues/78)) ([0439d17](https://github.com/frontegg/frontegg-react/commit/0439d179ed5c0abae510b7d132dbf03ae907f7f6)) + + + + + +# [1.3.0](https://github.com/frontegg/frontegg-react/compare/v1.2.0...v1.3.0) (2020-11-04) + + +### Bug Fixes + +* **auth:** bug fixes ([#76](https://github.com/frontegg/frontegg-react/issues/76)) ([a758642](https://github.com/frontegg/frontegg-react/commit/a758642458751e930dc4ea6de6acc50b3ede0b77)) +* **auth:** fix ui bugs ([#79](https://github.com/frontegg/frontegg-react/issues/79)) ([0e75d8d](https://github.com/frontegg/frontegg-react/commit/0e75d8dff80937dc2e4308a0ffdd527a12a84a39)) + + +### Features + +* **elements:** add FeInput element ([f23e439](https://github.com/frontegg/frontegg-react/commit/f23e4392ab849d32c5e0ba67e055f793ecfd5ceb)) +* add elements page ([9eb19a8](https://github.com/frontegg/frontegg-react/commit/9eb19a886a4cbc788ad236ce9f597c33da7f68ef)) +* add support for nextjs and angular ([#82](https://github.com/frontegg/frontegg-react/issues/82)) ([5fa36eb](https://github.com/frontegg/frontegg-react/commit/5fa36ebe7bfa6866c78455a746727ba8b1cafbbc)) + + + + + +# [1.2.0](https://github.com/frontegg/frontegg-react/compare/v1.1.0...v1.2.0) (2020-10-25) + + +### Bug Fixes + +* **elements:** UI elements small fixes ([effb9bd](https://github.com/frontegg/frontegg-react/commit/effb9bd54186133184010c34874af212c040ef90)) + + +### Features + +* **auth:** add SSO components ([#64](https://github.com/frontegg/frontegg-react/issues/64)) ([f083762](https://github.com/frontegg/frontegg-react/commit/f0837623073c1f9a636480c6a88d5969fb020a09)) +* **auth:** support all auth functionalities ([#70](https://github.com/frontegg/frontegg-react/issues/70)) ([26d725e](https://github.com/frontegg/frontegg-react/commit/26d725e2f386c6b4a703e4371fac8efef29676d1)) +* **core:** add dynamic elements for Frontegg Components ([#10](https://github.com/frontegg/frontegg-react/issues/10)) ([2bddbb2](https://github.com/frontegg/frontegg-react/commit/2bddbb2794ac0dc4af90c8df0f33b69a312e063a)) +* **core:** add FeSwitchToggle component ([#66](https://github.com/frontegg/frontegg-react/issues/66)) ([5b6c603](https://github.com/frontegg/frontegg-react/commit/5b6c603d912be45b1982c06b7f026badd8e82f1c)) + + + + + +# [1.1.0](https://github.com/frontegg/frontegg-react/compare/v1.0.89...v1.1.0) (2020-10-14) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [1.0.89](https://github.com/frontegg/frontegg-react/compare/v1.0.88...v1.0.89) (2020-10-13) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [1.0.88](https://github.com/frontegg/frontegg-react/compare/v1.0.87...v1.0.88) (2020-10-11) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## [1.0.87](https://github.com/frontegg/frontegg-react/compare/v1.0.86...v1.0.87) (2020-10-09) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## 1.0.85 (2020-10-08) + +**Note:** Version bump only for package @frontegg/demo-saas + + + + + +## 1.0.84 (2020-10-08) + +**Note:** Version bump only for package @frontegg/demo-saas diff --git a/packages/demo-saas/package.json b/packages/demo-saas/package.json index 0ed75d3d1..f89470dc7 100644 --- a/packages/demo-saas/package.json +++ b/packages/demo-saas/package.json @@ -1,27 +1,30 @@ { "name": "@frontegg/demo-saas", - "version": "7.15.2", + "version": "4.0.23", "private": true, "author": "Frontegg LTD", "scripts": { "start": "react-scripts start", + "cy-start": "OPEN_BROWSER=false && react-scripts -r @cypress/instrument-cra start", "build": "react-scripts build", "build:watch": "echo 'No build:watch script'", "test": "echo 'No Unit Tests'" }, "dependencies": { - "@emotion/react": "^11.4.1", - "@emotion/styled": "^11.3.0", - "@frontegg/react": "^7.15.2", - "@mui/icons-material": "5.11.0", - "@mui/material": "5.0.3", - "react": "^17.0.1", - "react-dom": "^17.0.1", - "react-router-dom": "^5.3.3" + "@frontegg/react-auth": "^4.0.23", + "@frontegg/react-core": "^4.0.23", + "classnames": "^2.2.6", + "react": ">16.8.6", + "react-dom": ">16.8.6", + "react-redux": "^7.1.1", + "react-router-dom": "^5.1.2", + "redux-saga": "^1.1.3" }, "devDependencies": { - "@types/react-router-dom": "^5.3.3", - "react-scripts": "^5.0.1" + "@types/react": "^16.9.19", + "@types/react-dom": "^16.9.8", + "@types/react-router-dom": "^5.1.2", + "react-scripts": "^3.4.3" }, "browserslist": { "production": [ diff --git a/packages/demo-saas/public/index.html b/packages/demo-saas/public/index.html index c7b5c6c04..b7eee9552 100644 --- a/packages/demo-saas/public/index.html +++ b/packages/demo-saas/public/index.html @@ -8,6 +8,10 @@ <meta name="description" content="Web site created using create-react-app" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> + <link + rel="stylesheet" + href="https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic&subset=Nunito+Sans&display=swap" + /> <title>Demo SaaS - Live Frontegg diff --git a/packages/demo-saas/src/App.tsx b/packages/demo-saas/src/App.tsx deleted file mode 100644 index 138fd7dc5..000000000 --- a/packages/demo-saas/src/App.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import React, { FC, useState } from 'react'; -import { BrowserRouter, Route, Switch } from 'react-router-dom'; -import Loader from '@mui/material/CircularProgress'; -import Box from '@mui/material/Box'; -import { FronteggProvider } from '@frontegg/react'; -import { FronteggAppOptions } from '@frontegg/types'; -import { authOptions } from './customizationOptions/authOptions'; -import HomePage from './HomePage'; -import CMCPage from './cmc/CMCPage'; -import ModalsStepUpPage from './stepUp/ModalsStepUpPage'; -import HOCStepUpPage from './stepUp/HOCStepUpPage'; -import SimpleStepUpButtonPage from './stepUp/SimpleStepUpButtonPage'; -import SmallMaxAgeStepUpPage from './stepUp/SmallMaxAgeStepUpPage'; -import TransferStepUpPage from './stepUp/TransferStepUpPage'; -import NoMaxAgeStepUpPage from './stepUp/NoMaxAgeStepUpPage'; -import EntitlementsPage from './entitlements/EntitlementsPage'; -import Fallback from './Fallback'; -import NotAFronteggPage from './NotAFronteggPage'; -import { DEFAULT_BASE_URL } from './consts'; -import { ROUTE_PATHS } from './BaseHomePage/components/Links'; - -const IS_HOSTED_LOGIN = true; - -const fronteggOptions: FronteggAppOptions = - // @ts-ignore - window.CYPRESS_CONFIG || - ({ - contextOptions: { - baseUrl: process.env.PUBLIC_URL || process.env.REACT_APP_BASE_URL || DEFAULT_BASE_URL, - clientId: process.env.REACT_APP_CLIENT_ID, - }, - ...authOptions, - enableOpenAppRoute: true, - } as FronteggAppOptions); - -export const App: FC = () => { - const [loading, setLoading] = useState(true); - - return ( - - - - - - - - - - - - - {/* For tests that use someurl as the authenticated-url */} - - { - return
Test
; - }} - /> - -
-
- - {loading && !IS_HOSTED_LOGIN && ( - - - - )} -
- ); -}; diff --git a/packages/demo-saas/src/App/App.tsx b/packages/demo-saas/src/App/App.tsx new file mode 100644 index 000000000..86547bc26 --- /dev/null +++ b/packages/demo-saas/src/App/App.tsx @@ -0,0 +1,113 @@ +import React, { FC } from 'react'; +import { Switch, Route, Link } from 'react-router-dom'; +import { AdminPortal } from '@frontegg/admin-portal'; +import { + ProtectedRoute, + Profile, + SSO, + useAuthUser, + Team, + AccountDropdown, + UserApiTokens, + SignUpPageComponent, +} from '@frontegg/react-auth'; +import { ElementsPage } from '../Elements/ElementsPage'; +import { PopupExample } from '../PopupExample'; +import { TableExample } from '../TableExample'; +import { ComponentsPage2 } from '../ComponentsPage2'; +import { GridExamples } from '../grid-examples'; +import { SelectorExample } from '../SelectorExample'; +import { NotificationsExample } from '../notifications-example'; +import { DialogExample } from '../DialogExample'; +import { AuditsExample } from 'auditsExample'; +import { TenantApiTokensExample } from 'apiTokensExample'; +import { + EmailComponent, + ConnectivityPage, + SlackComponent, + SMSComponent, + WebhookComponent, +} from '@frontegg/react-connectivity'; +import { Icons } from 'pages/Icons'; +import { Button } from '@frontegg/react-core'; + +const TestPage: FC = () => { + const user = useAuthUser(); + return
{JSON.stringify(user)}
; +}; + +const menus = [ + { + to: '/profile', + title: 'Profile', + children: ( + + + + ), + }, + { to: '/team', title: 'Team', component: Team.Page }, + { to: '/sso', title: 'SSO', children: }, + { to: '/tenant-api-tokens', title: 'Tenant Api tokens', children: }, + { to: '/user-api-tokens', title: 'User Api tokens', children: }, + { to: '/test-auth-user', title: 'Test Auth User', component: TestPage }, + { to: '/popup', title: 'Popup Examples', component: PopupExample }, + { to: '/table', title: 'Table Examples', component: TableExample }, + { to: '/select', title: 'Select Examples', component: SelectorExample }, + { to: '/connectivity', title: 'Connectivity', children: }, + { to: '/webhook', title: 'Webhook', component: WebhookComponent }, + { to: '/slack', title: 'Slack', component: SlackComponent }, + { to: '/emails', title: 'Email', component: EmailComponent }, + { to: '/sms', title: 'SMS', component: SMSComponent }, + { to: '/notifications', title: 'Notifications Example', children: }, + { to: '/components2', component: ComponentsPage2, exact: true }, + { to: '/components', component: ElementsPage }, + { to: '/grids', component: GridExamples }, + { to: '/dialog', component: DialogExample }, + { to: '/audits', title: 'Audits Example', component: AuditsExample }, + { to: '/icons', component: Icons }, + { to: '/signup', title: 'Sign up', component: SignUpPageComponent }, +]; + +class App extends React.Component { + render() { + return ( +
+
+ +
+ + +
+ {menus.map( + ({ to, title }, idx) => + title && ( +
+ {title}{' '} +
+ ) + )} + + +
+
+ {menus.map(({ to, component, children, exact }, idx) => ( + + {children} + + ))} + +
+
+ ); + } +} + +export default App; diff --git a/packages/demo-saas/src/App/index.ts b/packages/demo-saas/src/App/index.ts new file mode 100644 index 000000000..c866729a6 --- /dev/null +++ b/packages/demo-saas/src/App/index.ts @@ -0,0 +1 @@ +export { default as App } from './App'; diff --git a/packages/demo-saas/src/BaseHomePage/BaseHomePage.tsx b/packages/demo-saas/src/BaseHomePage/BaseHomePage.tsx deleted file mode 100644 index 1bdb94660..000000000 --- a/packages/demo-saas/src/BaseHomePage/BaseHomePage.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React, { FC } from 'react'; -import { ContextHolder } from '@frontegg/rest-api'; -import { useAuth } from '@frontegg/react-hooks'; - -import Box from '@mui/material/Box'; -import { EmbeddedBasedHomePage } from './components/EmbeddedHomePage'; -import { HostedBasedHomePage } from './components/HostedHomePage'; -import { BaseHomePageProps } from './interfaces'; -import { NoBaseUrlSection } from './components/NoBaseUrlSection/NoBaseUrlSection'; -import { DEFAULT_BASE_URL } from '../consts'; - -const BaseHomePage: FC = (props) => { - const { hostedLoginBox } = useAuth(); - const { baseUrl } = ContextHolder.getContext(); - - if (baseUrl === DEFAULT_BASE_URL) { - return ; - } - - return ( - - {hostedLoginBox ? : } - - ); -}; - -export default BaseHomePage; - -export const wrapWithBaseHomePage = (Component: any, wrapperStyles?: any) => { - return (props: any) => ( - - - - ); -}; diff --git a/packages/demo-saas/src/BaseHomePage/components/AdminPortalButton.tsx b/packages/demo-saas/src/BaseHomePage/components/AdminPortalButton.tsx deleted file mode 100644 index 746e81a12..000000000 --- a/packages/demo-saas/src/BaseHomePage/components/AdminPortalButton.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import { AdminPortal } from '@frontegg/js'; -import { DemoButton } from '../../DemoButton'; - -export const AdminPortalButton = () => ( - { - AdminPortal.show(); - }} - > - {'Open AdminBox'} - -); diff --git a/packages/demo-saas/src/BaseHomePage/components/ChildrenRenderer.tsx b/packages/demo-saas/src/BaseHomePage/components/ChildrenRenderer.tsx deleted file mode 100644 index d44b0a5bb..000000000 --- a/packages/demo-saas/src/BaseHomePage/components/ChildrenRenderer.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React, { FC, ReactNode } from 'react'; - -import Box from '@mui/material/Box'; - -export const ChildrenRenderer: FC<{ children: ReactNode; wrapperStyles?: any }> = ({ wrapperStyles, children }) => ( - - - {children} - - -); diff --git a/packages/demo-saas/src/BaseHomePage/components/EmbeddedHomePage.tsx b/packages/demo-saas/src/BaseHomePage/components/EmbeddedHomePage.tsx deleted file mode 100644 index b25cdbc2a..000000000 --- a/packages/demo-saas/src/BaseHomePage/components/EmbeddedHomePage.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React, { FC } from 'react'; -import { useAuthUser, useAuthActions, useIsAuthenticated } from '@frontegg/react-hooks'; - -import Box from '@mui/material/Box'; -import { BaseHomePageProps } from '../interfaces'; -import { DemoButton } from '../../DemoButton'; -import { AdminPortalButton } from './AdminPortalButton'; -import { ChildrenRenderer } from './ChildrenRenderer'; -import { Links } from './Links'; -import { User } from './User'; - -export const EmbeddedBasedHomePage: FC = ({ children, wrapperStyles }) => { - const user = useAuthUser(); - - const { logout } = useAuthActions(); - const isAuthenticated = useIsAuthenticated(); - - return ( - - - - - logout()}> - Logout - - - - -
Embedded
- - - - {isAuthenticated && {children}} -
- ); -}; diff --git a/packages/demo-saas/src/BaseHomePage/components/HostedHomePage.tsx b/packages/demo-saas/src/BaseHomePage/components/HostedHomePage.tsx deleted file mode 100644 index ab9bbc5eb..000000000 --- a/packages/demo-saas/src/BaseHomePage/components/HostedHomePage.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React, { FC, useEffect } from 'react'; -import { - useLoginWithRedirect, - useLoginWithRedirectV2, - useAuthActions, - useAuthUserOrNull, - useIsAuthenticated, -} from '@frontegg/react-hooks'; - -import Box from '@mui/material/Box'; -import { BaseHomePageProps } from '../interfaces'; -import { DemoButton } from '../../DemoButton'; -import { AdminPortalButton } from './AdminPortalButton'; -import { ChildrenRenderer } from './ChildrenRenderer'; -import { Links } from './Links'; -import { User } from './User'; - -export const HostedBasedHomePage: FC = ({ children, wrapperStyles }) => { - const user = useAuthUserOrNull(); - const loginWithRedirect = useLoginWithRedirect(); - const loginWithRedirectV2 = useLoginWithRedirectV2(); - - const { logout } = useAuthActions(); - const isAuthenticated = useIsAuthenticated(); - - // comment it to avoid redirect to login when not authenticated - useEffect(() => { - !isAuthenticated && loginWithRedirect(); - }, [isAuthenticated, loginWithRedirect]); - - return ( - <> - - - - {!isAuthenticated && ( - <> - { - loginWithRedirect(); - }} - > - Login - - - { - loginWithRedirectV2({ - shouldRedirectToLogin: true, - loginDirectAction: { - type: 'social-login', - data: 'google', - }, - }); - }} - > - Direct Login with redirect - - - )} - - {isAuthenticated && ( - logout()}> - Logout - - )} - - - -
Hosted
- - - - {isAuthenticated && {children}} - - ); -}; diff --git a/packages/demo-saas/src/BaseHomePage/components/Links.tsx b/packages/demo-saas/src/BaseHomePage/components/Links.tsx deleted file mode 100644 index b0dfffe8d..000000000 --- a/packages/demo-saas/src/BaseHomePage/components/Links.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react'; - -import Box from '@mui/material/Box'; -import { Link } from 'react-router-dom'; - -const linkStyle = { - padding: '4px 10px', - fontSize: '15px', - textDecoration: 'none', - background: '#1976d2', - color: 'white', - fontFamily: '"Roboto","Helvetica","Arial",sans-serif', - border: '1px solid #1976d2', - borderRadius: '4px', -}; - -export const ROUTE_PATHS = { - HOME_PAGE: '/', - ENTITLEMENTS: '/entitlements', - STEP_UP_HIGH_MAX_AGE: '/step-up-high-max-age', - STEP_UP_SMALL_MAX_AGE: '/step-up-small-max-age', - STEP_UP_NO_MAX_AGE: '/step-up-no-max-age', - STEP_UP_MODALS: '/step-up-modals', - STEP_UP_HOC: '/step-up-hoc', - STEP_UP_TRANSFER: '/step-up-transfer', - UNKNOWN_ROUTE: '/unknown-route', - TEST: '/test', - CMC: '/cmc', -}; - -const links = [ - { route: ROUTE_PATHS.HOME_PAGE, label: 'Home page' }, - { route: ROUTE_PATHS.ENTITLEMENTS, label: 'Entitlements' }, - { route: ROUTE_PATHS.STEP_UP_HIGH_MAX_AGE, label: 'Step up high max age' }, - { route: ROUTE_PATHS.STEP_UP_SMALL_MAX_AGE, label: 'Step up small max age' }, - { route: ROUTE_PATHS.STEP_UP_NO_MAX_AGE, label: 'Step up no nax age' }, - { route: ROUTE_PATHS.STEP_UP_MODALS, label: 'Step up modals' }, - { route: ROUTE_PATHS.STEP_UP_HOC, label: 'Step up HOC' }, - { route: ROUTE_PATHS.STEP_UP_TRANSFER, label: 'Step up transfer' }, - { route: ROUTE_PATHS.UNKNOWN_ROUTE, label: 'Fallback route' }, - { route: ROUTE_PATHS.TEST, label: 'Old test' }, - { route: ROUTE_PATHS.CMC, label: 'CMC' }, -]; - -export const Links = () => ( - - Pages: - {links.map(({ route, label }, index) => ( - - - {label} - - - ))} - -); diff --git a/packages/demo-saas/src/BaseHomePage/components/NoBaseUrlSection/NoBaseUrlSection.css b/packages/demo-saas/src/BaseHomePage/components/NoBaseUrlSection/NoBaseUrlSection.css deleted file mode 100644 index 748539662..000000000 --- a/packages/demo-saas/src/BaseHomePage/components/NoBaseUrlSection/NoBaseUrlSection.css +++ /dev/null @@ -1,51 +0,0 @@ -.App-logo { - height: 15vmin; - pointer-events: none; -} - -.Frontegg-logo { - height: 10vmin; - pointer-events: none; -} - -.App-logo-separator { - font-size: 80px; - margin-right: 40px; -} - -.App-logo-container { - justify-content: center; - align-items: center; - display: flex; - margin-bottom: 40px; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } -} - -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 1.5vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/packages/demo-saas/src/BaseHomePage/components/NoBaseUrlSection/NoBaseUrlSection.tsx b/packages/demo-saas/src/BaseHomePage/components/NoBaseUrlSection/NoBaseUrlSection.tsx deleted file mode 100644 index a584152a0..000000000 --- a/packages/demo-saas/src/BaseHomePage/components/NoBaseUrlSection/NoBaseUrlSection.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import logo from './logo.svg'; -import fronteggLogo from './fronteggLogo.svg'; -import './NoBaseUrlSection.css'; - -export const NoBaseUrlSection = () => ( -
-
-
- logo - + - logo -
- -

- Honey, edit FronteggOptions {'{ baseUrl: "" }'} to connect to your application -

- - - Learn how to use Frontegg - -
-
-); diff --git a/packages/demo-saas/src/BaseHomePage/components/NoBaseUrlSection/fronteggLogo.svg b/packages/demo-saas/src/BaseHomePage/components/NoBaseUrlSection/fronteggLogo.svg deleted file mode 100644 index 213d18671..000000000 --- a/packages/demo-saas/src/BaseHomePage/components/NoBaseUrlSection/fronteggLogo.svg +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/demo-saas/src/BaseHomePage/components/NoBaseUrlSection/logo.svg b/packages/demo-saas/src/BaseHomePage/components/NoBaseUrlSection/logo.svg deleted file mode 100644 index 9dfc1c058..000000000 --- a/packages/demo-saas/src/BaseHomePage/components/NoBaseUrlSection/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/demo-saas/src/BaseHomePage/components/User.tsx b/packages/demo-saas/src/BaseHomePage/components/User.tsx deleted file mode 100644 index c29e8ea14..000000000 --- a/packages/demo-saas/src/BaseHomePage/components/User.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import Box from '@mui/material/Box'; - -export const User = ({ user }: any) => ( - - {user?.email ? ( - <> - Authenticated as {user?.email} - - ) : ( - 'Not Authenticated' - )} - -); diff --git a/packages/demo-saas/src/BaseHomePage/interfaces.ts b/packages/demo-saas/src/BaseHomePage/interfaces.ts deleted file mode 100644 index 1c7997cf6..000000000 --- a/packages/demo-saas/src/BaseHomePage/interfaces.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ReactNode } from 'react'; - -export interface BaseHomePageProps { - children: ReactNode; - wrapperStyles?: any; -} diff --git a/packages/demo-saas/src/CheckboxPage.tsx b/packages/demo-saas/src/CheckboxPage.tsx new file mode 100644 index 000000000..b6ac82736 --- /dev/null +++ b/packages/demo-saas/src/CheckboxPage.tsx @@ -0,0 +1,15 @@ +import React, { FC } from 'react'; +import { uiLibrary as Semantic } from '@frontegg/react-elements-semantic'; +import { uiLibrary as Material } from '@frontegg/react-elements-material-ui'; +import { Elements, fronteggElements as Frontegg } from '@frontegg/react-core'; + +const FE = Frontegg as Elements; + +export const CheckboxPage: FC = () => { + return ( +
+ + +
+ ); +}; diff --git a/packages/demo-saas/src/ComponentsPage2.tsx b/packages/demo-saas/src/ComponentsPage2.tsx new file mode 100644 index 000000000..050182c60 --- /dev/null +++ b/packages/demo-saas/src/ComponentsPage2.tsx @@ -0,0 +1,1213 @@ +import React, { FC, useCallback } from 'react'; +import { Input, fronteggElements, Elements } from '@frontegg/react-core'; +import { uiLibrary as Se } from '@frontegg/react-elements-semantic'; +import { uiLibrary as Ma } from '@frontegg/react-elements-material-ui'; + +const Frontegg = fronteggElements as Elements; +const Material = Ma as Elements; +const Semantic = Se as Elements; +const { Table } = fronteggElements as Elements; +const data = [ + { + ip: '79.176.23.49', + user: 'Tillie Casias', + action: 'Accessed', + resource: 'Audit', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:15:15.600', + totalRows: 2250, + searchText: 'None', + piiReturned: 'true', + frontegg_id: 'cc4330b6-5882-4460-a0c1-6941f1d35d86', + }, + { + ip: '79.176.23.49', + user: 'Johnny Lu', + action: 'Accessed', + resource: 'Dashboard', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:15:15.462', + frontegg_id: '777293f9-de88-49c4-b432-e8c853d8e648', + }, + { + ip: '72.28.101.231', + user: 'Florine Pinion', + action: 'Sanity Check Finished', + scanId: '30eefef7-859f-4fce-9ba8-8666bc342591', + service: 'Payments', + infoLink: 'https://iapp.org/news/a/understanding-data-processors-iso-and-soc-2-credentials-for-gdpr-compliance/', + resource: 'Cluster', + severity: 'Attention', + standard: 'SOC2', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:00:10.221', + frontegg_id: 'e1cd9f43-505c-45fe-bd60-f712656235fa', + }, + { + ip: '72.92.55.231', + user: 'Verona Gonzalas', + action: 'Sanity Check Finished', + scanId: '9eea13e1-a91f-47be-8378-1c076f121527', + service: 'Cars', + infoLink: 'https://iapp.org/news/a/understanding-data-processors-iso-and-soc-2-credentials-for-gdpr-compliance/', + resource: 'Cluster', + severity: 'Info', + standard: 'SOC2', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:00:10.087', + frontegg_id: '7545fd29-c010-41ee-8238-bb39526c8f6a', + }, + { + ip: '23.92.49.21', + user: 'Melda Richert', + action: 'Periodic Scan Finished', + result: 'Success', + scanId: '428454cf-faac-49b1-b3c4-d904255da224', + service: 'Cars', + resource: 'Service', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:00:05.124', + frontegg_id: '62adc639-e701-4095-95a4-27cdbedf59ab', + }, + { + ip: '23.92.49.21', + user: 'Rhoda Blaylock', + action: 'Periodic Scan Finished', + result: 'Total Failure', + scanId: 'c96c6560-af2b-4abe-94d2-c8f2b741f6ea', + service: 'Cars', + resource: 'Service', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:00:05.069', + frontegg_id: '9b83dc83-204c-433a-97ad-639279cfd5e6', + }, + { + ip: '25.44.49.21', + user: 'Tillie Casias', + action: 'Settings Modified', + resource: 'Microservice', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:00:01.483', + microservice: 'Payments', + settingChanged: 'Authentication', + frontegg_id: '0ecfa8fd-8ca0-4736-bf1a-b2a77bcc0e3e', + }, + { + ip: '161.185.160.93', + user: 'Clement Gallop', + cause: 'Service Response Lag', + action: 'Remediated', + scanId: '50622ae6-9156-4894-bcce-a61a015fa5a1', + lagTime: '2500ms', + service: 'Payments', + resource: 'Service', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:00:01.482', + restartTime: '2000ms', + frontegg_id: '24a9e25f-9ca4-48c0-9c1c-17940bf2b919', + }, + { + ip: '161.185.160.93', + api: 'GET /payments', + user: 'Kieth Mason', + action: 'Liveness Check Perfomed', + result: 'Success', + scanId: '96212e63-fab9-451e-a8b0-61b6d59ad8e7', + resource: 'APIs', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:00:01.481', + frontegg_id: 'ba814ecd-7186-44b2-ac03-f9591cac3015', + }, + { + ip: '25.44.49.21', + user: 'Kieth Mason', + action: 'Settings Modified', + changed: 'Amount of Services', + cluster: 'Main', + resource: 'Cluster', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:00:01.481', + frontegg_id: '0f488824-c7af-4463-8639-2669a492c2e2', + }, + { + ip: '23.92.55.21', + user: 'Tillie Casias', + email: 'tilliecasias@example.com', + action: 'Added', + resource: 'Users', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:00:01.480', + frontegg_id: '92bb449b-ab0a-4f2c-9055-1ea03a7f7d83', + }, + { + ip: '23.92.55.231', + user: 'Ardelia Dismuke', + action: 'Settings Modified', + resource: 'Microservice', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:00:01.478', + microservice: 'Payments', + settingChanged: 'Authentication', + frontegg_id: '8597d041-257d-43ce-b561-cddab37dc2b4', + }, + { + ip: '72.28.101.231', + api: 'POST /products', + user: 'Marg Lovelace', + action: 'Liveness Check Perfomed', + result: 'Success', + scanId: '25638864-671a-4beb-8df8-22ce0e0b8147', + resource: 'APIs', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:00:01.477', + frontegg_id: '4d1ae8f7-8908-4df2-866b-0d906d14650e', + }, + { + ip: '3.92.49.21', + user: 'Ardelia Dismuke', + action: 'Lag Detected', + result: 'Service Response Lag', + scanId: '8da73d4b-3785-4421-a969-5fb19c7d2988', + lagTime: '14007ms', + service: 'Users', + resource: 'Service', + severity: 'Error', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:00:01.476', + frontegg_id: '6c579ddc-5d11-4220-a0a8-52b63df762e6', + }, + { + ip: '35.92.49.21', + user: 'Iris Basso', + action: 'Security Audit Perfomed', + scanId: '3e182f18-a295-4c05-a101-eaa9aa3cb706', + service: 'Users', + infoLink: 'https://owasp.org/www-project-top-ten/', + resource: 'Cluster', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:00:01.475', + owaspResult: 'Broken Authentication', + frontegg_id: 'e65e397d-820d-4bc2-a91b-1b1f462255b2', + }, + { + ip: '3.92.49.21', + user: 'Jennell Fant', + action: 'Remap', + changed: 'Amount of Services', + cluster: 'Main', + resource: 'Cluster', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:00:01.475', + frontegg_id: '44319670-695e-41ee-842e-1672e4ccb7db', + }, + { + ip: '72.28.55.231', + user: 'Verona Gonzalas', + action: 'Compliance Audit Performed', + scanId: '375e5feb-e0d8-4c8c-98d2-c06a5862f617', + service: 'Payments', + infoLink: 'https://iapp.org/news/a/understanding-data-processors-iso-and-soc-2-credentials-for-gdpr-compliance/', + resource: 'Cluster', + severity: 'Attention', + standard: 'SOC2', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:00:01.474', + frontegg_id: 'f4227fd6-3474-4fc2-94ee-5b172365c977', + }, + { + ip: '72.92.55.231', + user: 'Naida Rinker', + action: 'Security Audit Perfomed', + scanId: 'c4cd62f4-9d20-4773-bcde-eaba9a6208d9', + service: 'Users', + infoLink: 'https://owasp.org/www-project-top-ten/', + resource: 'Cluster', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:00:01.473', + owaspResult: 'Broken Authentication', + frontegg_id: '4dd797b7-8e52-4a82-a235-b338ce0afb0a', + }, + { + ip: '23.92.49.21', + info: '/insights#scan-id', + user: 'Florine Pinion', + action: 'Sanity Check Started', + failed: '2 Tests', + scanId: '30eefef7-859f-4fce-9ba8-8666bc342591', + cluster: 'Main', + resource: 'Cluster', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:00:01.472', + succeeded: '42 Tests', + testsPerfomed: '44 tests', + frontegg_id: 'dce52fbf-799a-4a8f-9c17-5ed382df8e52', + }, + { + ip: '72.28.55.231', + user: 'Debora Coddington', + action: 'Remap', + changed: 'Security Level', + cluster: 'Main', + resource: 'Cluster', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:00:01.471', + frontegg_id: 'b9bccc42-e440-4b3f-9dc2-0b54b7c721ae', + }, + { + ip: '3.92.49.21', + user: 'Deanna Post', + action: 'Remap', + changed: 'Amount of Services', + cluster: 'Main', + resource: 'Cluster', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-29 00:00:01.320', + frontegg_id: '54e2cfe6-d040-4671-b23e-053c511fbe4f', + }, + { + ip: '23.92.55.231', + user: 'Naida Rinker', + action: 'Downtime Detected', + result: 'Total Service Downtime', + scanId: 'e09e1684-f736-4277-851c-d59809e57d43', + service: 'Users', + resource: 'Service', + severity: 'Error', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-29 00:00:01.319', + frontegg_id: '58297c84-262a-45df-a054-dc7207a29d3b', + }, + { + ip: '72.28.101.231', + user: 'Herb Mcwain', + action: 'Cache Purged', + resource: 'API Gateway', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-29 00:00:01.318', + cachedItems: 462, + frontegg_id: '5570deca-2b0b-4779-bab3-d3451e5b3bc0', + }, + { + ip: '23.92.55.21', + user: 'Normand Menz', + action: 'Compliance Audit Performed', + scanId: 'b076bb81-74c7-425e-b384-f2f91f5dc60e', + service: 'Cars', + infoLink: 'https://iapp.org/news/a/understanding-data-processors-iso-and-soc-2-credentials-for-gdpr-compliance/', + resource: 'Cluster', + severity: 'Attention', + standard: 'HIPAA', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-29 00:00:01.316', + frontegg_id: '6985ca88-4b59-4abf-a124-980de32f1153', + }, + { + ip: '35.92.49.21', + user: 'Caitlin Hodes', + action: 'Security Audit Perfomed', + scanId: '9c44e4d5-9886-484d-9c14-2c81be2dedee', + service: 'Payments', + infoLink: 'https://owasp.org/www-project-top-ten/', + resource: 'Cluster', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-29 00:00:01.315', + owaspResult: 'Broken Authentication', + frontegg_id: '489a91f3-37b4-4beb-90d6-5f500a5fef58', + }, + { + ip: '3.92.49.21', + user: 'Valery Krieg', + action: 'Lag Detected', + result: 'Service Response Lag', + scanId: '0db8e45b-94d0-4159-ba98-4f47ad62c3de', + lagTime: '4021ms', + service: 'Payments', + resource: 'Service', + severity: 'Error', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-29 00:00:01.314', + frontegg_id: '18e3ccf2-56b4-48ea-b01c-4024158f1989', + }, + { + ip: '72.28.101.231', + user: 'Emelia Modeste', + email: 'emeliamodeste@example.com', + action: 'Added', + resource: 'Users', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-29 00:00:01.312', + frontegg_id: 'd0bad576-75a0-4f25-96e4-00cf30570485', + }, + { + ip: '3.92.49.21', + info: '/insights#scan-id', + user: 'Darcie Policastro', + action: 'Sanity Check Started', + failed: '2 Tests', + scanId: 'af7f4742-3ee0-4cdd-9cac-dbbcaf16eb7e', + cluster: 'Main', + resource: 'Cluster', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-29 00:00:01.310', + succeeded: '42 Tests', + testsPerfomed: '44 tests', + frontegg_id: 'fab4ecf7-1828-441c-933f-6d8ffb4f44b4', + }, + { + ip: '72.28.101.231', + user: 'Naida Rinker', + email: 'naidarinker@example.com', + action: 'Removed', + resource: 'Users', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-29 00:00:01.308', + frontegg_id: 'a384b58b-dcc9-4852-89eb-8cbfabc80406', + }, + { + ip: '72.92.55.231', + url: 'https://example.com/hook?services', + user: 'Geraldo Shupe', + title: 'Service events', + action: 'Edited', + events: 'Service.Added, Service.Deleted', + resource: 'WebHooks', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-29 00:00:01.307', + webHookId: '3aa06a19-edf9-4026-b030-dc6e3eb80712', + frontegg_id: 'ad393229-a2aa-42ec-9bee-2151e56637f5', + }, + { + ip: '25.42.49.21', + user: 'Herb Mcwain', + action: 'Settings Modified', + resource: 'Microservice', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-29 00:00:01.305', + microservice: 'Payments', + settingChanged: 'Amount of Pods', + frontegg_id: 'e6d8b14c-de90-45ab-b764-b8aa3e4de1b0', + }, + { + ip: '23.92.55.21', + user: 'Naida Rinker', + action: 'Sanity Check Finished', + scanId: '76cd6684-5103-4306-8a14-ca9b9c850183', + service: 'Users', + infoLink: 'https://iapp.org/news/a/understanding-data-processors-iso-and-soc-2-credentials-for-gdpr-compliance/', + resource: 'Cluster', + severity: 'Info', + standard: 'HIPAA', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:10.215', + frontegg_id: '89201527-644b-4a00-b5a7-30c287117999', + }, + { + ip: '25.44.49.21', + user: 'Marg Lovelace', + action: 'Sanity Check Finished', + scanId: 'e1ea57fa-b7bc-41c1-ae5e-bf92b69fb8bf', + service: 'Payments', + infoLink: 'https://iapp.org/news/a/understanding-data-processors-iso-and-soc-2-credentials-for-gdpr-compliance/', + resource: 'Cluster', + severity: 'Info', + standard: 'GDPR', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:10.068', + frontegg_id: 'b7247cae-f7d2-4cb0-82e4-7c512ce589c6', + }, + { + ip: '23.92.49.21', + user: 'Darcie Policastro', + action: 'Periodic Scan Finished', + result: 'Success', + scanId: 'f4d4ec1d-92c0-4fd4-bf72-0dd3a4d1198b', + service: 'Cars', + resource: 'Service', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:05.264', + frontegg_id: 'eaa61296-1f55-4939-8f82-5d5bca1046fb', + }, + { + ip: '3.92.49.21', + user: 'Ardelia Dismuke', + action: 'Periodic Scan Finished', + result: 'Success', + scanId: 'ed0358f2-d828-47d8-bc1c-70e667ffa447', + service: 'Payments', + resource: 'Service', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:05.208', + frontegg_id: 'e93c8137-4c96-45d9-b66e-eb9530c0a543', + }, + { + ip: '161.185.160.93', + user: 'Naida Rinker', + action: 'Security Audit Perfomed', + scanId: 'b3a11eea-4e99-4d92-90c5-4852e9b3b2fc', + service: 'Users', + infoLink: 'https://owasp.org/www-project-top-ten/', + resource: 'Cluster', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:01.237', + owaspResult: 'Broken Authentication', + frontegg_id: 'c97a790e-ead6-484e-a3c4-4b43f7ec0e3b', + }, + { + ip: '23.92.55.21', + user: 'Geraldo Shupe', + action: 'Compliance Audit Performed', + scanId: '54beb7e8-1150-4721-9f1f-c4af8012907d', + service: 'Payments', + infoLink: 'https://iapp.org/news/a/understanding-data-processors-iso-and-soc-2-credentials-for-gdpr-compliance/', + resource: 'Cluster', + severity: 'Info', + standard: 'SOC2', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:01.236', + frontegg_id: '89582bd3-cb4a-42fa-88c2-608b77927299', + }, + { + ip: '35.92.49.21', + user: 'Darcie Policastro', + action: 'Settings Modified', + resource: 'Microservice', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:01.236', + microservice: 'Payments', + settingChanged: 'Amount of Pods', + frontegg_id: '4dec5263-22ff-4efa-957c-4dca7ed34751', + }, + { + ip: '23.92.55.231', + info: '/insights#scan-id', + user: 'Naida Rinker', + action: 'Sanity Check Started', + failed: '2 Tests', + scanId: '76cd6684-5103-4306-8a14-ca9b9c850183', + cluster: 'Main', + resource: 'Cluster', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:01.235', + succeeded: '42 Tests', + testsPerfomed: '44 tests', + frontegg_id: 'a738aec1-2c59-4705-91dd-d50d3da8a8e0', + }, + { + ip: '25.44.49.21', + user: 'Deanna Post', + action: 'Cache Purged', + resource: 'API Gateway', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:01.233', + cachedItems: 651, + frontegg_id: '1111c87b-b480-40fe-b38a-6df73e10cff2', + }, + { + ip: '25.42.1.21', + user: 'Verona Gonzalas', + action: 'Settings Modified', + changed: 'Amount of Services', + cluster: 'Main', + resource: 'Cluster', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:01.232', + frontegg_id: '3e2eb99a-898b-4c66-a59c-c2c1bc9a32fb', + }, + { + ip: '25.44.49.21', + api: 'GET /insights/343', + user: 'Florine Pinion', + action: 'Liveness Check Perfomed', + result: 'Total Failure', + scanId: 'ac004f86-d25e-4e1e-81f0-5d7ef99ebc29', + resource: 'APIs', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:01.231', + frontegg_id: '76bc91af-7f90-4e56-bf4a-ae46abbbc40c', + }, + { + ip: '72.28.55.231', + user: 'Tillie Casias', + action: 'Settings Modified', + changed: 'Amount of Services', + cluster: 'Main', + resource: 'Cluster', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:01.230', + frontegg_id: '7c6aa166-1431-402e-8fb7-3b37e084702e', + }, + { + ip: '35.92.49.21', + user: 'Lenard Chicoine', + cause: 'Service Response Lag', + action: 'Remediated', + scanId: 'e5c21456-4c4f-4983-8b7f-a0f3eeef6566', + lagTime: '2500ms', + service: 'Payments', + resource: 'Service', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:01.230', + restartTime: '2000ms', + frontegg_id: '557eb503-08e1-4c87-88a0-d8e846a50911', + }, + { + ip: '161.185.160.93', + user: 'Normand Menz', + action: 'Lag Detected', + result: 'Service Response Lag', + scanId: '222f3518-9611-4e02-b67d-5390f59601b3', + lagTime: '7364ms', + service: 'Payments', + resource: 'Service', + severity: 'Error', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:01.229', + frontegg_id: '151d664c-094a-48a4-b69e-b036a12237cc', + }, + { + ip: '25.42.29.21', + user: 'Gail Blackerby', + action: 'Remap', + changed: 'Amount of Services', + cluster: 'Main', + resource: 'Cluster', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:01.228', + frontegg_id: '2f5e988d-7e64-4a40-a1f2-0eae475ad431', + }, + { + ip: '23.92.49.21', + user: 'Darcie Policastro', + action: 'Periodic Scan Started', + scanId: 'f4d4ec1d-92c0-4fd4-bf72-0dd3a4d1198b', + service: 'Cars', + resource: 'Service', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:01.227', + frontegg_id: '4f16e7bd-b7ef-412d-9883-ccb5587a512e', + }, + { + ip: '23.92.55.21', + user: 'Ardelia Dismuke', + action: 'Compliance Audit Performed', + scanId: '96c08187-927e-48b5-ba95-f9b928e70de8', + service: 'Users', + infoLink: 'https://iapp.org/news/a/understanding-data-processors-iso-and-soc-2-credentials-for-gdpr-compliance/', + resource: 'Cluster', + severity: 'Attention', + standard: 'GDPR', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:01.226', + frontegg_id: 'c039888a-bedb-4526-92f6-ebea7be67279', + }, + { + ip: '3.92.49.21', + user: 'Ardelia Dismuke', + action: 'Periodic Scan Started', + scanId: 'ed0358f2-d828-47d8-bc1c-70e667ffa447', + service: 'Payments', + resource: 'Service', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:01.224', + frontegg_id: '3a93bf83-330e-47fe-a793-7543a0b76ca9', + }, + { + ip: '72.92.55.231', + user: 'Debora Coddington', + action: 'Cache Purged', + resource: 'API Gateway', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:01.223', + cachedItems: 924, + frontegg_id: 'fd4911b1-7e43-4bd7-a952-1c85ea272a9e', + }, + { + ip: '25.42.1.21', + user: 'Lenna Nodine', + action: 'Remap', + changed: 'Amount of Services', + cluster: 'Main', + resource: 'Cluster', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:01.223', + frontegg_id: '2ec15823-9235-42d2-9397-d27745326252', + }, + { + ip: '161.185.160.93', + user: 'Jennell Fant', + cause: 'Service Response Lag', + action: 'Remediated', + scanId: 'b5f97e83-e295-4d2c-957b-ff939a69645c', + lagTime: '2500ms', + service: 'Payments', + resource: 'Service', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:01.221', + restartTime: '2000ms', + frontegg_id: '1432d1f6-a891-4de7-8156-b6cff363b24d', + }, + { + ip: '25.42.29.21', + api: 'GET /payments', + user: 'Clement Gallop', + action: 'Liveness Check Perfomed', + result: 'Success', + scanId: 'e619125f-52af-42a3-83d0-8f9067286dd4', + resource: 'APIs', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:01.216', + frontegg_id: '2000132d-6e59-4221-abb5-3ca50fff6c33', + }, + { + ip: '72.92.55.231', + user: 'Geraldo Shupe', + action: 'Settings Modified', + resource: 'Microservice', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:00.231', + microservice: 'Cars', + settingChanged: 'Amount of Pods', + frontegg_id: 'e6fb4081-22a5-4f4a-a954-392be36a47c9', + }, + { + ip: '72.28.101.231', + user: 'Kelvin Casella', + action: 'Lag Detected', + result: 'Service Response Lag', + scanId: '806a3362-4b3c-4d66-9347-44c7004f56d5', + lagTime: '13704ms', + service: 'Users', + resource: 'Service', + severity: 'Error', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:00.230', + frontegg_id: 'fb89e859-9261-49c4-9309-2fa729af290a', + }, + { + ip: '23.92.55.231', + info: '/insights#scan-id', + user: 'Marg Lovelace', + action: 'Sanity Check Started', + failed: '2 Tests', + scanId: 'e1ea57fa-b7bc-41c1-ae5e-bf92b69fb8bf', + cluster: 'Main', + resource: 'Cluster', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:00.228', + succeeded: '42 Tests', + testsPerfomed: '44 tests', + frontegg_id: '95c066eb-666e-457a-9bfb-f269ccd349ed', + }, + { + ip: '25.44.49.21', + user: 'Ardelia Dismuke', + action: 'Security Audit Perfomed', + scanId: '493fa109-88c0-4b54-8b08-6184440f385e', + service: 'Cars', + infoLink: 'https://owasp.org/www-project-top-ten/', + resource: 'Cluster', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:00.163', + owaspResult: 'Cross-Site Scripting', + frontegg_id: 'b73fd741-e974-496c-b808-a90f09830dec', + }, + { + ip: '23.92.55.21', + user: 'Clement Gallop', + action: 'Sanity Check Finished', + scanId: '42f3286a-f1fa-40d7-a4f7-a5a48e24da63', + service: 'Cars', + infoLink: 'https://iapp.org/news/a/understanding-data-processors-iso-and-soc-2-credentials-for-gdpr-compliance/', + resource: 'Cluster', + severity: 'Info', + standard: 'GDPR', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:10.134', + frontegg_id: '17210ae9-7288-4b24-aa28-f591d89454e3', + }, + { + ip: '25.42.29.21', + user: 'Valery Krieg', + action: 'Sanity Check Finished', + scanId: '853fc3e8-e000-4fb6-9c82-29f19388a423', + service: 'Users', + infoLink: 'https://iapp.org/news/a/understanding-data-processors-iso-and-soc-2-credentials-for-gdpr-compliance/', + resource: 'Cluster', + severity: 'Info', + standard: 'HIPAA', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:10.040', + frontegg_id: '1dbae207-aedd-4036-b23d-e18066bd7fab', + }, + { + ip: '25.42.29.21', + user: 'Geraldo Shupe', + action: 'Periodic Scan Finished', + result: 'Success', + scanId: '27ffb2c5-dd0e-4e2f-b3fd-e7b7b29d8208', + service: 'Users', + resource: 'Service', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:05.171', + frontegg_id: '8276babd-0e9f-4472-9cdd-8fc9daa15acc', + }, + { + ip: '23.92.49.21', + user: 'Tillie Casias', + action: 'Periodic Scan Finished', + result: 'Success', + scanId: 'cc484c12-5d8d-4078-b7c4-aefdba52108b', + service: 'Users', + resource: 'Service', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:05.052', + frontegg_id: '23cb0263-510e-4f75-8631-83ac326e46aa', + }, + { + ip: '35.92.49.21', + user: 'Debora Coddington', + action: 'Compliance Audit Performed', + scanId: '632091be-50e0-4894-800e-1e923157160c', + service: 'Payments', + infoLink: 'https://iapp.org/news/a/understanding-data-processors-iso-and-soc-2-credentials-for-gdpr-compliance/', + resource: 'Cluster', + severity: 'Info', + standard: 'SOC2', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:01.504', + frontegg_id: '44771d00-2524-41b7-a84a-95ba15e1fde6', + }, + { + ip: '25.42.29.21', + user: 'Geraldo Shupe', + action: 'Periodic Scan Started', + scanId: '27ffb2c5-dd0e-4e2f-b3fd-e7b7b29d8208', + service: 'Users', + resource: 'Service', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:01.503', + frontegg_id: 'd2a12db6-9921-4c4a-af35-3193d546f1ea', + }, + { + ip: '3.92.49.21', + user: 'Florine Pinion', + action: 'Cache Purged', + resource: 'API Gateway', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:01.502', + cachedItems: 210, + frontegg_id: '36015eb5-624b-45b4-901f-ed6ecea1fa9d', + }, + { + ip: '72.28.55.231', + user: 'Wendi Burghardt', + action: 'Settings Modified', + changed: 'Amount of Services', + cluster: 'Main', + resource: 'Cluster', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:01.501', + frontegg_id: 'be7155a7-990f-468d-a962-a669336032a0', + }, + { + ip: '25.44.49.21', + api: 'POST /products', + user: 'Herb Mcwain', + action: 'Liveness Check Perfomed', + result: '9 issues found', + scanId: 'a1a6c622-099b-4fc3-a1ae-2e87aa18b4f6', + resource: 'APIs', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:01.500', + frontegg_id: 'e253d2a2-3d81-4766-88d6-7f33fc4a1b25', + }, + { + ip: '25.44.49.21', + user: 'Florine Pinion', + action: 'Lag Detected', + result: 'Service Response Lag', + scanId: 'b9b83865-9315-4a04-8d4e-558ae41d7ed4', + lagTime: '16707ms', + service: 'Payments', + resource: 'Service', + severity: 'Error', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:01.500', + frontegg_id: '63bbfb77-65a7-4c25-9447-96273ee84c2a', + }, + { + ip: '25.42.1.21', + info: '/insights#scan-id', + user: 'Clement Gallop', + action: 'Sanity Check Started', + failed: '2 Tests', + scanId: '42f3286a-f1fa-40d7-a4f7-a5a48e24da63', + cluster: 'Main', + resource: 'Cluster', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:01.499', + succeeded: '42 Tests', + testsPerfomed: '44 tests', + frontegg_id: '9c56179f-56af-4567-b05e-0c4b85b103b6', + }, + { + ip: '23.92.49.21', + api: 'POST /products', + user: 'Deanna Post', + action: 'Liveness Check Perfomed', + result: 'Total Failure', + scanId: '622fb278-508d-4404-a0df-26d42c9cdf9d', + resource: 'APIs', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:01.498', + frontegg_id: '23a17fbe-fb65-41b5-9988-1ed5027e8809', + }, + { + ip: '25.44.49.21', + user: 'Kieth Mason', + action: 'Remap', + changed: 'Amount of Services', + cluster: 'Main', + resource: 'Cluster', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:01.497', + frontegg_id: 'd38483b9-64de-43ef-be18-e0b010e04932', + }, + { + ip: '72.92.55.231', + user: 'Geraldo Shupe', + cause: 'Service Response Lag', + action: 'Remediated', + scanId: '77a7c820-5415-400e-a350-afdd31a15ada', + lagTime: '2500ms', + service: 'Cars', + resource: 'Service', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:01.496', + restartTime: '2000ms', + frontegg_id: 'a68f1f78-edd5-41c8-85f9-ab8674f64072', + }, + { + ip: '72.28.101.231', + user: 'Iris Basso', + action: 'Compliance Audit Performed', + scanId: 'a994926f-eaca-4363-868a-34f3fe17e37d', + service: 'Cars', + infoLink: 'https://iapp.org/news/a/understanding-data-processors-iso-and-soc-2-credentials-for-gdpr-compliance/', + resource: 'Cluster', + severity: 'Attention', + standard: 'SOC2', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:01.495', + frontegg_id: 'f7a5bdda-8aed-4468-94fa-239b14507447', + }, + { + ip: '23.92.49.21', + user: 'Deanna Post', + action: 'Cache Purged', + resource: 'API Gateway', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:01.494', + cachedItems: 135, + frontegg_id: '722c46de-ec03-4949-ad03-9e4eb926a92e', + }, + { + ip: '72.28.55.231', + user: 'Thomas Salser', + action: 'Remap', + changed: 'Amount of Services', + cluster: 'Main', + resource: 'Cluster', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:01.493', + frontegg_id: 'a0d1c3b4-f8e0-4c79-a405-b38d408f4ed4', + }, + { + ip: '23.92.49.21', + user: 'Tillie Casias', + action: 'Periodic Scan Started', + scanId: 'cc484c12-5d8d-4078-b7c4-aefdba52108b', + service: 'Users', + resource: 'Service', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:01.492', + frontegg_id: '9d22b318-f9e2-467d-80a5-bda4b9e2066e', + }, + { + ip: '72.28.101.231', + user: 'Naida Rinker', + action: 'Security Audit Perfomed', + scanId: '1d0bf2c1-9aaa-42ff-9c08-e3aaa85be249', + service: 'Payments', + infoLink: 'https://owasp.org/www-project-top-ten/', + resource: 'Cluster', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:01.492', + owaspResult: 'Injection', + frontegg_id: '7ec9e56e-f79d-4179-9d9a-25d0f8060d02', + }, + { + ip: '25.44.49.21', + user: 'Tillie Casias', + action: 'Settings Modified', + resource: 'Microservice', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:01.491', + microservice: 'Cars', + settingChanged: 'Amount of Pods', + frontegg_id: '5a74d618-19b4-444f-8efa-0d624ea59579', + }, + { + ip: '35.92.49.21', + user: 'Iris Basso', + cause: 'Service Response Lag', + action: 'Remediated', + scanId: 'cc544e0a-8895-4608-b832-07718e461ccf', + lagTime: '2500ms', + service: 'Users', + resource: 'Service', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:01.490', + restartTime: '2000ms', + frontegg_id: 'c48789ae-574a-47a7-84c1-88d722aec057', + }, + { + ip: '23.92.55.231', + user: 'Rena Flanders', + action: 'Settings Modified', + resource: 'Microservice', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:00.306', + microservice: 'Payments', + settingChanged: 'Amount of Pods', + frontegg_id: '86d9c7d5-80c8-40f2-bd24-d090dad05f39', + }, + { + ip: '35.92.49.21', + user: 'Emelia Modeste', + action: 'Security Audit Perfomed', + scanId: '8f012caf-3f20-4c34-a21a-78e2bb87ae3b', + service: 'Cars', + infoLink: 'https://owasp.org/www-project-top-ten/', + resource: 'Cluster', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:00.305', + owaspResult: 'Injection', + frontegg_id: 'cf93bcd0-e368-4de4-be9d-6d110464abf6', + }, +]; +export const ComponentsPage2: FC = () => { + const renderExpandedComponent = useCallback((data) => { + return
{JSON.stringify(data, null, 2)}
; + }, []); + return ( +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
{ + return setFilterValue(e.target.value)} />; + }, + }, + { + accessor: 'createdAt', + Header: 'Time', + sortable: true, + Filter: ({ setFilterValue }) =>
, + }, + { accessor: 'resource', Header: 'Resource', sortable: true }, + { accessor: 'action', Header: 'Action', sortable: true }, + { accessor: 'severity', Header: 'Severity', sortable: true }, + { accessor: 'ip', Header: 'IP ADDRESS', sortable: true }, + ]} + data={data} + totalData={data.length} + rowKey='frontegg_id' + pagination='pages' + pageSize={20} + pageCount={100} + expandable + renderExpandedComponent={renderExpandedComponent} + + // selection='multi' + // onRowSelected={(selected) => { + // console.log(selected); + // }} + // toolbar + // sortBy={sortBy} + // onSortChange={(_sortBy) => { + // setSortBy(_sortBy); + // console.log('_sortBy', JSON.stringify(_sortBy, null, 2)); + // }} + // filters={filters} + // onFilterChange={(_filters) => { + // setFilters(_filters); + // console.log('_filters', JSON.stringify(_filters, null, 2)); + // }} + /> + + ); +}; diff --git a/packages/demo-saas/src/DemoButton.tsx b/packages/demo-saas/src/DemoButton.tsx deleted file mode 100644 index 77abb7997..000000000 --- a/packages/demo-saas/src/DemoButton.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; - -export const DemoButton = (props: any) => ( - - + + setOpen(false)}> + {}} + > + + + +
+ + + + + + + + +
+
+
+
+ + ); +}; diff --git a/packages/demo-saas/src/Elements/AccordionExample.tsx b/packages/demo-saas/src/Elements/AccordionExample.tsx new file mode 100644 index 000000000..fd9187c5a --- /dev/null +++ b/packages/demo-saas/src/Elements/AccordionExample.tsx @@ -0,0 +1,70 @@ +import React, { FC, useState } from 'react'; +import { Section, SubSection } from 'Sections/Sections'; +import { Libraries, Library } from './Libraries'; + +export const AccordionExample: FC = () => { + return ( +
+ {Libraries.map((lib) => ( + + + + ))} +
+ ); +}; + +const AccordionByLib: FC<{ lib: Library }> = ({ lib }) => { + const [open, setOpen] = useState(false); + + const { Accordion, AccordionContent, AccordionHeader, Icon } = lib.elements; + + if (!Accordion || !AccordionContent || !AccordionHeader) return <>{`Elem Accordion not found in lib ${lib.title}`}; + + return ( + <> + + Controlled + + Lorem ipsum dolor sit amet consectetur, adipisicing elit. Accusantium ab sint necessitatibus ut natus aut + reprehenderit iste delectus illum. Placeat, consequatur quis libero praesentium ea repellendus tempora + necessitatibus commodi vitae? + + + + Uncontrolled + + Lorem ipsum dolor sit amet consectetur, adipisicing elit. Accusantium ab sint necessitatibus ut natus aut + reprehenderit iste delectus illum. Placeat, consequatur quis libero praesentium ea repellendus tempora + necessitatibus commodi vitae? + + + + Controlled 2 + + Lorem ipsum dolor sit amet consectetur, adipisicing elit. Accusantium ab sint necessitatibus ut natus aut + reprehenderit iste delectus illum. Placeat, consequatur quis libero praesentium ea repellendus tempora + necessitatibus commodi vitae? + + + + Disabled + + Lorem ipsum dolor sit amet consectetur, adipisicing elit. Accusantium ab sint necessitatibus ut natus aut + reprehenderit iste delectus illum. Placeat, consequatur quis libero praesentium ea repellendus tempora + necessitatibus commodi vitae? + + + {Icon && ( + + }>Header Icon + + Lorem ipsum dolor sit amet consectetur, adipisicing elit. Accusantium ab sint necessitatibus ut natus aut + reprehenderit iste delectus illum. Placeat, consequatur quis libero praesentium ea repellendus tempora + necessitatibus commodi vitae? + + + )} + + ); +}; diff --git a/packages/demo-saas/src/Elements/ElementMenu/index.tsx b/packages/demo-saas/src/Elements/ElementMenu/index.tsx new file mode 100644 index 000000000..412ea0328 --- /dev/null +++ b/packages/demo-saas/src/Elements/ElementMenu/index.tsx @@ -0,0 +1,29 @@ +import { ElementType } from '@frontegg/react-core'; +import React, { FC } from 'react'; +import { Link } from 'react-router-dom'; +import { elements } from '../elements'; +import './style.scss'; + +interface ElementMenuProps { + basePath: string; + activeElement: string; +} + +export const ElementMenu: FC = ({ basePath, activeElement }) => { + return ( +
+ + Accordions + + {Object.keys(elements).map((elementType) => ( + + {elements[elementType as ElementType]?.title} + + ))} +
+ ); +}; diff --git a/packages/demo-saas/src/Elements/ElementMenu/style.scss b/packages/demo-saas/src/Elements/ElementMenu/style.scss new file mode 100644 index 000000000..8d32ed3e3 --- /dev/null +++ b/packages/demo-saas/src/Elements/ElementMenu/style.scss @@ -0,0 +1,37 @@ +.side-menu { + display: flex; + flex-direction: column; + position: fixed; + overflow: auto; + left: 0; + top: 0; + height: 100%; + border-radius: 0 1em 1em 0; + box-shadow: 1px 1px 5px 0px rgba(0, 0, 0, 0.3); + background-color: white; + + .link { + display: flex; + padding: 1em; + background-color: inherit; + transition: all 0.2s; + + &-active { + background-image: linear-gradient(90deg, #c4aeff, #f6f6f6); + pointer-events: none; + cursor: default; + } + + &:focus { + filter: brightness(0.95); + } + + &:hover { + filter: brightness(0.9); + } + + &:active { + filter: brightness(0.8); + } + } +} diff --git a/packages/demo-saas/src/Elements/ElementsPage.tsx b/packages/demo-saas/src/Elements/ElementsPage.tsx new file mode 100644 index 000000000..b134821c6 --- /dev/null +++ b/packages/demo-saas/src/Elements/ElementsPage.tsx @@ -0,0 +1,24 @@ +import React, { FC } from 'react'; +import { ElementType } from '@frontegg/react-core'; +import { Route, RouteComponentProps } from 'react-router-dom'; +import { AccordionExample } from './AccordionExample'; +import { elements } from './elements'; +import { ElementMenu } from './ElementMenu'; +import { ExampleElement } from './ExampleElement'; + +export const ElementsPage: FC = ({ match, location }) => { + return ( +
+ + + + + + {Object.keys(elements).map((elementType) => ( + + + + ))} +
+ ); +}; diff --git a/packages/demo-saas/src/Elements/ExampleElement/index.tsx b/packages/demo-saas/src/Elements/ExampleElement/index.tsx new file mode 100644 index 000000000..c5fba6956 --- /dev/null +++ b/packages/demo-saas/src/Elements/ExampleElement/index.tsx @@ -0,0 +1,66 @@ +import { ElementType } from '@frontegg/react-core'; +import React, { ReactElement, useState } from 'react'; +import { Section, SubSection } from 'Sections/Sections'; +import { ElementOption, PropToOptions } from '../interfaces'; +import { Libraries } from '../Libraries'; +import './style.scss'; + +interface ExampleElementProps { + elementOption: ElementOption; +} + +export function ExampleElement({ + elementOption, +}: ExampleElementProps): ReactElement> { + const [propToValue, setPropToValue] = useState(() => getInitialState(elementOption.propToOptions)); + return ( +
+
+ {getKeys(elementOption.propToOptions).map((prop, index) => ( +
+ {React.createElement(Libraries[2].elements.Select, { + fullWidth: false, + label: `${prop}`, + value: propToValue[prop], + getOptionLabel: (label) => `${label.label || label.value || label}`, + options: elementOption.propToOptions[prop] + .filter((_) => _ !== undefined) + .map((option) => ({ label: `${option}`, value: option })), + onChange: (_, newValues) => { + setPropToValue({ + ...propToValue, + [prop]: (newValues as any)?.value, + }); + }, + })} +
+ ))} +
+
+ {Libraries.map((lib) => ( + + {(lib.elements as any)[elementOption.type] + ? React.createElement((lib.elements as any)[elementOption.type], { ...propToValue }) + : `Element ${elementOption.type} not found in library ${lib.title}`} + + ))} +
+
+ ); +} + +const getInitialState = ( + propToOptions: PropToOptions +): { + [prop in keyof PropToOptions]?: PropToOptions[prop]; // Should be `PropToOptions[prop][0]` or somethig like that +} => + getKeys(propToOptions).reduce( + (agg, prop) => ({ + ...agg, + [prop]: propToOptions[prop][0], + }), + {} + ); + +const getKeys = (propToOptions: PropToOptions) => + Object.keys(propToOptions) as (keyof PropToOptions)[]; diff --git a/packages/demo-saas/src/Elements/ExampleElement/style.scss b/packages/demo-saas/src/Elements/ExampleElement/style.scss new file mode 100644 index 000000000..9b613bd03 --- /dev/null +++ b/packages/demo-saas/src/Elements/ExampleElement/style.scss @@ -0,0 +1,4 @@ +.prop-select { + display: inline-flex; + margin: 0.5em; +} diff --git a/packages/demo-saas/src/Elements/Libraries.ts b/packages/demo-saas/src/Elements/Libraries.ts new file mode 100644 index 000000000..7c9a49919 --- /dev/null +++ b/packages/demo-saas/src/Elements/Libraries.ts @@ -0,0 +1,34 @@ +import { Elements, fronteggElements as FE } from '@frontegg/react-core'; +import { uiLibrary as S } from '@frontegg/react-elements-semantic'; +import { uiLibrary as M } from '@frontegg/react-elements-material-ui'; + +const Semantic = S as Elements; +const Material = M as Elements; +const Frontegg = FE as Elements; + +export interface Library { + title: string; + elements: Elements; + customizeNotes: string; +} + +export const Libraries: Library[] = [ + { + title: 'Frontegg', + elements: Frontegg, + customizeNotes: + 'In order to customize Frontegg Elements your have to override the --fe-* css variables or from FronteggPortal', + }, + { + title: 'Semantic', + elements: Semantic, + customizeNotes: + 'In order to Customize Semantic Elements visit https://react.semantic-ui.com/theming', + }, + { + title: 'Material', + elements: Material, + customizeNotes: + 'In order to Customize Material Elements visit https://material-ui.com/styles/advanced/', + }, +]; diff --git a/packages/demo-saas/src/Elements/elements.ts b/packages/demo-saas/src/Elements/elements.ts new file mode 100644 index 000000000..3f1037d60 --- /dev/null +++ b/packages/demo-saas/src/Elements/elements.ts @@ -0,0 +1,83 @@ +import { ElementOptions } from './interfaces'; + +export const elements: ElementOptions = { + Input: { + title: 'Inputs', + type: 'Input', + propToOptions: { + variant: [undefined, 'default', 'primary', 'secondary', 'danger'], + fullWidth: [false, true], + error: [undefined, 'Some error'], + size: ['medium', 'small', 'large'], + disabled: [false, true], + label: [undefined, 'Some label'], + type: [undefined, 'text', 'password', 'search'], + multiline: [false, true], + inForm: [false, true], + labelButton: [undefined, { children: 'label button' }], + placeholder: ['Placeholder'], + }, + }, + InputChip: { + title: 'InputChip', + type: 'InputChip', + propToOptions: { + chips: [ + ['chip1', 'chip2'], + ['chip1', 'chip2', 'chip3'], + ], + onKeyPress: [() => {}], + onBlur: [() => {}], + onDelete: [() => {}], + }, + }, + Button: { + title: 'Buttons', + type: 'Button', + propToOptions: { + children: ['Button'], + variant: [undefined, 'default', 'primary', 'secondary', 'danger'], + isCancel: [false, true], + fullWidth: [false, true], + size: ['medium', 'small', 'large'], + disabled: [false, true], + loading: [false, true], + }, + }, + Checkbox: { + title: 'Checkbox', + type: 'Checkbox', + propToOptions: { + label: ['Inline Checkbox 1', 'Inline Checkbox 2'], + indeterminate: [false, true], + fullWidth: [false, true], + }, + }, + Tag: { + title: 'Tags', + type: 'Tag', + propToOptions: { + children: ['Tag', 'Some Long Tag'], + variant: [undefined, 'default', 'primary', 'secondary', 'danger'], + disabled: [false, true], + size: ['medium', 'small', 'large'], + onClick: [undefined, () => {}], + onDelete: [undefined, () => {}], + }, + }, + Loader: { + title: 'Loaders', + type: 'Loader', + propToOptions: { + variant: [undefined, 'default', 'primary', 'secondary', 'danger'], + center: [false, true], + }, + }, + SwitchToggle: { + title: 'SwitchToggles', + type: 'SwitchToggle', + propToOptions: { + labels: [['Disabled', 'Enabled']], + }, + }, +}; diff --git a/packages/demo-saas/src/Elements/interfaces.ts b/packages/demo-saas/src/Elements/interfaces.ts new file mode 100644 index 000000000..8e7bf5b16 --- /dev/null +++ b/packages/demo-saas/src/Elements/interfaces.ts @@ -0,0 +1,21 @@ +import { Elements, ElementType } from '@frontegg/react-core'; + +export type PropsOf = TComponentOrTProps extends React.ComponentType + ? TProps + : TComponentOrTProps; + +export type PropsOfElement = PropsOf; + +export type PropToOptions = { + [prop in keyof PropsOfElement]: PropsOfElement[prop][]; +}; + +export interface ElementOption { + title: string; + type: T; + propToOptions: PropToOptions; +} + +export type ElementOptions = { + [type in ElementType]?: ElementOption; +}; diff --git a/packages/demo-saas/src/Fallback.tsx b/packages/demo-saas/src/Fallback.tsx deleted file mode 100644 index d4ba7cbd4..000000000 --- a/packages/demo-saas/src/Fallback.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react'; -import { wrapWithBaseHomePage } from './BaseHomePage/BaseHomePage'; - -export const Fallback = () => <>Fallback; - -export default wrapWithBaseHomePage(Fallback); diff --git a/packages/demo-saas/src/HomePage.tsx b/packages/demo-saas/src/HomePage.tsx deleted file mode 100644 index 22a42240c..000000000 --- a/packages/demo-saas/src/HomePage.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import React, { FC } from 'react'; -import { wrapWithBaseHomePage } from './BaseHomePage/BaseHomePage'; - -const HomePage: FC = () => { - return <>Home Page; -}; - -export default wrapWithBaseHomePage(HomePage); diff --git a/packages/demo-saas/src/NotAFronteggPage.tsx b/packages/demo-saas/src/NotAFronteggPage.tsx deleted file mode 100644 index 771bae1e6..000000000 --- a/packages/demo-saas/src/NotAFronteggPage.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; - -export const NotAFronteggPage = () => <>Not a frontegg page; - -export default NotAFronteggPage; diff --git a/packages/demo-saas/src/OldApp.tsx b/packages/demo-saas/src/OldApp.tsx new file mode 100644 index 000000000..0e6c51dbc --- /dev/null +++ b/packages/demo-saas/src/OldApp.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { Switch, Route, Link, RouteProps } from 'react-router-dom'; +import { AuditsExample } from 'auditsExample'; +import { + EmailComponent, + ConnectivityPage, + SlackComponent, + SMSComponent, + WebhookComponent, +} from '@frontegg/react-connectivity'; + +const menus: (RouteProps & { title: string })[] = [ + { path: '/connectivity', title: 'Connectivity', children: }, + { path: '/webhook', title: 'Webhook', component: WebhookComponent }, + { path: '/slack', title: 'Slack', component: SlackComponent }, + { path: '/emails', title: 'Email', component: EmailComponent }, + { path: '/sms', title: 'SMS', component: SMSComponent }, + { path: '/audits', title: 'Audits Example', component: AuditsExample }, +]; + +class OldApp extends React.Component { + render() { + return ( +
+ + +
+ {menus.map( + ({ path, title }, idx) => + title && ( +
+ {title}{' '} +
+ ) + )} +
+
+ {menus.map(({ path, component, children, exact }, idx) => ( + + {children} + + ))} +
+
+ ); + } +} + +export default OldApp; diff --git a/packages/demo-saas/src/PopupExample.tsx b/packages/demo-saas/src/PopupExample.tsx new file mode 100644 index 000000000..913bc2576 --- /dev/null +++ b/packages/demo-saas/src/PopupExample.tsx @@ -0,0 +1,149 @@ +import React, { FC } from 'react'; +import { Elements, fronteggElements } from '@frontegg/react-core'; +import { uiLibrary as S } from '@frontegg/react-elements-semantic'; +import { uiLibrary as M } from '@frontegg/react-elements-material-ui'; + +const SE = S as Elements; +const FE = fronteggElements as Elements; +const ME = M as Elements; + +const positions = [ + { + vertical: 'top', + horizontal: 'left', + }, + { + vertical: 'top', + horizontal: 'center', + }, + { + vertical: 'top', + horizontal: 'right', + }, + { + vertical: 'center', + horizontal: 'right', + }, + { + vertical: 'bottom', + horizontal: 'right', + }, + { + vertical: 'bottom', + horizontal: 'center', + }, + { + vertical: 'bottom', + horizontal: 'left', + }, + { + vertical: 'center', + horizontal: 'left', + }, +]; +export const PopupExample: FC = () => { + return ( +
+
+

Hover Popup Example

+

Frontegg Popup

+
+ {positions.map((position: any) => ( + Hover
} + trigger={ + + Trigger {position.vertical} {position.horizontal} + + } + /> + ))} +
+

Semantic Popup

+
+ {positions.map((position: any) => ( + Hover
} + trigger={ + + Trigger {position.vertical} {position.horizontal} + + } + /> + ))} + +

Material Popup

+
+ {positions.map((position: any) => ( + Hover
} + trigger={ + + Trigger {position.vertical} {position.horizontal} + + } + /> + ))} + + +
+
+

Click Popup Example

+

Frontegg Popup

+
+ {positions.map((position: any) => ( + Hover
} + trigger={ + + Trigger {position.vertical} {position.horizontal} + + } + /> + ))} + +

Semantic Popup

+
+ {positions.map((position: any) => ( + Hover
} + trigger={ + + Trigger {position.vertical} {position.horizontal} + + } + /> + ))} + +

Material Popup

+
+ {positions.map((position: any) => ( + Hover
} + trigger={ + + Trigger {position.vertical} {position.horizontal} + + } + /> + ))} + +
+
+
+
+ + ); +}; diff --git a/packages/demo-saas/src/Routes/index.tsx b/packages/demo-saas/src/Routes/index.tsx new file mode 100644 index 000000000..eb5e47d42 --- /dev/null +++ b/packages/demo-saas/src/Routes/index.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { Route, Switch, Redirect } from 'react-router-dom'; + +import { HomePage } from '../pages/HomePage'; + +export const Routes = () => { + return ( + + + + + ); +}; diff --git a/packages/demo-saas/src/Sections/Sections.tsx b/packages/demo-saas/src/Sections/Sections.tsx new file mode 100644 index 000000000..4b50778f6 --- /dev/null +++ b/packages/demo-saas/src/Sections/Sections.tsx @@ -0,0 +1,20 @@ +import React, { FC } from 'react'; +import './style.scss'; + +export const Section: FC<{ title: string }> = ({ title, children }) => { + return ( +
+

{title}

+ {children} +
+ ); +}; + +export const SubSection: FC<{ title: string }> = ({ title, children }) => { + return ( +
+
{title}
+ {children} +
+ ); +}; diff --git a/packages/demo-saas/src/Sections/style.scss b/packages/demo-saas/src/Sections/style.scss new file mode 100644 index 000000000..38d3622b1 --- /dev/null +++ b/packages/demo-saas/src/Sections/style.scss @@ -0,0 +1,22 @@ +.components-section { + margin: auto; + max-width: 80%; + border: 1px solid #ddd; + border-radius: 0.5rem; + padding: 2rem; + background: #f1f1f1; + margin-top: 2rem; +} + +.components-sub-section { + margin-top: 2rem; + background: white; + position: relative; + padding: 1rem; + border-radius: 0.5rem; + border: 1px solid #eee; + + &:first-child { + margin-top: 0; + } +} diff --git a/packages/demo-saas/src/SelectorExample.tsx b/packages/demo-saas/src/SelectorExample.tsx new file mode 100644 index 000000000..43b2d474a --- /dev/null +++ b/packages/demo-saas/src/SelectorExample.tsx @@ -0,0 +1,126 @@ +import React, { FC, useCallback, useState } from 'react'; +import { uiLibrary as Semantic } from '@frontegg/react-elements-semantic'; +import { uiLibrary as Material } from '@frontegg/react-elements-material-ui'; +import { Elements, fronteggElements as Frontegg } from '@frontegg/react-core'; + +const FE = Frontegg as Elements; +const ME = Material as Elements; +const SE = Semantic as Elements; + +const top100Films = [ + { label: 'The Shawshank Redemption', value: '1994' }, + { label: 'The Godfather', value: '1972' }, + { label: 'The Godfather: Part II', value: '1974' }, + { label: 'The Dark Knight', value: '2008' }, + { label: '12 Angry Men', value: '1957' }, + { label: "Schindler's List", value: '1993' }, + { label: 'Pulp Fiction', value: '19924' }, + { label: 'The Lord of the Rings: The Return of the King', value: '2003' }, + { label: 'The Good, the Bad and the Ugly', value: '1966' }, + { label: 'Fight Club', value: '1999' }, + { label: 'The Lord of the Rings: The Fellowship of the Ring', value: '2001' }, + { label: 'Star Wars: Episode V - The Empire Strikes Back', value: '1980' }, + { label: 'Forrest Gump', value: '43223' }, + { label: 'Inception', value: '2010' }, + { label: 'The Lord of the Rings: The Two Towers', value: '2002' }, + { label: "One Flew Over the Cuckoo's Nest", value: '1975' }, + { label: 'Goodfellas', value: '1990' }, + { label: 'The Matrix', value: '19991' }, +]; + +export const SelectorExample: FC = () => { + const [value, setValue] = useState([{ label: 'The Matrix', value: '19991' }]); + + const [open, setOpen] = useState({ + material: false, + semantic: false, + core: false, + }); + + const [loading] = useState(false); + + const onOpenMaterial = () => setOpen({ material: true, core: false, semantic: false }); + const onCloseMaterial = () => setOpen({ material: false, core: false, semantic: false }); + const onOpenSemantic = () => setOpen({ material: false, core: false, semantic: true }); + const onCloseSemantic = () => setOpen({ material: false, core: false, semantic: false }); + const onOpenCore = () => setOpen({ material: false, core: true, semantic: false }); + const onCloseCore = () => setOpen({ material: false, core: false, semantic: false }); + + const getOptionLabel = (option: any) => { + return option.label; + }; + + // const renderOption = (option: any, state: any) => { + // return {option.label}; + // }; + + const onChange = useCallback((_e, newValue: Array) => { + setValue(newValue); + }, []); + + return ( +
+ +
+
+ + +
+
+ + +
+
+
+
+
+ + +
+ ); +}; diff --git a/packages/demo-saas/src/TableExample.tsx b/packages/demo-saas/src/TableExample.tsx new file mode 100644 index 000000000..72c715d73 --- /dev/null +++ b/packages/demo-saas/src/TableExample.tsx @@ -0,0 +1,1202 @@ +import React, { FC, useCallback } from 'react'; +import { Elements } from '@frontegg/react-core'; +// import { uiLibrary as S } from '@frontegg/react-elements-semantic'; +import { uiLibrary as M } from '@frontegg/react-elements-material-ui'; +import { Input } from '@frontegg/react-core'; + +// const SE = S as Elements; +// const FE = fronteggElements as Elements; +const ME = M as Elements; + +const data = [ + { + ip: '79.176.23.49', + user: 'Tillie Casias', + action: 'Accessed', + resource: 'Audit', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:15:15.600', + totalRows: 2250, + searchText: 'None', + piiReturned: 'true', + frontegg_id: 'cc4330b6-5882-4460-a0c1-6941f1d35d86', + }, + { + ip: '79.176.23.49', + user: 'Johnny Lu', + action: 'Accessed', + resource: 'Dashboard', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:15:15.462', + frontegg_id: '777293f9-de88-49c4-b432-e8c853d8e648', + }, + { + ip: '72.28.101.231', + user: 'Florine Pinion', + action: 'Sanity Check Finished', + scanId: '30eefef7-859f-4fce-9ba8-8666bc342591', + service: 'Payments', + infoLink: 'https://iapp.org/news/a/understanding-data-processors-iso-and-soc-2-credentials-for-gdpr-compliance/', + resource: 'Cluster', + severity: 'Attention', + standard: 'SOC2', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:00:10.221', + frontegg_id: 'e1cd9f43-505c-45fe-bd60-f712656235fa', + }, + { + ip: '72.92.55.231', + user: 'Verona Gonzalas', + action: 'Sanity Check Finished', + scanId: '9eea13e1-a91f-47be-8378-1c076f121527', + service: 'Cars', + infoLink: 'https://iapp.org/news/a/understanding-data-processors-iso-and-soc-2-credentials-for-gdpr-compliance/', + resource: 'Cluster', + severity: 'Info', + standard: 'SOC2', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:00:10.087', + frontegg_id: '7545fd29-c010-41ee-8238-bb39526c8f6a', + }, + { + ip: '23.92.49.21', + user: 'Melda Richert', + action: 'Periodic Scan Finished', + result: 'Success', + scanId: '428454cf-faac-49b1-b3c4-d904255da224', + service: 'Cars', + resource: 'Service', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:00:05.124', + frontegg_id: '62adc639-e701-4095-95a4-27cdbedf59ab', + }, + { + ip: '23.92.49.21', + user: 'Rhoda Blaylock', + action: 'Periodic Scan Finished', + result: 'Total Failure', + scanId: 'c96c6560-af2b-4abe-94d2-c8f2b741f6ea', + service: 'Cars', + resource: 'Service', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:00:05.069', + frontegg_id: '9b83dc83-204c-433a-97ad-639279cfd5e6', + }, + { + ip: '25.44.49.21', + user: 'Tillie Casias', + action: 'Settings Modified', + resource: 'Microservice', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:00:01.483', + microservice: 'Payments', + settingChanged: 'Authentication', + frontegg_id: '0ecfa8fd-8ca0-4736-bf1a-b2a77bcc0e3e', + }, + { + ip: '161.185.160.93', + user: 'Clement Gallop', + cause: 'Service Response Lag', + action: 'Remediated', + scanId: '50622ae6-9156-4894-bcce-a61a015fa5a1', + lagTime: '2500ms', + service: 'Payments', + resource: 'Service', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:00:01.482', + restartTime: '2000ms', + frontegg_id: '24a9e25f-9ca4-48c0-9c1c-17940bf2b919', + }, + { + ip: '161.185.160.93', + api: 'GET /payments', + user: 'Kieth Mason', + action: 'Liveness Check Perfomed', + result: 'Success', + scanId: '96212e63-fab9-451e-a8b0-61b6d59ad8e7', + resource: 'APIs', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:00:01.481', + frontegg_id: 'ba814ecd-7186-44b2-ac03-f9591cac3015', + }, + { + ip: '25.44.49.21', + user: 'Kieth Mason', + action: 'Settings Modified', + changed: 'Amount of Services', + cluster: 'Main', + resource: 'Cluster', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:00:01.481', + frontegg_id: '0f488824-c7af-4463-8639-2669a492c2e2', + }, + { + ip: '23.92.55.21', + user: 'Tillie Casias', + email: 'tilliecasias@example.com', + action: 'Added', + resource: 'Users', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:00:01.480', + frontegg_id: '92bb449b-ab0a-4f2c-9055-1ea03a7f7d83', + }, + { + ip: '23.92.55.231', + user: 'Ardelia Dismuke', + action: 'Settings Modified', + resource: 'Microservice', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:00:01.478', + microservice: 'Payments', + settingChanged: 'Authentication', + frontegg_id: '8597d041-257d-43ce-b561-cddab37dc2b4', + }, + { + ip: '72.28.101.231', + api: 'POST /products', + user: 'Marg Lovelace', + action: 'Liveness Check Perfomed', + result: 'Success', + scanId: '25638864-671a-4beb-8df8-22ce0e0b8147', + resource: 'APIs', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:00:01.477', + frontegg_id: '4d1ae8f7-8908-4df2-866b-0d906d14650e', + }, + { + ip: '3.92.49.21', + user: 'Ardelia Dismuke', + action: 'Lag Detected', + result: 'Service Response Lag', + scanId: '8da73d4b-3785-4421-a969-5fb19c7d2988', + lagTime: '14007ms', + service: 'Users', + resource: 'Service', + severity: 'Error', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:00:01.476', + frontegg_id: '6c579ddc-5d11-4220-a0a8-52b63df762e6', + }, + { + ip: '35.92.49.21', + user: 'Iris Basso', + action: 'Security Audit Perfomed', + scanId: '3e182f18-a295-4c05-a101-eaa9aa3cb706', + service: 'Users', + infoLink: 'https://owasp.org/www-project-top-ten/', + resource: 'Cluster', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:00:01.475', + owaspResult: 'Broken Authentication', + frontegg_id: 'e65e397d-820d-4bc2-a91b-1b1f462255b2', + }, + { + ip: '3.92.49.21', + user: 'Jennell Fant', + action: 'Remap', + changed: 'Amount of Services', + cluster: 'Main', + resource: 'Cluster', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:00:01.475', + frontegg_id: '44319670-695e-41ee-842e-1672e4ccb7db', + }, + { + ip: '72.28.55.231', + user: 'Verona Gonzalas', + action: 'Compliance Audit Performed', + scanId: '375e5feb-e0d8-4c8c-98d2-c06a5862f617', + service: 'Payments', + infoLink: 'https://iapp.org/news/a/understanding-data-processors-iso-and-soc-2-credentials-for-gdpr-compliance/', + resource: 'Cluster', + severity: 'Attention', + standard: 'SOC2', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:00:01.474', + frontegg_id: 'f4227fd6-3474-4fc2-94ee-5b172365c977', + }, + { + ip: '72.92.55.231', + user: 'Naida Rinker', + action: 'Security Audit Perfomed', + scanId: 'c4cd62f4-9d20-4773-bcde-eaba9a6208d9', + service: 'Users', + infoLink: 'https://owasp.org/www-project-top-ten/', + resource: 'Cluster', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:00:01.473', + owaspResult: 'Broken Authentication', + frontegg_id: '4dd797b7-8e52-4a82-a235-b338ce0afb0a', + }, + { + ip: '23.92.49.21', + info: '/insights#scan-id', + user: 'Florine Pinion', + action: 'Sanity Check Started', + failed: '2 Tests', + scanId: '30eefef7-859f-4fce-9ba8-8666bc342591', + cluster: 'Main', + resource: 'Cluster', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:00:01.472', + succeeded: '42 Tests', + testsPerfomed: '44 tests', + frontegg_id: 'dce52fbf-799a-4a8f-9c17-5ed382df8e52', + }, + { + ip: '72.28.55.231', + user: 'Debora Coddington', + action: 'Remap', + changed: 'Security Level', + cluster: 'Main', + resource: 'Cluster', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 00:00:01.471', + frontegg_id: 'b9bccc42-e440-4b3f-9dc2-0b54b7c721ae', + }, + { + ip: '3.92.49.21', + user: 'Deanna Post', + action: 'Remap', + changed: 'Amount of Services', + cluster: 'Main', + resource: 'Cluster', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-29 00:00:01.320', + frontegg_id: '54e2cfe6-d040-4671-b23e-053c511fbe4f', + }, + { + ip: '23.92.55.231', + user: 'Naida Rinker', + action: 'Downtime Detected', + result: 'Total Service Downtime', + scanId: 'e09e1684-f736-4277-851c-d59809e57d43', + service: 'Users', + resource: 'Service', + severity: 'Error', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-29 00:00:01.319', + frontegg_id: '58297c84-262a-45df-a054-dc7207a29d3b', + }, + { + ip: '72.28.101.231', + user: 'Herb Mcwain', + action: 'Cache Purged', + resource: 'API Gateway', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-29 00:00:01.318', + cachedItems: 462, + frontegg_id: '5570deca-2b0b-4779-bab3-d3451e5b3bc0', + }, + { + ip: '23.92.55.21', + user: 'Normand Menz', + action: 'Compliance Audit Performed', + scanId: 'b076bb81-74c7-425e-b384-f2f91f5dc60e', + service: 'Cars', + infoLink: 'https://iapp.org/news/a/understanding-data-processors-iso-and-soc-2-credentials-for-gdpr-compliance/', + resource: 'Cluster', + severity: 'Attention', + standard: 'HIPAA', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-29 00:00:01.316', + frontegg_id: '6985ca88-4b59-4abf-a124-980de32f1153', + }, + { + ip: '35.92.49.21', + user: 'Caitlin Hodes', + action: 'Security Audit Perfomed', + scanId: '9c44e4d5-9886-484d-9c14-2c81be2dedee', + service: 'Payments', + infoLink: 'https://owasp.org/www-project-top-ten/', + resource: 'Cluster', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-29 00:00:01.315', + owaspResult: 'Broken Authentication', + frontegg_id: '489a91f3-37b4-4beb-90d6-5f500a5fef58', + }, + { + ip: '3.92.49.21', + user: 'Valery Krieg', + action: 'Lag Detected', + result: 'Service Response Lag', + scanId: '0db8e45b-94d0-4159-ba98-4f47ad62c3de', + lagTime: '4021ms', + service: 'Payments', + resource: 'Service', + severity: 'Error', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-29 00:00:01.314', + frontegg_id: '18e3ccf2-56b4-48ea-b01c-4024158f1989', + }, + { + ip: '72.28.101.231', + user: 'Emelia Modeste', + email: 'emeliamodeste@example.com', + action: 'Added', + resource: 'Users', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-29 00:00:01.312', + frontegg_id: 'd0bad576-75a0-4f25-96e4-00cf30570485', + }, + { + ip: '3.92.49.21', + info: '/insights#scan-id', + user: 'Darcie Policastro', + action: 'Sanity Check Started', + failed: '2 Tests', + scanId: 'af7f4742-3ee0-4cdd-9cac-dbbcaf16eb7e', + cluster: 'Main', + resource: 'Cluster', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-29 00:00:01.310', + succeeded: '42 Tests', + testsPerfomed: '44 tests', + frontegg_id: 'fab4ecf7-1828-441c-933f-6d8ffb4f44b4', + }, + { + ip: '72.28.101.231', + user: 'Naida Rinker', + email: 'naidarinker@example.com', + action: 'Removed', + resource: 'Users', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-29 00:00:01.308', + frontegg_id: 'a384b58b-dcc9-4852-89eb-8cbfabc80406', + }, + { + ip: '72.92.55.231', + url: 'https://example.com/hook?services', + user: 'Geraldo Shupe', + title: 'Service events', + action: 'Edited', + events: 'Service.Added, Service.Deleted', + resource: 'WebHooks', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-29 00:00:01.307', + webHookId: '3aa06a19-edf9-4026-b030-dc6e3eb80712', + frontegg_id: 'ad393229-a2aa-42ec-9bee-2151e56637f5', + }, + { + ip: '25.42.49.21', + user: 'Herb Mcwain', + action: 'Settings Modified', + resource: 'Microservice', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-29 00:00:01.305', + microservice: 'Payments', + settingChanged: 'Amount of Pods', + frontegg_id: 'e6d8b14c-de90-45ab-b764-b8aa3e4de1b0', + }, + { + ip: '23.92.55.21', + user: 'Naida Rinker', + action: 'Sanity Check Finished', + scanId: '76cd6684-5103-4306-8a14-ca9b9c850183', + service: 'Users', + infoLink: 'https://iapp.org/news/a/understanding-data-processors-iso-and-soc-2-credentials-for-gdpr-compliance/', + resource: 'Cluster', + severity: 'Info', + standard: 'HIPAA', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:10.215', + frontegg_id: '89201527-644b-4a00-b5a7-30c287117999', + }, + { + ip: '25.44.49.21', + user: 'Marg Lovelace', + action: 'Sanity Check Finished', + scanId: 'e1ea57fa-b7bc-41c1-ae5e-bf92b69fb8bf', + service: 'Payments', + infoLink: 'https://iapp.org/news/a/understanding-data-processors-iso-and-soc-2-credentials-for-gdpr-compliance/', + resource: 'Cluster', + severity: 'Info', + standard: 'GDPR', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:10.068', + frontegg_id: 'b7247cae-f7d2-4cb0-82e4-7c512ce589c6', + }, + { + ip: '23.92.49.21', + user: 'Darcie Policastro', + action: 'Periodic Scan Finished', + result: 'Success', + scanId: 'f4d4ec1d-92c0-4fd4-bf72-0dd3a4d1198b', + service: 'Cars', + resource: 'Service', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:05.264', + frontegg_id: 'eaa61296-1f55-4939-8f82-5d5bca1046fb', + }, + { + ip: '3.92.49.21', + user: 'Ardelia Dismuke', + action: 'Periodic Scan Finished', + result: 'Success', + scanId: 'ed0358f2-d828-47d8-bc1c-70e667ffa447', + service: 'Payments', + resource: 'Service', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:05.208', + frontegg_id: 'e93c8137-4c96-45d9-b66e-eb9530c0a543', + }, + { + ip: '161.185.160.93', + user: 'Naida Rinker', + action: 'Security Audit Perfomed', + scanId: 'b3a11eea-4e99-4d92-90c5-4852e9b3b2fc', + service: 'Users', + infoLink: 'https://owasp.org/www-project-top-ten/', + resource: 'Cluster', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:01.237', + owaspResult: 'Broken Authentication', + frontegg_id: 'c97a790e-ead6-484e-a3c4-4b43f7ec0e3b', + }, + { + ip: '23.92.55.21', + user: 'Geraldo Shupe', + action: 'Compliance Audit Performed', + scanId: '54beb7e8-1150-4721-9f1f-c4af8012907d', + service: 'Payments', + infoLink: 'https://iapp.org/news/a/understanding-data-processors-iso-and-soc-2-credentials-for-gdpr-compliance/', + resource: 'Cluster', + severity: 'Info', + standard: 'SOC2', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:01.236', + frontegg_id: '89582bd3-cb4a-42fa-88c2-608b77927299', + }, + { + ip: '35.92.49.21', + user: 'Darcie Policastro', + action: 'Settings Modified', + resource: 'Microservice', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:01.236', + microservice: 'Payments', + settingChanged: 'Amount of Pods', + frontegg_id: '4dec5263-22ff-4efa-957c-4dca7ed34751', + }, + { + ip: '23.92.55.231', + info: '/insights#scan-id', + user: 'Naida Rinker', + action: 'Sanity Check Started', + failed: '2 Tests', + scanId: '76cd6684-5103-4306-8a14-ca9b9c850183', + cluster: 'Main', + resource: 'Cluster', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:01.235', + succeeded: '42 Tests', + testsPerfomed: '44 tests', + frontegg_id: 'a738aec1-2c59-4705-91dd-d50d3da8a8e0', + }, + { + ip: '25.44.49.21', + user: 'Deanna Post', + action: 'Cache Purged', + resource: 'API Gateway', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:01.233', + cachedItems: 651, + frontegg_id: '1111c87b-b480-40fe-b38a-6df73e10cff2', + }, + { + ip: '25.42.1.21', + user: 'Verona Gonzalas', + action: 'Settings Modified', + changed: 'Amount of Services', + cluster: 'Main', + resource: 'Cluster', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:01.232', + frontegg_id: '3e2eb99a-898b-4c66-a59c-c2c1bc9a32fb', + }, + { + ip: '25.44.49.21', + api: 'GET /insights/343', + user: 'Florine Pinion', + action: 'Liveness Check Perfomed', + result: 'Total Failure', + scanId: 'ac004f86-d25e-4e1e-81f0-5d7ef99ebc29', + resource: 'APIs', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:01.231', + frontegg_id: '76bc91af-7f90-4e56-bf4a-ae46abbbc40c', + }, + { + ip: '72.28.55.231', + user: 'Tillie Casias', + action: 'Settings Modified', + changed: 'Amount of Services', + cluster: 'Main', + resource: 'Cluster', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:01.230', + frontegg_id: '7c6aa166-1431-402e-8fb7-3b37e084702e', + }, + { + ip: '35.92.49.21', + user: 'Lenard Chicoine', + cause: 'Service Response Lag', + action: 'Remediated', + scanId: 'e5c21456-4c4f-4983-8b7f-a0f3eeef6566', + lagTime: '2500ms', + service: 'Payments', + resource: 'Service', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:01.230', + restartTime: '2000ms', + frontegg_id: '557eb503-08e1-4c87-88a0-d8e846a50911', + }, + { + ip: '161.185.160.93', + user: 'Normand Menz', + action: 'Lag Detected', + result: 'Service Response Lag', + scanId: '222f3518-9611-4e02-b67d-5390f59601b3', + lagTime: '7364ms', + service: 'Payments', + resource: 'Service', + severity: 'Error', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:01.229', + frontegg_id: '151d664c-094a-48a4-b69e-b036a12237cc', + }, + { + ip: '25.42.29.21', + user: 'Gail Blackerby', + action: 'Remap', + changed: 'Amount of Services', + cluster: 'Main', + resource: 'Cluster', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:01.228', + frontegg_id: '2f5e988d-7e64-4a40-a1f2-0eae475ad431', + }, + { + ip: '23.92.49.21', + user: 'Darcie Policastro', + action: 'Periodic Scan Started', + scanId: 'f4d4ec1d-92c0-4fd4-bf72-0dd3a4d1198b', + service: 'Cars', + resource: 'Service', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:01.227', + frontegg_id: '4f16e7bd-b7ef-412d-9883-ccb5587a512e', + }, + { + ip: '23.92.55.21', + user: 'Ardelia Dismuke', + action: 'Compliance Audit Performed', + scanId: '96c08187-927e-48b5-ba95-f9b928e70de8', + service: 'Users', + infoLink: 'https://iapp.org/news/a/understanding-data-processors-iso-and-soc-2-credentials-for-gdpr-compliance/', + resource: 'Cluster', + severity: 'Attention', + standard: 'GDPR', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:01.226', + frontegg_id: 'c039888a-bedb-4526-92f6-ebea7be67279', + }, + { + ip: '3.92.49.21', + user: 'Ardelia Dismuke', + action: 'Periodic Scan Started', + scanId: 'ed0358f2-d828-47d8-bc1c-70e667ffa447', + service: 'Payments', + resource: 'Service', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:01.224', + frontegg_id: '3a93bf83-330e-47fe-a793-7543a0b76ca9', + }, + { + ip: '72.92.55.231', + user: 'Debora Coddington', + action: 'Cache Purged', + resource: 'API Gateway', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:01.223', + cachedItems: 924, + frontegg_id: 'fd4911b1-7e43-4bd7-a952-1c85ea272a9e', + }, + { + ip: '25.42.1.21', + user: 'Lenna Nodine', + action: 'Remap', + changed: 'Amount of Services', + cluster: 'Main', + resource: 'Cluster', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:01.223', + frontegg_id: '2ec15823-9235-42d2-9397-d27745326252', + }, + { + ip: '161.185.160.93', + user: 'Jennell Fant', + cause: 'Service Response Lag', + action: 'Remediated', + scanId: 'b5f97e83-e295-4d2c-957b-ff939a69645c', + lagTime: '2500ms', + service: 'Payments', + resource: 'Service', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:01.221', + restartTime: '2000ms', + frontegg_id: '1432d1f6-a891-4de7-8156-b6cff363b24d', + }, + { + ip: '25.42.29.21', + api: 'GET /payments', + user: 'Clement Gallop', + action: 'Liveness Check Perfomed', + result: 'Success', + scanId: 'e619125f-52af-42a3-83d0-8f9067286dd4', + resource: 'APIs', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:01.216', + frontegg_id: '2000132d-6e59-4221-abb5-3ca50fff6c33', + }, + { + ip: '72.92.55.231', + user: 'Geraldo Shupe', + action: 'Settings Modified', + resource: 'Microservice', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:00.231', + microservice: 'Cars', + settingChanged: 'Amount of Pods', + frontegg_id: 'e6fb4081-22a5-4f4a-a954-392be36a47c9', + }, + { + ip: '72.28.101.231', + user: 'Kelvin Casella', + action: 'Lag Detected', + result: 'Service Response Lag', + scanId: '806a3362-4b3c-4d66-9347-44c7004f56d5', + lagTime: '13704ms', + service: 'Users', + resource: 'Service', + severity: 'Error', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:00.230', + frontegg_id: 'fb89e859-9261-49c4-9309-2fa729af290a', + }, + { + ip: '23.92.55.231', + info: '/insights#scan-id', + user: 'Marg Lovelace', + action: 'Sanity Check Started', + failed: '2 Tests', + scanId: 'e1ea57fa-b7bc-41c1-ae5e-bf92b69fb8bf', + cluster: 'Main', + resource: 'Cluster', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:00.228', + succeeded: '42 Tests', + testsPerfomed: '44 tests', + frontegg_id: '95c066eb-666e-457a-9bfb-f269ccd349ed', + }, + { + ip: '25.44.49.21', + user: 'Ardelia Dismuke', + action: 'Security Audit Perfomed', + scanId: '493fa109-88c0-4b54-8b08-6184440f385e', + service: 'Cars', + infoLink: 'https://owasp.org/www-project-top-ten/', + resource: 'Cluster', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 23:00:00.163', + owaspResult: 'Cross-Site Scripting', + frontegg_id: 'b73fd741-e974-496c-b808-a90f09830dec', + }, + { + ip: '23.92.55.21', + user: 'Clement Gallop', + action: 'Sanity Check Finished', + scanId: '42f3286a-f1fa-40d7-a4f7-a5a48e24da63', + service: 'Cars', + infoLink: 'https://iapp.org/news/a/understanding-data-processors-iso-and-soc-2-credentials-for-gdpr-compliance/', + resource: 'Cluster', + severity: 'Info', + standard: 'GDPR', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:10.134', + frontegg_id: '17210ae9-7288-4b24-aa28-f591d89454e3', + }, + { + ip: '25.42.29.21', + user: 'Valery Krieg', + action: 'Sanity Check Finished', + scanId: '853fc3e8-e000-4fb6-9c82-29f19388a423', + service: 'Users', + infoLink: 'https://iapp.org/news/a/understanding-data-processors-iso-and-soc-2-credentials-for-gdpr-compliance/', + resource: 'Cluster', + severity: 'Info', + standard: 'HIPAA', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:10.040', + frontegg_id: '1dbae207-aedd-4036-b23d-e18066bd7fab', + }, + { + ip: '25.42.29.21', + user: 'Geraldo Shupe', + action: 'Periodic Scan Finished', + result: 'Success', + scanId: '27ffb2c5-dd0e-4e2f-b3fd-e7b7b29d8208', + service: 'Users', + resource: 'Service', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:05.171', + frontegg_id: '8276babd-0e9f-4472-9cdd-8fc9daa15acc', + }, + { + ip: '23.92.49.21', + user: 'Tillie Casias', + action: 'Periodic Scan Finished', + result: 'Success', + scanId: 'cc484c12-5d8d-4078-b7c4-aefdba52108b', + service: 'Users', + resource: 'Service', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:05.052', + frontegg_id: '23cb0263-510e-4f75-8631-83ac326e46aa', + }, + { + ip: '35.92.49.21', + user: 'Debora Coddington', + action: 'Compliance Audit Performed', + scanId: '632091be-50e0-4894-800e-1e923157160c', + service: 'Payments', + infoLink: 'https://iapp.org/news/a/understanding-data-processors-iso-and-soc-2-credentials-for-gdpr-compliance/', + resource: 'Cluster', + severity: 'Info', + standard: 'SOC2', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:01.504', + frontegg_id: '44771d00-2524-41b7-a84a-95ba15e1fde6', + }, + { + ip: '25.42.29.21', + user: 'Geraldo Shupe', + action: 'Periodic Scan Started', + scanId: '27ffb2c5-dd0e-4e2f-b3fd-e7b7b29d8208', + service: 'Users', + resource: 'Service', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:01.503', + frontegg_id: 'd2a12db6-9921-4c4a-af35-3193d546f1ea', + }, + { + ip: '3.92.49.21', + user: 'Florine Pinion', + action: 'Cache Purged', + resource: 'API Gateway', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:01.502', + cachedItems: 210, + frontegg_id: '36015eb5-624b-45b4-901f-ed6ecea1fa9d', + }, + { + ip: '72.28.55.231', + user: 'Wendi Burghardt', + action: 'Settings Modified', + changed: 'Amount of Services', + cluster: 'Main', + resource: 'Cluster', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:01.501', + frontegg_id: 'be7155a7-990f-468d-a962-a669336032a0', + }, + { + ip: '25.44.49.21', + api: 'POST /products', + user: 'Herb Mcwain', + action: 'Liveness Check Perfomed', + result: '9 issues found', + scanId: 'a1a6c622-099b-4fc3-a1ae-2e87aa18b4f6', + resource: 'APIs', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:01.500', + frontegg_id: 'e253d2a2-3d81-4766-88d6-7f33fc4a1b25', + }, + { + ip: '25.44.49.21', + user: 'Florine Pinion', + action: 'Lag Detected', + result: 'Service Response Lag', + scanId: 'b9b83865-9315-4a04-8d4e-558ae41d7ed4', + lagTime: '16707ms', + service: 'Payments', + resource: 'Service', + severity: 'Error', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:01.500', + frontegg_id: '63bbfb77-65a7-4c25-9447-96273ee84c2a', + }, + { + ip: '25.42.1.21', + info: '/insights#scan-id', + user: 'Clement Gallop', + action: 'Sanity Check Started', + failed: '2 Tests', + scanId: '42f3286a-f1fa-40d7-a4f7-a5a48e24da63', + cluster: 'Main', + resource: 'Cluster', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:01.499', + succeeded: '42 Tests', + testsPerfomed: '44 tests', + frontegg_id: '9c56179f-56af-4567-b05e-0c4b85b103b6', + }, + { + ip: '23.92.49.21', + api: 'POST /products', + user: 'Deanna Post', + action: 'Liveness Check Perfomed', + result: 'Total Failure', + scanId: '622fb278-508d-4404-a0df-26d42c9cdf9d', + resource: 'APIs', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:01.498', + frontegg_id: '23a17fbe-fb65-41b5-9988-1ed5027e8809', + }, + { + ip: '25.44.49.21', + user: 'Kieth Mason', + action: 'Remap', + changed: 'Amount of Services', + cluster: 'Main', + resource: 'Cluster', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:01.497', + frontegg_id: 'd38483b9-64de-43ef-be18-e0b010e04932', + }, + { + ip: '72.92.55.231', + user: 'Geraldo Shupe', + cause: 'Service Response Lag', + action: 'Remediated', + scanId: '77a7c820-5415-400e-a350-afdd31a15ada', + lagTime: '2500ms', + service: 'Cars', + resource: 'Service', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:01.496', + restartTime: '2000ms', + frontegg_id: 'a68f1f78-edd5-41c8-85f9-ab8674f64072', + }, + { + ip: '72.28.101.231', + user: 'Iris Basso', + action: 'Compliance Audit Performed', + scanId: 'a994926f-eaca-4363-868a-34f3fe17e37d', + service: 'Cars', + infoLink: 'https://iapp.org/news/a/understanding-data-processors-iso-and-soc-2-credentials-for-gdpr-compliance/', + resource: 'Cluster', + severity: 'Attention', + standard: 'SOC2', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:01.495', + frontegg_id: 'f7a5bdda-8aed-4468-94fa-239b14507447', + }, + { + ip: '23.92.49.21', + user: 'Deanna Post', + action: 'Cache Purged', + resource: 'API Gateway', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:01.494', + cachedItems: 135, + frontegg_id: '722c46de-ec03-4949-ad03-9e4eb926a92e', + }, + { + ip: '72.28.55.231', + user: 'Thomas Salser', + action: 'Remap', + changed: 'Amount of Services', + cluster: 'Main', + resource: 'Cluster', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:01.493', + frontegg_id: 'a0d1c3b4-f8e0-4c79-a405-b38d408f4ed4', + }, + { + ip: '23.92.49.21', + user: 'Tillie Casias', + action: 'Periodic Scan Started', + scanId: 'cc484c12-5d8d-4078-b7c4-aefdba52108b', + service: 'Users', + resource: 'Service', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:01.492', + frontegg_id: '9d22b318-f9e2-467d-80a5-bda4b9e2066e', + }, + { + ip: '72.28.101.231', + user: 'Naida Rinker', + action: 'Security Audit Perfomed', + scanId: '1d0bf2c1-9aaa-42ff-9c08-e3aaa85be249', + service: 'Payments', + infoLink: 'https://owasp.org/www-project-top-ten/', + resource: 'Cluster', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:01.492', + owaspResult: 'Injection', + frontegg_id: '7ec9e56e-f79d-4179-9d9a-25d0f8060d02', + }, + { + ip: '25.44.49.21', + user: 'Tillie Casias', + action: 'Settings Modified', + resource: 'Microservice', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:01.491', + microservice: 'Cars', + settingChanged: 'Amount of Pods', + frontegg_id: '5a74d618-19b4-444f-8efa-0d624ea59579', + }, + { + ip: '35.92.49.21', + user: 'Iris Basso', + cause: 'Service Response Lag', + action: 'Remediated', + scanId: 'cc544e0a-8895-4608-b832-07718e461ccf', + lagTime: '2500ms', + service: 'Users', + resource: 'Service', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:01.490', + restartTime: '2000ms', + frontegg_id: 'c48789ae-574a-47a7-84c1-88d722aec057', + }, + { + ip: '23.92.55.231', + user: 'Rena Flanders', + action: 'Settings Modified', + resource: 'Microservice', + severity: 'Info', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:00.306', + microservice: 'Payments', + settingChanged: 'Amount of Pods', + frontegg_id: '86d9c7d5-80c8-40f2-bd24-d090dad05f39', + }, + { + ip: '35.92.49.21', + user: 'Emelia Modeste', + action: 'Security Audit Perfomed', + scanId: '8f012caf-3f20-4c34-a21a-78e2bb87ae3b', + service: 'Cars', + infoLink: 'https://owasp.org/www-project-top-ten/', + resource: 'Cluster', + severity: 'Attention', + tenantId: 'my-tenant-id', + vendorId: '93447df4-edcc-45e5-8664-9fb8c196cf44', + createdAt: '2020-09-28 22:00:00.305', + owaspResult: 'Injection', + frontegg_id: 'cf93bcd0-e368-4de4-be9d-6d110464abf6', + }, +]; + +export const TableExample: FC = () => { + const renderExpandedComponent = useCallback((data) => { + return <>{JSON.stringify(data, null, 2)}; + }, []); + + return ( +
+

Material Table

+ { + return setFilterValue(e.target.value)} />; + }, + }, + { + accessor: 'createdAt', + Header: 'Time', + sortable: true, + Filter: ({ setFilterValue }) =>
, + }, + { accessor: 'resource', Header: 'Resource', sortable: true }, + { accessor: 'action', Header: 'Action', sortable: true }, + { accessor: 'severity', Header: 'Severity', sortable: true }, + { accessor: 'ip', Header: 'Ip Address', sortable: true }, + ]} + data={data} + totalData={data.length} + rowKey='frontegg_id' + pagination='pages' + pageSize={10} + pageCount={100} + // onPageChange={(pageSize, page) => { + // console.log(pageSize, page); + // }} + expandable + renderExpandedComponent={renderExpandedComponent} + selection='multi' + onRowSelected={(selected) => { + // console.log(selected); + }} + toolbar + isMultiSort + // sortBy={sortBy} + // onSortChange={(_sortBy) => { + // setSortBy(_sortBy); + // console.log('_sortBy', JSON.stringify(_sortBy, null, 2)); + // }} + // filters={filters} + // onFilterChange={(_filters) => { + // setFilters(_filters); + // console.log('_filters', JSON.stringify(_filters, null, 2)); + // }} + /> +
+ ); +}; diff --git a/packages/demo-saas/src/apiTokensExample/index.tsx b/packages/demo-saas/src/apiTokensExample/index.tsx new file mode 100644 index 000000000..8955aec55 --- /dev/null +++ b/packages/demo-saas/src/apiTokensExample/index.tsx @@ -0,0 +1,17 @@ +import React, { FC } from 'react'; +import { TenantApiTokens } from '@frontegg/react-auth'; + +export const TenantApiTokensExample: FC = () => { + return ( + + + + + + + + + + + ); +}; diff --git a/packages/demo-saas/src/auditsExample/index.tsx b/packages/demo-saas/src/auditsExample/index.tsx new file mode 100644 index 000000000..70c53c5c2 --- /dev/null +++ b/packages/demo-saas/src/auditsExample/index.tsx @@ -0,0 +1,11 @@ +import React, { FC } from 'react'; +import { Audits } from '@frontegg/react-audits'; + +export const AuditsExample: FC = () => { + return ( +
+ + +
+ ); +}; diff --git a/packages/demo-saas/src/cmc/CMCPage.tsx b/packages/demo-saas/src/cmc/CMCPage.tsx deleted file mode 100644 index 14715b942..000000000 --- a/packages/demo-saas/src/cmc/CMCPage.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import React, { useState, useEffect, useLayoutEffect } from 'react'; -import { - UsersTable, - InviteUserDialog, - ChangePasswordForm, - ProfilePage, - useUsersTable, - useInviteUserDialog, -} from '@frontegg/react'; -import { wrapWithBaseHomePage } from '../BaseHomePage/BaseHomePage'; - -const INVITE_DIALOG_ID = 'my-invite-dialog'; - -const Page = () => { - const { openDialog: openInviteUserDialog } = useInviteUserDialog(INVITE_DIALOG_ID); - const { onSearch } = useUsersTable(); - const [searchInput, setSearchInput] = useState(undefined); - const [openChangePasswordForm, setOpenChangePasswordForm] = useState(false); - useLayoutEffect(() => { - const debounceTimeout = setTimeout(() => { - if (searchInput !== undefined) { - onSearch(searchInput); - } - }, 500); // 500ms debounce time - - return () => clearTimeout(debounceTimeout); - }, [searchInput, onSearch]); - - return ( -
- - -
-
-

Profile Page

- -
-
-

Users Table

- { - setSearchInput(e.target.value); - }} - /> - -
-
-

Change Password Form

- -
- -
-
- ); -}; - -export default wrapWithBaseHomePage(Page, { - width: '80vw', - minWidth: '300px', -}); diff --git a/packages/demo-saas/src/components/App.tsx b/packages/demo-saas/src/components/App.tsx new file mode 100644 index 000000000..d8d6bf536 --- /dev/null +++ b/packages/demo-saas/src/components/App.tsx @@ -0,0 +1,14 @@ +import React, { FC } from 'react'; +import { AppHeader } from './AppHeader/AppHeader'; +import { AppSidebar } from './AppSidebar/AppSidebar'; +import { AppContent } from './AppContent/AppContent'; + +export const App = () => { + return ( +
+ + + +
+ ); +}; diff --git a/packages/demo-saas/src/components/AppContent/AppContent.tsx b/packages/demo-saas/src/components/AppContent/AppContent.tsx new file mode 100644 index 000000000..c969e1bcf --- /dev/null +++ b/packages/demo-saas/src/components/AppContent/AppContent.tsx @@ -0,0 +1,11 @@ +import React, { FC } from 'react'; +import { Routes } from '../../Routes'; + +export const AppContent: FC = () => { + return ( +
+ AppContent + +
+ ); +}; diff --git a/packages/demo-saas/src/components/AppHeader/AppHeader.scss b/packages/demo-saas/src/components/AppHeader/AppHeader.scss new file mode 100644 index 000000000..13fb3648b --- /dev/null +++ b/packages/demo-saas/src/components/AppHeader/AppHeader.scss @@ -0,0 +1,4 @@ +.app-header { + height: 2rem; + background: green; +} diff --git a/packages/demo-saas/src/components/AppHeader/AppHeader.tsx b/packages/demo-saas/src/components/AppHeader/AppHeader.tsx new file mode 100644 index 000000000..a13e0570c --- /dev/null +++ b/packages/demo-saas/src/components/AppHeader/AppHeader.tsx @@ -0,0 +1,6 @@ +import React, { FC } from 'react'; +import './AppHeader.scss'; + +export const AppHeader: FC = () => { + return
AppHeader
; +}; diff --git a/packages/demo-saas/src/components/AppSidebar/AppSidebar.tsx b/packages/demo-saas/src/components/AppSidebar/AppSidebar.tsx new file mode 100644 index 000000000..5ffb8ebed --- /dev/null +++ b/packages/demo-saas/src/components/AppSidebar/AppSidebar.tsx @@ -0,0 +1,5 @@ +import React, { FC } from 'react'; + +export const AppSidebar: FC = () => { + return
AppSidebar
; +}; diff --git a/packages/demo-saas/src/consts.ts b/packages/demo-saas/src/consts.ts deleted file mode 100644 index 5ea1ce6f9..000000000 --- a/packages/demo-saas/src/consts.ts +++ /dev/null @@ -1 +0,0 @@ -export const DEFAULT_BASE_URL = 'https://sub-domain.frontegg.com'; diff --git a/packages/demo-saas/src/customizationOptions/authOptions.ts b/packages/demo-saas/src/customizationOptions/authOptions.ts deleted file mode 100644 index 13f5817a1..000000000 --- a/packages/demo-saas/src/customizationOptions/authOptions.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { FronteggAppOptions } from '@frontegg/types'; - -export const authOptions: { authOptions: FronteggAppOptions['authOptions'] } = { - authOptions: { - keepSessionAlive: true, - }, -}; - -export const hostedLoginAuthOptions: { authOptions: FronteggAppOptions['authOptions'] } = { - authOptions: { - keepSessionAlive: true, - hostedLoginOptions: { - loadUserOnFirstLoad: true, - }, - }, -}; - -export const redirectUrlAuthOptions: { authOptions: FronteggAppOptions['authOptions'] } = { - authOptions: { - keepSessionAlive: true, - enforceRedirectToSameSite: true, - allowedRedirectOrigins: ['https://*.dev.acme.com', 'https://dev.*.acme.com', 'https://dev-*.acme.com'], - }, -}; diff --git a/packages/demo-saas/src/entitlements/EntitlementsPage.tsx b/packages/demo-saas/src/entitlements/EntitlementsPage.tsx deleted file mode 100644 index b63fbfe22..000000000 --- a/packages/demo-saas/src/entitlements/EntitlementsPage.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import Box from '@mui/material/Box'; -import { useAuthActions, useIsAuthenticated } from '@frontegg/react-hooks'; -import { wrapWithBaseHomePage } from '../BaseHomePage/BaseHomePage'; -import { EntitlementBoxes } from './components/EntitlementBoxes'; -import { DemoButton } from '../DemoButton'; - -const Page = () => { - const isAuthenticated = useIsAuthenticated(); - - const { loadEntitlements } = useAuthActions(); - - const onLoadEntitlements = () => { - loadEntitlements({ - callback: (isSucceeded: boolean) => - console.log(`Load entitlements with a callback ${isSucceeded ? 'succeeded' : 'failed'}`), - }); - }; - - return isAuthenticated ? ( - - - - - loadEntitlements()}>Load entitlements - Load entitlements with a callback - - - ) : ( - <>Not authenticated - ); -}; - -export default wrapWithBaseHomePage(Page); diff --git a/packages/demo-saas/src/entitlements/components/EntitlementBoxes.css b/packages/demo-saas/src/entitlements/components/EntitlementBoxes.css deleted file mode 100644 index 77584bc8e..000000000 --- a/packages/demo-saas/src/entitlements/components/EntitlementBoxes.css +++ /dev/null @@ -1,38 +0,0 @@ -.entitlement-item { - width: 400px; - height: 100px; - animation: fadeIn 5s; - margin: 10px; - display: flex; - justify-content: center; - align-items: center; - border-radius: 10px; - border: 1px solid black; -} - -@keyframes fadeIn { - 0% { - opacity: 0; - } - 100% { - opacity: 1; - } -} - -.entitlement-item-entitled { - background-color: lightgreen; -} - -.entitlement-item-not-entitled { - background-color: pink; -} - -.entitlement-item-not-entitled i { - padding-top: 15px; -} - -.load-entitlement-btn { - width: 200px; - height: 40px; - font-size: 15px; -} diff --git a/packages/demo-saas/src/entitlements/components/EntitlementBoxes.tsx b/packages/demo-saas/src/entitlements/components/EntitlementBoxes.tsx deleted file mode 100644 index ad6b68288..000000000 --- a/packages/demo-saas/src/entitlements/components/EntitlementBoxes.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React from 'react'; -import Box from '@mui/material/Box'; - -import EntitlementComponent from './EntitlementComponent'; -import FeatureComponent from './FeatureComponent'; -import PermissionComponent from './PermissionComponent'; - -import './EntitlementBoxes.css'; - -enum HOOKS { - FEATURE = 'useFeatureEntitlements', - PERMISSION = 'usePermissionEntitlements', - ENTITLEMENTS = 'useEntitlements', -} - -const HOOKS_TO_COMPONENT = { - [HOOKS.FEATURE]: FeatureComponent, - [HOOKS.PERMISSION]: PermissionComponent, - [HOOKS.ENTITLEMENTS]: EntitlementComponent, -}; - -const queries: any[] = [ - { - featureKey: 'sso', - UIComponent: HOOKS_TO_COMPONENT[HOOKS.FEATURE], - }, - { - featureKey: 'sso', - UIComponent: HOOKS_TO_COMPONENT[HOOKS.FEATURE], - customAttributes: { env: 'dev' }, - }, - { - featureKey: 'proteins.*', - UIComponent: HOOKS_TO_COMPONENT[HOOKS.ENTITLEMENTS], - customAttributes: { pro: '20gr' }, - }, - { - permissionKey: 'dora.protein.*', - UIComponent: HOOKS_TO_COMPONENT[HOOKS.PERMISSION], - }, - { - permissionKey: 'fe.secure.*', - UIComponent: HOOKS_TO_COMPONENT[HOOKS.ENTITLEMENTS], - }, - { - permissionKey: 'fe.secure.*', - UIComponent: HOOKS_TO_COMPONENT[HOOKS.PERMISSION], - customAttributes: { env: 'dev' }, - }, -]; - -export const EntitlementBoxes = () => ( - - {queries.map(({ UIComponent, ...rest }, i) => ( - - ))} - -); diff --git a/packages/demo-saas/src/entitlements/components/EntitlementComponent.tsx b/packages/demo-saas/src/entitlements/components/EntitlementComponent.tsx deleted file mode 100644 index d442921be..000000000 --- a/packages/demo-saas/src/entitlements/components/EntitlementComponent.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; -import { CustomAttributes } from '@frontegg/types'; -import EntitlementBase from './EntitlementsBase'; -import { useEntitlements } from '@frontegg/react-hooks'; -import { EntitledToOptions } from '@frontegg/redux-store'; - -const getDescription = ({ - featureKey, - permissionKey, - customAttributes, -}: { - featureKey?: string; - permissionKey?: string; - customAttributes?: CustomAttributes; -}) => - `useEntitlements(${ - (featureKey && `{ featureKey: ${featureKey} }`) || - (permissionKey && `{ permissionKey: ${permissionKey} }`) - }${customAttributes ? `, ${JSON.stringify(customAttributes).replaceAll('"', '').replaceAll(':', ': ')}` : ''} - `; - -const EntitlementComponent = ({ - featureKey, - permissionKey, - customAttributes, -}: { - featureKey?: string; - permissionKey?: string; - customAttributes?: CustomAttributes; -}) => { - const entitlementResult = useEntitlements( - (featureKey ? { featureKey } : { permissionKey }) as EntitledToOptions, - customAttributes - ); - - return ( - - ); -}; - -export default EntitlementComponent; diff --git a/packages/demo-saas/src/entitlements/components/EntitlementsBase.tsx b/packages/demo-saas/src/entitlements/components/EntitlementsBase.tsx deleted file mode 100644 index 0b6f4e2c9..000000000 --- a/packages/demo-saas/src/entitlements/components/EntitlementsBase.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import Box from '@mui/material/Box'; -import { Entitlement } from '@frontegg/types'; - -const EntitlementBase = ({ - keyName, - isEntitled, - justification, -}: { - keyName: string; - isEntitled: Entitlement['isEntitled']; - justification?: Entitlement['justification']; -}) => { - return ( - - ${keyName}` }} - > - - {justification && {justification}} - - ); -}; - -export default EntitlementBase; diff --git a/packages/demo-saas/src/entitlements/components/FeatureComponent.tsx b/packages/demo-saas/src/entitlements/components/FeatureComponent.tsx deleted file mode 100644 index 93ec2f875..000000000 --- a/packages/demo-saas/src/entitlements/components/FeatureComponent.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import { CustomAttributes } from '@frontegg/types'; -import EntitlementBase from './EntitlementsBase'; -import { useFeatureEntitlements } from '@frontegg/react-hooks'; - -const FeatureComponent = ({ - featureKey, - customAttributes, -}: { - featureKey: string; - customAttributes?: CustomAttributes; -}) => { - const entitlementResult = useFeatureEntitlements(featureKey, customAttributes); - - return ( - ${featureKey}${ - customAttributes ? `, ${JSON.stringify(customAttributes).replaceAll('"', '').replaceAll(':', ': ')}` : '' - })`} - {...entitlementResult} - /> - ); -}; - -export default FeatureComponent; diff --git a/packages/demo-saas/src/entitlements/components/PermissionComponent.tsx b/packages/demo-saas/src/entitlements/components/PermissionComponent.tsx deleted file mode 100644 index cc9753e49..000000000 --- a/packages/demo-saas/src/entitlements/components/PermissionComponent.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import { CustomAttributes } from '@frontegg/types'; -import EntitlementBase from './EntitlementsBase'; -import { usePermissionEntitlements } from '@frontegg/react-hooks'; - -const PermissionComponent = ({ - permissionKey, - customAttributes, -}: { - permissionKey: string; - customAttributes?: CustomAttributes; -}) => { - const entitlementResult = usePermissionEntitlements(permissionKey, customAttributes); - - return ( - ${permissionKey}${ - customAttributes ? `, ${JSON.stringify(customAttributes).replaceAll('"', '').replaceAll(':', ': ')}` : '' - })`} - {...entitlementResult} - /> - ); -}; - -export default PermissionComponent; diff --git a/packages/demo-saas/src/grid-examples.tsx b/packages/demo-saas/src/grid-examples.tsx new file mode 100644 index 000000000..814ca9bbb --- /dev/null +++ b/packages/demo-saas/src/grid-examples.tsx @@ -0,0 +1,200 @@ +import React, { FC, useState } from 'react'; +import { Elements, fronteggElements as FE } from '@frontegg/react-core'; +// import { uiLibrary as S } from '@frontegg/react-elements-semantic'; +import { uiLibrary as M } from '@frontegg/react-elements-material-ui'; + +// const Semantic = S as Elements; +const Material = M as Elements; +const Frontegg = FE as Elements; + +const Item: FC = ({ children }) => { + return ( +
+ {children} +
+ ); +}; +export const GridExamples: FC = () => { + const [ex, setEx] = useState(1); + return ( +
+
+ {[1, 2, 3, 4].map((i) => ( + setEx(i)} + > + Example {i} + + ))} +
+ {ex === 1 && + [ + [Frontegg, 'Frontegg Grid'], + [Material, 'Material Grid'], + ].map(([ll, name]) => { + const Lib = ll as any; + return ( + <> +

{name} EX.1

+ + + xs=12 + + + xs=6 + + + xs=6 + + + + xs=3 + + + xs=3 + + + xs=3 + + + xs=3 + + + + ); + })} + + {ex === 2 && + [ + [Frontegg, 'Frontegg Grid'], + [Material, 'Material Grid'], + ].map(([ll, name]) => { + const Lib = ll as any; + return ( + <> +

{name} EX.2 (Grid with breakpoints)

+ + + + xs=12 + + + xs=12 sm=6 + + + xs=12 sm=6 + + + xs=6 sm=3 + + + xs=6 sm=3 + + + xs=6 sm=3 + + + xs=6 sm=3 + + + + ); + })} + + {ex === 3 && + [ + [Frontegg, 'Frontegg Grid'], + [Material, 'Material Grid'], + ].map(([ll, name]) => { + const Lib = ll as any; + return ( + <> +

{name} EX.3 (Auto Layout)

+ + + xs + + + xs + + + xs + + + + + xs + + + xs=6 + + + xs + + + + ); + })} + + {ex === 4 && + [ + [Frontegg, 'Frontegg Grid'], + [Material, 'Material Grid'], + ].map(([ll, name]) => { + const Lib = ll as any; + + function FormRow() { + return ( + + + item + + + item + + + item + + + ); + } + + return ( + <> +

{name} EX.3 (Nested Grid)

+ + + + + + + + + + + + + ); + })} +
+ ); +}; diff --git a/packages/demo-saas/src/index.scss b/packages/demo-saas/src/index.scss new file mode 100644 index 000000000..854255ad9 --- /dev/null +++ b/packages/demo-saas/src/index.scss @@ -0,0 +1,82 @@ +html, +body { + margin: 0; + padding: 0; + overflow-x: hidden; + min-width: 320px; + background: #fff; + font-family: 'Nunito Sans', Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif; + font-size: 14px; + line-height: 1.5; + font-smoothing: antialiased; +} + +* { + box-sizing: border-box; +} + +#root, +.app { + height: 100%; +} + +div.fe-table { + // theme 1 + + --color-primary: #245dac; + --color-primary-lighter: #d2dbe7; + --fe-table-header-bg: #edf3f8; + --fe-table-header-height: 2.8rem; + --fe-table-header-font-size: 0.95rem; + --fe-table-header-font-color: #3b5973; + --element-height: 1.5rem; + + .fe-table__thead .fe-table__thead-tr { + border-bottom: 1px solid #dee3e7; + border-top: 1px solid #dee3e7; + } + + .fe-table__thead .fe-table__thead-tr-th { + text-transform: none; + font-weight: 800; + } + + .fe-table__thead-tr-th.fe-table__thead-sortable-asc { + //border-top-color: #2360ad; + border-top-color: #000c38; + } + + .fe-table__thead-tr-th.fe-table__thead-sortable-desc { + border-bottom-color: #000c38; + } + + .fe-table-cell__description { + color: #769db3; + } + + .fe-table__thead-tr-th__first-cell__expander { + min-width: 60px; + } + + .fe-audits__severity.fe-audits__severity-info { + font-size: 0.9rem; + } + .fe-audits__severity-dot { + display: none; + } + + // theme 2 + + //--fe-table-header-bg: #edf3f8; + //--fe-table-header-font-size: 0.95rem; + //--fe-table-header-font-color: #2360ad; + + //.fe-table__thead .fe-table__thead-tr-th { + // text-transform: none; + // font-weight: 800; + //} + + //.fe-table__thead-tr-th.fe-table__thead-sortable-asc { + //border-top-color: #2360ad; + //} +} diff --git a/packages/demo-saas/src/index.tsx b/packages/demo-saas/src/index.tsx index 6c5bf793d..030b0edb6 100644 --- a/packages/demo-saas/src/index.tsx +++ b/packages/demo-saas/src/index.tsx @@ -1,10 +1,21 @@ -import React, { StrictMode } from 'react'; +import React from 'react'; import ReactDOM from 'react-dom'; -import { App } from './App'; +import './index.scss'; +// import { App } from './App'; +import OldApp from './OldApp'; +import { + // withFrontegg, + withOldFrontegg, +} from './withFrontegg'; +import { BrowserRouter } from 'react-router-dom'; + +// const AppWithFrontegg = withFrontegg(App); +const AuditLogsConnectivityApp = withOldFrontegg(OldApp); ReactDOM.render( - - - , + + {/**/} + + , document.getElementById('root') ); diff --git a/packages/demo-saas/src/notifications-example/index.tsx b/packages/demo-saas/src/notifications-example/index.tsx new file mode 100644 index 000000000..1c518f2a6 --- /dev/null +++ b/packages/demo-saas/src/notifications-example/index.tsx @@ -0,0 +1,6 @@ +import React, { FC } from 'react'; +// import { Notifications } from '@frontegg/react-notifications'; + +export const NotificationsExample: FC = () => { + return
{/**/}
; +}; diff --git a/packages/demo-saas/src/pages/HomePage.tsx b/packages/demo-saas/src/pages/HomePage.tsx new file mode 100644 index 000000000..4afddbce9 --- /dev/null +++ b/packages/demo-saas/src/pages/HomePage.tsx @@ -0,0 +1,5 @@ +import React, { FC } from 'react'; + +export const HomePage: FC = (props) => { + return
HOME PAGE
; +}; diff --git a/packages/demo-saas/src/pages/Icons.tsx b/packages/demo-saas/src/pages/Icons.tsx new file mode 100644 index 000000000..46582a8da --- /dev/null +++ b/packages/demo-saas/src/pages/Icons.tsx @@ -0,0 +1,39 @@ +import { Grid, Icon, IconNames } from '@frontegg/react-core'; +import React, { FC } from 'react'; + +const icons: IconNames[] = [ + 'back', + 'checkmark', + 'copy', + 'delete', + 'down-arrow', + 'edit', + 'filters', + 'image', + 'indeterminate', + 'left-arrow', + 'person-add', + 'right-arrow', + 'search', + 'send', + 'sort-arrows-asc', + 'sort-arrows-desc', + 'sort-arrows', + 'up-arrow', + 'vertical-dots', + 'visibility-off', + 'visibility', + 'warning', + 'globe', + 'close', +]; +export const Icons: FC = () => ( + + {icons.map((name, idx) => ( + + +
{name}
+
+ ))} +
+); diff --git a/packages/demo-saas/src/stepUp/HOCStepUpPage.tsx b/packages/demo-saas/src/stepUp/HOCStepUpPage.tsx deleted file mode 100644 index f63865eee..000000000 --- a/packages/demo-saas/src/stepUp/HOCStepUpPage.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React, { FC } from 'react'; -import { wrapWithBaseHomePage } from '../BaseHomePage/BaseHomePage'; -import { MaxAge } from './components/MaxAge'; -import { SteppedUpMessage } from './components/SteppedUpMessage'; -import { SteppedUpContent } from '@frontegg/react'; - -const MAX_AGE = 5000; - -const HomePage: FC = () => { - return ( - <> - - - - - - - ); -}; - -export default wrapWithBaseHomePage(HomePage); diff --git a/packages/demo-saas/src/stepUp/ModalsStepUpPage.tsx b/packages/demo-saas/src/stepUp/ModalsStepUpPage.tsx deleted file mode 100644 index f9d192b1b..000000000 --- a/packages/demo-saas/src/stepUp/ModalsStepUpPage.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React, { FC, useState } from 'react'; -import QPWalletModal from './Wallet/QPWallet'; -import LSWalletModal from './Wallet/LSWallet'; -import { wrapWithBaseHomePage } from '../BaseHomePage/BaseHomePage'; - -const HomePage: FC = () => { - const [balance, setBalance] = useState(2000); - - return ( - <> - - - - ); -}; - -export default wrapWithBaseHomePage(HomePage); diff --git a/packages/demo-saas/src/stepUp/NoMaxAgeStepUpPage.tsx b/packages/demo-saas/src/stepUp/NoMaxAgeStepUpPage.tsx deleted file mode 100644 index d1167805f..000000000 --- a/packages/demo-saas/src/stepUp/NoMaxAgeStepUpPage.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { wrapWithBaseHomePage } from '../BaseHomePage/BaseHomePage'; -import { StepUpSimpleButtonScenario } from './components/StepUpSimpleButtonScenario'; - -export default wrapWithBaseHomePage(StepUpSimpleButtonScenario); diff --git a/packages/demo-saas/src/stepUp/SimpleStepUpButtonPage.tsx b/packages/demo-saas/src/stepUp/SimpleStepUpButtonPage.tsx deleted file mode 100644 index e8246c8d0..000000000 --- a/packages/demo-saas/src/stepUp/SimpleStepUpButtonPage.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; - -import { wrapWithBaseHomePage } from '../BaseHomePage/BaseHomePage'; -import { StepUpSimpleButtonScenario } from './components/StepUpSimpleButtonScenario'; - -const MAX_AGE = 5000; - -const Page = () => ; - -export default wrapWithBaseHomePage(Page); diff --git a/packages/demo-saas/src/stepUp/SmallMaxAgeStepUpPage.tsx b/packages/demo-saas/src/stepUp/SmallMaxAgeStepUpPage.tsx deleted file mode 100644 index 406805cf4..000000000 --- a/packages/demo-saas/src/stepUp/SmallMaxAgeStepUpPage.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; - -import { wrapWithBaseHomePage } from '../BaseHomePage/BaseHomePage'; -import { StepUpSimpleButtonScenario } from './components/StepUpSimpleButtonScenario'; - -const MAX_AGE = 35; - -const Page = () => ; - -export default wrapWithBaseHomePage(Page); diff --git a/packages/demo-saas/src/stepUp/TransferStepUpPage.tsx b/packages/demo-saas/src/stepUp/TransferStepUpPage.tsx deleted file mode 100644 index 05d52cd6e..000000000 --- a/packages/demo-saas/src/stepUp/TransferStepUpPage.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { useStepUp, useIsSteppedUp } from '@frontegg/react-hooks'; - -import TextField from '@mui/material/TextField'; -import Box from '@mui/material/Box'; -import { StepUpButton } from './components/Buttons'; -import { getLocalStorage, addLocalStorage, removeLocalStorage, getRandomInt } from './Wallet/utils'; -import CircularWithValueLabel from './Wallet/Loader'; -import { wrapWithBaseHomePage } from '../BaseHomePage/BaseHomePage'; -import { MaxAge } from './components/MaxAge'; - -export const TRANSFER_ID_LOCAL_STORAGE = 'transfer-id'; -export const TRANSFER_NAME_LOCAL_STORAGE = 'transfer-name'; -export const TRANSFER_REASON_LOCAL_STORAGE = 'transfer-reason'; - -const MAX_AGE = 5000; - -const HomePage = () => { - const [balance, setBalance] = useState(2000); - const [name, setName] = useState(getLocalStorage(TRANSFER_NAME_LOCAL_STORAGE) || ''); - const [reason, setReason] = useState(getLocalStorage(TRANSFER_REASON_LOCAL_STORAGE) || ''); - const [progress, setProgress] = React.useState(100); - - const isSteppedUp = useIsSteppedUp({ maxAge: MAX_AGE }); - const stepUp = useStepUp(); - - const isDuringTransfer = !!getLocalStorage(TRANSFER_ID_LOCAL_STORAGE); - - async function transfer(transferID?: string) { - async function checkID(id: string | null) { - return new Promise((res) => { - setTimeout(() => { - res(!!id); - }, 1000); - }); - } - - if (await checkID(transferID || getLocalStorage(TRANSFER_ID_LOCAL_STORAGE))) { - console.log(`Performing bank transfer of 100$ from ${name} - ${reason}`); - setBalance(balance - 100); - setProgress(0); - } else { - console.log('Invalid transfer operation'); - } - - removeLocalStorage(TRANSFER_REASON_LOCAL_STORAGE, TRANSFER_NAME_LOCAL_STORAGE, TRANSFER_ID_LOCAL_STORAGE); - } - - useEffect(() => { - if (!isDuringTransfer) return; - const isDuringTransferDoubleCheck = !!getLocalStorage(TRANSFER_ID_LOCAL_STORAGE); - - if (!isDuringTransferDoubleCheck) return; - - // when use go back from the step up page without 2 factor - need to remove and don't do operation - if (!isSteppedUp) { - console.log('removing local storage'); - removeLocalStorage(TRANSFER_ID_LOCAL_STORAGE); - } - - transfer(); - }, [isDuringTransfer]); - - return ( - <> - {progress !== 100 ? ( - - ) : ( - <> - - -
- Your Frontegg balance is {balance}! -
- - {isDuringTransfer ? ( - 'Transfer in progress' - ) : ( - <> - - - { - if (isSteppedUp) { - transfer(getRandomInt(1000, 9999).toString()); - return; - } - - addLocalStorage(TRANSFER_ID_LOCAL_STORAGE, getRandomInt(1000, 9999).toString()); - addLocalStorage(TRANSFER_REASON_LOCAL_STORAGE, reason || ''); - addLocalStorage(TRANSFER_NAME_LOCAL_STORAGE, name || ''); - - stepUp({ maxAge: MAX_AGE }); - }} - > - Transfer 100$! - - - )} - - )} - - ); -}; - -const MoneyTransferForm = ({ setName, setReason, reason, name }: any) => ( - - setName(event.target.value)} margin='normal' /> - setReason(event.target.value)} margin='normal' /> - -); - -export default wrapWithBaseHomePage(HomePage, { minHeight: '250px', minWidth: '240px' }); diff --git a/packages/demo-saas/src/stepUp/Wallet/LSWallet.tsx b/packages/demo-saas/src/stepUp/Wallet/LSWallet.tsx deleted file mode 100644 index f662a878c..000000000 --- a/packages/demo-saas/src/stepUp/Wallet/LSWallet.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { useStepUp, useIsSteppedUp } from '@frontegg/react-hooks'; - -import TextField from '@mui/material/TextField'; -import Box from '@mui/material/Box'; -import Modal from '@mui/material/Modal'; -import Typography from '@mui/material/Typography'; -import { StepUpButton } from '../components/Buttons'; -import { style, getLocalStorage, addLocalStorage, removeLocalStorage } from './utils'; -import CircularWithValueLabel from './Loader'; -import { MaxAge } from '../components/MaxAge'; - -export const WALLET_LOCAL_STORAGE = 'wallet-modal-open'; -export const TRANSFER_LOCAL_STORAGE = 'trnasfer'; -export const TRANSFER_NAME_LOCAL_STORAGE = 'transfer-name'; -export const TRANSFER_REASON_LOCAL_STORAGE = 'transfer-reason'; - -const MAX_AGE = 35; - -export default function LSWalletModal({ - balance, - setBalance, -}: { - balance: number; - setBalance: (balance: number) => void; -}) { - const [open, setOpen] = useState(getLocalStorage(WALLET_LOCAL_STORAGE) === 'true'); - const [name, setName] = useState(getLocalStorage(TRANSFER_NAME_LOCAL_STORAGE)); - const [reason, setReason] = useState(getLocalStorage(TRANSFER_REASON_LOCAL_STORAGE)); - const [progress, setProgress] = React.useState(100); - - const handleOpen = () => { - setOpen(true); - }; - - const handleClose = () => { - setOpen(false); - removeLocalStorage(TRANSFER_REASON_LOCAL_STORAGE); - removeLocalStorage(TRANSFER_NAME_LOCAL_STORAGE); - setName(''); - setReason(''); - }; - - const isSteppedUp = useIsSteppedUp({ maxAge: MAX_AGE }); - const stepUp = useStepUp(); - - const isDuringTransfer = getLocalStorage(TRANSFER_LOCAL_STORAGE); - - function transfer() { - console.log(`Performing bank transfer of 100$ from ${name} - ${reason}`); - setBalance(balance - 100); - removeLocalStorage(WALLET_LOCAL_STORAGE); - removeLocalStorage(TRANSFER_REASON_LOCAL_STORAGE); - removeLocalStorage(TRANSFER_NAME_LOCAL_STORAGE); - setProgress(0); - } - - useEffect(() => { - if (!isDuringTransfer) return; - const isDuringTransferDoubleCheck = getLocalStorage(TRANSFER_LOCAL_STORAGE); - - if (!isDuringTransferDoubleCheck) return; - - console.log('removing local storage'); - removeLocalStorage(TRANSFER_LOCAL_STORAGE); - - // when use go back from the step up page without 2 factor - need to remove and don't do operation - if (!isSteppedUp) return; - - transfer(); - }, [isDuringTransfer]); - - return ( -
- - - Local Storage Wallet - - - {progress !== 100 ? ( - - ) : ( - <> - - Welcome to your local storage Wallet! - - - You're entitled to the step-up demo! - - -
- Your Frontegg balance is {balance}! -
- - - - {!isDuringTransfer && ( - { - // should allow not to pass payload - if (isSteppedUp) { - transfer(); - return; - } - - addLocalStorage(WALLET_LOCAL_STORAGE, 'true'); - addLocalStorage(TRANSFER_LOCAL_STORAGE, 'true'); - addLocalStorage(TRANSFER_REASON_LOCAL_STORAGE, reason || ''); - addLocalStorage(TRANSFER_NAME_LOCAL_STORAGE, name || ''); - - stepUp({ maxAge: MAX_AGE }); - }} - > - Transfer 100$! - - )} - - )} -
-
-
- ); -} - -const MoneyTransferForm = ({ setName, setReason, reason, name }: any) => { - const handleNameChange = (event: any) => { - setName(event.target.value); - }; - - const handleReasonChange = (event: any) => { - setReason(event.target.value); - }; - - return ( - - - - - ); -}; diff --git a/packages/demo-saas/src/stepUp/Wallet/Loader.tsx b/packages/demo-saas/src/stepUp/Wallet/Loader.tsx deleted file mode 100644 index f185bca3f..000000000 --- a/packages/demo-saas/src/stepUp/Wallet/Loader.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import * as React from 'react'; -import CircularProgress, { CircularProgressProps } from '@mui/material/CircularProgress'; -import Typography from '@mui/material/Typography'; -import Box from '@mui/material/Box'; - -function CircularProgressWithLabel(props: CircularProgressProps & { value: number }) { - return ( - - - - {`${Math.round( - props.value - )}%`} - - - ); -} - -export default function CircularWithValueLabel({ setProgress, progress }: any) { - React.useEffect(() => { - const timer = setInterval(() => { - setProgress((prevProgress: number) => (prevProgress >= 100 ? 0 : prevProgress + 10)); - }, 100); - return () => { - clearInterval(timer); - }; - }, []); - - return ; -} diff --git a/packages/demo-saas/src/stepUp/Wallet/MyBalance.tsx b/packages/demo-saas/src/stepUp/Wallet/MyBalance.tsx deleted file mode 100644 index db1bb6614..000000000 --- a/packages/demo-saas/src/stepUp/Wallet/MyBalance.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { getRandomInt } from './utils'; - -const MyBalance = () => { - const [balance, setBalance] = useState(0); - const [isLoaded, setIsLoaded] = useState(false); - - useEffect(() => { - setTimeout(() => { - setBalance(getRandomInt(1000, 2000)); - setIsLoaded(true); - }); - }, [setBalance, setIsLoaded]); - - return isLoaded ? ( -
- Your Frontegg balance is {balance}! -
- ) : ( - <>Loading balance - ); -}; - -export default MyBalance; diff --git a/packages/demo-saas/src/stepUp/Wallet/QPWallet.tsx b/packages/demo-saas/src/stepUp/Wallet/QPWallet.tsx deleted file mode 100644 index d098f0fb5..000000000 --- a/packages/demo-saas/src/stepUp/Wallet/QPWallet.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { useStepUp, useIsSteppedUp } from '@frontegg/react-hooks'; - -import Box from '@mui/material/Box'; -import Modal from '@mui/material/Modal'; -import Typography from '@mui/material/Typography'; -import { StepUpButton } from '../components/Buttons'; -import { style, getQueryParam, addQueryParam, removeQueryParam } from './utils'; -import MyBalance from './MyBalance'; -import { MaxAge } from '../components/MaxAge'; - -export const WALLET_QUERY_PARAM = 'wallet-modal-open'; -const MAX_AGE = 5000; - -export default function QPWalletModal() { - const isSteppedUp = useIsSteppedUp({ maxAge: MAX_AGE }); - const stepUp = useStepUp(); - - const [open, setOpen] = useState(getQueryParam(WALLET_QUERY_PARAM) === 'true'); - - useEffect(() => { - isSteppedUp && removeQueryParam(WALLET_QUERY_PARAM); - }, [isSteppedUp]); - - return ( -
- - - setOpen(true)}>Query Params Wallet - setOpen(false)} - aria-labelledby='modal-modal-title' - aria-describedby='modal-modal-description' - > - - - Welcome to your query-param Wallet! - - - You're entitled to the step-up demo! - - - {isSteppedUp ? ( - - ) : ( - { - addQueryParam(WALLET_QUERY_PARAM, 'true'); - - // should allow not to pass payload - stepUp({ maxAge: MAX_AGE }); - }} - > - Show my balance - - )} - - -
- ); -} diff --git a/packages/demo-saas/src/stepUp/Wallet/utils.ts b/packages/demo-saas/src/stepUp/Wallet/utils.ts deleted file mode 100644 index 23e8ea003..000000000 --- a/packages/demo-saas/src/stepUp/Wallet/utils.ts +++ /dev/null @@ -1,40 +0,0 @@ -export const style = { - display: 'flex', - 'flex-direction': 'column', - 'align-items': 'center', - position: 'absolute' as 'absolute', - top: '50%', - left: '50%', - transform: 'translate(-50%, -50%)', - width: 400, - bgcolor: 'background.paper', - border: '2px solid #000', - boxShadow: 24, - p: 4, -}; - -export function addQueryParam(key: string, value: string) { - const newUrl = new URL(window.location.href); - newUrl.searchParams.set(key, value); - - // Push the new URL to the history without navigation - window.history.pushState({ path: newUrl.href }, '', newUrl.href); -} - -export function removeQueryParam(key: string) { - const currentUrl = new URL(window.location.href); - currentUrl.searchParams.delete(key); - - // Push the updated URL to the history without navigation - window.history.pushState({ path: currentUrl.href }, '', currentUrl.href); -} - -export const getQueryParam = (key: string) => new URLSearchParams(window.location.search).get(key); - -export const addLocalStorage = (key: string, value: string) => localStorage.setItem(key, value); -export const getLocalStorage = (key: string) => localStorage.getItem(key); -export const removeLocalStorage = (...keys: string[]) => keys.forEach((key) => localStorage.removeItem(key)); - -export function getRandomInt(min: number, max: number): number { - return Math.floor(Math.random() * (max - min + 1)) + min; -} diff --git a/packages/demo-saas/src/stepUp/components/Buttons.tsx b/packages/demo-saas/src/stepUp/components/Buttons.tsx deleted file mode 100644 index 330560c55..000000000 --- a/packages/demo-saas/src/stepUp/components/Buttons.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; -import TrendingUpIcon from '@mui/icons-material/TrendingUp'; -import { DemoButton } from '../../DemoButton'; - -export const StepUpButton = (props: any) => ( - } {...props} /> -); diff --git a/packages/demo-saas/src/stepUp/components/MaxAge.tsx b/packages/demo-saas/src/stepUp/components/MaxAge.tsx deleted file mode 100644 index 67656bbde..000000000 --- a/packages/demo-saas/src/stepUp/components/MaxAge.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; - -export const MaxAge = ({ maxAge }: { maxAge?: number }) => { - if (maxAge) { - return ( -
- Max age is {maxAge} -
- ); - } - - return ( -
- No max age -
- ); -}; diff --git a/packages/demo-saas/src/stepUp/components/StepUpSimpleButtonScenario.tsx b/packages/demo-saas/src/stepUp/components/StepUpSimpleButtonScenario.tsx deleted file mode 100644 index 342ec9627..000000000 --- a/packages/demo-saas/src/stepUp/components/StepUpSimpleButtonScenario.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import { useStepUp, useIsSteppedUp } from '@frontegg/react-hooks'; - -import { StepUpButton } from './Buttons'; -import { MaxAge } from './MaxAge'; -import { SteppedUpMessage } from './SteppedUpMessage'; - -export const StepUpSimpleButtonScenario = ({ maxAge }: { maxAge?: number }) => { - const maxAgeOptions = maxAge ? { maxAge } : undefined; - const stepUp = useStepUp(); - const isSteppedUp = useIsSteppedUp(maxAgeOptions); - - return ( - <> - - - {isSteppedUp ? ( - - ) : ( - { - stepUp(maxAgeOptions); - }} - > - Step me Up - - )} - - ); -}; diff --git a/packages/demo-saas/src/stepUp/components/SteppedUpMessage.tsx b/packages/demo-saas/src/stepUp/components/SteppedUpMessage.tsx deleted file mode 100644 index a90bf632f..000000000 --- a/packages/demo-saas/src/stepUp/components/SteppedUpMessage.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; - -export const SteppedUpMessage = () => ( -
- Honey, you are STEPPED UP! -
-); diff --git a/packages/demo-saas/src/withFrontegg.tsx b/packages/demo-saas/src/withFrontegg.tsx new file mode 100644 index 000000000..a906d683e --- /dev/null +++ b/packages/demo-saas/src/withFrontegg.tsx @@ -0,0 +1,51 @@ +import React, { ComponentType, useContext, useEffect, useRef } from 'react'; +import { ContextOptions, FronteggProvider, PluginConfig } from '@frontegg/react-core'; +import { AuthPlugin } from '@frontegg/react-auth'; +import { ConnectivityPlugin } from '@frontegg/react-connectivity'; +import { AuditsPlugin } from '@frontegg/react-audits'; +import { uiLibrary } from '@frontegg/react-elements-material-ui'; +import { FronteggStoreContext } from '@frontegg/react-hooks'; +import { initialize } from '@frontegg/admin-portal'; + +const contextOptions: ContextOptions = { + baseUrl: `https://david.frontegg.com`, + requestCredentials: 'include', + auditsOptions: { + virtualScroll: true, + }, +}; + +const plugins: PluginConfig[] = [AuthPlugin(), ConnectivityPlugin(), AuditsPlugin()]; + +const ConnectAdminPortal = ({ children }: any) => { + const appRef = useRef(null); + const { store } = useContext(FronteggStoreContext); + + useEffect(() => { + appRef.current = + store && + initialize({ + version: 'next', + contextOptions, + customLoader: true, + customLoginBox: true, + store, + usingFronteggReactCore: true, + } as any); + }, [store]); + return <>{children}; +}; +export const withFrontegg = (Component: ComponentType) => () => ( + + + + + +); + +// const oldAppPlugins = [ConnectivityPlugin(), AuditsPlugin()] +export const withOldFrontegg = (Component: ComponentType) => () => ( + + + +); diff --git a/packages/demo-saas/tsconfig.json b/packages/demo-saas/tsconfig.json index c65f1ea13..6b5f4ac3c 100644 --- a/packages/demo-saas/tsconfig.json +++ b/packages/demo-saas/tsconfig.json @@ -35,8 +35,7 @@ "outDir": "./dist", "baseUrl": "./src", "noEmit": true, - "module": "esnext", - "noFallthroughCasesInSwitch": true + "module": "esnext" }, "include": [ "./src/**/*.tsx", diff --git a/packages/elements-material-ui/CHANGELOG.md b/packages/elements-material-ui/CHANGELOG.md new file mode 100644 index 000000000..daafe76b1 --- /dev/null +++ b/packages/elements-material-ui/CHANGELOG.md @@ -0,0 +1,546 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [2.8.8](https://github.com/frontegg/frontegg-react/compare/v2.8.7...v2.8.8) (2021-07-06) + +**Note:** Version bump only for package @frontegg/react-elements-material-ui + + + + + +## [2.8.3](https://github.com/frontegg/frontegg-react/compare/v2.8.1...v2.8.3) (2021-06-23) + + +### Bug Fixes + +* align all dependencies versions ([f1d5c48](https://github.com/frontegg/frontegg-react/commit/f1d5c48aba34827ea06a554cfb61734f1a40c93b)) +* update libraries version ([77da072](https://github.com/frontegg/frontegg-react/commit/77da072c67cb11e6cccb86b044a3a1a22b09625c)) + + + + + +## [2.8.2](https://github.com/frontegg/frontegg-react/compare/v2.8.1...v2.8.2) (2021-06-23) + + +### Bug Fixes + +* update libraries version ([77da072](https://github.com/frontegg/frontegg-react/commit/77da072c67cb11e6cccb86b044a3a1a22b09625c)) + + + + + +# [2.2.0](https://github.com/frontegg/frontegg-react/compare/v2.1.0...v2.2.0) (2021-04-28) + +**Note:** Version bump only for package @frontegg/react-elements-material-ui + + + + + +# [2.0.0](https://github.com/frontegg/frontegg-react/compare/v1.28.0...v2.0.0) (2021-04-11) + + +### Bug Fixes + +* FR-2312 - add success variant support for all elements libraries ([58b85b7](https://github.com/frontegg/frontegg-react/commit/58b85b7fe2f07a954a95ba87a17d44567efd946f)) +* **elements:** fix onclick event for material menu item ([71193e1](https://github.com/frontegg/frontegg-react/commit/71193e1bbda6c3300bd73fde612f1ba6f5b60ad8)) +* FR-2100 - fix expandable table styles ([9586201](https://github.com/frontegg/frontegg-react/commit/9586201e2f95c43648f5c72b3b353c6a3c9766e2)) + + + + + +# [1.28.0](https://github.com/frontegg/frontegg-react/compare/v1.27.0...v1.28.0) (2021-03-22) + + +### Bug Fixes + +* FR-2220 - add loader for MenuItem ([4a3e62e](https://github.com/frontegg/frontegg-react/commit/4a3e62e68f7041e0d376ffc411c57198557c20f1)) + + + + + +## [1.22.1](https://github.com/frontegg/frontegg-react/compare/v1.22.0...v1.22.1) (2021-02-16) + +**Note:** Version bump only for package @frontegg/react-elements-material-ui + + + + + +# [1.22.0](https://github.com/frontegg/frontegg-react/compare/v1.21.1...v1.22.0) (2021-02-13) + + +### Features + +* **elements:** add support ref for Input elements in UI libraries ([39c1ebc](https://github.com/frontegg/frontegg-react/commit/39c1ebc05262aa0f1ee47dbae8c23bb37d0a0a0d)) + + + + + +## [1.21.1](https://github.com/frontegg/frontegg-react/compare/v1.21.0...v1.21.1) (2021-02-04) + +**Note:** Version bump only for package @frontegg/react-elements-material-ui + + + + + +# [1.21.0](https://github.com/frontegg/frontegg-react/compare/v1.20.1...v1.21.0) (2021-02-04) + +**Note:** Version bump only for package @frontegg/react-elements-material-ui + + + + + +## [1.20.1](https://github.com/frontegg/frontegg-react/compare/v1.20.0...v1.20.1) (2021-02-02) + +**Note:** Version bump only for package @frontegg/react-elements-material-ui + + + + + +# [1.20.0](https://github.com/frontegg/frontegg-react/compare/v1.19.1...v1.20.0) (2021-02-01) + +**Note:** Version bump only for package @frontegg/react-elements-material-ui + + + + + +## [1.19.1](https://github.com/frontegg/frontegg-react/compare/v1.19.0...v1.19.1) (2021-01-20) + +**Note:** Version bump only for package @frontegg/react-elements-material-ui + + + + + +# [1.19.0](https://github.com/frontegg/frontegg-react/compare/v1.18.6...v1.19.0) (2021-01-20) + +**Note:** Version bump only for package @frontegg/react-elements-material-ui + + + + + +## [1.18.6](https://github.com/frontegg/frontegg-react/compare/v1.18.5...v1.18.6) (2021-01-19) + + +### Bug Fixes + +* Add more space to first column in Table components FR-1171 ([c0b6b38](https://github.com/frontegg/frontegg-react/commit/c0b6b38479b52b3fce66439a640be8e7b4a59809)) + + + + + +## [1.18.5](https://github.com/frontegg/frontegg-react/compare/v1.18.4...v1.18.5) (2021-01-18) + + +### Bug Fixes + +* **core:** fix perfomance for the INputChip component ([cea5e19](https://github.com/frontegg/frontegg-react/commit/cea5e19aef64a6cf42f75d3cbb77cd74fb01f560)) + + + + + +## [1.18.4](https://github.com/frontegg/frontegg-react/compare/v1.18.3...v1.18.4) (2021-01-18) + +**Note:** Version bump only for package @frontegg/react-elements-material-ui + + + + + +## [1.18.3](https://github.com/frontegg/frontegg-react/compare/v1.18.2...v1.18.3) (2021-01-17) + +**Note:** Version bump only for package @frontegg/react-elements-material-ui + + + + + +## [1.18.2](https://github.com/frontegg/frontegg-react/compare/v1.18.1...v1.18.2) (2021-01-15) + +**Note:** Version bump only for package @frontegg/react-elements-material-ui + + + + + +## [1.18.1](https://github.com/frontegg/frontegg-react/compare/v1.18.0...v1.18.1) (2021-01-15) + +**Note:** Version bump only for package @frontegg/react-elements-material-ui + + + + + +# [1.18.0](https://github.com/frontegg/frontegg-react/compare/v1.17.3...v1.18.0) (2021-01-14) + +**Note:** Version bump only for package @frontegg/react-elements-material-ui + + + + + +## [1.17.3](https://github.com/frontegg/frontegg-react/compare/v1.17.2...v1.17.3) (2021-01-13) + +**Note:** Version bump only for package @frontegg/react-elements-material-ui + + + + + +## [1.17.2](https://github.com/frontegg/frontegg-react/compare/v1.17.1...v1.17.2) (2021-01-12) + +**Note:** Version bump only for package @frontegg/react-elements-material-ui + + + + + +## [1.17.1](https://github.com/frontegg/frontegg-react/compare/v1.17.0...v1.17.1) (2021-01-05) + +**Note:** Version bump only for package @frontegg/react-elements-material-ui + + + + + +# [1.17.0](https://github.com/frontegg/frontegg-react/compare/v1.16.2...v1.17.0) (2021-01-03) + +**Note:** Version bump only for package @frontegg/react-elements-material-ui + + + + + +## [1.16.2](https://github.com/frontegg/frontegg-react/compare/v1.16.0...v1.16.2) (2020-12-27) + +**Note:** Version bump only for package @frontegg/react-elements-material-ui + + + + + +## [1.16.1](https://github.com/frontegg/frontegg-react/compare/v1.16.0...v1.16.1) (2020-12-27) + +**Note:** Version bump only for package @frontegg/react-elements-material-ui + + + + + +# [1.16.0](https://github.com/frontegg/frontegg-react/compare/v1.15.2...v1.16.0) (2020-12-24) + +**Note:** Version bump only for package @frontegg/react-elements-material-ui + + + + + +## [1.15.2](https://github.com/frontegg/frontegg-react/compare/v1.15.1...v1.15.2) (2020-12-24) + +**Note:** Version bump only for package @frontegg/react-elements-material-ui + + + + + +## [1.15.1](https://github.com/frontegg/frontegg-react/compare/v1.15.0...v1.15.1) (2020-12-21) + +**Note:** Version bump only for package @frontegg/react-elements-material-ui + + + + + +# [1.15.0](https://github.com/frontegg/frontegg-react/compare/v1.14.1...v1.15.0) (2020-12-20) + +**Note:** Version bump only for package @frontegg/react-elements-material-ui + + + + + +## [1.14.1](https://github.com/frontegg/frontegg-react/compare/v1.14.0...v1.14.1) (2020-12-17) + +**Note:** Version bump only for package @frontegg/react-elements-material-ui + + + + + +# [1.14.0](https://github.com/frontegg/frontegg-react/compare/v1.13.2...v1.14.0) (2020-12-16) + + +### Bug Fixes + +* **elements:** fix console error for the InputChip component ([0f25e0f](https://github.com/frontegg/frontegg-react/commit/0f25e0f12673301d8f3cbfb4ccbf7a532e573840)) + + + + + +## [1.13.2](https://github.com/frontegg/frontegg-react/compare/v1.13.1...v1.13.2) (2020-12-13) + + +### Bug Fixes + +* **elements:** fix the fullWidth style for the InputChip component in the material library ([2cac48f](https://github.com/frontegg/frontegg-react/commit/2cac48f2fb55a3810a1d2fe41f2bdacc25f56c01)) + + + + + +## [1.13.1](https://github.com/frontegg/frontegg-react/compare/v1.13.0...v1.13.1) (2020-12-10) + + +### Bug Fixes + +* **audits:** FR-999 add x btn for ip popup ([6efc8ea](https://github.com/frontegg/frontegg-react/commit/6efc8ea6e229b6434d75f9af59c0b6f0993ec9cd)) + + + + + +# [1.13.0](https://github.com/frontegg/frontegg-react/compare/v1.12.0...v1.13.0) (2020-12-09) + + +### Bug Fixes + +* fix testId error in material components ([0e3d2a6](https://github.com/frontegg/frontegg-react/commit/0e3d2a610f762d9065eee261dd996ecea77e1c8d)), closes [#119](https://github.com/frontegg/frontegg-react/issues/119) + + + + + +# [1.12.0](https://github.com/frontegg/frontegg-react/compare/v1.11.1...v1.12.0) (2020-12-09) + + +### Bug Fixes + +* **audits:** fix scroll to top on page change for material lib ([1878dd4](https://github.com/frontegg/frontegg-react/commit/1878dd491bc3d86b182f88469841392215d589e8)) +* **audits:** FR-1000 fix updating filter value ([00f84d4](https://github.com/frontegg/frontegg-react/commit/00f84d427db5cf1faaedb69d7025debe0513debf)) +* **audits:** FR-1002 add globe icon when location is unknown ([53265fd](https://github.com/frontegg/frontegg-react/commit/53265fd5cdc56d3fc141ad77469d452e2f6dc430)) +* **audits:** FR-1004 prevent call action onPageChange after init render ([bca3bc1](https://github.com/frontegg/frontegg-react/commit/bca3bc14818c01589a5c33ce9f8a1f2c6923a585)) +* **audits:** FR-1004 remove debounce filter, prevent onFilterChange call after init render ([b842cd6](https://github.com/frontegg/frontegg-react/commit/b842cd6186f032750fb6daed3e01e01d5b135498)) +* **elements:** fix styles for the InputChip component in the material library ([8f6404a](https://github.com/frontegg/frontegg-react/commit/8f6404aa9cb659512c17ce3ec7b03e48b6f0f2e4)) + + + + + +## [1.11.1](https://github.com/frontegg/frontegg-react/compare/v1.11.0...v1.11.1) (2020-12-02) + +**Note:** Version bump only for package @frontegg/react-elements-material-ui + + + + + +# [1.11.0](https://github.com/frontegg/frontegg-react/compare/v1.10.0...v1.11.0) (2020-11-30) + + +### Bug Fixes + +* Remove debugger line ([58643c1](https://github.com/frontegg/frontegg-react/commit/58643c19e05fea9b9fbabf507eeccb3595bc4903)) + + + + + +# [1.10.0](https://github.com/frontegg/frontegg-react/compare/v1.9.0...v1.10.0) (2020-11-27) + +**Note:** Version bump only for package @frontegg/react-elements-material-ui + + + + + +# [1.9.0](https://github.com/frontegg/frontegg-react/compare/v1.8.0...v1.9.0) (2020-11-25) + + +### Bug Fixes + +* fix console errors ([47a0679](https://github.com/frontegg/frontegg-react/commit/47a0679cb426eeb09bd5d97e0b28fe697c24e3b2)), closes [#119](https://github.com/frontegg/frontegg-react/issues/119) + + +### Features + +* **auth:** New AccountDropdown component added to AuthPlugin ([018f2f8](https://github.com/frontegg/frontegg-react/commit/018f2f8db3ad22981f9270bd2e166b56cf6eb7ff)) +* **elements:** add dupport the ClassName property to the Table component ([610ba5a](https://github.com/frontegg/frontegg-react/commit/610ba5a6cd8e432da5ad15c631621b27eb329563)) +* New plugin for Audits ([#106](https://github.com/frontegg/frontegg-react/issues/106)) ([921b37a](https://github.com/frontegg/frontegg-react/commit/921b37aca98f23c800c8b5d094ed595d52617679)) + + + + + +# [1.8.0](https://github.com/frontegg/frontegg-react/compare/v1.7.0...v1.8.0) (2020-11-23) + + +### Features + +* Add Connectivity Plugin ([#98](https://github.com/frontegg/frontegg-react/issues/98)) ([db77431](https://github.com/frontegg/frontegg-react/commit/db77431b6d744b93431430543019fedd1c0dbae2)) + + + + + +# [1.7.0](https://github.com/frontegg/frontegg-react/compare/v1.6.1...v1.7.0) (2020-11-22) + +**Note:** Version bump only for package @frontegg/react-elements-material-ui + + + + + +## [1.6.2](https://github.com/frontegg/frontegg-react/compare/v1.6.1...v1.6.2) (2020-11-19) + +**Note:** Version bump only for package @frontegg/react-elements-material-ui + + + + + +## [1.6.1](https://github.com/frontegg/frontegg-react/compare/v1.6.0...v1.6.1) (2020-11-15) + +**Note:** Version bump only for package @frontegg/react-elements-material-ui + + + + + +# [1.6.0](https://github.com/frontegg/frontegg-react/compare/v1.5.0...v1.6.0) (2020-11-12) + + +### Bug Fixes + +* fix pagination bug in TeamTable ([8ba1c3d](https://github.com/frontegg/frontegg-react/commit/8ba1c3d861257231b1890766c5042cba58998965)) + + + + + +# [1.5.0](https://github.com/frontegg/frontegg-react/compare/v1.4.0...v1.5.0) (2020-11-10) + +**Note:** Version bump only for package @frontegg/react-elements-material-ui + + + + + +# [1.4.0](https://github.com/frontegg/frontegg-react/compare/v1.3.0...v1.4.0) (2020-11-09) + + +### Bug Fixes + +* add option to add user without roles ([4d17333](https://github.com/frontegg/frontegg-react/commit/4d17333fc0f157d3c5d4462f20d8f2269b579a65)) + + +### Features + +* notifications plugin ([#78](https://github.com/frontegg/frontegg-react/issues/78)) ([0439d17](https://github.com/frontegg/frontegg-react/commit/0439d179ed5c0abae510b7d132dbf03ae907f7f6)) + + + + + +# [1.3.0](https://github.com/frontegg/frontegg-react/compare/v1.2.0...v1.3.0) (2020-11-04) + + +### Bug Fixes + +* fix material button console errors ([5558c52](https://github.com/frontegg/frontegg-react/commit/5558c52c61276847109d137b698fd857ffbfcf2e)) +* fix material table head position sticky ([99a9423](https://github.com/frontegg/frontegg-react/commit/99a9423e43596d932f1e1f234e2dc569c5e166eb)) +* **auth:** bug fixes ([#76](https://github.com/frontegg/frontegg-react/issues/76)) ([a758642](https://github.com/frontegg/frontegg-react/commit/a758642458751e930dc4ea6de6acc50b3ede0b77)) +* **auth:** fix ui bugs ([#79](https://github.com/frontegg/frontegg-react/issues/79)) ([0e75d8d](https://github.com/frontegg/frontegg-react/commit/0e75d8dff80937dc2e4308a0ffdd527a12a84a39)) + + +### Features + +* **elements:** add FeInput element ([f23e439](https://github.com/frontegg/frontegg-react/commit/f23e4392ab849d32c5e0ba67e055f793ecfd5ceb)) +* add support for nextjs and angular ([#82](https://github.com/frontegg/frontegg-react/issues/82)) ([5fa36eb](https://github.com/frontegg/frontegg-react/commit/5fa36ebe7bfa6866c78455a746727ba8b1cafbbc)) + + + + + +# [1.2.0](https://github.com/frontegg/frontegg-react/compare/v1.1.0...v1.2.0) (2020-10-25) + + +### Bug Fixes + +* **elements:** UI elements small fixes ([effb9bd](https://github.com/frontegg/frontegg-react/commit/effb9bd54186133184010c34874af212c040ef90)) + + +### Features + +* **auth:** add SSO components ([#64](https://github.com/frontegg/frontegg-react/issues/64)) ([f083762](https://github.com/frontegg/frontegg-react/commit/f0837623073c1f9a636480c6a88d5969fb020a09)) +* **auth:** support all auth functionalities ([#70](https://github.com/frontegg/frontegg-react/issues/70)) ([26d725e](https://github.com/frontegg/frontegg-react/commit/26d725e2f386c6b4a703e4371fac8efef29676d1)) +* **core:** add dynamic elements for Frontegg Components ([#10](https://github.com/frontegg/frontegg-react/issues/10)) ([2bddbb2](https://github.com/frontegg/frontegg-react/commit/2bddbb2794ac0dc4af90c8df0f33b69a312e063a)) +* **core:** add FeSwitchToggle component ([#66](https://github.com/frontegg/frontegg-react/issues/66)) ([5b6c603](https://github.com/frontegg/frontegg-react/commit/5b6c603d912be45b1982c06b7f026badd8e82f1c)) +* **core:** Add FeTabs component ([3699299](https://github.com/frontegg/frontegg-react/commit/36992997e64907345a254a4b44580759705186bb)), closes [#67](https://github.com/frontegg/frontegg-react/issues/67) + + + + + +# [1.1.0](https://github.com/frontegg/frontegg-react/compare/v1.0.89...v1.1.0) (2020-10-14) + +**Note:** Version bump only for package @frontegg/react-elements-material-ui + + + + + +## [1.0.89](https://github.com/frontegg/frontegg-react/compare/v1.0.88...v1.0.89) (2020-10-13) + +**Note:** Version bump only for package @frontegg/react-elements-material-ui + + + + + +## [1.0.88](https://github.com/frontegg/frontegg-react/compare/v1.0.87...v1.0.88) (2020-10-11) + +**Note:** Version bump only for package @frontegg/react-elements-material-ui + + + + + +## [1.0.87](https://github.com/frontegg/frontegg-react/compare/v1.0.86...v1.0.87) (2020-10-09) + + +### Bug Fixes + +* **packaging:** move cjs.js to index.js for main entry point ([0f8f700](https://github.com/frontegg/frontegg-react/commit/0f8f70016566a8f1940d16441a5afa1707dc02a2)) + + + + + +## 1.0.85 (2020-10-08) + +**Note:** Version bump only for package @frontegg/react-elements-material-ui + + + + + +## 1.0.84 (2020-10-08) + +**Note:** Version bump only for package @frontegg/react-elements-material-ui diff --git a/packages/elements-material-ui/package.json b/packages/elements-material-ui/package.json new file mode 100644 index 000000000..a1f4ea640 --- /dev/null +++ b/packages/elements-material-ui/package.json @@ -0,0 +1,73 @@ +{ + "name": "@frontegg/react-elements-material-ui", + "libName": "FronteggElementsMaterialUi", + "version": "4.0.23", + "author": "Frontegg LTD", + "main": "dist/index.js", + "module": "dist/index.esm.js", + "es2015": "dist/index.es.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "rollup -c ../../scripts/rollup.config.js && echo DONE", + "build:watch": "rollup -w -c ../../scripts/rollup.config.js", + "test": "jest --runInBand --passWithNoTests -c ../../scripts/jest.config.json --rootDir . && echo DONE" + }, + "dependencies": { + "classnames": "^2.2.6", + "react-popper-tooltip": "^3.1.0", + "react-waypoint": "^10.1.0", + "underscore": "^1.10.2" + }, + "devDependencies": { + "@material-ui/core": "^4.11.0", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "^4.0.0-alpha.56", + "@types/classnames": "^2.2.10", + "@types/react": "^16.9.19", + "@types/underscore": "^1.10.2" + }, + "jest": { + "preset": "ts-jest", + "testEnvironment": "jsdom", + "testPathIgnorePatterns": [ + "dist/" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "prettier": { + "printWidth": 120, + "semi": true, + "singleQuote": true, + "jsxSingleQuote": true, + "trailingComma": "all", + "jsxBracketSameLine": true, + "endOfLine": "lf", + "tabWidth": 2 + }, + "standard": { + "ignore": [ + "node_modules/", + "dist/" + ], + "globals": [ + "describe", + "it", + "test", + "expect", + "afterAll", + "jest" + ] + }, + "gitHead": "00c0bd425a211916eec1170b8359cf302727b338" +} diff --git a/packages/elements-material-ui/src/Accordion/Accordion.tsx b/packages/elements-material-ui/src/Accordion/Accordion.tsx new file mode 100644 index 000000000..bf00f580d --- /dev/null +++ b/packages/elements-material-ui/src/Accordion/Accordion.tsx @@ -0,0 +1,15 @@ +import React, { forwardRef } from 'react'; +import { AccordionProps } from '@frontegg/react-core'; +import { Accordion as MaterialAccordion, AccordionProps as MaterialAccordionProps } from '@material-ui/core'; + +const mapper = (props: AccordionProps): MaterialAccordionProps => { + const { onChange, ...rest } = props; + return { + onChange: onChange ? (_, expended) => onChange(expended) : undefined, + ...rest, + }; +}; + +export const Accordion = forwardRef((props, ref) => { + return ; +}); diff --git a/packages/elements-material-ui/src/Accordion/AccordionContent.tsx b/packages/elements-material-ui/src/Accordion/AccordionContent.tsx new file mode 100644 index 000000000..dca2a1e14 --- /dev/null +++ b/packages/elements-material-ui/src/Accordion/AccordionContent.tsx @@ -0,0 +1,7 @@ +import React, { forwardRef } from 'react'; +import { AccordionContentProps } from '@frontegg/react-core'; +import { AccordionDetails as MaterialAccordionContent } from '@material-ui/core'; + +export const AccordionContent = forwardRef((props, ref) => { + return ; +}); diff --git a/packages/elements-material-ui/src/Accordion/AccordionHeader.tsx b/packages/elements-material-ui/src/Accordion/AccordionHeader.tsx new file mode 100644 index 000000000..5a4ff1023 --- /dev/null +++ b/packages/elements-material-ui/src/Accordion/AccordionHeader.tsx @@ -0,0 +1,7 @@ +import React, { forwardRef } from 'react'; +import { AccordionHeaderProps } from '@frontegg/react-core'; +import { AccordionSummary as MaterialAccordionHeader } from '@material-ui/core'; + +export const AccordionHeader = forwardRef((props, ref) => { + return ; +}); diff --git a/packages/elements-material-ui/src/Accordion/index.ts b/packages/elements-material-ui/src/Accordion/index.ts new file mode 100644 index 000000000..f07823a19 --- /dev/null +++ b/packages/elements-material-ui/src/Accordion/index.ts @@ -0,0 +1,3 @@ +export * from './Accordion'; +export * from './AccordionHeader'; +export * from './AccordionContent'; diff --git a/packages/elements-material-ui/src/Button/index.tsx b/packages/elements-material-ui/src/Button/index.tsx new file mode 100644 index 000000000..7b84d1fb3 --- /dev/null +++ b/packages/elements-material-ui/src/Button/index.tsx @@ -0,0 +1,103 @@ +import React, { FC } from 'react'; +import { ButtonProps } from '@frontegg/react-core'; +import { + Button as MaterialButton, + ButtonProps as MaterialButtonProps, + makeStyles, + IconButton as MaterialIconButton, +} from '@material-ui/core'; +import classNames from 'classnames'; +import { Loader } from '../Loader'; + +const useStyles = makeStyles({ + dangerStyle: { + color: 'var(--color-white)', + backgroundColor: 'var(--color-red-7)', + '&:hover': { + backgroundColor: 'var(--color-red-8)', + }, + }, + successStyle: { + color: 'var(--color-success)', + backgroundColor: 'var(--color-success-25)', + '&:hover': { + backgroundColor: 'var(--color-success-50)', + }, + }, + asLink: { + backgroundColor: 'transparent', + boxShadow: 'none', + textTransform: 'none', + textDecoration: 'underline', + padding: 0, + + '&:hover': { + backgroundColor: 'transparent', + boxShadow: 'none', + textTransform: 'none', + textDecoration: 'underline', + filter: 'brightness(0.8)', + }, + }, +}); + +const mapper = (props: ButtonProps): MaterialButtonProps => { + const { + className, + inForm, + variant, + fullWidth, + loading, + disabled, + type, + onClick, + isCancel, + size, + asLink, + iconButton, + transparent, + testId, + ...restProps + } = props; + const variantColor = variant === 'danger' || variant === 'disabled' || variant === 'success' ? 'default' : variant; + + const classes = useStyles(); + const calculatedVariant = isCancel || transparent ? 'text' : 'contained'; + + return { + ...restProps, + fullWidth, + onClick, + type, + size, + disabled: loading || disabled, + variant: calculatedVariant, + color: variantColor, + classes: { + root: classNames(className, { + [classes.dangerStyle]: variant === 'danger', + [classes.successStyle]: variant === 'success', + [classes.asLink]: asLink, + }), + }, + }; +}; + +export const Button: FC = (props) => { + const { children, loading } = props; + const buttonProps = mapper(props); + const { fullWidth, size, ...iconButtonProps } = buttonProps; + if (props.iconButton) { + return ( + + {children} + + ); + } + return ( + + {children} + {loading && } + + ); +}; diff --git a/packages/elements-material-ui/src/Checkbox/index.tsx b/packages/elements-material-ui/src/Checkbox/index.tsx new file mode 100644 index 000000000..8401cdbc5 --- /dev/null +++ b/packages/elements-material-ui/src/Checkbox/index.tsx @@ -0,0 +1,46 @@ +import React, { forwardRef } from 'react'; +import { CheckboxProps } from '@frontegg/react-core'; +import './style.scss'; +import { + Checkbox as MaterialCheckbox, + CheckboxProps as MaterialCheckboxProps, + FormControlLabel, +} from '@material-ui/core'; +import classNames from 'classnames'; + +const mapper = ({ + inForm, + indeterminate, + fullWidth, + type, + className, + size, + label, + checked, + defaultChecked, + ...rest +}: CheckboxProps): MaterialCheckboxProps => ({ + className: classNames('fe-material-checkbox', className, { + 'fe-material-checkbox__disabled': rest.disabled, + }), + size: size === 'large' ? undefined : size, + color: 'primary', + title: label, + indeterminate, + checked: indeterminate ? true : checked, + defaultChecked, + inputProps: { + ...rest, + }, +}); + +export const Checkbox = forwardRef((props, ref) => { + const components = ( + } label={props.label} /> + ); + + if (props.fullWidth) { + return
{components}
; + } + return components; +}); diff --git a/packages/elements-material-ui/src/Checkbox/style.scss b/packages/elements-material-ui/src/Checkbox/style.scss new file mode 100644 index 000000000..381c9ec5e --- /dev/null +++ b/packages/elements-material-ui/src/Checkbox/style.scss @@ -0,0 +1,3 @@ +.fe-material-checkbox__disabled { + opacity: 0.8; +} diff --git a/packages/elements-material-ui/src/Dialog/index.tsx b/packages/elements-material-ui/src/Dialog/index.tsx new file mode 100644 index 000000000..61e3e46a2 --- /dev/null +++ b/packages/elements-material-ui/src/Dialog/index.tsx @@ -0,0 +1,36 @@ +import React, { FC } from 'react'; +import { DialogProps } from '@frontegg/react-core'; +import { + Dialog as MaterialDialog, + DialogProps as MaterialDialogProps, + DialogTitle, + DialogContent, +} from '@material-ui/core'; +import './style.scss'; +import classNames from 'classnames'; + +const mapSize: any = { + mini: 'xs', + tiny: 'xs', + small: 'sm', + large: 'lg', + fullscreen: 'xl', +}; +const dialogPropsMapper = (props: DialogProps): MaterialDialogProps => ({ + open: props.open ?? false, + onClose: props.onClose, + className: classNames('fe-material-dialog', props.className), + maxWidth: props.size ? mapSize[props.size] : 'md', + fullScreen: props.size === 'fullscreen', + fullWidth: true, +}); + +export const Dialog: FC = (props) => { + const modalProps = dialogPropsMapper(props); + return ( + + {props.header && {props.header}} + {props.children} + + ); +}; diff --git a/packages/elements-material-ui/src/Dialog/style.scss b/packages/elements-material-ui/src/Dialog/style.scss new file mode 100644 index 000000000..466cc488b --- /dev/null +++ b/packages/elements-material-ui/src/Dialog/style.scss @@ -0,0 +1,11 @@ +.fe-material-dialog { + .fe-dialog__footer { + display: flex; + flex-direction: row; + justify-content: flex-end; + width: calc(100% + 4rem); + margin: 2rem -2rem 0; + padding: 1.5rem 2rem; + background: transparent; + } +} diff --git a/packages/elements-material-ui/src/Grid/index.tsx b/packages/elements-material-ui/src/Grid/index.tsx new file mode 100644 index 000000000..676ebf40d --- /dev/null +++ b/packages/elements-material-ui/src/Grid/index.tsx @@ -0,0 +1,12 @@ +import React, { forwardRef } from 'react'; +import { ButtonProps, GridProps } from '@frontegg/react-core'; +import { Grid as MaterialGrid, GridProps as MaterialGridProps } from '@material-ui/core'; + +const mapperMaterialProps = ({ justifyContent, ...restProps }: GridProps): MaterialGridProps => ({ + ...restProps, + justify: justifyContent, +}); + +export const Grid = forwardRef((props, ref) => { + return ; +}); diff --git a/packages/elements-material-ui/src/Icon/index.tsx b/packages/elements-material-ui/src/Icon/index.tsx new file mode 100644 index 000000000..c88868d19 --- /dev/null +++ b/packages/elements-material-ui/src/Icon/index.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import { IconProps, IconNames } from '@frontegg/react-core'; +import classNames from 'classnames'; +import { + Edit, + Search, + Visibility, + CheckRounded, + ImageRounded, + DeleteRounded, + VisibilityOff, + WarningRounded, + FileCopyRounded, + MoreVertRounded, + SendRounded, + Cached, + CalendarToday, + FlashOn, + PictureAsPdf, + GridOn, + ArrowBackRounded, + PersonAddRounded, + KeyboardArrowUpRounded, + KeyboardArrowDownRounded, + KeyboardArrowLeftRounded, + KeyboardArrowRightRounded, + IndeterminateCheckBoxRounded, + ArrowUpward, + ArrowDownward, + FilterList, + Subject, + ExitToAppRounded, + CachedRounded, + FaceRounded, + Language, + Close, +} from '@material-ui/icons'; +import './style.scss'; + +const iconMap: { [K in IconNames]: any } = { + 'down-arrow': KeyboardArrowDownRounded, + 'left-arrow': KeyboardArrowLeftRounded, + 'person-add': PersonAddRounded, + 'right-arrow': KeyboardArrowRightRounded, + 'sort-arrows-asc': ArrowUpward, + 'sort-arrows-desc': ArrowDownward, + 'sort-arrows': DeleteRounded, + 'up-arrow': KeyboardArrowUpRounded, + 'vertical-dots': MoreVertRounded, + 'visibility-off': VisibilityOff, + back: ArrowBackRounded, + checkmark: CheckRounded, + copy: FileCopyRounded, + delete: DeleteRounded, + edit: Edit, + filters: FilterList, + image: ImageRounded, + indeterminate: IndeterminateCheckBoxRounded, + search: Search, + send: SendRounded, + refresh: Cached, + 'calendar-today': CalendarToday, + flash: FlashOn, + pdf: PictureAsPdf, + csv: GridOn, + visibility: Visibility, + warning: WarningRounded, + list: Subject, + exit: ExitToAppRounded, + swap: CachedRounded, + profile: FaceRounded, + globe: Language, + close: Close, +}; + +export class Icon extends React.Component { + render() { + const IconComponent = iconMap[this.props.name]; + if (IconComponent) { + return ; + } + } +} diff --git a/packages/elements-material-ui/src/Icon/style.scss b/packages/elements-material-ui/src/Icon/style.scss new file mode 100644 index 000000000..0352152d9 --- /dev/null +++ b/packages/elements-material-ui/src/Icon/style.scss @@ -0,0 +1,3 @@ +.fe-icon { + max-width: 100%; +} diff --git a/packages/elements-material-ui/src/Input/index.tsx b/packages/elements-material-ui/src/Input/index.tsx new file mode 100644 index 000000000..83848545e --- /dev/null +++ b/packages/elements-material-ui/src/Input/index.tsx @@ -0,0 +1,149 @@ +import React, { FC, forwardRef, useCallback, useState } from 'react'; +import { InputProps } from '@frontegg/react-core'; +import { + InputProps as MaterialInputProps, + TextField as MaterialTextField, + TextFieldProps as MaterialTextFieldProps, + IconButton, + InputAdornment, + InputAdornmentProps, + makeStyles, +} from '@material-ui/core'; +import { Search, Visibility, VisibilityOff } from '@material-ui/icons'; +import { Button } from '../Button'; +import classNames from 'classnames'; + +const useStyles = makeStyles({ + inForm: { + margin: '0.5rem 0 1rem', + }, +}); + +const useFooterStyles = makeStyles({ + footer: { + display: 'flex', + }, + labelButton: { + marginLeft: 'auto', + }, +}); + +const appendAdornment = ( + props: Partial | undefined, + at: keyof MaterialInputProps, + iconAction: InputProps['iconAction'], + icon: InputProps['prefixIcon'] | InputProps['suffixIcon'] +) => { + const position: InputAdornmentProps['position'] = at === 'startAdornment' ? 'start' : 'end'; + + return { + ...props, + InputProps: { + [at]: ( + + {iconAction ? {icon} : icon} + + ), + }, + }; +}; + +const useInputTypeIcon = ({ + type, + onSearch, + value, +}: { + type: InputProps['type']; + onSearch: InputProps['onSearch']; + value: InputProps['value']; +}): [boolean, JSX.Element] => { + const [showPassword, setShowPassword] = useState(false); + const toggleShowPassword = useCallback(() => setShowPassword((_) => !_), []); + + const Icon = type === 'password' ? (showPassword ? Visibility : VisibilityOff) : Search; + const onClick = type === 'password' ? toggleShowPassword : () => onSearch?.(value); + + return [ + showPassword, + Icon && ( + + + + ), + ]; +}; + +const materialTextFieldMapper = (props: InputProps): MaterialTextFieldProps => { + const { + className, + inForm, + fullWidth, + error, + iconAction, + multiline, + variant, + labelButton, + onSearch, + size, + type, + ...restPropsWithIcons + } = props; + + let { prefixIcon, suffixIcon, ...pureProps } = restPropsWithIcons; + + const [showPassword, InputTypeIcon] = useInputTypeIcon({ type, onSearch, value: props.value }); + + const styles = useStyles(); + const footerStyles = useFooterStyles(); + + let mappedProps = { + ...pureProps, + className: classNames(className, { [styles.inForm]: inForm }), + fullWidth, + rows: 4, + type: type === 'password' && showPassword ? 'text' : type, + multiline, + size: size ? (size === 'large' ? 'medium' : size) : 'small', + variant: 'outlined', + color: variant === 'primary' || variant === 'secondary' ? variant : undefined, + disabled: props.disabled || variant === 'disabled', + error: !!error, + FormHelperTextProps: { + component: 'div', + }, + helperText: ( +
+ {error} + {labelButton && ( +
+ ), + } as MaterialTextFieldProps; + + if (type === 'password' || type === 'search') { + suffixIcon = ( + <> + {InputTypeIcon} + {suffixIcon} + + ); + } + + if (prefixIcon) { + mappedProps = appendAdornment(mappedProps, 'startAdornment', iconAction, prefixIcon); + } else if (suffixIcon) { + mappedProps = appendAdornment(mappedProps, 'endAdornment', iconAction, suffixIcon); + } + + return mappedProps; +}; + +export const Input = forwardRef((props, forwardRef) => { + return ; +}); diff --git a/packages/elements-material-ui/src/InputChip/index.tsx b/packages/elements-material-ui/src/InputChip/index.tsx new file mode 100644 index 000000000..262bec9a1 --- /dev/null +++ b/packages/elements-material-ui/src/InputChip/index.tsx @@ -0,0 +1,33 @@ +import { IInputChip, useCombinedRefs } from '@frontegg/react-core'; +import classNames from 'classnames'; +import { Chip, Grid } from '@material-ui/core'; +import React, { FC, forwardRef, useRef } from 'react'; + +import './styles.scss'; + +export const InputChip = forwardRef( + ({ chips, label, onDelete, error, fullWidth, className, ...inputProps }, ref) => { + const inputRef = useRef(null); + const refCallback = useCombinedRefs([ref, inputRef]); + + return ( +
inputRef.current && inputRef.current.focus()} + > + {!!label &&
{label}
} + + {chips.map((chip, idx) => ( + + onDelete(idx)} /> + + ))} + + + + + {error &&
{error}
} +
+ ); + } +); diff --git a/packages/elements-material-ui/src/InputChip/styles.scss b/packages/elements-material-ui/src/InputChip/styles.scss new file mode 100644 index 000000000..be1c2e03d --- /dev/null +++ b/packages/elements-material-ui/src/InputChip/styles.scss @@ -0,0 +1,12 @@ +.MuiChipInput { + input { + border: none; + background-color: transparent; + &:focus { + outline: none; + } + } + &-fullWidth { + width: 100%; + } +} diff --git a/packages/elements-material-ui/src/Loader/index.tsx b/packages/elements-material-ui/src/Loader/index.tsx new file mode 100644 index 000000000..22e58947d --- /dev/null +++ b/packages/elements-material-ui/src/Loader/index.tsx @@ -0,0 +1,22 @@ +import React, { FC } from 'react'; +import { LoaderProps } from '@frontegg/react-core'; +import { CircularProgress, CircularProgressProps as MaterialLoaderProps } from '@material-ui/core'; +import classNames from 'classnames'; +import './style.scss'; + +const mapper = (props: LoaderProps): MaterialLoaderProps => { + const { className, center, variant, color, ...rest } = props; + const variantColor = + variant === 'danger' || variant === 'disabled' || variant === 'default' || variant === 'success' + ? 'inherit' + : variant; + return { + className: classNames(className, { 'fe-material-loader__centered': center }), + color: variantColor, + ...rest, + }; +}; + +export const Loader: FC = (props) => { + return ; +}; diff --git a/packages/elements-material-ui/src/Loader/style.scss b/packages/elements-material-ui/src/Loader/style.scss new file mode 100644 index 000000000..27afe77b6 --- /dev/null +++ b/packages/elements-material-ui/src/Loader/style.scss @@ -0,0 +1,8 @@ +.fe-material-loader__centered { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + margin: auto; +} diff --git a/packages/elements-material-ui/src/Menu/index.tsx b/packages/elements-material-ui/src/Menu/index.tsx new file mode 100644 index 000000000..8a67825f3 --- /dev/null +++ b/packages/elements-material-ui/src/Menu/index.tsx @@ -0,0 +1,45 @@ +import React, { FC, useCallback, useMemo, useState } from 'react'; +import { MenuItemProps, MenuProps } from '@frontegg/react-core'; +import { Menu as MaterialMenu } from '@material-ui/core'; +import { MenuItem } from '../MenuItem'; + +export const Menu: FC = (props) => { + const { items } = props; + const withIcons = useMemo(() => items.reduce((p: boolean, n: MenuItemProps) => p && !!n.icon, true), [items]); + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + + const handleMenuOpen = useCallback((e) => { + setAnchorEl(e.target); + }, []); + + const handleMenuClose = useCallback(() => { + setAnchorEl(null); + }, []); + + const menuRenderer = useCallback( + (props: MenuItemProps, index: number) => { + return ( + { + props.onClick?.(e, props); + handleMenuClose(); + }} + /> + ); + }, + [withIcons] + ); + + return ( + <> + {React.cloneElement(props.trigger, { onClick: handleMenuOpen })} + + {items.map(menuRenderer)} + + + ); +}; diff --git a/packages/elements-material-ui/src/MenuItem/index.tsx b/packages/elements-material-ui/src/MenuItem/index.tsx new file mode 100644 index 000000000..bfc92b19c --- /dev/null +++ b/packages/elements-material-ui/src/MenuItem/index.tsx @@ -0,0 +1,36 @@ +import React, { cloneElement, FC, useCallback } from 'react'; +import { Loader, MenuItemProps } from '@frontegg/react-core'; +import { ListItemIcon, ListItemText, MenuItem as MaterialMenuItem } from '@material-ui/core'; + +export const MenuItem: FC = (props) => { + const { withIcons, loading, icon, iconClassName } = props; + + const renderIcon = useCallback(() => { + if (loading) return ; + if (icon) return cloneElement(icon, { className: iconClassName }); + return null; + }, [loading, icon, iconClassName]); + + if (withIcons) { + return ( + props.onClick?.(e, props)} + > + {renderIcon()} + {props.text} + + ); + } else { + return ( + props.onClick?.(e, props)} + > + {props.text} + + ); + } +}; diff --git a/packages/elements-material-ui/src/Pagination/index.tsx b/packages/elements-material-ui/src/Pagination/index.tsx new file mode 100644 index 000000000..b0af7a96a --- /dev/null +++ b/packages/elements-material-ui/src/Pagination/index.tsx @@ -0,0 +1,30 @@ +import React, { FC } from 'react'; +import { makeStyles } from '@material-ui/core'; +import { PaginationProps as MaterialPaginationProps, Pagination as MaterialPagination } from '@material-ui/lab'; +import { PaginationProps } from '@frontegg/react-core'; + +const useStyles = makeStyles((theme) => ({ + root: { + '& > * + *': { + marginTop: theme.spacing(2), + }, + }, +})); + +const mapper = ({ onChange, ...rest }: PaginationProps): MaterialPaginationProps => { + return { + ...rest, + }; +}; + +export const Pagination: FC = (props) => { + const paginationProps = mapper(props); + const { onChange } = props; + const classes = useStyles(); + + return ( +
+ onChange?.(e, p)} {...paginationProps} /> +
+ ); +}; diff --git a/packages/elements-material-ui/src/Popup/PopupClick.tsx b/packages/elements-material-ui/src/Popup/PopupClick.tsx new file mode 100644 index 000000000..dc401215b --- /dev/null +++ b/packages/elements-material-ui/src/Popup/PopupClick.tsx @@ -0,0 +1,55 @@ +import React, { useCallback, forwardRef, useState, useEffect, useImperativeHandle, useRef } from 'react'; +import { Popover, Box } from '@material-ui/core'; +import classnames from 'classnames'; +import { useStyles } from './styles'; +import { IPopoverProps } from './types'; + +export const PopupClick = forwardRef((props, ref) => { + const { trigger, content, anchorOrigin, transformOrigin, mountNode } = props; + const classes = useStyles(); + const [open, setOpen] = useState(false); + const [anchorEl, setAnchorEl] = useState(null); + + useImperativeHandle(ref, () => ({ + closePopup: () => handleClose(), + })); + + const handleClick = useCallback((event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + setOpen(true); + }, []); + + const handleClose = useCallback(() => { + setAnchorEl(null); + setOpen(false); + props.onClose?.(); + }, []); + + useEffect(() => { + if (open) { + props.onOpen?.(); + } + }, [open]); + + useEffect(() => { + if (props.open != null) { + setOpen(props.open); + } + }, [props.open]); + + return ( + <> + {React.cloneElement(trigger, { onClick: handleClick, ref: ref })} + + {content} + + + ); +}); diff --git a/packages/elements-material-ui/src/Popup/PopupFocus.tsx b/packages/elements-material-ui/src/Popup/PopupFocus.tsx new file mode 100644 index 000000000..aa51ceb64 --- /dev/null +++ b/packages/elements-material-ui/src/Popup/PopupFocus.tsx @@ -0,0 +1,53 @@ +import React, { forwardRef, useCallback, useEffect, useState } from 'react'; +import { Popover, Box } from '@material-ui/core'; +import classnames from 'classnames'; +import { IPopoverProps } from './types'; +import { useStyles } from './styles'; + +export const PopupFocus = forwardRef((props, ref) => { + const { trigger, content, anchorOrigin, transformOrigin, mountNode } = props; + const classes = useStyles(); + const [open, setOpen] = useState(false); + const [anchorEl, setAnchorEl] = useState(null); + const [focused, setFocused] = useState(false); + + const handleFocus = useCallback( + (event: React.FocusEvent) => { + if (focused) { + setFocused(false); + } else { + setAnchorEl(event.currentTarget); + setOpen(true); + setFocused(true); + } + }, + [focused] + ); + + const handleClose = useCallback(() => { + setAnchorEl(null); + setOpen(false); + props.onClose?.(); + }, []); + + useEffect(() => { + if (open) { + props.onOpen?.(); + } + }, [open]); + return ( + <> + {React.cloneElement(trigger, { onFocus: handleFocus, ref })} + + {content} + + + ); +}); diff --git a/packages/elements-material-ui/src/Popup/PopupHover.tsx b/packages/elements-material-ui/src/Popup/PopupHover.tsx new file mode 100644 index 000000000..20bae56c6 --- /dev/null +++ b/packages/elements-material-ui/src/Popup/PopupHover.tsx @@ -0,0 +1,63 @@ +import React, { forwardRef, MouseEvent, useCallback, useEffect, useRef, useState } from 'react'; +import { Popover, Box } from '@material-ui/core'; +import classnames from 'classnames'; +import { IPopoverProps } from './types'; +import { useStyles } from './styles'; + +const debounceDelay = 100; +export const PopupHover = forwardRef((props, ref) => { + const { trigger, content, anchorOrigin, transformOrigin, mountNode } = props; + + const debounceRef = useRef(0); + const classes = useStyles(); + const [anchorEl, setAnchorEl] = useState(null); + + const open = Boolean(anchorEl); + + const onMouseEnter = useCallback((e: MouseEvent) => { + clearTimeout(debounceRef.current); + setAnchorEl(e.currentTarget); + }, []); + const onMouseLeave = useCallback((e: MouseEvent) => { + clearTimeout(debounceRef.current); + debounceRef.current = setTimeout(() => { + setAnchorEl(null); + }, debounceDelay); + }, []); + + const onMouseEnterPopup = useCallback((e: MouseEvent) => { + clearTimeout(debounceRef.current); + }, []); + + useEffect(() => { + if (open) { + props.onOpen?.(); + } else { + props.onClose?.(); + } + }, [open]); + + return ( + <> + {React.cloneElement(trigger, { ref, onMouseEnter, onMouseLeave })} + setAnchorEl(null)} + disableRestoreFocus + > + + {content} + + + + ); +}); diff --git a/packages/elements-material-ui/src/Popup/index.tsx b/packages/elements-material-ui/src/Popup/index.tsx new file mode 100644 index 000000000..3e37dc12d --- /dev/null +++ b/packages/elements-material-ui/src/Popup/index.tsx @@ -0,0 +1,62 @@ +import React, { forwardRef, useMemo } from 'react'; +import { PopupProps } from '@frontegg/react-core'; +import { PopoverProps } from '@material-ui/core'; +import { PopupClick } from './PopupClick'; +import { PopupHover } from './PopupHover'; +import { PopupFocus } from './PopupFocus'; + +const invertedVerticalPositions: { [key in string]: 'top' | 'bottom' | 'center' } = { + top: 'bottom', + bottom: 'top', + center: 'center', +}; +const invertedHorizontalPositions: { [key in string]: 'left' | 'right' | 'center' } = { + left: 'right', + right: 'left', + center: 'center', +}; + +const mapper = (props: PopupProps): Omit => { + const { position: { vertical, horizontal } = { vertical: 'bottom', horizontal: 'center' } } = props; + return { + anchorOrigin: { + vertical, + horizontal, + }, + + transformOrigin: { + vertical: invertedVerticalPositions[vertical], + horizontal: invertedHorizontalPositions[horizontal], + }, + }; +}; + +export const Popup = forwardRef((props, ref) => { + const { action, content, trigger } = props; + const popupProps: any = mapper(props); + + const Component = useMemo(() => { + switch (action) { + case 'click': + return PopupClick; + case 'hover': + return PopupHover; + case 'focus': + return PopupFocus; + default: + return PopupClick; + } + }, [action]); + + return ( + + ); +}); diff --git a/packages/elements-material-ui/src/Popup/styles.ts b/packages/elements-material-ui/src/Popup/styles.ts new file mode 100644 index 000000000..93b79764d --- /dev/null +++ b/packages/elements-material-ui/src/Popup/styles.ts @@ -0,0 +1,13 @@ +import { makeStyles, createStyles, Theme } from '@material-ui/core'; + +export const useStyles = makeStyles((theme: Theme) => + createStyles({ + popover: { + pointerEvents: 'none', + }, + box: { + pointerEvents: 'all', + padding: theme.spacing(2), + }, + }) +); diff --git a/packages/elements-material-ui/src/Popup/types.ts b/packages/elements-material-ui/src/Popup/types.ts new file mode 100644 index 000000000..b141878cf --- /dev/null +++ b/packages/elements-material-ui/src/Popup/types.ts @@ -0,0 +1,6 @@ +import { PopoverProps } from '@material-ui/core'; +import { PopupProps } from '@frontegg/react-core'; + +export type IPopoverProps = Omit & + Pick & + Pick; diff --git a/packages/elements-material-ui/src/Select/index.tsx b/packages/elements-material-ui/src/Select/index.tsx new file mode 100644 index 000000000..172bd3766 --- /dev/null +++ b/packages/elements-material-ui/src/Select/index.tsx @@ -0,0 +1,113 @@ +import React, { FC, useCallback, useState } from 'react'; +import { SelectOptionProps, SelectProps, useT } from '@frontegg/react-core'; +import { TextField, Chip, CircularProgress, makeStyles } from '@material-ui/core'; +import { Autocomplete, AutocompleteProps as MaterialSelectProps } from '@material-ui/lab'; +import classNames from 'classnames'; + +const mapper = ({ multiselect, theme, ...rest }: SelectProps): MaterialSelectProps => { + const restProps: any = rest; + const color = theme === 'danger' || theme === 'secondary' ? 'secondary' : 'primary'; + + return { + ...restProps, + color, + multiple: !multiselect ? undefined : multiselect, + }; +}; +const useStyles = makeStyles({ + inForm: { + margin: '0.5rem 0 1rem', + }, +}); +export const Select: FC = (props) => { + const styles = useStyles(); + const { t } = useT(); + const p = mapper(props); + const [open, setOpen] = useState(false); + + const { + size, + value, + loading, + onChange, + options, + onOpen, + onBlur, + onClose, + multiple, + fullWidth, + loadingText, + renderOption, + noOptionsText, + getOptionLabel, + open: propOpen, + } = p; + const color: any = p.color; + const [remountCount, setRemountCount] = useState(0); + const refresh = () => setRemountCount(remountCount + 1); + + const handleChange = useCallback( + (e, newValue, reson) => { + onChange?.(e, newValue, reson); + }, + [onChange] + ); + + const renderTag = useCallback( + (option, getTagProps, index) => { + const state = { ...getTagProps({ index }) }; + return renderOption ? ( + {renderOption(option, state)} + ) : ( + + ); + }, + [renderOption] + ); + + return ( + (onOpen ? onOpen(e) : setOpen(true))} + onClose={(e, reson) => (onClose ? onClose(e, reson) : setOpen(false))} + onChange={(e, newValue, reson) => { + handleChange(e, newValue, reson); + setTimeout(() => refresh()); + }} + getOptionSelected={(option: any, value: any) => option.value === value.value} + getOptionLabel={(option: any) => (getOptionLabel ? getOptionLabel(option) : option.label)} + renderTags={(tagValue, getTagProps) => tagValue.map((option, index) => renderTag(option, getTagProps, index))} + renderInput={(params) => ( + + {loading ? : null} + {params.InputProps.endAdornment} + + ), + }} + /> + )} + /> + ); +}; diff --git a/packages/elements-material-ui/src/SwitchToggle/index.tsx b/packages/elements-material-ui/src/SwitchToggle/index.tsx new file mode 100644 index 000000000..4a71fedbe --- /dev/null +++ b/packages/elements-material-ui/src/SwitchToggle/index.tsx @@ -0,0 +1,51 @@ +import React, { FC, useRef } from 'react'; +import { SwitchToggleProps } from '@frontegg/react-core'; +import './style.scss'; +import { Switch as MaterialSwitch, SwitchProps as MaterialSwitchProps } from '@material-ui/core'; +import classNames from 'classnames'; + +const mapper = (props: SwitchToggleProps): MaterialSwitchProps => ({ + disabled: props.disabled || props.loading, + checked: props.value, + onChange: (e, value) => (props.readOnly ? undefined : props.onChange?.(value)), +}); + +export const SwitchToggle: FC = (props) => { + const ref = useRef(null); + const { labels } = props; + const { className, ...toggleProps } = mapper(props); + + const toggle = ; + + if (labels) { + return ( +
+ { + props.value && ref?.current?.click(); + }} + > + {labels[0]} + + {toggle} + { + !props.value && ref?.current?.click(); + }} + > + {labels[1]} + +
+ ); + } + return toggle; +}; diff --git a/packages/elements-material-ui/src/SwitchToggle/style.scss b/packages/elements-material-ui/src/SwitchToggle/style.scss new file mode 100644 index 000000000..eea036346 --- /dev/null +++ b/packages/elements-material-ui/src/SwitchToggle/style.scss @@ -0,0 +1,41 @@ +.fe-switch-toggle { + &__with_labels { + font-size: 0.9rem; + text-transform: uppercase; + color: #99a6b9; + display: flex; + align-items: center; + justify-content: center; + } + + &__label { + margin: 0 1rem; + transition: color 0.3s ease-in; + cursor: pointer; + user-select: none; + + &:hover { + color: black; + } + } + + &__active-left .fe-switch-toggle__label:first-child, + &__active-right .fe-switch-toggle__label:last-child { + color: black; + font-weight: bold; + } + + &__loading { + cursor: progress !important; + + * { + cursor: progress !important; + } + + label:after { + left: 1.1rem !important; + box-shadow: none; + opacity: 0.5 !important; + } + } +} diff --git a/packages/elements-material-ui/src/Table/Table.tsx b/packages/elements-material-ui/src/Table/Table.tsx new file mode 100644 index 000000000..21a7cf7ce --- /dev/null +++ b/packages/elements-material-ui/src/Table/Table.tsx @@ -0,0 +1,351 @@ +import React, { FC, useCallback, useMemo, useEffect, useRef } from 'react'; +import classNames from 'classnames'; +import { TableProps, FeTableColumnProps, FeTableColumnOptions } from '@frontegg/react-core'; +import { Table as MaUTable, Checkbox, IconButton, TablePagination, Paper } from '@material-ui/core'; +import './style.scss'; +import { + useTable, + useFilters, + useSortBy, + TableState, + UseTableOptions, + UseFiltersOptions, + UseFiltersState, + UseSortByOptions, + UseSortByState, + useExpanded, + UseExpandedOptions, + Cell, + UseExpandedRowProps, + Row, + Column, + useFlexLayout, + usePagination, + PluginHook, + UsePaginationOptions, + UsePaginationState, + UsePaginationInstanceProps, + UseTableInstanceProps, + TableInstance, + useRowSelect, + UseRowSelectRowProps, + UseRowSelectOptions, + UseRowSelectInstanceProps, + UseRowSelectState, +} from 'react-table'; +import { TableHead } from './TableHead'; +import { TableBody } from './TableBody'; +import { Loader } from '../Loader'; +import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown'; +import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp'; +import { TablePaginationActions } from './TablePaginationActions'; +import { makeStyles } from '@material-ui/core/styles'; + +const useStyles = makeStyles((theme) => ({ + expandIcon: { + margin: '-12px -12px', + }, + checkBox: { + margin: '-9px 0', + }, + table: { + minWidth: '750px', + }, + paper: { + width: '100%', + overflowY: 'auto', + maxHeight: '100vh', + height: '100%', + }, + footer: { + zIndex: 1, + bottom: '0px', + left: '0px', + right: '0px', + width: '100%', + position: 'sticky', + background: theme.palette.background.paper, + }, +})); + +export const Table: FC = (props: TableProps) => { + const classes = useStyles(); + const tableRef = useRef(null); + const firstRender = useRef(true); + const columns = useMemo(() => { + const columns = props.columns.map( + ({ sortable, Filter, Header, ...rest }) => + ({ + ...rest, + disableSortBy: !sortable, + disableFilters: !Filter, + Filter, + Header: Header ??
, + } as FeTableColumnOptions) + ); + + if (props.expandable) { + columns.unshift({ + id: 'fe-expander', + minWidth: 60, + maxWidth: '60px' as any, + Header:
, + Cell: (cell: Cell) => { + const row = cell.row as Row & UseExpandedRowProps; + return ( + + {row.isExpanded ? : } + + ); + }, + }); + } + if (props.selection) { + columns.unshift({ + id: 'fe-selection', + minWidth: 60, + maxWidth: '60px' as any, + Cell: (cell: Cell) => { + const row = cell.row as Row & UseRowSelectRowProps; + return ( + onRowSelected(row.original, e.target.checked)} + /> + ); + }, + }); + } + return columns as Column[]; + }, [props.columns, props.expandable]); + + const tableHooks: PluginHook[] = [useFilters, useSortBy]; + if (props.expandable) { + tableHooks.push(useExpanded); + } + if (props.pagination) { + tableHooks.push(usePagination); + } + if (props.selection) { + tableHooks.push(useRowSelect); + } + tableHooks.push(useFlexLayout); + + const { + getTableProps, + getTableBodyProps, + headerGroups, + rows, + prepareRow, + state, + + // The page controls ;) + page, + // canPreviousPage, + // canNextPage, + pageOptions, + pageCount, + gotoPage, + nextPage, + previousPage, + // setPageSize, + + // select props + toggleAllRowsSelected, + isAllRowsSelected, + selectedFlatRows, + toggleRowSelected, + } = useTable( + { + columns, + data: props.data, + getRowId: (row: any) => row[props.rowKey], + manualSortBy: !!props.onSortChange, + manualFilters: !!props.onFilterChange, + manualPagination: !!props.onPageChange, + manualRowSelectedKey: props.rowKey, + pageCount: !!props.onPageChange ? props.pageCount : undefined, + autoResetPage: !props.onPageChange, + useControlledState: (state1: any) => { + return { + ...state1, + sortBy: props.sortBy ?? state1.sortBy, + filters: props.filters ?? state1.filters, + selectedRowIds: props.selectedRowIds ?? state1.selectedRowIds, + } as TableState & UseFiltersState & UseSortByState & UseRowSelectState; + }, + expandSubRows: false, + initialState: { + pageIndex: 0, + pageSize: props.pageSize ?? 0, + selectedRowIds: props.selectedRowIds || {}, + }, + } as UseTableOptions & + UseFiltersOptions & + UseSortByOptions & + UseExpandedOptions & + UseRowSelectOptions & + UsePaginationOptions, + ...tableHooks + ) as TableInstance & UseTableInstanceProps & UsePaginationInstanceProps & UseRowSelectInstanceProps; + + if (props.expandable && !props.renderExpandedComponent) { + throw Error('FeTable: you must provide renderExpandedComponent property if the table is expandable'); + } + if (props.hasOwnProperty('sortBy') && !props.onSortChange) { + throw Error('FeTable: you must provide onSortChange property if sortBy is controlled'); + } + if (props.hasOwnProperty('filters') && !props.onFilterChange) { + throw Error('FeTable: you must provide onFilterChange property if filters is controlled'); + } + if (props.hasOwnProperty('pagination') && !props.pageSize) { + throw Error('FeTable: you must provide pageSize property if pagination enabled'); + } + if (props.hasOwnProperty('onPageChange') && !props.pageCount) { + throw Error('FeTable: you must provide pageCount property if onPageChange is controlled'); + } + + const tableState = state as UseSortByState & UseFiltersState & UsePaginationState & UseRowSelectState; + + const onSortChange = useCallback( + (column: FeTableColumnProps) => { + if (props.hasOwnProperty('sortBy')) { + const sortBy = props.isMultiSort ? tableState.sortBy.filter(({ id }) => id !== column.id) : []; + if (!column.isSorted) { + sortBy.push({ id: column.id, desc: false }); + } else if (!column.isSortedDesc) { + sortBy.push({ id: column.id, desc: true }); + } + props.onSortChange?.(sortBy); + } else { + if (column.isSorted && column.isSortedDesc) { + column.clearSortBy(); + } else { + column.toggleSortBy(column.isSorted, props.isMultiSort ?? false); + } + } + }, + [props.onSortChange] + ); + + const onFilterChange = useCallback( + (column: FeTableColumnProps, filterValue?: any) => { + if (props.hasOwnProperty('filters')) { + const filters = tableState.filters.filter(({ id }) => id !== column.id); + if (filterValue != null) { + filters.push({ id: column.id, value: filterValue }); + } + props.onFilterChange?.(filters); + } else { + column.setFilter(filterValue); + } + }, + [props.onFilterChange, tableState] + ); + + const onToggleAllRowsSelected = useCallback( + (value: boolean) => { + if (props.hasOwnProperty('selectedRowIds')) { + const selectedIds = props.data.reduce((p, n: any) => ({ ...p, [n[props.rowKey]]: true }), {}); + props.onRowSelected?.(value ? selectedIds : {}); + } else { + toggleAllRowsSelected(value); + } + }, + [props.onRowSelected] + ); + + const onRowSelected = useCallback( + (row: any, value: boolean) => { + const id = row[props.rowKey]; + if (props.hasOwnProperty('selectedRowIds')) { + const newSelectedRows: any = { ...props.selectedRowIds }; + if (value) { + newSelectedRows[id] = true; + } else { + delete newSelectedRows[id]; + } + props.onRowSelected?.(newSelectedRows); + } else { + toggleRowSelected(id, value); + } + }, + [props.onRowSelected] + ); + + const handleOnPageChange = useCallback(() => { + if (pagination === 'pages') { + tableRef.current?.scroll?.({ top: 0, left: 0, behavior: 'smooth' }); + } + props.onPageChange?.(tableState.pageSize, tableState.pageIndex); + }, [tableState.pageIndex]); + + useEffect(() => { + !props.hasOwnProperty('sortBy') && props.onSortChange?.(tableState.sortBy); + }, [props.sortBy, tableState.sortBy]); + + useEffect(() => { + !props.hasOwnProperty('filters') && props.onFilterChange?.(tableState.filters); + }, [props.filters, tableState.filters]); + + useEffect(() => { + firstRender.current ? (firstRender.current = false) : handleOnPageChange(); + }, [tableState.pageIndex]); + + useEffect(() => { + !props.hasOwnProperty('selectedRowIds') && props.onRowSelected?.(tableState.selectedRowIds as any); + }, [tableState.selectedRowIds]); + + const onPageChangeHandler = (page: number) => { + if (page > tableState.pageIndex) { + nextPage(); + } else { + previousPage(); + } + }; + + const { className, loading, pagination, totalData, pageSize } = props; + + return ( + + + + & UseExpandedRowProps)[]} + renderExpandedComponent={props.renderExpandedComponent} + onInfiniteScroll={handleOnPageChange} + /> + + + {loading && pagination === 'pages' && rows.length > 0 && } + {pagination === 'pages' && ( + onPageChangeHandler(page)} + ActionsComponent={(props) => ( + + )} + /> + )} + + ); +}; diff --git a/packages/elements-material-ui/src/Table/TableBody.tsx b/packages/elements-material-ui/src/Table/TableBody.tsx new file mode 100644 index 000000000..8ba4ecdbf --- /dev/null +++ b/packages/elements-material-ui/src/Table/TableBody.tsx @@ -0,0 +1,138 @@ +import React, { FC, useMemo } from 'react'; +import { makeStyles } from '@material-ui/core'; +import { useT, TableProps } from '@frontegg/react-core'; +import { TableBody as MTableBody, TableRow, TableCell } from '@material-ui/core'; +import { Row, TableBodyPropGetter, TableBodyProps, UseExpandedRowProps } from 'react-table'; +import { TableExpandable } from './TableExpandable'; +import { Loader } from '../Loader'; +import classNames from 'classnames'; +import { Waypoint } from 'react-waypoint'; + +type TableTBodyProps = { + pagination?: TableProps['pagination']; + loading?: boolean; + getTableBodyProps: (propGetter?: TableBodyPropGetter) => TableBodyProps; + prepareRow: (row: Row) => void; + rows: (Row & UseExpandedRowProps)[]; + renderExpandedComponent?: (data: T, index: number) => React.ReactNode; + pageSize?: number; + onInfiniteScroll?: () => void; +}; + +const useRowStyles = makeStyles({ + root: { + '& > *': { + borderBottom: 'unset', + }, + }, + cell: { + wordWrap: 'break-word', + display: 'flex', + alignItems: 'center', + }, + firstCell: { + paddingLeft: '2rem', + }, +}); + +export const TableBody: FC> = (props: TableTBodyProps) => { + const { + getTableBodyProps, + prepareRow, + rows, + renderExpandedComponent, + loading, + pagination, + onInfiniteScroll, + pageSize, + } = props; + const { t } = useT(); + const classes = useRowStyles(); + + const isInfiniteScroll = pagination === 'infinite-scroll'; + const isFirstWaypoint = useMemo(() => rows.length <= (pageSize ?? 20), [pageSize, rows.length]); + + const renderWaypoint = (index: number) => { + const itemsAfterWaypoint = 15; + const itemsAfterWaypointOnFirstRender = 4; + const waypoint = ( + { + if (!loading && previousPosition !== 'above') { + onInfiniteScroll?.(); + } + }} + /> + ); + if (isFirstWaypoint && index === rows.length - itemsAfterWaypointOnFirstRender) { + return waypoint; + } + if (!isFirstWaypoint && index === rows.length - itemsAfterWaypoint) { + return waypoint; + } + }; + + return ( + <> + + {rows.map((row, index) => { + prepareRow(row); + return ( + + + {row.cells.map((cell, index) => { + const cellProps = cell.getCellProps(); + cellProps.className = classNames(classes.cell, { + [classes.firstCell]: index === 0, + }); + if (cell.column.id.includes('fe-expander')) { + return ( + + {cell.render('Cell')} + + ); + } + + return {cell.render('Cell')}; + })} + + + {isInfiniteScroll && renderWaypoint(index)} + + ); + })} + {isInfiniteScroll && loading && rows.length !== 0 && ( + + + + + + )} + + {loading && rows.length === 0 && ( + + + + + + )} + {!loading && rows.length === 0 && ( + + + {t('common.noResults')} + + + )} + + + ); +}; diff --git a/packages/elements-material-ui/src/Table/TableExpandable.tsx b/packages/elements-material-ui/src/Table/TableExpandable.tsx new file mode 100644 index 000000000..36aa54150 --- /dev/null +++ b/packages/elements-material-ui/src/Table/TableExpandable.tsx @@ -0,0 +1,22 @@ +import React, { FC, useEffect, useRef } from 'react'; +import { Row } from 'react-table'; +import { Box, Collapse, TableCell, TableRow } from '@material-ui/core'; + +type TableExpandableProps = { + isExpanded: boolean; + renderExpandedComponent?: (data: T, index: number) => React.ReactNode; + row: Row; +}; +export const TableExpandable: FC> = (props: TableExpandableProps) => { + const { isExpanded, renderExpandedComponent, row } = props; + + return ( + + + + {renderExpandedComponent?.(row.original, row.index)} + + + + ); +}; diff --git a/packages/elements-material-ui/src/Table/TableFilterColumn.tsx b/packages/elements-material-ui/src/Table/TableFilterColumn.tsx new file mode 100644 index 000000000..b12817cc9 --- /dev/null +++ b/packages/elements-material-ui/src/Table/TableFilterColumn.tsx @@ -0,0 +1,67 @@ +import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; +import { Popup } from '../Popup'; +import { FeTableColumnProps } from '@frontegg/react-core'; +import FilterListIcon from '@material-ui/icons/FilterList'; +import { Box, IconButton, Tooltip } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; + +const useStyles = makeStyles((theme) => ({ + filterButton: { + margin: '-16px 0', + }, + filterIcon: { + fontSize: '1.4rem', + }, +})); + +type FeTableFilterColumnProps = { + column: FeTableColumnProps; + onFilterChange?: (column: FeTableColumnProps, value: any) => void; +}; + +export const TableFilterColumn: FC = ({ + column, + onFilterChange, +}: FeTableFilterColumnProps) => { + const [filterValue, setFilterValue] = useState(column.filterValue); + const classes = useStyles(); + const popupRef = useRef(null); + + useEffect(() => { + setFilterValue(column.filterValue); + }, [column.filterValue]); + + const closePopup = useCallback(() => { + if (popupRef.current) { + (popupRef.current as any)?.closePopup?.(); + } + }, [popupRef]); + + const FilterComponent = column.Filter; + return ( + e.stopPropagation()}> + onFilterChange?.(column, value)} + closePopup={closePopup} + /> + } + action={'click'} + trigger={ + + + + + + } + /> + + ); +}; diff --git a/packages/elements-material-ui/src/Table/TableHead.tsx b/packages/elements-material-ui/src/Table/TableHead.tsx new file mode 100644 index 000000000..2db616dd1 --- /dev/null +++ b/packages/elements-material-ui/src/Table/TableHead.tsx @@ -0,0 +1,112 @@ +import React, { FC } from 'react'; +import { TableFilterColumn } from './TableFilterColumn'; +import { FeTableColumnProps } from '@frontegg/react-core'; +import { HeaderGroup, TableSortByToggleProps } from 'react-table'; +import { TableHead as MaterialTableHead, TableRow, TableCell, TableSortLabel, Checkbox, Box } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import classNames from 'classnames'; + +const useStyles = makeStyles((theme) => ({ + checkBox: { + margin: '-9px 0', + }, + head: { + position: 'sticky', + top: '0px', + zIndex: 10, + '& > *': { + position: 'sticky', + top: '0px', + zIndex: 10, + background: theme.palette.background.paper, + }, + }, + firstHeadCell: { + paddingLeft: '2rem', + }, + expander: { + padding: 0, + minWidth: '0 !important', + width: 0, + }, +})); + +type FeTableTHeadProps = { + headerGroups: HeaderGroup[]; + onSortChange?: (column: FeTableColumnProps) => void; + onFilterChange?: (column: FeTableColumnProps, filterValue?: any) => void; + toggleAllRowsSelected?: (value: boolean) => void; + isAllRowsSelected?: boolean; + selectedFlatRows?: T[]; +}; + +export const TableHead: FC> = (props: FeTableTHeadProps) => { + const { + headerGroups, + onSortChange, + onFilterChange, + toggleAllRowsSelected, + selectedFlatRows, + isAllRowsSelected, + } = props; + const classes = useStyles(); + + return ( + + {headerGroups.map((headerGroup) => ( + + {headerGroup.headers.map((c, index) => { + const column = c as FeTableColumnProps; + if (column.id === 'fe-selection') { + return ( + + 0} + checked={isAllRowsSelected} + onChange={() => toggleAllRowsSelected?.(!isAllRowsSelected)} + /> + + ); + } + const withExpander = headerGroup.headers[0].id === 'fe-expander'; + const tableCellProps = column.getHeaderProps( + column.getSortByToggleProps((p: Partial) => ({ + ...p, + onClick: column.canSort ? () => onSortChange?.(column) : undefined, + })) + ); + const minWidth = headerGroup.headers[0].minWidth || 0; + const ownWidth = column.width || 0; + const width = index === 1 && withExpander ? { width: Number(ownWidth) + minWidth, paddingLeft: 32 } : {}; + const cellStyle = { ...tableCellProps?.style, ...width }; + tableCellProps.className = classNames(tableCellProps.className, { + [classes.firstHeadCell]: index === 0, + [classes.expander]: index === 0 && withExpander, + }); + return ( + + + + {column.canSort ? ( + + {column.render('Header')} + + ) : ( + <>{column.render('Header')} + )} + + {column.canFilter && } + + + ); + })} + + ))} + + ); +}; diff --git a/packages/elements-material-ui/src/Table/TablePaginationActions.tsx b/packages/elements-material-ui/src/Table/TablePaginationActions.tsx new file mode 100644 index 000000000..0f7acdfc3 --- /dev/null +++ b/packages/elements-material-ui/src/Table/TablePaginationActions.tsx @@ -0,0 +1,80 @@ +import React, { FC, useCallback } from 'react'; +import { IconButton } from '@material-ui/core'; +import FirstPageIcon from '@material-ui/icons/FirstPage'; +import KeyboardArrowLeft from '@material-ui/icons/KeyboardArrowLeft'; +import KeyboardArrowRight from '@material-ui/icons/KeyboardArrowRight'; +import LastPageIcon from '@material-ui/icons/LastPage'; +import { makeStyles, useTheme } from '@material-ui/core/styles'; + +type FeTablePaginationProps = { + count: number; + page: number; + rowsPerPage: number; + pageOptions: number[]; + onChangePage: (e: any, nextPage: number) => void; + gotoPage: (updater: ((pageIndex: number) => number) | number) => void; +}; + +const useStyles = makeStyles((theme) => ({ + root: { + flexShrink: 0, + marginLeft: theme.spacing(2.5), + display: 'flex', + alignItems: 'center', + }, +})); + +export const TablePaginationActions: FC> = ( + props: FeTablePaginationProps +) => { + const classes = useStyles(); + const theme = useTheme(); + const { count, page, rowsPerPage, onChangePage, gotoPage } = props; + + const handleFirstPageButtonClick = useCallback(() => { + gotoPage(0); + }, []); + + const handleBackButtonClick = useCallback( + (event: any) => { + onChangePage(event, page - 1); + }, + [page] + ); + + const handleNextButtonClick = useCallback( + (event: any) => { + onChangePage(event, page + 1); + }, + [page] + ); + + const handleLastPageButtonClick = useCallback(() => { + gotoPage(Math.max(0, Math.ceil(count / rowsPerPage) - 1)); + }, []); + + return ( +
+ + {theme.direction === 'rtl' ? : } + + + {theme.direction === 'rtl' ? : } + + = Math.ceil(count / rowsPerPage) - 1} + aria-label='next page' + > + {theme.direction === 'rtl' ? : } + + = Math.ceil(count / rowsPerPage) - 1} + aria-label='last page' + > + {theme.direction === 'rtl' ? : } + +
+ ); +}; diff --git a/packages/elements-material-ui/src/Table/index.ts b/packages/elements-material-ui/src/Table/index.ts new file mode 100644 index 000000000..75193adc3 --- /dev/null +++ b/packages/elements-material-ui/src/Table/index.ts @@ -0,0 +1 @@ +export * from './Table'; diff --git a/packages/elements-material-ui/src/Table/style.scss b/packages/elements-material-ui/src/Table/style.scss new file mode 100644 index 000000000..96ca1db30 --- /dev/null +++ b/packages/elements-material-ui/src/Table/style.scss @@ -0,0 +1,11 @@ +.fe-sortLabel { + flex: 1 1 auto; + + svg { + font-size: 1.2rem; + } +} + +.fe-table__tbody__loading { + opacity: 0.8; +} diff --git a/packages/elements-material-ui/src/Tabs/index.tsx b/packages/elements-material-ui/src/Tabs/index.tsx new file mode 100644 index 000000000..e965295b6 --- /dev/null +++ b/packages/elements-material-ui/src/Tabs/index.tsx @@ -0,0 +1,22 @@ +import React, { FC } from 'react'; +import { TabProps, TabItem } from '@frontegg/react-core'; +import { Tabs as MaterialTabs, TabsProps as MaterialTabsProps, Tab as MaterialTab } from '@material-ui/core'; + +const mapper = ({ activeTab, onTabChange, className }: TabProps): MaterialTabsProps => ({ + value: activeTab, + onChange: ((event: any, value: any) => onTabChange(event, value)) as any, + className, +}); + +export const Tabs: FC = (props) => { + const tabs = props.items.map(({ Title, disabled }: TabItem, index: number) => ( + + )); + + const tabsProps = mapper(props); + return ( + + {tabs} + + ); +}; diff --git a/packages/elements-material-ui/src/Tag/index.tsx b/packages/elements-material-ui/src/Tag/index.tsx new file mode 100644 index 000000000..0014f9b97 --- /dev/null +++ b/packages/elements-material-ui/src/Tag/index.tsx @@ -0,0 +1,41 @@ +import React, { FC } from 'react'; +import { TagProps } from '@frontegg/react-core'; +import { Chip, ChipProps as MaterialChipProps, makeStyles } from '@material-ui/core'; +import classNames from 'classnames'; + +const useStyles = makeStyles({ + dangerStyle: { + color: 'var(--color-white)', + backgroundColor: 'var(--color-red-7)', + border: 'none', + }, + successStyle: { + color: 'var(--color-white)', + backgroundColor: 'var(--color-light-green-a3-75)', + border: 'none', + }, +}); + +const mapper = (props: TagProps): MaterialChipProps => { + const { children, className, size, variant, color, ...rest } = props; + const classes = useStyles(); + const variantColor = variant === 'danger' || variant === 'disabled' || variant === 'success' ? 'default' : variant; + return { + classes: { + root: classNames(className, { + [classes.dangerStyle]: variant === 'danger', + [classes.successStyle]: variant === 'success', + }), + }, + variant: 'default', + color: variantColor, + label: children, + size: size === 'large' ? 'medium' : size, + disabled: variant === 'disabled', + ...rest, + }; +}; + +export const Tag: FC = (props) => { + return ; +}; diff --git a/packages/elements-material-ui/src/index.ts b/packages/elements-material-ui/src/index.ts new file mode 100644 index 000000000..e033764d5 --- /dev/null +++ b/packages/elements-material-ui/src/index.ts @@ -0,0 +1,46 @@ +import { Elements } from '@frontegg/react-core'; +import { Accordion } from './Accordion'; +import { AccordionContent } from './Accordion'; +import { AccordionHeader } from './Accordion'; +import { Button } from './Button'; +import { Input } from './Input'; +import { Loader } from './Loader'; +import { SwitchToggle } from './SwitchToggle'; +import { Tabs } from './Tabs'; +import { Icon } from './Icon'; +import { Dialog } from './Dialog'; +import { Popup } from './Popup'; +import { Checkbox } from './Checkbox'; +import { Table } from './Table'; +import { Tag } from './Tag'; +import { Grid } from './Grid'; +import { Select } from './Select'; +import { Menu } from './Menu'; +import { MenuItem } from './MenuItem'; +import { InputChip } from './InputChip'; +import { Pagination } from './Pagination'; + +export const type = 'material-ui'; +export const version = '4.11.0'; +export const uiLibrary: Partial = { + Accordion, + AccordionContent, + AccordionHeader, + Button, + Input, + Loader, + SwitchToggle, + Tabs, + Icon, + Dialog, + Grid, + InputChip, + Popup, + Checkbox, + Table, + Tag, + Select, + Menu, + MenuItem, + Pagination, +}; diff --git a/packages/elements-material-ui/tsconfig.json b/packages/elements-material-ui/tsconfig.json new file mode 100644 index 000000000..03d2236d8 --- /dev/null +++ b/packages/elements-material-ui/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "baseUrl": "./src", + "noEmit": true + }, + "include": [ + "./src/**/*.tsx", + "./src/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist", + "src/**/*.stories.tsx", + "src/**/*.test.tsx", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.spec.tsx" + ] +} diff --git a/packages/elements-semantic/CHANGELOG.md b/packages/elements-semantic/CHANGELOG.md new file mode 100644 index 000000000..d2707a7a2 --- /dev/null +++ b/packages/elements-semantic/CHANGELOG.md @@ -0,0 +1,500 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [2.8.3](https://github.com/frontegg/frontegg-react/compare/v2.8.1...v2.8.3) (2021-06-23) + + +### Bug Fixes + +* align all dependencies versions ([f1d5c48](https://github.com/frontegg/frontegg-react/commit/f1d5c48aba34827ea06a554cfb61734f1a40c93b)) +* update libraries version ([77da072](https://github.com/frontegg/frontegg-react/commit/77da072c67cb11e6cccb86b044a3a1a22b09625c)) + + + + + +## [2.8.2](https://github.com/frontegg/frontegg-react/compare/v2.8.1...v2.8.2) (2021-06-23) + + +### Bug Fixes + +* update libraries version ([77da072](https://github.com/frontegg/frontegg-react/commit/77da072c67cb11e6cccb86b044a3a1a22b09625c)) + + + + + +# [2.4.0](https://github.com/frontegg/frontegg-react/compare/v2.3.2...v2.4.0) (2021-05-19) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +# [2.2.0](https://github.com/frontegg/frontegg-react/compare/v2.1.0...v2.2.0) (2021-04-28) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +# [2.0.0](https://github.com/frontegg/frontegg-react/compare/v1.28.0...v2.0.0) (2021-04-11) + + +### Bug Fixes + +* FR-2312 - add success variant support for all elements libraries ([58b85b7](https://github.com/frontegg/frontegg-react/commit/58b85b7fe2f07a954a95ba87a17d44567efd946f)) + + + + + +## [1.22.1](https://github.com/frontegg/frontegg-react/compare/v1.22.0...v1.22.1) (2021-02-16) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +# [1.22.0](https://github.com/frontegg/frontegg-react/compare/v1.21.1...v1.22.0) (2021-02-13) + + +### Features + +* **elements:** add support ref for Input elements in UI libraries ([39c1ebc](https://github.com/frontegg/frontegg-react/commit/39c1ebc05262aa0f1ee47dbae8c23bb37d0a0a0d)) + + + + + +## [1.21.1](https://github.com/frontegg/frontegg-react/compare/v1.21.0...v1.21.1) (2021-02-04) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +# [1.21.0](https://github.com/frontegg/frontegg-react/compare/v1.20.1...v1.21.0) (2021-02-04) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +## [1.20.1](https://github.com/frontegg/frontegg-react/compare/v1.20.0...v1.20.1) (2021-02-02) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +# [1.20.0](https://github.com/frontegg/frontegg-react/compare/v1.19.1...v1.20.0) (2021-02-01) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +## [1.19.1](https://github.com/frontegg/frontegg-react/compare/v1.19.0...v1.19.1) (2021-01-20) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +# [1.19.0](https://github.com/frontegg/frontegg-react/compare/v1.18.6...v1.19.0) (2021-01-20) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +## [1.18.6](https://github.com/frontegg/frontegg-react/compare/v1.18.5...v1.18.6) (2021-01-19) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +## [1.18.5](https://github.com/frontegg/frontegg-react/compare/v1.18.4...v1.18.5) (2021-01-18) + + +### Bug Fixes + +* **core:** fix perfomance for the INputChip component ([cea5e19](https://github.com/frontegg/frontegg-react/commit/cea5e19aef64a6cf42f75d3cbb77cd74fb01f560)) + + + + + +## [1.18.4](https://github.com/frontegg/frontegg-react/compare/v1.18.3...v1.18.4) (2021-01-18) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +## [1.18.3](https://github.com/frontegg/frontegg-react/compare/v1.18.2...v1.18.3) (2021-01-17) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +## [1.18.2](https://github.com/frontegg/frontegg-react/compare/v1.18.1...v1.18.2) (2021-01-15) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +## [1.18.1](https://github.com/frontegg/frontegg-react/compare/v1.18.0...v1.18.1) (2021-01-15) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +# [1.18.0](https://github.com/frontegg/frontegg-react/compare/v1.17.3...v1.18.0) (2021-01-14) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +## [1.17.3](https://github.com/frontegg/frontegg-react/compare/v1.17.2...v1.17.3) (2021-01-13) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +## [1.17.2](https://github.com/frontegg/frontegg-react/compare/v1.17.1...v1.17.2) (2021-01-12) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +## [1.17.1](https://github.com/frontegg/frontegg-react/compare/v1.17.0...v1.17.1) (2021-01-05) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +# [1.17.0](https://github.com/frontegg/frontegg-react/compare/v1.16.2...v1.17.0) (2021-01-03) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +## [1.16.2](https://github.com/frontegg/frontegg-react/compare/v1.16.0...v1.16.2) (2020-12-27) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +## [1.16.1](https://github.com/frontegg/frontegg-react/compare/v1.16.0...v1.16.1) (2020-12-27) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +# [1.16.0](https://github.com/frontegg/frontegg-react/compare/v1.15.2...v1.16.0) (2020-12-24) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +## [1.15.2](https://github.com/frontegg/frontegg-react/compare/v1.15.1...v1.15.2) (2020-12-24) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +## [1.15.1](https://github.com/frontegg/frontegg-react/compare/v1.15.0...v1.15.1) (2020-12-21) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +# [1.15.0](https://github.com/frontegg/frontegg-react/compare/v1.14.1...v1.15.0) (2020-12-20) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +## [1.14.1](https://github.com/frontegg/frontegg-react/compare/v1.14.0...v1.14.1) (2020-12-17) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +# [1.14.0](https://github.com/frontegg/frontegg-react/compare/v1.13.2...v1.14.0) (2020-12-16) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +## [1.13.2](https://github.com/frontegg/frontegg-react/compare/v1.13.1...v1.13.2) (2020-12-13) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +## [1.13.1](https://github.com/frontegg/frontegg-react/compare/v1.13.0...v1.13.1) (2020-12-10) + + +### Bug Fixes + +* **audits:** FR-999 add x btn for ip popup ([6efc8ea](https://github.com/frontegg/frontegg-react/commit/6efc8ea6e229b6434d75f9af59c0b6f0993ec9cd)) + + + + + +# [1.13.0](https://github.com/frontegg/frontegg-react/compare/v1.12.0...v1.13.0) (2020-12-09) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +# [1.12.0](https://github.com/frontegg/frontegg-react/compare/v1.11.1...v1.12.0) (2020-12-09) + + +### Bug Fixes + +* **audits:** FR-1002 add globe icon when location is unknown ([53265fd](https://github.com/frontegg/frontegg-react/commit/53265fd5cdc56d3fc141ad77469d452e2f6dc430)) +* **elements:** fix className property for the semantic Button element ([8646833](https://github.com/frontegg/frontegg-react/commit/864683387e221ab350f7f3f439918a6b411254d9)) +* **elements:** fix styles and behavior for the semantic InputChip component ([29671d2](https://github.com/frontegg/frontegg-react/commit/29671d2b3ea070e712c1352fbb7356d236d710d9)) + + +### Features + +* **elements:** add the TextArea component to the semantic library ([bacc6c3](https://github.com/frontegg/frontegg-react/commit/bacc6c35eb52cf0bf644503b9ca654f4e3073e50)) + + + + + +## [1.11.1](https://github.com/frontegg/frontegg-react/compare/v1.11.0...v1.11.1) (2020-12-02) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +# [1.11.0](https://github.com/frontegg/frontegg-react/compare/v1.10.0...v1.11.0) (2020-11-30) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +# [1.10.0](https://github.com/frontegg/frontegg-react/compare/v1.9.0...v1.10.0) (2020-11-27) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +# [1.9.0](https://github.com/frontegg/frontegg-react/compare/v1.8.0...v1.9.0) (2020-11-25) + + +### Features + +* **auth:** New AccountDropdown component added to AuthPlugin ([018f2f8](https://github.com/frontegg/frontegg-react/commit/018f2f8db3ad22981f9270bd2e166b56cf6eb7ff)) +* New plugin for Audits ([#106](https://github.com/frontegg/frontegg-react/issues/106)) ([921b37a](https://github.com/frontegg/frontegg-react/commit/921b37aca98f23c800c8b5d094ed595d52617679)) + + + + + +# [1.8.0](https://github.com/frontegg/frontegg-react/compare/v1.7.0...v1.8.0) (2020-11-23) + + +### Features + +* Add Connectivity Plugin ([#98](https://github.com/frontegg/frontegg-react/issues/98)) ([db77431](https://github.com/frontegg/frontegg-react/commit/db77431b6d744b93431430543019fedd1c0dbae2)) + + + + + +# [1.7.0](https://github.com/frontegg/frontegg-react/compare/v1.6.1...v1.7.0) (2020-11-22) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +## [1.6.2](https://github.com/frontegg/frontegg-react/compare/v1.6.1...v1.6.2) (2020-11-19) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +## [1.6.1](https://github.com/frontegg/frontegg-react/compare/v1.6.0...v1.6.1) (2020-11-15) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +# [1.6.0](https://github.com/frontegg/frontegg-react/compare/v1.5.0...v1.6.0) (2020-11-12) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +# [1.5.0](https://github.com/frontegg/frontegg-react/compare/v1.4.0...v1.5.0) (2020-11-10) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +# [1.4.0](https://github.com/frontegg/frontegg-react/compare/v1.3.0...v1.4.0) (2020-11-09) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +# [1.3.0](https://github.com/frontegg/frontegg-react/compare/v1.2.0...v1.3.0) (2020-11-04) + + +### Bug Fixes + +* **auth:** bug fixes ([#76](https://github.com/frontegg/frontegg-react/issues/76)) ([a758642](https://github.com/frontegg/frontegg-react/commit/a758642458751e930dc4ea6de6acc50b3ede0b77)) +* **auth:** fix ui bugs ([#79](https://github.com/frontegg/frontegg-react/issues/79)) ([0e75d8d](https://github.com/frontegg/frontegg-react/commit/0e75d8dff80937dc2e4308a0ffdd527a12a84a39)) + + +### Features + +* **elements:** add FeInput element ([f23e439](https://github.com/frontegg/frontegg-react/commit/f23e4392ab849d32c5e0ba67e055f793ecfd5ceb)) + + + + + +# [1.2.0](https://github.com/frontegg/frontegg-react/compare/v1.1.0...v1.2.0) (2020-10-25) + + +### Bug Fixes + +* **elements:** UI elements small fixes ([effb9bd](https://github.com/frontegg/frontegg-react/commit/effb9bd54186133184010c34874af212c040ef90)) + + +### Features + +* **auth:** add SSO components ([#64](https://github.com/frontegg/frontegg-react/issues/64)) ([f083762](https://github.com/frontegg/frontegg-react/commit/f0837623073c1f9a636480c6a88d5969fb020a09)) +* **auth:** support all auth functionalities ([#70](https://github.com/frontegg/frontegg-react/issues/70)) ([26d725e](https://github.com/frontegg/frontegg-react/commit/26d725e2f386c6b4a703e4371fac8efef29676d1)) +* **core:** add dynamic elements for Frontegg Components ([#10](https://github.com/frontegg/frontegg-react/issues/10)) ([2bddbb2](https://github.com/frontegg/frontegg-react/commit/2bddbb2794ac0dc4af90c8df0f33b69a312e063a)) +* **core:** add FeSwitchToggle component ([#66](https://github.com/frontegg/frontegg-react/issues/66)) ([5b6c603](https://github.com/frontegg/frontegg-react/commit/5b6c603d912be45b1982c06b7f026badd8e82f1c)) +* **core:** Add FeTabs component ([3699299](https://github.com/frontegg/frontegg-react/commit/36992997e64907345a254a4b44580759705186bb)), closes [#67](https://github.com/frontegg/frontegg-react/issues/67) + + + + + +# [1.1.0](https://github.com/frontegg/frontegg-react/compare/v1.0.89...v1.1.0) (2020-10-14) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +## [1.0.89](https://github.com/frontegg/frontegg-react/compare/v1.0.88...v1.0.89) (2020-10-13) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +## [1.0.88](https://github.com/frontegg/frontegg-react/compare/v1.0.87...v1.0.88) (2020-10-11) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +## [1.0.87](https://github.com/frontegg/frontegg-react/compare/v1.0.86...v1.0.87) (2020-10-09) + + +### Bug Fixes + +* **packaging:** move cjs.js to index.js for main entry point ([0f8f700](https://github.com/frontegg/frontegg-react/commit/0f8f70016566a8f1940d16441a5afa1707dc02a2)) + + + + + +## 1.0.85 (2020-10-08) + +**Note:** Version bump only for package @frontegg/react-elements-semantic + + + + + +## 1.0.84 (2020-10-08) + +**Note:** Version bump only for package @frontegg/react-elements-semantic diff --git a/packages/elements-semantic/package.json b/packages/elements-semantic/package.json new file mode 100644 index 000000000..cd1bda97c --- /dev/null +++ b/packages/elements-semantic/package.json @@ -0,0 +1,66 @@ +{ + "name": "@frontegg/react-elements-semantic", + "libName": "FronteggElementsSemantic", + "version": "4.0.23", + "author": "Frontegg LTD", + "main": "dist/index.js", + "module": "dist/index.esm.js", + "es2015": "dist/index.es.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "rollup -c ../../scripts/rollup.config.js && echo DONE", + "build:watch": "rollup -w -c ../../scripts/rollup.config.js", + "test": "jest --runInBand --passWithNoTests -c ../../scripts/jest.config.json --rootDir . && echo DONE" + }, + "dependencies": { + "classnames": "^2.2.6", + "semantic-ui-react": "^1.2.1" + }, + "devDependencies": { + "@types/classnames": "^2.2.10" + }, + "jest": { + "preset": "ts-jest", + "testEnvironment": "jsdom", + "testPathIgnorePatterns": [ + "dist/" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "prettier": { + "printWidth": 120, + "semi": true, + "singleQuote": true, + "jsxSingleQuote": true, + "trailingComma": "all", + "jsxBracketSameLine": true, + "endOfLine": "lf", + "tabWidth": 2 + }, + "standard": { + "ignore": [ + "node_modules/", + "dist/" + ], + "globals": [ + "describe", + "it", + "test", + "expect", + "afterAll", + "jest" + ] + }, + "gitHead": "00c0bd425a211916eec1170b8359cf302727b338" +} diff --git a/packages/elements-semantic/src/Accordion/Accordion.tsx b/packages/elements-semantic/src/Accordion/Accordion.tsx new file mode 100644 index 000000000..65fb25217 --- /dev/null +++ b/packages/elements-semantic/src/Accordion/Accordion.tsx @@ -0,0 +1,28 @@ +import React, { FC, useCallback, useState } from 'react'; +import { AccordionProps } from '@frontegg/react-core'; +import { Accordion as SemanticAccordion } from 'semantic-ui-react'; +import { ActiveContext } from './AccordionActiveContext'; +import classNames from 'classnames'; +import './style.scss'; + +export const Accordion: FC = (props) => { + const [innerActive, setInnerActive] = useState(false); + + const { onChange = setInnerActive, children, expanded, ...rest } = props; + + const active = expanded ?? innerActive; + + const toggleActive = useCallback(() => { + onChange(!active); + }, [onChange, active]); + + const className = classNames(props.className, { ['fe-semantic-accordion__disabled']: props.disabled }); + + return ( + + + {children} + + + ); +}; diff --git a/packages/elements-semantic/src/Accordion/AccordionActiveContext.ts b/packages/elements-semantic/src/Accordion/AccordionActiveContext.ts new file mode 100644 index 000000000..3c43ca40e --- /dev/null +++ b/packages/elements-semantic/src/Accordion/AccordionActiveContext.ts @@ -0,0 +1,8 @@ +import { createContext } from 'react'; + +const defaultActiveContext = { + active: false, + toggleActive: () => {}, +}; + +export const ActiveContext = createContext(defaultActiveContext); diff --git a/packages/elements-semantic/src/Accordion/AccordionContent.tsx b/packages/elements-semantic/src/Accordion/AccordionContent.tsx new file mode 100644 index 000000000..07cc0f7c5 --- /dev/null +++ b/packages/elements-semantic/src/Accordion/AccordionContent.tsx @@ -0,0 +1,9 @@ +import React, { FC, useContext } from 'react'; +import { AccordionContentProps } from '@frontegg/react-core'; +import { AccordionContent as SemanticAccordionContent } from 'semantic-ui-react'; +import { ActiveContext } from './AccordionActiveContext'; + +export const AccordionContent: FC = (props) => { + const { active } = useContext(ActiveContext); + return ; +}; diff --git a/packages/elements-semantic/src/Accordion/AccordionHeader.tsx b/packages/elements-semantic/src/Accordion/AccordionHeader.tsx new file mode 100644 index 000000000..9b8c79e4a --- /dev/null +++ b/packages/elements-semantic/src/Accordion/AccordionHeader.tsx @@ -0,0 +1,15 @@ +import React, { FC, useContext } from 'react'; +import { AccordionHeaderProps, Icon } from '@frontegg/react-core'; +import { AccordionTitle as SemanticAccordionHeader } from 'semantic-ui-react'; +import { ActiveContext } from './AccordionActiveContext'; + +export const AccordionHeader: FC = (props) => { + const { children, expandIcon, ...rest } = props; + const { active, toggleActive } = useContext(ActiveContext); + return ( + + {children} + {expandIcon} + + ); +}; diff --git a/packages/elements-semantic/src/Accordion/index.ts b/packages/elements-semantic/src/Accordion/index.ts new file mode 100644 index 000000000..f07823a19 --- /dev/null +++ b/packages/elements-semantic/src/Accordion/index.ts @@ -0,0 +1,3 @@ +export * from './Accordion'; +export * from './AccordionHeader'; +export * from './AccordionContent'; diff --git a/packages/elements-semantic/src/Accordion/style.scss b/packages/elements-semantic/src/Accordion/style.scss new file mode 100644 index 000000000..f66e6c366 --- /dev/null +++ b/packages/elements-semantic/src/Accordion/style.scss @@ -0,0 +1,7 @@ +.fe-semantic-accordion__disabled { + background-color: var(--color-gray-3); + color: var(--color-text-disabled); + border: none; + pointer-events: none; + cursor: default; +} diff --git a/packages/elements-semantic/src/Button/index.tsx b/packages/elements-semantic/src/Button/index.tsx new file mode 100644 index 000000000..bf3775cc8 --- /dev/null +++ b/packages/elements-semantic/src/Button/index.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { ButtonProps } from '@frontegg/react-core'; +import { Button as SemanticButton, ButtonProps as SemanticButtonProps, Form } from 'semantic-ui-react'; +import classNames from 'classnames'; +import './style.scss'; +const mapper = (props: ButtonProps): SemanticButtonProps => { + const { + variant, + fullWidth, + inForm, + submit, + formikDisableIfNotDirty, + loading, + disabled, + type, + iconButton, + className, + isCancel, + testId, + transparent, + ...rest + } = props; + return { + ...rest, + loading, + className: classNames(className, { + 'fe-semantic-button__icon-button': iconButton, + 'fe-semantic-button__transparent': transparent, + }), + disabled: loading || disabled, + primary: variant === 'primary' ? true : undefined, + secondary: variant === 'secondary' ? true : undefined, + color: variant === 'danger' ? 'red' : undefined, + fluid: fullWidth, + 'test-id': testId, + type: submit ? 'submit' : type ?? 'button', + }; +}; + +export class Button extends React.Component { + render() { + const { children, inForm } = this.props; + const buttonProps = mapper(this.props); + let ButtonComponent: any = SemanticButton; + if (inForm) { + ButtonComponent = Form.Button; + } + + return {children}; + } +} diff --git a/packages/elements-semantic/src/Button/style.scss b/packages/elements-semantic/src/Button/style.scss new file mode 100644 index 000000000..0e7f9cd64 --- /dev/null +++ b/packages/elements-semantic/src/Button/style.scss @@ -0,0 +1,16 @@ +.ui.button.fe-semantic-button { + &__transparent { + border: none; + &:not(:hover) { + background: transparent; + } + } + + &__icon-button { + text-align: center; + + > .fe-icon { + margin: auto !important; + } + } +} diff --git a/packages/elements-semantic/src/Checkbox/index.tsx b/packages/elements-semantic/src/Checkbox/index.tsx new file mode 100644 index 000000000..e06e4561e --- /dev/null +++ b/packages/elements-semantic/src/Checkbox/index.tsx @@ -0,0 +1,18 @@ +import React, { forwardRef } from 'react'; +import { CheckboxProps } from '@frontegg/react-core'; +import { Checkbox as SemanticCheckbox, CheckboxProps as SemanticCheckboxProps, Form } from 'semantic-ui-react'; +import './style.scss'; +import classNames from 'classnames'; + +const mapper = ({ inForm, fullWidth, onChange, type, className, ...rest }: CheckboxProps): SemanticCheckboxProps => ({ + ...rest, + className: classNames('fe-semantic-checkbox', className), + onChange: (e: any) => onChange?.(e), +}); + +export const Checkbox = forwardRef((props, ref) => { + if (props.fullWidth) { + return ; + } + return ; +}); diff --git a/packages/elements-semantic/src/Checkbox/style.scss b/packages/elements-semantic/src/Checkbox/style.scss new file mode 100644 index 000000000..eb3cd8f0d --- /dev/null +++ b/packages/elements-semantic/src/Checkbox/style.scss @@ -0,0 +1,4 @@ +.fe-semantic-checkbox { + margin-right: calc(var(--element-spacing) * 2); + margin-bottom: var(--element-spacing); +} diff --git a/packages/elements-semantic/src/Dialog/index.tsx b/packages/elements-semantic/src/Dialog/index.tsx new file mode 100644 index 000000000..d360282a2 --- /dev/null +++ b/packages/elements-semantic/src/Dialog/index.tsx @@ -0,0 +1,28 @@ +import React, { FC } from 'react'; +import { DialogProps } from '@frontegg/react-core'; +import { Modal, ModalProps } from 'semantic-ui-react'; +import './style.scss'; +import classNames from 'classnames'; + +const dialogPropsMapper = (props: DialogProps): ModalProps => ({ + size: props.size, + open: props.open, + onOpen: props.onOpen, + onClose: props.onClose, + closeOnDimmerClick: props.closeOnDimmerClick, + closeOnEscape: props.closeOnEscape, + className: classNames('fe-semantic-dialog', props.className), + dimmer: { + className: 'fe-dimmer', + }, +}); + +export const Dialog: FC = (props) => { + const modalProps = dialogPropsMapper(props); + return ( + + {props.header && {props.header}} + {props.children} + + ); +}; diff --git a/packages/elements-semantic/src/Dialog/style.scss b/packages/elements-semantic/src/Dialog/style.scss new file mode 100644 index 000000000..9736a8ce3 --- /dev/null +++ b/packages/elements-semantic/src/Dialog/style.scss @@ -0,0 +1,12 @@ +.fe-dimmer.ui.dimmer { + background-color: var(--color-black-40); +} + +.fe-semantic-dialog { + .fe-dialog__footer { + display: flex; + flex-direction: row; + justify-content: flex-end; + margin-top: 1rem; + } +} diff --git a/packages/elements-semantic/src/Form/index.tsx b/packages/elements-semantic/src/Form/index.tsx new file mode 100644 index 000000000..78584114a --- /dev/null +++ b/packages/elements-semantic/src/Form/index.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import { Form as SemanticForm, FormProps as SemanticFormProps } from 'semantic-ui-react'; +import { FormProps } from '@frontegg/react-core'; + +export class Form extends React.Component { + render() { + return ; + } +} diff --git a/packages/elements-semantic/src/Icon/index.tsx b/packages/elements-semantic/src/Icon/index.tsx new file mode 100644 index 000000000..031a0460a --- /dev/null +++ b/packages/elements-semantic/src/Icon/index.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { IconNames, IconProps } from '@frontegg/react-core'; +import { Icon as SemanticIcon, IconProps as SemanticIconProps } from 'semantic-ui-react'; +import { SemanticICONS } from 'semantic-ui-react/dist/commonjs/generic'; +import classNames from 'classnames'; + +const iconMap: { [K in IconNames]: SemanticICONS } = { + 'down-arrow': 'angle down', + 'left-arrow': 'angle left', + 'person-add': 'add user', + 'right-arrow': 'angle right', + 'sort-arrows-asc': 'trash', + 'sort-arrows-desc': 'trash', + 'sort-arrows': 'trash', + 'up-arrow': 'angle up', + 'vertical-dots': 'ellipsis vertical', + 'visibility-off': 'eye slash', + back: 'angle left', + checkmark: 'checkmark', + copy: 'copy', + delete: 'trash', + edit: 'edit outline', + filters: 'trash', + image: 'image', + indeterminate: 'minus', + search: 'search', + send: 'send', + refresh: 'refresh', + 'calendar-today': 'calendar outline', + flash: 'lightning', + pdf: 'file pdf', + csv: 'grid layout', + visibility: 'eye', + warning: 'warning sign', + list: 'trash', + exit: 'sign-out', + swap: 'exchange', + profile: 'user circle', + globe: 'globe', + close: 'close', +}; +const mapper = (props: IconProps): SemanticIconProps => ({ + size: props.size === 'medium' ? undefined : props.size, + name: iconMap[props.name] ?? props.name, + className: classNames('fe-icon', props.className), + onClick: props.onClick, +}); + +export class Icon extends React.Component { + render() { + return ; + } +} diff --git a/packages/elements-semantic/src/Input/index.tsx b/packages/elements-semantic/src/Input/index.tsx new file mode 100644 index 000000000..dcbd4029e --- /dev/null +++ b/packages/elements-semantic/src/Input/index.tsx @@ -0,0 +1,68 @@ +import React, { forwardRef, useMemo } from 'react'; +import { InputProps } from '@frontegg/react-core'; +import { Form, StrictInputProps, StrictTextAreaProps } from 'semantic-ui-react'; +import { Button } from '../Button'; +import { StrictFormInputProps } from 'semantic-ui-react/dist/commonjs/collections/Form/FormInput'; +import classNames from 'classnames'; +import './style.scss'; + +const mapper = (props: InputProps): StrictInputProps | StrictFormInputProps | StrictTextAreaProps => { + const { inForm, fullWidth, className, prefixIcon, suffixIcon, multiline, ...rest } = props; + const data = { + ...rest, + className: classNames('fe-semantic-input', className, { fluid: multiline && fullWidth }), + } as any; + + if (!multiline) { + data.fluid = fullWidth; + } + + if (prefixIcon) { + data.iconPosition = 'left'; + data.actionPosition = 'left'; + } + return data; +}; + +export const Input = forwardRef( + ({ children, labelButton, multiline, label, ...restProps }, forwardRef) => { + const inputLabel = useMemo( + () => + labelButton ? ( +