├── .eslintrc.json ├── .github ├── dependabot.yml └── workflows │ ├── build-docs.yml │ ├── build-v1.yml │ ├── dependabot-reviewer.yml │ ├── npm-publish-v1.yml │ ├── npm-publish.yml │ └── stale.yml ├── .gitignore ├── .npmignore ├── .nvmrc ├── .storybook ├── main.js └── preview.js ├── LICENSE ├── README.md ├── VERSION ├── jest.config.js ├── package.json ├── scripts └── icons │ ├── generator.js │ └── scss.hbs ├── src ├── assets │ ├── favicon │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ └── favicon.ico │ ├── fonts │ │ ├── argo-icon.eot │ │ ├── argo-icon.svg │ │ ├── argo-icon.ttf │ │ └── argo-icon.woff │ └── images │ │ ├── Download-200.png │ │ ├── Message-200.png │ │ ├── User-Manual-200.png │ │ └── logo.png ├── components │ ├── autocomplete │ │ ├── autocomplete-field.tsx │ │ ├── autocomplete.scss │ │ └── autocomplete.tsx │ ├── checkbox.spec.tsx │ ├── checkbox.tsx │ ├── data-loader.spec.tsx │ ├── data-loader.tsx │ ├── dropdown-menu.tsx │ ├── dropdown │ │ ├── dropdown.scss │ │ └── dropdown.tsx │ ├── duration.tsx │ ├── error-notification.tsx │ ├── form-field │ │ ├── form-field.scss │ │ ├── form-field.tsx │ │ └── index.ts │ ├── help-icon │ │ └── help-icon.tsx │ ├── index.ts │ ├── layout │ │ ├── layout.scss │ │ └── layout.tsx │ ├── logs-viewer │ │ ├── logs-viewer.scss │ │ └── logs-viewer.tsx │ ├── mockup-list │ │ ├── mockup-list.scss │ │ └── mockup-list.tsx │ ├── nav-bar │ │ ├── nav-bar.scss │ │ └── nav-bar.tsx │ ├── navigation.ts │ ├── notifications │ │ ├── notification-manager.ts │ │ └── notifications.tsx │ ├── page │ │ ├── page.scss │ │ └── page.tsx │ ├── popup │ │ ├── popup-manager.spec.tsx │ │ ├── popup-manager.tsx │ │ ├── popup.scss │ │ └── popup.tsx │ ├── select │ │ ├── select.scss │ │ └── select.tsx │ ├── slide-contents │ │ ├── slide-contents.scss │ │ └── slide-contents.tsx │ ├── sliding-panel │ │ ├── sliding-panel.scss │ │ └── sliding-panel.tsx │ ├── tabs │ │ ├── tabs.scss │ │ └── tabs.tsx │ ├── ticker.tsx │ ├── tooltip │ │ └── tooltip.tsx │ ├── top-bar │ │ ├── top-bar.scss │ │ └── top-bar.tsx │ └── utils.ts ├── context.tsx ├── index.ts ├── models │ ├── index.ts │ └── kubernetes.ts ├── setupTests.ts └── styles │ ├── argo-icon.scss │ ├── config.scss │ ├── elements │ ├── buttons.scss │ ├── containers.scss │ ├── form-controls.scss │ ├── table-list.scss │ └── utils.scss │ ├── icons │ ├── add-attribute.svg │ ├── addstorage.svg │ ├── admin-access.svg │ ├── application.svg │ ├── applications.svg │ ├── approval.svg │ ├── appstore.svg │ ├── artifact.svg │ ├── aws-logo.svg │ ├── axcluster.svg │ ├── axlogo.svg │ ├── back.svg │ ├── bitbucket.svg │ ├── branch.svg │ ├── build.svg │ ├── calendar-menu.svg │ ├── calendar.svg │ ├── cancel-2-1.svg │ ├── cancel-2.svg │ ├── cancel.svg │ ├── cashboard.svg │ ├── catalog.svg │ ├── checked.svg │ ├── checkmark.svg │ ├── checkout.svg │ ├── clock.svg │ ├── close.svg │ ├── code-file.svg │ ├── collapse-arrow.svg │ ├── commit.svg │ ├── concurrent-usage.svg │ ├── configs.svg │ ├── configurations.svg │ ├── connect.svg │ ├── console.svg │ ├── dashboards.svg │ ├── delete-attribute.svg │ ├── delete.svg │ ├── deploy.svg │ ├── deployment.svg │ ├── docker.svg │ ├── docs.svg │ ├── download.svg │ ├── edit-property.svg │ ├── edit-user.svg │ ├── edit.svg │ ├── expand-arrow.svg │ ├── expand.svg │ ├── external-link.svg │ ├── fav-selected.svg │ ├── fav.svg │ ├── filter.svg │ ├── fixture.svg │ ├── fixturecat.svg │ ├── fixturenew.svg │ ├── folder-lock.svg │ ├── gcp-logo.svg │ ├── git.svg │ ├── github.svg │ ├── helm.svg │ ├── help.svg │ ├── hosts.svg │ ├── import.svg │ ├── info.svg │ ├── integrations.svg │ ├── intlogo.svg │ ├── jira.svg │ ├── job.svg │ ├── label.svg │ ├── launcher.svg │ ├── lock.svg │ ├── logout.svg │ ├── manage.svg │ ├── menu-2.svg │ ├── metrics.svg │ ├── more.svg │ ├── new.svg │ ├── nonmarkingreturn.svg │ ├── notification.svg │ ├── oci.svg │ ├── pencil.svg │ ├── play-2.svg │ ├── play.svg │ ├── pod.svg │ ├── policies.svg │ ├── policy.svg │ ├── profile.svg │ ├── push.svg │ ├── report-card.svg │ ├── resubmit-failed.svg │ ├── retry.svg │ ├── right-navigation-toolbar.svg │ ├── safe.svg │ ├── sales-channels.svg │ ├── sample.svg │ ├── scale.svg │ ├── search.svg │ ├── settings.svg │ ├── slack-02.svg │ ├── stop-property.svg │ ├── stop.svg │ ├── storageclass.svg │ ├── storageprovider.svg │ ├── tag.svg │ ├── template.svg │ ├── terminate.svg │ ├── test.svg │ ├── timeline.svg │ ├── tools.svg │ ├── user-groups.svg │ ├── user-profile.svg │ ├── user.svg │ ├── users.svg │ ├── volume.svg │ ├── warning.svg │ ├── workflow.svg │ └── yaml.svg │ ├── main.scss │ └── theme.scss ├── stories ├── data-loader.stories.tsx ├── dropdown.stories.tsx ├── forms.stories.tsx ├── logs-viewer.stories.tsx ├── notifications.stories.tsx ├── page.stories.tsx ├── popup.stories.tsx ├── select.stories.tsx ├── table.stories.tsx ├── tabs.stories.tsx └── utils.tsx ├── tsconfig.json ├── v2 ├── .babelrc ├── .gitignore ├── .prettierrc ├── .storybook │ ├── Argo.js │ ├── images │ │ ├── argo-favicon.png │ │ └── argo-icon-color-square.png │ ├── main.js │ ├── manager.js │ └── preview.js ├── README.md ├── components │ ├── action-button │ │ ├── action-button.scss │ │ ├── action-button.stories.tsx │ │ └── action-button.tsx │ ├── alert │ │ ├── alert.scss │ │ ├── alert.stories.tsx │ │ └── alert.tsx │ ├── autocomplete │ │ ├── autocomplete.scss │ │ ├── autocomplete.stories.tsx │ │ └── autocomplete.tsx │ ├── box │ │ ├── box.scss │ │ ├── box.stories.tsx │ │ └── box.tsx │ ├── checkbox │ │ ├── checkbox.scss │ │ └── checkbox.tsx │ ├── effect-div │ │ ├── effect-div.scss │ │ ├── effect-div.stories.tsx │ │ └── effect-div.tsx │ ├── filler │ │ ├── filler.scss │ │ └── filler.tsx │ ├── flexy │ │ └── flexy.tsx │ ├── header │ │ ├── header.scss │ │ ├── header.stories.tsx │ │ └── header.tsx │ ├── index.ts │ ├── info-item │ │ ├── info-item-row.stories.tsx │ │ ├── info-item.scss │ │ ├── info-item.stories.tsx │ │ └── info-item.tsx │ ├── input │ │ ├── input.scss │ │ ├── input.stories.tsx │ │ └── input.tsx │ ├── menu │ │ ├── menu.scss │ │ ├── menu.stories.tsx │ │ └── menu.tsx │ ├── row │ │ ├── row.scss │ │ └── row.tsx │ ├── text │ │ ├── text.scss │ │ ├── text.stories.tsx │ │ └── text.tsx │ ├── theme-div │ │ ├── theme-div.stories.tsx │ │ └── theme-div.tsx │ ├── theme-toggle │ │ └── theme-toggle.tsx │ ├── ticker │ │ ├── ticker.scss │ │ ├── ticker.stories.tsx │ │ └── ticker.tsx │ ├── tooltip │ │ ├── tooltip.scss │ │ ├── tooltip.stories.tsx │ │ └── tooltip.tsx │ └── wait-for │ │ ├── loading-bar.scss │ │ ├── spinner.scss │ │ └── wait-for.tsx ├── index.ts ├── shared │ ├── context │ │ └── theme.tsx │ ├── index.ts │ └── keypress.tsx ├── styles │ └── colors.scss └── utils │ ├── index.ts │ ├── utils.tsx │ └── watch.ts └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:@typescript-eslint/recommended", 5 | "plugin:react/recommended" 6 | ], 7 | "parser": "@typescript-eslint/parser", 8 | "parserOptions": { 9 | "ecmaVersion": "latest", 10 | "sourceType": "module" 11 | }, 12 | "plugins": ["@typescript-eslint", "react"], 13 | "rules": { 14 | "@typescript-eslint/no-explicit-any": "off", 15 | "@typescript-eslint/no-non-null-assertion": "off" 16 | }, 17 | "overrides": [ 18 | { 19 | "files": "./stories/*.tsx", 20 | "rules": { 21 | "react/display-name": "off", 22 | "@typescript-eslint/no-unused-vars": "off" 23 | } 24 | } 25 | ], 26 | "settings": { 27 | "react": { 28 | "version": "detect" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # prod dependencies 4 | - package-ecosystem: "npm" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | day: "saturday" 9 | # ignore all non-security updates: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#open-pull-requests-limit 10 | open-pull-requests-limit: 0 11 | labels: 12 | - type/dependencies 13 | - javascript 14 | commit-message: 15 | prefix: chore(deps) 16 | prefix-development: chore(deps-dev) 17 | 18 | # build / CI dependencies 19 | - package-ecosystem: "github-actions" 20 | directory: "/" 21 | schedule: 22 | interval: "weekly" 23 | day: "saturday" 24 | # ignore all non-security updates: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#open-pull-requests-limit 25 | open-pull-requests-limit: 0 26 | labels: 27 | - type/dependencies 28 | - github_actions 29 | commit-message: 30 | prefix: chore(deps) 31 | -------------------------------------------------------------------------------- /.github/workflows/build-docs.yml: -------------------------------------------------------------------------------- 1 | name: Build Storybook Docs v2 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | build-docs: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: actions/setup-node@v4 12 | with: 13 | node-version-file: ".nvmrc" 14 | - run: yarn install 15 | - run: yarn build-v2 16 | -------------------------------------------------------------------------------- /.github/workflows/build-v1.yml: -------------------------------------------------------------------------------- 1 | name: Build v1 2 | 3 | on: 4 | push: 5 | pull_request: 6 | branches: 7 | - "master" 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version-file: ".nvmrc" 17 | - run: yarn install 18 | - run: yarn deduplicate 19 | - run: yarn build 20 | - run: yarn lint 21 | - run: yarn tsc -p tsconfig.json 22 | - run: yarn test --ci --runInBand 23 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-reviewer.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/automating-dependabot-with-github-actions 2 | name: Approve and enable auto-merge for dependabot 3 | on: pull_request 4 | 5 | permissions: 6 | pull-requests: write 7 | contents: write 8 | 9 | jobs: 10 | review: 11 | runs-on: ubuntu-latest 12 | if: ${{ github.actor == 'dependabot[bot]' }} 13 | steps: 14 | - name: Dependabot metadata 15 | id: metadata 16 | uses: dependabot/fetch-metadata@v1.6.0 17 | with: 18 | github-token: "${{ secrets.GITHUB_TOKEN }}" 19 | - name: Approve PR 20 | run: gh pr review --approve "$PR_URL" 21 | env: 22 | PR_URL: ${{github.event.pull_request.html_url}} 23 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 24 | - name: Enable auto-merge for Dependabot PRs 25 | run: gh pr merge --auto --squash "$PR_URL" 26 | env: 27 | PR_URL: ${{github.event.pull_request.html_url}} 28 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} -------------------------------------------------------------------------------- /.github/workflows/npm-publish-v1.yml: -------------------------------------------------------------------------------- 1 | name: Release NPM package v1 2 | 3 | on: 4 | push: 5 | tags: 6 | - v1.* 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version-file: ".nvmrc" 16 | - run: npm publish 17 | env: 18 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 19 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Release NPM package v2 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | tag: 7 | description: Git tag to build release from 8 | required: true 9 | 10 | jobs: 11 | publish: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | ref: ${{ github.event.inputs.tag }} 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version-file: ".nvmrc" 20 | - run: cd v2 && npm publish 21 | env: 22 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 23 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/actions/stale 2 | name: Mark stale issues and pull requests 3 | 4 | on: 5 | schedule: 6 | - cron: '0 2 * * *' # once a day at 2am 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | stale: 13 | permissions: 14 | issues: write # for commenting on an issue and editing labels 15 | pull-requests: write # for commenting on a PR and editing labels 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 19 | with: 20 | repo-token: ${{ secrets.GITHUB_TOKEN }} 21 | # timing 22 | days-before-stale: 14 # 2 weeks of inactivity 23 | days-before-close: 14 # 2 more weeks of inactivity 24 | # labels to watch for, add, and remove 25 | only-labels: 'problem/more information needed' # only mark issues/PRs as stale if they have this label 26 | labels-to-remove-when-unstale: 'problem/more information needed' # remove label when unstale -- should be manually added back if information is insufficient 27 | stale-issue-label: 'problem/stale' 28 | stale-pr-label: 'problem/stale' 29 | # automated messages to issue/PR authors 30 | stale-issue-message: > 31 | This issue has been automatically marked as stale because it has not had recent activity and needs more information. 32 | It will be closed if no further activity occurs. 33 | stale-pr-message: > 34 | This PR has been automatically marked as stale because it has not had recent activity and needs further changes. 35 | It will be closed if no further activity occurs. 36 | close-issue-message: > 37 | This issue has been closed due to inactivity and lack of information. 38 | If you still encounter this issue, please add the requested information and re-open. 39 | close-pr-message: 40 | This PR has been closed due to inactivity and lack of changes. 41 | If you would like to still work on this PR, please address the review comments and re-open. 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | bundle 4 | .vscode 5 | .idea 6 | *.log 7 | coverage/ 8 | .DS_STORE 9 | docs/ 10 | v2/storybook-static/ 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | v2 2 | node_modules -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20 2 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | stories: ['../stories/*.stories.tsx'], 5 | addons: ['@storybook/addon-essentials'], 6 | typescript: { 7 | check: false, // typecheck separately 8 | reactDocgen: false, // substantially improves performance: https://github.com/storybookjs/storybook/issues/22164#issuecomment-1603627308 9 | }, 10 | webpackFinal: async (config, {configType}) => { 11 | config.devtool = false; // perf per: https://github.com/storybookjs/storybook/issues/19736#issuecomment-1478103817 12 | config.module.rules.push({ 13 | test: /\.scss$/, 14 | exclude: /node_modules/, 15 | include: path.resolve(__dirname, '../'), 16 | sideEffects: true, // get side-effect styles to load per: https://github.com/storybookjs/storybook/issues/4690#issuecomment-435909433 17 | loader: 'style-loader!raw-loader!sass-loader' 18 | }); 19 | return config; 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | // import '../src/styles/main.scss'; -- this seems to not work and also makes the Storybook freeze for multiple minutes 😕 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Argo UI Components 2 | 3 | Argo Image 4 | 5 | Set of React components used by [Argo Workflows](https://github.com/argoproj/argo-workflows), [Argo CD](https://github.com/argoproj/argo-cd), and [Argo Rollouts](https://github.com/argoproj/argo-rollouts). 6 | 7 | ## Build & Run 8 | 9 | 1. Install Toolset: [NodeJS](https://nodejs.org/en/download/) and [Yarn v1](https://classic.yarnpkg.com/en/docs) 10 | 1. Install Dependencies: run `yarn install` 11 | 1. Run: `yarn start` - starts the [Storybook v6](https://storybook.js.org/docs/6.5/get-started/install) dev server 12 | 13 | ## Local Development 14 | 15 | To test your changes locally against Argo CD or another Argo project, we recommend using [`yalc`](https://github.com/wclr/yalc). 16 | 17 | First, install `yalc`: 18 | 19 | ```sh 20 | npm i -g yalc 21 | ``` 22 | 23 | Next, in your local `argo-ui` directory, run 24 | 25 | ```sh 26 | yalc publish 27 | ``` 28 | 29 | Finally, in your local `argo-cd/ui` directory, run 30 | 31 | ```sh 32 | yalc add argo-ui 33 | ``` 34 | 35 | Your local changes to the `argo-ui` package will now be seen by your local `argo-cd`. 36 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | v2.2.1 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | collectCoverage: true, 4 | collectCoverageFrom: [ 5 | 'src/**/*.{ts,tsx}' 6 | ], 7 | "moduleNameMapper": { 8 | "\\.(css|less|scss|sass)$": "identity-obj-proxy" 9 | }, 10 | snapshotSerializers: ['enzyme-to-json/serializer'], 11 | setupFilesAfterEnv: ['src/setupTests.ts'], 12 | globals: { 13 | 'ts-jest': { 14 | tsconfig: '/tsconfig.json' 15 | } 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /scripts/icons/generator.js: -------------------------------------------------------------------------------- 1 | 'use strict;'; 2 | 3 | const webfontsGenerator = require('webfonts-generator'); 4 | const path = require('path'); 5 | const glob = require('glob'); 6 | const fs = require('fs'); 7 | const FONT_TYPES = ['svg', 'ttf', 'woff', 'eot']; 8 | 9 | webfontsGenerator({ 10 | files: glob.sync('src/styles/icons/*.svg'), 11 | dest: 'src/assets/fonts', 12 | fontName: 'argo-icon', 13 | types: FONT_TYPES, 14 | cssTemplate: path.resolve(__dirname, './scss.hbs'), 15 | templateOptions: { 16 | baseTag: 'i', 17 | classPrefix: 'argo-icon-', 18 | baseSelector: '.argo-icon' 19 | } 20 | }, function (error) { 21 | if (error) { 22 | console.log('Fail!', error); 23 | } else { 24 | const scss = fs.readFileSync('src/assets/fonts/argo-icon.css', 'utf-8').replace(/url\(\"argo-icon/g, 'url\($argo-icon-fonts-root + \"argo-icon'); 25 | fs.writeFileSync('src/styles/argo-icon.scss', scss); 26 | fs.unlinkSync('src/assets/fonts/argo-icon.css'); 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /scripts/icons/scss.hbs: -------------------------------------------------------------------------------- 1 | // AUTOGENERATED. Don't modify this file. Use 'yarn utils:icons' to regenerate style after new icons added. 2 | 3 | $argo-icon-fonts-root: '/' !default; 4 | 5 | [class^='{{fontName}}'], 6 | [class*=' {{fontName}}'] { 7 | display: inline-block; 8 | vertical-align: middle; 9 | font: normal normal normal 18px/1 '{{fontName}}'; 10 | font-size: inherit; 11 | -webkit-font-smoothing: antialiased; 12 | -moz-osx-font-smoothing: grayscale; 13 | } 14 | 15 | @font-face { 16 | font-family: "{{ fontName }}"; 17 | src: {{{ src }}}; 18 | } 19 | 20 | {{ baseTag }} { 21 | line-height: 1; 22 | } 23 | 24 | [data-icon]:before { 25 | font-family: "{{ fontName }}" !important; 26 | content: attr(data-icon); 27 | font-style: normal !important; 28 | font-weight: normal !important; 29 | font-variant: normal !important; 30 | text-transform: none !important; 31 | speak: none; 32 | line-height: 1; 33 | -webkit-font-smoothing: antialiased; 34 | -moz-osx-font-smoothing: grayscale; 35 | } 36 | 37 | [class^="{{ fontName }}-"]:before, 38 | [class*=" {{ fontName }}-"]:before { 39 | font-family: "{{ fontName }}" !important; 40 | font-style: normal !important; 41 | font-weight: normal !important; 42 | font-variant: normal !important; 43 | text-transform: none !important; 44 | speak: none; 45 | line-height: 1; 46 | -webkit-font-smoothing: antialiased; 47 | -moz-osx-font-smoothing: grayscale; 48 | } 49 | 50 | [class^="{{ fontName }}-"]:before, 51 | [class*=" {{ fontName }}-"]:before { 52 | font-family: "{{ fontName }}" !important; 53 | font-style: normal !important; 54 | font-weight: normal !important; 55 | font-variant: normal !important; 56 | text-transform: none !important; 57 | speak: none; 58 | line-height: 1; 59 | -webkit-font-smoothing: antialiased; 60 | -moz-osx-font-smoothing: grayscale; 61 | } 62 | 63 | {{# each codepoints }} 64 | .{{ ../classPrefix }}{{ @key }}:before { 65 | content: "\\{{ this }}"; 66 | } 67 | {{/ each }} 68 | -------------------------------------------------------------------------------- /src/assets/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/argoproj/argo-ui/5cf36101733ce43eed57242a12389f2a7e40bd2b/src/assets/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /src/assets/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/argoproj/argo-ui/5cf36101733ce43eed57242a12389f2a7e40bd2b/src/assets/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /src/assets/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/argoproj/argo-ui/5cf36101733ce43eed57242a12389f2a7e40bd2b/src/assets/favicon/favicon.ico -------------------------------------------------------------------------------- /src/assets/fonts/argo-icon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/argoproj/argo-ui/5cf36101733ce43eed57242a12389f2a7e40bd2b/src/assets/fonts/argo-icon.eot -------------------------------------------------------------------------------- /src/assets/fonts/argo-icon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/argoproj/argo-ui/5cf36101733ce43eed57242a12389f2a7e40bd2b/src/assets/fonts/argo-icon.ttf -------------------------------------------------------------------------------- /src/assets/fonts/argo-icon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/argoproj/argo-ui/5cf36101733ce43eed57242a12389f2a7e40bd2b/src/assets/fonts/argo-icon.woff -------------------------------------------------------------------------------- /src/assets/images/Download-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/argoproj/argo-ui/5cf36101733ce43eed57242a12389f2a7e40bd2b/src/assets/images/Download-200.png -------------------------------------------------------------------------------- /src/assets/images/Message-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/argoproj/argo-ui/5cf36101733ce43eed57242a12389f2a7e40bd2b/src/assets/images/Message-200.png -------------------------------------------------------------------------------- /src/assets/images/User-Manual-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/argoproj/argo-ui/5cf36101733ce43eed57242a12389f2a7e40bd2b/src/assets/images/User-Manual-200.png -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/argoproj/argo-ui/5cf36101733ce43eed57242a12389f2a7e40bd2b/src/assets/images/logo.png -------------------------------------------------------------------------------- /src/components/autocomplete/autocomplete-field.tsx: -------------------------------------------------------------------------------- 1 | import {default as classNames} from 'classnames'; 2 | import * as React from 'react'; 3 | import * as ReactForm from 'react-form'; 4 | 5 | import {Autocomplete, AutocompleteOption, AutocompleteProps} from './autocomplete'; 6 | 7 | export const AutocompleteField = ReactForm.FormField((props: AutocompleteProps & {fieldApi: ReactForm.FieldApi; className?: string}) => { 8 | const {fieldApi: {getValue, setValue, setTouched}, ...rest} = props; 9 | const value = getValue(); 10 | 11 | const [forceHasValue, setForceHasValue] = React.useState(false); 12 | 13 | return ( 14 | { 17 | setValue(item.value); 18 | }} 19 | inputProps={{ 20 | className: props.className, 21 | style: {borderBottom: 'none', position: 'unset'}, 22 | }} 23 | value={value} 24 | renderInput={(inputProps) => ( 25 | { 29 | if (inputProps.onFocus) { 30 | inputProps.onFocus(e); 31 | } 32 | setForceHasValue(true); 33 | }} 34 | onBlur={(e) => { 35 | if (inputProps.onBlur) { 36 | inputProps.onBlur(e); 37 | } 38 | setForceHasValue(false); 39 | setTouched(true); 40 | }} 41 | /> 42 | )} 43 | onChange={(val) => setValue(val.target.value)} 44 | {...rest} 45 | /> 46 | ); 47 | }) as React.ComponentType; 48 | -------------------------------------------------------------------------------- /src/components/autocomplete/autocomplete.scss: -------------------------------------------------------------------------------- 1 | .autocomplete { 2 | &__items__item { 3 | &:last-child { 4 | border-bottom: none; 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/components/checkbox.spec.tsx: -------------------------------------------------------------------------------- 1 | import {shallow} from 'enzyme'; 2 | import * as React from 'react'; 3 | import {Checkbox} from './checkbox'; 4 | 5 | describe('Checkbox', () => { 6 | it('should invoke onChange when clicked', () => { 7 | const onChange = jest.fn(); 8 | 9 | const checkbox = shallow(); 10 | 11 | checkbox.find('input').simulate('change'); 12 | 13 | expect(onChange).toHaveBeenCalledWith(false); 14 | }); 15 | 16 | it.each([true, false])('should render the checkbox with the correct value (%s)', (checked) => { 17 | const checkbox = shallow(); 18 | expect(checkbox.find('input').filterWhere((item) => item.prop('checked') === checked).length).toBe(1); 19 | }); 20 | 21 | it('should set the id of the resulting input', () => { 22 | const checkbox = shallow(); 23 | 24 | expect(checkbox.find('input').is('#foo')).toBe(true); 25 | checkbox.setProps({id: 'bar'}); 26 | expect(checkbox.find('input').is('#bar')).toBe(true); 27 | }); 28 | 29 | it('should set disabled of the resulting input', () => { 30 | const checkbox = shallow(); 31 | 32 | expect(checkbox.find('input').filterWhere((item) => item.prop('disabled') === true).length).toBe(1); 33 | checkbox.setProps({disabled: false}); 34 | expect(checkbox.find('input').filterWhere((item) => item.prop('disabled') === false).length).toBe(1); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/components/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export const Checkbox = (props: { 4 | disabled?: boolean, 5 | checked?: boolean, 6 | onChange?: (val: boolean) => any, 7 | id?: string, 8 | }) => ( 9 | 10 | props.onChange && props.onChange(!props.checked)}/> 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /src/components/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { DropDown } from './dropdown/dropdown'; 3 | 4 | export interface MenuItem { 5 | title: string | React.ReactElement; 6 | iconClassName?: string; 7 | action: () => any; 8 | } 9 | 10 | export interface DropDownMenuProps { 11 | items: MenuItem[]; 12 | anchor: React.ComponentType; 13 | qeId?: string; 14 | } 15 | 16 | export class DropDownMenu extends React.PureComponent { 17 | 18 | private dropdown: DropDown; 19 | 20 | public render() { 21 | return ( 22 | this.dropdown = dropdown} qeId={this.props.qeId}> 23 |
    24 | {this.props.items.map((item, i) =>
  • this.onItemClick(item, event)} key={i}> 26 | {item.iconClassName && } {item.title} 27 |
  • )} 28 |
29 |
30 | ); 31 | } 32 | 33 | private onItemClick(item: MenuItem, event: any) { 34 | item.action(); 35 | event.stopPropagation(); 36 | if (this.dropdown) { 37 | this.dropdown.close(); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/components/dropdown/dropdown.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/config'; 2 | 3 | .argo-dropdown { 4 | display: inline-block; 5 | 6 | &__anchor { 7 | cursor: pointer; 8 | } 9 | 10 | &__content { 11 | position: fixed; 12 | z-index: $notifications-z-index; 13 | padding: 1rem; 14 | background-color: $white-color; 15 | box-shadow: 0 0 4px rgba(#000, .2); 16 | transition: opacity .2s, transform .2s, visibility .2s; 17 | 18 | &:not(.opened) { 19 | transform: translateY(-30%); 20 | opacity: 0; 21 | visibility: hidden; 22 | transition: opacity .2s, transform .2s .2s, visibility .2s; 23 | } 24 | 25 | &.is-menu { 26 | overflow: auto; 27 | max-height: 360px; 28 | padding: 0; 29 | border: 0; 30 | 31 | ul { 32 | margin: 0; 33 | list-style-type: none; 34 | white-space: nowrap; 35 | text-align: left; 36 | cursor: pointer; 37 | min-width: 150px; 38 | 39 | li { 40 | padding: 0.5em 1em; 41 | font-size: 14px; 42 | border-bottom: 1px solid $argo-color-gray-2; 43 | color: $argo-color-gray-6; 44 | cursor: pointer; 45 | 46 | i { 47 | margin-right: 2px; 48 | } 49 | 50 | &:hover { 51 | background-color: $argo-color-gray-1; 52 | } 53 | 54 | &:last-child { 55 | border-bottom: none; 56 | } 57 | } 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/components/duration.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { formatDuration } from '../../v2'; 4 | 5 | /** 6 | * Output a string duration from a number of seconds 7 | * 8 | * @param {number} props.durationS - The number of seconds. 9 | */ 10 | export function Duration(props: {durationS: number}) { 11 | return {formatDuration(props.durationS, 2)}; 12 | } 13 | -------------------------------------------------------------------------------- /src/components/error-notification.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export const ErrorNotification = (props: { title?: string; e: any }) => { 4 | let message; 5 | if (props.e.response && props.e.response.text) { 6 | try { 7 | const apiError = JSON.parse(props.e.response.text); 8 | if (apiError.error) { 9 | message = apiError.error; 10 | } 11 | } catch { 12 | // do nothing 13 | } 14 | } 15 | if (!message) { 16 | if (props.e.message) { 17 | message = props.e.message; 18 | } 19 | } 20 | if (!message) { 21 | message = 'Internal error'; 22 | } 23 | if (props.title) { 24 | message = `${props.title}: ${message}`; 25 | } 26 | return ({message}); 27 | }; 28 | -------------------------------------------------------------------------------- /src/components/form-field/form-field.scss: -------------------------------------------------------------------------------- 1 | .form-field__select { 2 | border-bottom: none !important; 3 | } 4 | 5 | .form-field__select:not(.argo-has-value) { 6 | &::before { 7 | content: ''; 8 | display: inline-block; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/components/form-field/index.ts: -------------------------------------------------------------------------------- 1 | export * from './form-field'; 2 | -------------------------------------------------------------------------------- /src/components/help-icon/help-icon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {Tooltip} from '../tooltip/tooltip'; 3 | 4 | export const HelpIcon = ({title}: {title: React.ReactChild | React.ReactChild[]}) => ( 5 | 6 | 7 | {' '} 8 | 9 | 10 | 11 | ); 12 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | require('../styles/main.scss'); 2 | 3 | export { Utils } from './utils'; 4 | export { Layout } from './layout/layout'; 5 | export { Page, PageContext, type PageContextProps } from './page/page'; 6 | export { MockupList } from './mockup-list/mockup-list'; 7 | export { DropDown } from './dropdown/dropdown'; 8 | export { DropDownMenu, type DropDownMenuProps, type MenuItem } from './dropdown-menu'; 9 | export { Checkbox } from './checkbox'; 10 | export { type TopBarProps, type Toolbar, type TopBarFilter } from './top-bar/top-bar'; 11 | export { type Tab, Tabs } from './tabs/tabs'; 12 | export { Duration } from './duration'; 13 | export { SlidingPanel } from './sliding-panel/sliding-panel'; 14 | export { LogsViewer } from './logs-viewer/logs-viewer'; 15 | export * from './notifications/notifications'; 16 | export * from './notifications/notification-manager'; 17 | export * from './popup/popup'; 18 | export * from './popup/popup-manager'; 19 | export { Select, type SelectOption, type SelectProps } from './select/select'; 20 | export { HelpIcon } from './help-icon/help-icon'; 21 | export { Tooltip } from './tooltip/tooltip'; 22 | export * from './ticker'; 23 | export * from './data-loader'; 24 | export * from './error-notification'; 25 | export * from './navigation'; 26 | export * from './form-field'; 27 | export * from './slide-contents/slide-contents'; 28 | export * from './autocomplete/autocomplete'; 29 | export * from './autocomplete/autocomplete-field'; 30 | -------------------------------------------------------------------------------- /src/components/layout/layout.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/config'; 2 | @import '../../styles/theme'; 3 | 4 | .layout { 5 | overflow: hidden; 6 | @include themify($themes) { 7 | background-color: themed('background-1'); 8 | } 9 | &__loader { 10 | @include themify($themes) { 11 | background-color: themed('layout-loader-bg'); 12 | } 13 | position: fixed; 14 | left: 0; 15 | top: 0; 16 | right: 0; 17 | bottom: 0; 18 | z-index: 999999; 19 | 20 | .loader-inner { 21 | left: 50%; 22 | position: absolute; 23 | bottom: 50%; 24 | transform: translateX(-50%) translateY(-50%); 25 | & > div { 26 | background: #F07A51; 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/layout/layout.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {NavBar, NavBarStyle} from '../nav-bar/nav-bar'; 3 | 4 | require('./layout.scss'); 5 | 6 | export interface LayoutProps { 7 | navItems: Array<{ path: string; iconClassName: string; title: string; }>; 8 | version?: () => React.ReactElement; 9 | navBarStyle?: NavBarStyle; 10 | theme?: string; 11 | children?: React.ReactNode; 12 | } 13 | 14 | export const Layout = (props: LayoutProps) => ( 15 |
16 |
17 | 18 | {props.children} 19 |
20 |
21 | ); 22 | -------------------------------------------------------------------------------- /src/components/logs-viewer/logs-viewer.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/config'; 2 | @import 'node_modules/xterm/css/xterm'; 3 | 4 | .logs-viewer { 5 | font: normal 13px/1.2 'Courier', sans-serif; 6 | line-height: 20px; 7 | height: 100%; 8 | overflow: hidden; 9 | 10 | &__container { 11 | height: 100%; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/mockup-list/mockup-list.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/config'; 2 | 3 | @keyframes wavemove { 4 | from {left: -100%;} 5 | to {left: 100%;} 6 | } 7 | $wave-time: 2s; 8 | 9 | .mockup-list { 10 | 11 | &__item { 12 | position: relative; 13 | overflow: hidden; 14 | font-size: .875em; 15 | color: $argo-color-gray-6; 16 | background-color: #fff; 17 | border-radius: 4px; 18 | box-shadow: 1px 2px 3px rgba($argo-color-gray-9, .1); 19 | opacity: 0.3; 20 | } 21 | 22 | &__wave-loader { 23 | position: relative; 24 | overflow: hidden; 25 | background: linear-gradient(to right, #fff, $argo-color-teal-3, #fff); 26 | width: 50%; 27 | height: 100%; 28 | animation: wavemove $wave-time infinite; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/mockup-list/mockup-list.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export interface MockupListProps { height?: number; marginTop?: number; } 4 | 5 | require('./mockup-list.scss'); 6 | 7 | export class MockupList extends React.Component { 8 | 9 | constructor(props: MockupListProps) { 10 | super(props); 11 | this.state = { count: 0 }; 12 | } 13 | 14 | public componentDidMount() { 15 | this.setState({count: Math.round(window.innerHeight / ( this.itemHeight + this.itemMarginTop )) }); 16 | } 17 | 18 | public render() { 19 | const items = []; 20 | for (let i = 0; i < this.state.count; i++) { 21 | items.push(i); 22 | } 23 | return ( 24 |
25 | { items.map((i: number) => ( 26 |
27 |
28 |
29 | ))} 30 |
); 31 | } 32 | 33 | private get itemHeight() { 34 | return this.props.height || 60; 35 | } 36 | 37 | private get itemMarginTop() { 38 | return this.props.marginTop || 20; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/components/nav-bar/nav-bar.tsx: -------------------------------------------------------------------------------- 1 | import {default as classNames} from 'classnames'; 2 | import * as PropTypes from 'prop-types'; 3 | import * as React from 'react'; 4 | 5 | import { AppContext } from '../../context'; 6 | import {Tooltip} from '../tooltip/tooltip'; 7 | 8 | require('./nav-bar.scss'); 9 | 10 | export interface NavBarProps { 11 | items: Array<{ path: string; iconClassName: string; title: string; }>; 12 | version?: () => React.ReactElement; 13 | style?: NavBarStyle; 14 | } 15 | 16 | export interface NavBarStyle { 17 | backgroundColor?: string; 18 | } 19 | 20 | export function isActiveRoute(locationPath: string, path: string) { 21 | return locationPath === path || locationPath.startsWith(`${path}/`); 22 | } 23 | 24 | export const NavBar: React.FunctionComponent = (props: NavBarProps, context: AppContext) => { 25 | const locationPath = context.router.route.location.pathname; 26 | const navBarStyle = { 27 | ...(props.style?.backgroundColor && {background: `linear-gradient(to bottom, ${props.style?.backgroundColor}, #999`}), 28 | }; 29 | return ( 30 |
= 10, 32 | })} style={navBarStyle}> 33 |
34 | Argo 35 |
{props.version && props.version()}
36 | {(props.items || []).map((item) => ( 37 | 38 |
context.router.history.push(item.path)}> 40 | 41 |
42 |
43 | ))} 44 |
45 |
46 | ); 47 | }; 48 | 49 | NavBar.contextTypes = { 50 | router: PropTypes.object, 51 | }; 52 | -------------------------------------------------------------------------------- /src/components/navigation.ts: -------------------------------------------------------------------------------- 1 | import { History } from 'history'; 2 | import * as React from 'react'; 3 | 4 | export interface NavigationApi { 5 | goto(path: string, query?: {[name: string]: any}, options?: { event?: React.MouseEvent, replace?: boolean }): void; 6 | } 7 | 8 | export class NavigationManager implements NavigationApi { 9 | 10 | private history: History; 11 | 12 | constructor(history: History) { 13 | this.history = history; 14 | } 15 | 16 | public goto(path: string, query: {[name: string]: any} = {}, options?: { event?: React.MouseEvent, replace?: boolean }): void { 17 | if (path.startsWith('.')) { 18 | path = this.history.location.pathname + path.slice(1); 19 | } 20 | const noPathChange = path === this.history.location.pathname; 21 | const params = noPathChange ? new URLSearchParams(this.history.location.search) : new URLSearchParams(); 22 | for (const name of Object.keys(query)) { 23 | const val = query[name]; 24 | params.delete(name); 25 | if (val !== undefined && val !== null) { 26 | if (val instanceof Array) { 27 | for (const item of val) { 28 | params.append(name, item); 29 | } 30 | } else { 31 | params.set(name, val); 32 | } 33 | } 34 | } 35 | const urlQuery = params.toString(); 36 | if (urlQuery !== '') { 37 | path = `${path}?${urlQuery}`; 38 | } 39 | options = options || {}; 40 | if (options.event && (options.event.metaKey || options.event.ctrlKey || options.event.button === 1)) { 41 | window.open(path, '_blank'); 42 | } else { 43 | if (options.replace) { 44 | this.history.replace(path); 45 | } else { 46 | this.history.push(path); 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/components/notifications/notification-manager.ts: -------------------------------------------------------------------------------- 1 | import { ReplaySubject } from 'rxjs'; 2 | import { NotificationInfo } from './notifications'; 3 | 4 | export interface NotificationsApi { 5 | show(notification: NotificationInfo, autoHideMs?: number): void; 6 | } 7 | 8 | export class NotificationsManager { 9 | private readonly notificationsSubject = new ReplaySubject(1); 10 | 11 | public get notifications() { 12 | return this.notificationsSubject.asObservable(); 13 | } 14 | 15 | public show(notification: NotificationInfo) { 16 | this.notificationsSubject.next(notification); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/components/page/page.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/config'; 2 | 3 | .page { 4 | padding-left: $nav-width; 5 | padding-top: $top-bar-height; 6 | 7 | &__content-wrapper { 8 | position: relative; 9 | display: block; 10 | min-height: calc(100vh - #{$top-bar-height}); 11 | margin: 0 auto; 12 | overflow: hidden; 13 | } 14 | 15 | &__top-bar { 16 | position: fixed; 17 | top: 0; 18 | right: 0; 19 | left: $nav-width; 20 | height: $top-bar-height; 21 | z-index: $top-bar-z-index; 22 | } 23 | 24 | &--has-toolbar { 25 | padding-top: 2 * $top-bar-height; 26 | 27 | .page__top-bar { 28 | height: 2 * $top-bar-height; 29 | } 30 | .page__content-wrapper { 31 | min-height: calc(100vh - 2 * #{$top-bar-height}); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/components/popup/popup-manager.spec.tsx: -------------------------------------------------------------------------------- 1 | import { mount } from 'enzyme'; 2 | import {from} from 'rxjs'; 3 | import {TestScheduler} from 'rxjs/testing'; 4 | import { Popup, PopupProps } from './popup'; 5 | import {PopupManager} from './popup-manager'; 6 | 7 | describe('PopupManager', () => { 8 | let scheduler: TestScheduler; 9 | 10 | beforeEach(() => { 11 | scheduler = new TestScheduler((a, b) => expect(a).toEqual(b)); 12 | }); 13 | 14 | describe('confirm', () => { 15 | it.each([ 16 | ['OK', true, '[qe-id="argo-popup-ok-button"]'], 17 | ['Cancel', false, '[qe-id="argo-popup-cancel-button"]'], 18 | ])('%s', async (_, promiseResult, btnSelector) => { 19 | const fn = jest.fn(); 20 | const manager = new PopupManager(); 21 | 22 | from(manager.popupProps, scheduler).subscribe(fn); 23 | scheduler.flush(); 24 | 25 | expect(fn).toHaveBeenCalledTimes(1); 26 | expect(fn).toHaveBeenLastCalledWith(null); 27 | 28 | const promise = manager.confirm('foo', 'bar'); 29 | scheduler.flush(); 30 | 31 | expect(fn).toHaveBeenCalledTimes(2); 32 | 33 | const props = fn.mock.calls[1][0]!; 34 | 35 | expect(typeof props).toBe('object'); 36 | 37 | const popup = mount(Popup(props)); 38 | 39 | expect(popup.find('.popup-container__header').text().trim()).toBe('foo'); 40 | expect(popup.find('.popup-container__body').text()).toBe('bar'); 41 | 42 | popup.find(btnSelector).simulate('click'); 43 | 44 | await expect(promise).resolves.toBe(promiseResult); 45 | scheduler.flush(); 46 | 47 | expect(fn).toHaveBeenCalledTimes(3); 48 | expect(fn).toHaveBeenLastCalledWith(null); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/components/popup/popup.tsx: -------------------------------------------------------------------------------- 1 | import {default as classNames} from 'classnames'; 2 | import * as React from 'react'; 3 | 4 | export interface BasePopupProps { 5 | icon?: { name: string; color: string; }; 6 | titleColor?: string; 7 | title: string | React.ReactNode; 8 | footer?: React.ReactNode; 9 | } 10 | 11 | export type PopupPropsWithContent = BasePopupProps & { content: React.ComponentType }; 12 | export type PopupPropsWithChildren = BasePopupProps & { children: React.ReactNode}; 13 | export type PopupProps = PopupPropsWithContent | PopupPropsWithChildren; 14 | 15 | function isPopupWithChildren(value: PopupProps): value is PopupPropsWithChildren { 16 | return (value as any).children !== undefined; 17 | } 18 | 19 | require('./popup.scss'); 20 | 21 | export const Popup = (props: PopupProps) => ( 22 |
23 |
24 |
25 | {props.title} 26 |
27 |
28 | {props.icon && 29 |
30 | 31 |
32 | } 33 |
34 | {isPopupWithChildren(props) ? props.children : } 35 |
36 |
37 | 38 |
39 | {props.footer} 40 |
41 |
42 |
43 | ); 44 | -------------------------------------------------------------------------------- /src/components/slide-contents/slide-contents.scss: -------------------------------------------------------------------------------- 1 | .slide-contents { 2 | .slide-contents--title { 3 | cursor: pointer; 4 | } 5 | .slide-contents--contents { 6 | overflow-y: hidden; 7 | max-height: 100%; 8 | 9 | &.slide-contents--contents-hidden { 10 | max-height: 0; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/slide-contents/slide-contents.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | require('./slide-contents.scss'); 4 | 5 | export interface SlideContentsProps { 6 | title: string; 7 | contents: JSX.Element; 8 | className: string; 9 | } 10 | 11 | export interface SlideContentsState { 12 | hidden: boolean; 13 | } 14 | 15 | export class SlideContents extends React.Component { 16 | constructor(props: SlideContentsProps) { 17 | super(props); 18 | 19 | this.state = { 20 | hidden: true, 21 | }; 22 | 23 | this.showContents = this.showContents.bind(this); 24 | this.hideContents = this.hideContents.bind(this); 25 | } 26 | 27 | public showContents() { 28 | this.setState({ hidden: false }); 29 | } 30 | 31 | public hideContents() { 32 | this.setState({ hidden: true }); 33 | } 34 | 35 | public render() { 36 | const { title, contents, className } = this.props; 37 | const { hidden } = this.state; 38 | let toggleSwitch: JSX.Element | undefined; 39 | let clickAction: () => void; 40 | if (hidden) { 41 | toggleSwitch =