mirror of
https://github.com/ansible/awx.git
synced 2024-10-31 23:51:09 +03:00
Merge pull request #4188 from mabashian/awx-pf-migration
Pull beginning of new ui application using React and Patternfly into AWX Reviewed-by: https://github.com/softwarefactory-project-zuul[bot]
This commit is contained in:
commit
3371a6f386
3
.gitignore
vendored
3
.gitignore
vendored
@ -28,6 +28,9 @@ awx/ui/build_test
|
||||
awx/ui/client/languages
|
||||
awx/ui/templates/ui/index.html
|
||||
awx/ui/templates/ui/installing.html
|
||||
awx/ui_next/node_modules/
|
||||
awx/ui_next/coverage/
|
||||
awx/ui_next/build/locales/_build
|
||||
/tower-license
|
||||
/tower-license/**
|
||||
tools/prometheus/data
|
||||
|
17
Makefile
17
Makefile
@ -73,6 +73,9 @@ clean-ui:
|
||||
rm -rf awx/ui/test/spec/reports/
|
||||
rm -rf awx/ui/test/e2e/reports/
|
||||
rm -rf awx/ui/client/languages/
|
||||
rm -rf awx/ui_next/node_modules/
|
||||
rm -rf awx/ui_next/coverage/
|
||||
rm -rf awx/ui_next/build/locales/_build/
|
||||
rm -f $(UI_DEPS_FLAG_FILE)
|
||||
rm -f $(UI_RELEASE_DEPS_FLAG_FILE)
|
||||
rm -f $(UI_RELEASE_FLAG_FILE)
|
||||
@ -515,6 +518,20 @@ jshint: $(UI_DEPS_FLAG_FILE)
|
||||
# END UI TASKS
|
||||
# --------------------------------------
|
||||
|
||||
# UI NEXT TASKS
|
||||
# --------------------------------------
|
||||
|
||||
ui-next-lint:
|
||||
$(NPM_BIN) --prefix awx/ui_next install
|
||||
$(NPM_BIN) run --prefix awx/ui_next lint
|
||||
|
||||
ui-next-test:
|
||||
$(NPM_BIN) --prefix awx/ui_next install
|
||||
$(NPM_BIN) run --prefix awx/ui_next test
|
||||
|
||||
# END UI NEXT TASKS
|
||||
# --------------------------------------
|
||||
|
||||
# Build a pip-installable package into dist/ with a timestamped version number.
|
||||
dev_build:
|
||||
$(PYTHON) setup.py dev_build
|
||||
|
9
awx/ui_next/.eslintignore
Normal file
9
awx/ui_next/.eslintignore
Normal file
@ -0,0 +1,9 @@
|
||||
jest.*.js
|
||||
webpack.*.js
|
||||
|
||||
etc
|
||||
coverage
|
||||
build
|
||||
node_modules
|
||||
dist
|
||||
images
|
61
awx/ui_next/.eslintrc
Normal file
61
awx/ui_next/.eslintrc
Normal file
@ -0,0 +1,61 @@
|
||||
{
|
||||
"parser": "babel-eslint",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"jsx": true,
|
||||
"modules": true
|
||||
}
|
||||
},
|
||||
"extends": ["airbnb", "prettier", "prettier/react"],
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"webpack": {
|
||||
"config": "webpack.config.js"
|
||||
}
|
||||
},
|
||||
"react": {
|
||||
"version": "16.5.2"
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"jest": true
|
||||
},
|
||||
"globals": {
|
||||
"window": true
|
||||
},
|
||||
"rules": {
|
||||
"camelcase": "off",
|
||||
"arrow-parens": "off",
|
||||
"comma-dangle": "off",
|
||||
"//": "https://github.com/benmosher/eslint-plugin-import/issues/479#issuecomment-252500896",
|
||||
"import/no-extraneous-dependencies": "off",
|
||||
"max-len": [
|
||||
"error",
|
||||
{
|
||||
"code": 100,
|
||||
"ignoreStrings": true,
|
||||
"ignoreTemplateLiterals": true
|
||||
}
|
||||
],
|
||||
"no-continue": "off",
|
||||
"no-debugger": "off",
|
||||
"no-mixed-operators": "off",
|
||||
"no-param-reassign": "off",
|
||||
"no-plusplus": "off",
|
||||
"no-underscore-dangle": "off",
|
||||
"no-use-before-define": "off",
|
||||
"no-multiple-empty-lines": ["error", { "max": 1 }],
|
||||
"object-curly-newline": "off",
|
||||
"no-trailing-spaces": ["error"],
|
||||
"no-unused-expressions": ["error", { "allowShortCircuit": true }],
|
||||
"react/prefer-stateless-function": "off",
|
||||
"react/prop-types": "off",
|
||||
"react/sort-comp": ["error", {}],
|
||||
"jsx-a11y/label-has-for": "off",
|
||||
"jsx-a11y/label-has-associated-control": "off"
|
||||
}
|
||||
}
|
5
awx/ui_next/.linguirc
Normal file
5
awx/ui_next/.linguirc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"localeDir": "build/locales/",
|
||||
"srcPathDirs": ["src/"],
|
||||
"format": "po"
|
||||
}
|
8
awx/ui_next/.prettierrc
Normal file
8
awx/ui_next/.prettierrc
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true
|
||||
}
|
305
awx/ui_next/CONTRIBUTING.md
Normal file
305
awx/ui_next/CONTRIBUTING.md
Normal file
@ -0,0 +1,305 @@
|
||||
# Ansible AWX UI With PatternFly
|
||||
|
||||
Hi there! We're excited to have you as a contributor.
|
||||
|
||||
Have questions about this document or anything not covered here? Feel free to reach out to any of the contributors of this repository.
|
||||
|
||||
## Table of contents
|
||||
|
||||
* [Things to know prior to submitting code](#things-to-know-prior-to-submitting-code)
|
||||
* [Setting up your development environment](#setting-up-your-development-environment)
|
||||
* [Prerequisites](#prerequisites)
|
||||
* [Node and npm](#node-and-npm)
|
||||
* [Build the user interface](#build-the-user-interface)
|
||||
* [Accessing the AWX web interface](#accessing-the-awx-web-interface)
|
||||
* [AWX REST API Interaction](#awx-rest-api-interaction)
|
||||
* [Handling API Errors](#handling-api-errors)
|
||||
* [Working with React](#working-with-react)
|
||||
* [App structure](#app-structure)
|
||||
* [Naming files](#naming-files)
|
||||
* [Class constructors vs Class properties](#class-constructors-vs-class-properties)
|
||||
* [Binding](#binding)
|
||||
* [Typechecking with PropTypes](#typechecking-with-proptypes)
|
||||
* [Naming Functions](#naming-functions)
|
||||
* [Default State Initialization](#default-state-initialization)
|
||||
* [Internationalization](#internationalization)
|
||||
|
||||
|
||||
## Things to know prior to submitting code
|
||||
|
||||
- All code submissions are done through pull requests against the `devel` branch.
|
||||
- If collaborating with someone else on the same branch, please use `--force-with-lease` instead of `--force` when pushing up code. This will prevent you from accidentally overwriting commits pushed by someone else. For more information, see https://git-scm.com/docs/git-push#git-push---force-with-leaseltrefnamegt
|
||||
|
||||
## Setting up your development environment
|
||||
|
||||
The UI is built using [ReactJS](https://reactjs.org/docs/getting-started.html) and [Patternfly](https://www.patternfly.org/).
|
||||
|
||||
### Prerequisites
|
||||
|
||||
#### Node and npm
|
||||
|
||||
The AWX UI requires the following:
|
||||
|
||||
- Node 10.x LTS
|
||||
- NPM 6.x LTS
|
||||
|
||||
Run the following to install all the dependencies:
|
||||
```bash
|
||||
(host) $ npm run install
|
||||
```
|
||||
|
||||
#### Build the User Interface
|
||||
|
||||
Run the following to build the AWX UI:
|
||||
|
||||
```bash
|
||||
(host) $ npm run start
|
||||
```
|
||||
|
||||
## Accessing the AWX web interface
|
||||
|
||||
You can now log into the AWX web interface at [https://127.0.0.1:3001](https://127.0.0.1:3001).
|
||||
|
||||
## AWX REST API Interaction
|
||||
|
||||
This interface is built on top of the AWX REST API. If a component needs to interact with the API then the model that corresponds to that base endpoint will need to be imported from the api module.
|
||||
|
||||
Example:
|
||||
|
||||
`import { OrganizationsAPI, UsersAPI } from '../../../api';`
|
||||
|
||||
All models extend a `Base` class which provides an interface to the standard HTTP methods (GET, POST, PUT etc). Methods that are specific to that endpoint should be added directly to model's class.
|
||||
|
||||
**Mixins** - For related endpoints that apply to several different models a mixin should be used. Mixins are classes with a number of methods and can be used to avoid adding the same methods to a number of different models. A good example of this is the Notifications mixin. This mixin provides generic methods for reading notification templates and toggling them on and off.
|
||||
Note that mixins can be chained. See the example below.
|
||||
|
||||
Example of a model using multiple mixins:
|
||||
|
||||
```
|
||||
import NotificationsMixin from '../mixins/Notifications.mixin';
|
||||
import InstanceGroupsMixin from '../mixins/InstanceGroups.mixin';
|
||||
|
||||
class Organizations extends InstanceGroupsMixin(NotificationsMixin(Base)) {
|
||||
...
|
||||
}
|
||||
|
||||
export default Organizations;
|
||||
```
|
||||
|
||||
**Testing** - The easiest way to mock the api module in tests is to use jest's [automatic mock](https://jestjs.io/docs/en/es6-class-mocks#automatic-mock). This syntax will replace the class with a mock constructor and mock out all methods to return undefined by default. If necessary, you can still override these mocks for specific tests. See the example below.
|
||||
|
||||
Example of mocking a specific method for every test in a suite:
|
||||
|
||||
```
|
||||
import { OrganizationsAPI } from '../../../../src/api';
|
||||
|
||||
// Mocks out all available methods. Comparable to:
|
||||
// OrganizationsAPI.readAccessList = jest.fn();
|
||||
// but for every available method
|
||||
jest.mock('../../../../src/api');
|
||||
|
||||
// Return a specific mock value for the readAccessList method
|
||||
beforeEach(() => {
|
||||
OrganizationsAPI.readAccessList.mockReturnValue({ foo: 'bar' });
|
||||
});
|
||||
|
||||
// Reset mocks
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
## Handling API Errors
|
||||
API requests can and will fail occasionally so they should include explicit error handling. The three _main_ categories of errors from our perspective are: content loading errors, form submission errors, and other errors. The patterns currently in place for these are described below:
|
||||
|
||||
- **content loading errors** - These are any errors that occur when fetching data to initialize a page or populate a list. For these, we conditionally render a _content error component_ in place of the unresolved content.
|
||||
|
||||
- **form submission errors** - If an error is encountered when submitting a form, we display the error message on the form. For field-specific validation errors, we display the error message beneath the specific field(s). For general errors, we display the error message at the bottom of the form near the action buttons. An error that happens when requesting data to populate a form is not a form submission error, it is still a content error and is handled as such (see above).
|
||||
|
||||
- **other errors** - Most errors will fall into the first two categories, but for miscellaneous actions like toggling notifications, deleting a list item, etc. we display an alert modal to notify the user that their requested action couldn't be performed.
|
||||
|
||||
## Working with React
|
||||
|
||||
### App structure
|
||||
|
||||
All source code lives in the `/src` directory and all tests are colocated with the components that they test.
|
||||
|
||||
Inside these folders, the internal structure is:
|
||||
- **/api** - All classes used to interact with API's are found here. See [AWX REST API Interaction](#awx-rest-api-interaction) for more information.
|
||||
- **/components** - All generic components that are meant to be used in multiple contexts throughout awx. Things like buttons, tabs go here.
|
||||
- **/contexts** - Components which utilize react's context api.
|
||||
- **/screens** - Based on the various routes of awx.
|
||||
- **/shared** - Components that are meant to be used specifically by a particular route, but might be sharable across pages of that route. For example, a form component which is used on both add and edit screens.
|
||||
- **/util** - Stateless helper functions that aren't tied to react.
|
||||
|
||||
#### Bootstrapping the application (root src/ files)
|
||||
|
||||
In the root of `/src`, there are a few files which are used to initialize the react app. These are
|
||||
|
||||
- **index.jsx**
|
||||
- Connects react app to root dom node.
|
||||
- Sets up root route structure, navigation grouping and login modal
|
||||
- Calls base context providers
|
||||
- Imports .scss styles.
|
||||
- **app.jsx**
|
||||
- Sets standard page layout, about modal, and root dialog modal.
|
||||
- **RootProvider.jsx**
|
||||
- Sets up all context providers.
|
||||
- Initializes i18n and router
|
||||
|
||||
### Naming files
|
||||
|
||||
Ideally, files should be named the same as the component they export, and tests with `.test` appended. In other words, `<FooBar>` would be defined in `FooBar.jsx`, and its tests would be defined in `FooBar.test.jsx`.
|
||||
|
||||
#### Naming components that use the context api
|
||||
|
||||
**File naming** - Since contexts export both consumer and provider (and potentially in withContext function form), the file can be simplified to be named after the consumer export. In other words, the file containing the `Network` context components would be named `Network.jsx`.
|
||||
|
||||
**Component naming and conventions** - In order to provide a consistent interface with react-router and lingui, as well as make their usage easier and less verbose, context components follow these conventions:
|
||||
- Providers are wrapped in a component in the `FooProvider` format.
|
||||
- The value prop of the provider should be pulled from state. This is recommended by the react docs, [here](https://reactjs.org/docs/context.html#caveats).
|
||||
- The provider should also be able to accept its value by prop for testing.
|
||||
- Any sort of code related to grabbing data to put on the context should be done in this component.
|
||||
- Consumers are wrapped in a component in the `Foo` format.
|
||||
- If it makes sense, consumers can be exported as a function in the `withFoo()` format. If a component is wrapped in this function, its context values are available on the component as props.
|
||||
|
||||
### Class constructors vs Class properties
|
||||
It is good practice to use constructor-bound instance methods rather than methods as class properties. Methods as arrow functions provide lexical scope and are bound to the Component class instance instead of the class itself. This makes it so we cannot easily test a Component's methods without invoking an instance of the Component and calling the method directly within our tests.
|
||||
|
||||
BAD:
|
||||
```javascript
|
||||
class MyComponent extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
myEventHandler = () => {
|
||||
// do a thing
|
||||
}
|
||||
}
|
||||
```
|
||||
GOOD:
|
||||
```javascript
|
||||
class MyComponent extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.myEventHandler = this.myEventHandler.bind(this);
|
||||
}
|
||||
|
||||
myEventHandler() {
|
||||
// do a thing
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Binding
|
||||
It is good practice to bind our class methods within our class constructor method for the following reasons:
|
||||
1. Avoid defining the method every time `render()` is called.
|
||||
2. [Performance advantages](https://stackoverflow.com/a/44844916).
|
||||
3. Ease of [testing](https://github.com/airbnb/enzyme/issues/365).
|
||||
|
||||
### Typechecking with PropTypes
|
||||
Shared components should have their prop values typechecked. This will help catch bugs when components get refactored/renamed.
|
||||
```javascript
|
||||
About.propTypes = {
|
||||
ansible_version: PropTypes.string,
|
||||
isOpen: PropTypes.bool,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
version: PropTypes.string,
|
||||
};
|
||||
|
||||
About.defaultProps = {
|
||||
ansible_version: null,
|
||||
isOpen: false,
|
||||
version: null,
|
||||
};
|
||||
```
|
||||
|
||||
### Naming Functions
|
||||
Here are the guidelines for how to name functions.
|
||||
|
||||
| Naming Convention | Description |
|
||||
|----------|-------------|
|
||||
|`handle<x>`| Use for methods that process events |
|
||||
|`on<x>`| Use for component prop names |
|
||||
|`toggle<x>`| Use for methods that flip one value to the opposite value |
|
||||
|`show<x>`| Use for methods that always set a value to show or add an element |
|
||||
|`hide<x>`| Use for methods that always set a value to hide or remove an element |
|
||||
|`create<x>`| Use for methods that make API `POST` requests |
|
||||
|`read<x>`| Use for methods that make API `GET` requests |
|
||||
|`update<x>`| Use for methods that make API `PATCH` requests |
|
||||
|`destroy<x>`| Use for methods that make API `DESTROY` requests |
|
||||
|`replace<x>`| Use for methods that make API `PUT` requests |
|
||||
|`disassociate<x>`| Use for methods that pass `{ disassociate: true }` as a data param to an endpoint |
|
||||
|`associate<x>`| Use for methods that pass a resource id as a data param to an endpoint |
|
||||
|`can<x>`| Use for props dealing with RBAC to denote whether a user has access to something |
|
||||
|
||||
### Default State Initialization
|
||||
When declaring empty initial states, prefer the following instead of leaving them undefined:
|
||||
|
||||
```javascript
|
||||
this.state = {
|
||||
somethingA: null,
|
||||
somethingB: [],
|
||||
somethingC: 0,
|
||||
somethingD: {},
|
||||
somethingE: '',
|
||||
}
|
||||
```
|
||||
|
||||
### Testing components that use contexts
|
||||
|
||||
We have several React contexts that wrap much of the app, including those from react-router, lingui, and some of our own. When testing a component that depends on one or more of these, you can use the `mountWithContexts()` helper function found in `testUtils/enzymeHelpers.jsx`. This can be used just like Enzyme's `mount()` function, except it will wrap the component tree with the necessary context providers and basic stub data.
|
||||
|
||||
If you want to stub the value of a context, or assert actions taken on it, you can customize a contexts value by passing a second parameter to `mountWithContexts`. For example, this provides a custom value for the `Config` context:
|
||||
|
||||
```
|
||||
const config = {
|
||||
custom_virtualenvs: ['foo', 'bar'],
|
||||
};
|
||||
mountWithContexts(<OrganizationForm />, {
|
||||
context: { config },
|
||||
});
|
||||
```
|
||||
|
||||
Now that these custom virtual environments are available in this `OrganizationForm` test we can assert that the component that displays
|
||||
them is rendering properly.
|
||||
|
||||
The object containing context values looks for five known contexts, identified by the keys `linguiPublisher`, `router`, `config`, `network`, and `dialog` — the latter three each referring to the contexts defined in `src/contexts`. You can pass `false` for any of these values, and the corresponding context will be omitted from your test. For example, this will mount your component without the dialog context:
|
||||
|
||||
```
|
||||
mountWithContexts(<Organization />< {
|
||||
context: {
|
||||
dialog: false,
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Internationalization
|
||||
|
||||
Internationalization leans on the [lingui](https://github.com/lingui/js-lingui) project. [Official documentation here](https://lingui.js.org/). We use this libary to mark our strings for translation. If you want to see this in action you'll need to take the following steps:
|
||||
|
||||
### Marking strings for translation and replacement in the UI
|
||||
|
||||
The lingui library provides various React helpers for dealing with both marking strings for translation, and replacing strings that have been traslated. For consistency and ease of use, we have consolidated on one pattern for the codebase. To set strings to be translated in the UI:
|
||||
|
||||
- import the withI18n function and wrap the export of your component in it (i.e. `export default withI18n()(Foo)`)
|
||||
- doing the above gives you access to the i18n object on props. Make sure to put it in the scope of the function that contains strings needed to be translated (i.e. `const { i18n } = this.props;`)
|
||||
- import the t template tag function from the @lingui/macro package.
|
||||
- wrap your string using the following format: ```i18n._(t`String to be translated`)```
|
||||
|
||||
**Note:** Variables that are put inside the t-marked template tag will not be translated. If you have a variable string with text that needs translating, you must wrap it in ```i18n._(t``)``` where it is defined.
|
||||
|
||||
**Note:** We do not use the `I18n` consumer, `i18nMark` function, or `<Trans>` component lingui gives us access to in this repo. i18nMark does not actually replace the string in the UI (leading to the potential for untranslated bugs), and the other helpers are redundant. Settling on a consistent, single pattern helps us ease the mental overhead of the need to understand the ins and outs of the lingui API.
|
||||
|
||||
You can learn more about the ways lingui and its React helpers at [this link](https://lingui.js.org/tutorials/react-patterns.html).
|
||||
|
||||
### Setting up .po files to give to translation team
|
||||
|
||||
1) `npm run add-locale` to add the language that you want to translate to (we should only have to do this once and the commit to repo afaik). Example: `npm run add-locale en es fr` # Add English, Spanish and French locale
|
||||
2) `npm run extract-strings` to create .po files for each language specified. The .po files will be placed in src/locales but this is configurable.
|
||||
3) Open up the .po file for the language you want to test and add some translations. In production we would pass this .po file off to the translation team.
|
||||
4) Once you've edited your .po file (or we've gotten a .po file back from the translation team) run `npm run compile-strings`. This command takes the .po files and turns them into a minified JSON object and can be seen in the `messages.js` file in each locale directory. These files get loaded at the App root level (see: App.jsx).
|
||||
5) Change the language in your browser and reload the page. You should see your specified translations in place of English strings.
|
29
awx/ui_next/README.md
Normal file
29
awx/ui_next/README.md
Normal file
@ -0,0 +1,29 @@
|
||||
# AWX-PF
|
||||
|
||||
## Requirements
|
||||
- node 10.x LTS, npm 6.x LTS, make, git
|
||||
|
||||
## Usage
|
||||
|
||||
* `git clone git@github.com:ansible/awx.git`
|
||||
* cd awx/ui_next
|
||||
* npm install
|
||||
* npm start
|
||||
* visit `https://127.0.0.1:3001/`
|
||||
|
||||
**note:** These instructions assume you have the [awx](https://github.com/ansible/awx/blob/devel/CONTRIBUTING.md#running-the-environment) development api server up and running at `localhost:8043`. You can use a different backend server with the `TAGET_HOST` and `TARGET_PORT` environment variables when starting the development server:
|
||||
|
||||
```shell
|
||||
# use a non-default host and port when starting the development server
|
||||
TARGET_HOST='ec2-awx.amazonaws.com' TARGET_PORT='443' npm run start
|
||||
```
|
||||
|
||||
## Unit Tests
|
||||
|
||||
To run the unit tests on files that you've changed:
|
||||
* `npm test`
|
||||
|
||||
To run a single test (in this case the login page test):
|
||||
* `npm test -- testUtils/pages/Login.test.jsx`
|
||||
|
||||
**note:** Once the test watcher is up and running you can hit `a` to run all the tests
|
7
awx/ui_next/__mocks__/fileMock.js
Normal file
7
awx/ui_next/__mocks__/fileMock.js
Normal file
@ -0,0 +1,7 @@
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
process (src, filename) {
|
||||
return `module.exports=${JSON.stringify(path.basename(filename))};`;
|
||||
},
|
||||
};
|
1
awx/ui_next/__mocks__/styleMock.js
Normal file
1
awx/ui_next/__mocks__/styleMock.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = {};
|
18
awx/ui_next/babel.config.js
Normal file
18
awx/ui_next/babel.config.js
Normal file
@ -0,0 +1,18 @@
|
||||
module.exports = api => {
|
||||
api.cache(false);
|
||||
return {
|
||||
plugins: [
|
||||
'babel-plugin-styled-components',
|
||||
'@babel/plugin-proposal-class-properties',
|
||||
'macros'
|
||||
],
|
||||
presets: [
|
||||
['@babel/preset-env', {
|
||||
targets: {
|
||||
node: '8.11'
|
||||
}
|
||||
}],
|
||||
'@babel/preset-react'
|
||||
]
|
||||
};
|
||||
};
|
1
awx/ui_next/build/locales/en/messages.js
Normal file
1
awx/ui_next/build/locales/en/messages.js
Normal file
File diff suppressed because one or more lines are too long
700
awx/ui_next/build/locales/en/messages.po
Normal file
700
awx/ui_next/build/locales/en/messages.po
Normal file
@ -0,0 +1,700 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"POT-Creation-Date: 2018-12-10 10:08-0500\n"
|
||||
"Mime-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: @lingui/cli\n"
|
||||
"Language: en\n"
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/contexts/Network.jsx:52
|
||||
msgid "404"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationBreadcrumb.jsx:60
|
||||
#~ msgid "> add"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationBreadcrumb.jsx:53
|
||||
#~ msgid "> edit"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/components/BrandLogo/BrandLogo.jsx:71
|
||||
msgid "AWX Logo"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/PageHeaderToolbar.jsx:88
|
||||
msgid "About"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/About.jsx:59
|
||||
#~ msgid "AboutModal Logo"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/index.jsx:148
|
||||
#: src/pages/Organizations/Organizations.jsx:43
|
||||
#: src/pages/Organizations/screens/Organization/Organization.jsx:124
|
||||
msgid "Access"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/PaginatedDataList/ToolbarAddButton.jsx:25
|
||||
#: src/components/PaginatedDataList/ToolbarAddButton.jsx:32
|
||||
#: src/components/PaginatedDataList/ToolbarAddButton.jsx:42
|
||||
msgid "Add"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AddRole/AddResourceRole.jsx:147
|
||||
msgid "Add Roles"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AddRole/AddResourceRole.jsx:144
|
||||
msgid "Add Team Roles"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AddRole/AddResourceRole.jsx:141
|
||||
msgid "Add User Roles"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:169
|
||||
msgid "Administration"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationListItem.jsx:66
|
||||
#~ msgid "Admins"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationForm.jsx:123
|
||||
#: src/pages/Organizations/components/OrganizationForm.jsx:128
|
||||
#: src/pages/Organizations/screens/Organization/OrganizationDetail.jsx:79
|
||||
msgid "Ansible Environment"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/About.jsx:73
|
||||
msgid "Ansible Version"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Applications.jsx:19
|
||||
msgid "Applications"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AddRole/AddResourceRole.jsx:203
|
||||
msgid "Apply roles"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/PaginatedDataList/ToolbarDeleteButton.jsx:155
|
||||
msgid "Are you sure you want to delete:"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/DeleteRoleConfirmationModal.jsx:51
|
||||
msgid "Are you sure you want to remove {0} access from {1}? Doing so affects all members of the team."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/DeleteRoleConfirmationModal.jsx:58
|
||||
msgid "Are you sure you want to remove {0} access from {username}?"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:204
|
||||
msgid "Authentication"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/AuthSettings.jsx:19
|
||||
msgid "Authentication Settings"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/About.jsx:57
|
||||
msgid "Brand Image"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/FormActionGroup/FormActionGroup.jsx:27
|
||||
#: src/components/FormActionGroup/FormActionGroup.jsx:27
|
||||
#: src/components/Lookup/Lookup.jsx:162
|
||||
#: src/components/PaginatedDataList/ToolbarDeleteButton.jsx:151
|
||||
#: src/pages/Organizations/components/DeleteRoleConfirmationModal.jsx:45
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/Organizations.jsx:77
|
||||
msgid "Cannot find organization with ID"
|
||||
msgstr ""
|
||||
|
||||
#: src/contexts/Network.jsx:53
|
||||
msgid "Cannot find resource."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/NotifyAndRedirect.jsx:23
|
||||
msgid "Cannot find route {0}."
|
||||
msgstr ""
|
||||
|
||||
#: src/App.jsx:109
|
||||
#: src/components/CardCloseButton.jsx:22
|
||||
#: src/components/CardCloseButton.jsx:23
|
||||
#: src/components/CardCloseButton.jsx:34
|
||||
#: src/components/Lookup/Lookup.jsx:162
|
||||
#: src/pages/Organizations/screens/OrganizationAdd.jsx:67
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/ExpandCollapse/ExpandCollapse.jsx:34
|
||||
msgid "Collapse"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/About.jsx:55
|
||||
#~ msgid "Copyright 2018 Red Hat, Inc."
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/components/About.jsx:55
|
||||
msgid "Copyright 2019 Red Hat, Inc."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/Organizations.jsx:39
|
||||
#: src/pages/Organizations/Organizations.jsx:25
|
||||
msgid "Create New Organization"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/InstanceGroupsLookup.jsx:50
|
||||
#: src/pages/Organizations/screens/Organization/OrganizationDetail.jsx:83
|
||||
#: src/pages/Organizations/screens/OrganizationsList.jsx:164
|
||||
msgid "Created"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:173
|
||||
#: src/pages/CredentialTypes.jsx:19
|
||||
msgid "Credential Types"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:126
|
||||
#: src/pages/Credentials.jsx:19
|
||||
msgid "Credentials"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:29
|
||||
msgid "Current page"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:95
|
||||
#: src/pages/Dashboard.jsx:19
|
||||
msgid "Dashboard"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/PaginatedDataList/ToolbarDeleteButton.jsx:95
|
||||
#: src/components/PaginatedDataList/ToolbarDeleteButton.jsx:119
|
||||
#: src/components/PaginatedDataList/ToolbarDeleteButton.jsx:143
|
||||
#: src/pages/Organizations/components/DeleteRoleConfirmationModal.jsx:42
|
||||
msgid "Delete"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/PaginatedDataList/ToolbarDeleteButton.jsx:132
|
||||
msgid "Delete {0}"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/PaginatedDataList/ToolbarDeleteButton.jsx:131
|
||||
msgid "Delete {itemName}"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationForm.jsx:113
|
||||
#: src/pages/Organizations/screens/Organization/OrganizationDetail.jsx:75
|
||||
msgid "Description"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/Organizations.jsx:42
|
||||
#: src/pages/Organizations/screens/Organization/Organization.jsx:123
|
||||
msgid "Details"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/screens/Organization/OrganizationDetail.jsx:110
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/Organizations.jsx:41
|
||||
msgid "Edit Details"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/ExpandCollapse/ExpandCollapse.jsx:44
|
||||
msgid "Expand"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/NotificationsList/NotificationListItem.jsx:82
|
||||
msgid "Failure"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:170
|
||||
#~ msgid "First"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:24
|
||||
msgid "Go to first page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:26
|
||||
msgid "Go to last page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:27
|
||||
msgid "Go to next page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:25
|
||||
msgid "Go to previous page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/PageHeaderToolbar.jsx:80
|
||||
msgid "Help"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/DeleteRoleConfirmationModal.jsx:54
|
||||
msgid "If you {0} want to remove access for this particular user, please remove them from the team."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/PageHeaderToolbar.jsx:66
|
||||
msgid "Info"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:188
|
||||
#: src/pages/InstanceGroups.jsx:19
|
||||
#: src/pages/Organizations/components/InstanceGroupsLookup.jsx:24
|
||||
#: src/pages/Organizations/components/InstanceGroupsLookup.jsx:42
|
||||
#: src/pages/Organizations/screens/Organization/OrganizationDetail.jsx:93
|
||||
msgid "Instance Groups"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:193
|
||||
msgid "Integrations"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Login.jsx:94
|
||||
msgid "Invalid username or password. Please try again."
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:136
|
||||
#: src/pages/Inventories.jsx:19
|
||||
msgid "Inventories"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:141
|
||||
#: src/pages/InventoryScripts.jsx:19
|
||||
msgid "Inventory Scripts"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:142
|
||||
#~ msgid "Items Per Page"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:22
|
||||
msgid "Items per page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:162
|
||||
#~ msgid "Items {itemMin} – {itemMax} of {count}"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/index.jsx:100
|
||||
#: src/index.jsx:209
|
||||
#: src/pages/Jobs.jsx:19
|
||||
msgid "Jobs"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/JobsSettings.jsx:19
|
||||
msgid "Jobs Settings"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:213
|
||||
#~ msgid "Last"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/pages/Organizations/screens/Organization/OrganizationDetail.jsx:87
|
||||
msgid "Last Modified"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/screens/Organization/OrganizationAccess.jsx:175
|
||||
msgid "Last Name"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:224
|
||||
#: src/pages/License.jsx:19
|
||||
msgid "License"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AddRole/SelectResourceStep.jsx:89
|
||||
msgid "Loading..."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/PageHeaderToolbar.jsx:120
|
||||
msgid "Logout"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:183
|
||||
#: src/pages/ManagementJobs.jsx:19
|
||||
msgid "Management Jobs"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationListItem.jsx:91
|
||||
msgid "Members"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/InstanceGroupsLookup.jsx:49
|
||||
#: src/pages/Organizations/screens/OrganizationsList.jsx:163
|
||||
msgid "Modified"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:110
|
||||
#: src/pages/Portal.jsx:19
|
||||
msgid "My View"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AddRole/AddResourceRole.jsx:134
|
||||
#: src/components/PaginatedDataList/PaginatedDataList.jsx:99
|
||||
#: src/pages/Organizations/components/InstanceGroupsLookup.jsx:48
|
||||
#: src/pages/Organizations/components/OrganizationAccessItem.jsx:100
|
||||
#: src/pages/Organizations/components/OrganizationForm.jsx:105
|
||||
#: src/pages/Organizations/screens/Organization/OrganizationAccess.jsx:173
|
||||
#: src/pages/Organizations/screens/Organization/OrganizationDetail.jsx:71
|
||||
#: src/pages/Organizations/screens/OrganizationsList.jsx:162
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:204
|
||||
#~ msgid "Next"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/components/PaginatedDataList/PaginatedDataList.jsx:119
|
||||
msgid "No {0} Found"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/NotificationTemplates.jsx:19
|
||||
msgid "Notification Templates"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:178
|
||||
#: src/pages/Organizations/Organizations.jsx:45
|
||||
#: src/pages/Organizations/screens/Organization/Organization.jsx:130
|
||||
msgid "Notifications"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/views/Organization.add.jsx:79
|
||||
#~ msgid "Organization Add"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/pages/Organizations/screens/Organization/Organization.jsx:144
|
||||
msgid "Organization detail tabs"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:152
|
||||
#: src/pages/Organizations/Organizations.jsx:38
|
||||
#: src/pages/Organizations/Organizations.jsx:24
|
||||
msgid "Organizations"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/views/Organizations.list.jsx:218
|
||||
#~ msgid "Organizations List"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:190
|
||||
#~ msgid "Page"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:189
|
||||
#~ msgid "Page <0/> of {pageCount}"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:193
|
||||
#~ msgid "Page Number"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:30
|
||||
msgid "Pagination"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Login.jsx:92
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:158
|
||||
#~ msgid "Per Page"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/components/PaginatedDataList/PaginatedDataList.jsx:122
|
||||
msgid "Please add {0} to populate this list"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/PaginatedDataList/PaginatedDataList.jsx:136
|
||||
#: src/components/PaginatedDataList/PaginatedDataList.jsx:199
|
||||
#~ msgid "Please add {0} {itemName} to populate this list"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/App.jsx:203
|
||||
#~ msgid "Portal Mode"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:179
|
||||
#~ msgid "Previous"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/index.jsx:88
|
||||
msgid "Primary Navigation"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:131
|
||||
#: src/pages/Projects.jsx:19
|
||||
msgid "Projects"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/DeleteRoleConfirmationModal.jsx:28
|
||||
msgid "Remove {0} Access"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:117
|
||||
msgid "Resources"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AddRole/AddResourceRole.jsx:214
|
||||
#: src/components/FormActionGroup/FormActionGroup.jsx:24
|
||||
#: src/components/FormActionGroup/FormActionGroup.jsx:24
|
||||
#: src/components/Lookup/Lookup.jsx:161
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:105
|
||||
#: src/pages/Schedules.jsx:19
|
||||
msgid "Schedules"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Search/Search.jsx:138
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Search/Search.jsx:131
|
||||
msgid "Search text input"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:28
|
||||
msgid "Select"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AnsibleSelect/AnsibleSelect.jsx:28
|
||||
msgid "Select Input"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AddRole/AddResourceRole.jsx:153
|
||||
msgid "Select Users Or Teams"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/PaginatedDataList/ToolbarDeleteButton.jsx:97
|
||||
msgid "Select a row to delete"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/DataListToolbar/DataListToolbar.jsx:108
|
||||
msgid "Select all"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AddRole/AddResourceRole.jsx:172
|
||||
msgid "Select items from list"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationForm.jsx:141
|
||||
msgid "Select the Instance Groups for this Organization to run on."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Lookup/Lookup.jsx:157
|
||||
msgid "Select {header}"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AddRole/AddResourceRole.jsx:181
|
||||
#: src/components/AddRole/AddResourceRole.jsx:192
|
||||
#: src/components/AddRole/AddResourceRole.jsx:209
|
||||
#: src/components/AddRole/SelectRoleStep.jsx:29
|
||||
#: src/components/Lookup/Lookup.jsx:187
|
||||
msgid "Selected"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:200
|
||||
msgid "Settings"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Sort/Sort.jsx:135
|
||||
msgid "Sort"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/NotificationsList/NotificationListItem.jsx:70
|
||||
msgid "Successful"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:214
|
||||
msgid "System"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/SystemSettings.jsx:19
|
||||
msgid "System Settings"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/DeleteRoleConfirmationModal.jsx:28
|
||||
msgid "Team"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationAccessItem.jsx:122
|
||||
msgid "Team Roles"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AddRole/AddResourceRole.jsx:163
|
||||
#: src/index.jsx:162
|
||||
#: src/pages/Organizations/Organizations.jsx:44
|
||||
#: src/pages/Organizations/components/OrganizationListItem.jsx:99
|
||||
#: src/pages/Organizations/screens/Organization/Organization.jsx:125
|
||||
#: src/pages/Teams.jsx:19
|
||||
msgid "Teams"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:121
|
||||
#: src/pages/Templates.jsx:19
|
||||
msgid "Templates"
|
||||
msgstr ""
|
||||
|
||||
#: src/util/validators.jsx:6
|
||||
msgid "This field must not be blank"
|
||||
msgstr ""
|
||||
|
||||
#: src/util/validators.jsx:16
|
||||
msgid "This field must not exceed {max} characters"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/NotificationsList/NotificationListItem.jsx:90
|
||||
msgid "Toggle notification failure"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/NotificationsList/NotificationListItem.jsx:78
|
||||
msgid "Toggle notification success"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AnsibleSelect/AnsibleSelect.jsx:35
|
||||
msgid "Use Default {label}"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/DeleteRoleConfirmationModal.jsx:28
|
||||
msgid "User"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/PageHeaderToolbar.jsx:113
|
||||
msgid "User Details"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:219
|
||||
msgid "User Interface"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/UISettings.jsx:19
|
||||
msgid "User Interface Settings"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationAccessItem.jsx:112
|
||||
msgid "User Roles"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AddRole/AddResourceRole.jsx:130
|
||||
#: src/pages/Login.jsx:91
|
||||
#: src/pages/Organizations/screens/Organization/OrganizationAccess.jsx:174
|
||||
msgid "Username"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AddRole/AddResourceRole.jsx:158
|
||||
#: src/index.jsx:157
|
||||
#: src/pages/Users.jsx:19
|
||||
msgid "Users"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:91
|
||||
msgid "Views"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Login.jsx:86
|
||||
msgid "Welcome to Ansible {brandName}! Please Sign In."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/PaginatedDataList/ToolbarDeleteButton.jsx:90
|
||||
msgid "You do not have permission to delete the following {0}: {itemsUnableToDelete}"
|
||||
msgstr ""
|
||||
|
||||
#: src/contexts/Network.jsx:40
|
||||
msgid "You have been logged out."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationDetail.jsx:56
|
||||
#~ msgid "add {currentTab}"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationDetail.jsx:45
|
||||
#~ msgid "adding {currentTab}"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/components/PaginatedDataList/ToolbarDeleteButton.jsx:148
|
||||
msgid "cancel delete"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/PaginatedDataList/ToolbarDeleteButton.jsx:140
|
||||
msgid "confirm delete"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationDetail.jsx:38
|
||||
#~ msgid "confirm removal of {currentTab}/cancel and go back to {currentTab} view."
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationDetail.jsx:60
|
||||
#~ msgid "delete {currentTab}"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationDetail.jsx:36
|
||||
#~ msgid "deleting {currentTab} association with orgs"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationEdit.jsx:20
|
||||
#~ msgid "edit view"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/components/Lookup/Lookup.jsx:128
|
||||
#: src/components/Pagination/Pagination.jsx:20
|
||||
msgid "items"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:198
|
||||
#~ msgid "of {pageCount}"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:21
|
||||
msgid "pages"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:23
|
||||
msgid "per page"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationEdit.jsx:22
|
||||
#~ msgid "save/cancel and go back to view"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationDetail.jsx:47
|
||||
#~ msgid "save/cancel and go back to {currentTab} view"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationListItem.jsx:29
|
||||
#~ msgid "select organization {itemId}"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationDetail.jsx:82
|
||||
#~ msgid "{0}"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/components/PaginatedDataList/PaginatedDataList.jsx:135
|
||||
msgid "{0} List"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationDetail.jsx:54
|
||||
#~ msgid "{currentTab} detail view"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:163
|
||||
#~ msgid "{itemMin} - {itemMax} of {count}"
|
||||
#~ msgstr ""
|
1
awx/ui_next/build/locales/ja/messages.js
Normal file
1
awx/ui_next/build/locales/ja/messages.js
Normal file
File diff suppressed because one or more lines are too long
700
awx/ui_next/build/locales/ja/messages.po
Normal file
700
awx/ui_next/build/locales/ja/messages.po
Normal file
@ -0,0 +1,700 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"POT-Creation-Date: 2018-12-10 10:08-0500\n"
|
||||
"Mime-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: @lingui/cli\n"
|
||||
"Language: ja\n"
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/contexts/Network.jsx:52
|
||||
msgid "404"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationBreadcrumb.jsx:60
|
||||
#~ msgid "> add"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationBreadcrumb.jsx:53
|
||||
#~ msgid "> edit"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/components/BrandLogo/BrandLogo.jsx:71
|
||||
msgid "AWX Logo"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/PageHeaderToolbar.jsx:88
|
||||
msgid "About"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/About.jsx:59
|
||||
#~ msgid "AboutModal Logo"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/index.jsx:148
|
||||
#: src/pages/Organizations/Organizations.jsx:43
|
||||
#: src/pages/Organizations/screens/Organization/Organization.jsx:124
|
||||
msgid "Access"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/PaginatedDataList/ToolbarAddButton.jsx:25
|
||||
#: src/components/PaginatedDataList/ToolbarAddButton.jsx:32
|
||||
#: src/components/PaginatedDataList/ToolbarAddButton.jsx:42
|
||||
msgid "Add"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AddRole/AddResourceRole.jsx:147
|
||||
msgid "Add Roles"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AddRole/AddResourceRole.jsx:144
|
||||
msgid "Add Team Roles"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AddRole/AddResourceRole.jsx:141
|
||||
msgid "Add User Roles"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:169
|
||||
msgid "Administration"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationListItem.jsx:66
|
||||
#~ msgid "Admins"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationForm.jsx:123
|
||||
#: src/pages/Organizations/components/OrganizationForm.jsx:128
|
||||
#: src/pages/Organizations/screens/Organization/OrganizationDetail.jsx:79
|
||||
msgid "Ansible Environment"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/About.jsx:73
|
||||
msgid "Ansible Version"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Applications.jsx:19
|
||||
msgid "Applications"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AddRole/AddResourceRole.jsx:203
|
||||
msgid "Apply roles"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/PaginatedDataList/ToolbarDeleteButton.jsx:155
|
||||
msgid "Are you sure you want to delete:"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/DeleteRoleConfirmationModal.jsx:51
|
||||
msgid "Are you sure you want to remove {0} access from {1}? Doing so affects all members of the team."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/DeleteRoleConfirmationModal.jsx:58
|
||||
msgid "Are you sure you want to remove {0} access from {username}?"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:204
|
||||
msgid "Authentication"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/AuthSettings.jsx:19
|
||||
msgid "Authentication Settings"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/About.jsx:57
|
||||
msgid "Brand Image"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/FormActionGroup/FormActionGroup.jsx:27
|
||||
#: src/components/FormActionGroup/FormActionGroup.jsx:27
|
||||
#: src/components/Lookup/Lookup.jsx:162
|
||||
#: src/components/PaginatedDataList/ToolbarDeleteButton.jsx:151
|
||||
#: src/pages/Organizations/components/DeleteRoleConfirmationModal.jsx:45
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/Organizations.jsx:77
|
||||
msgid "Cannot find organization with ID"
|
||||
msgstr ""
|
||||
|
||||
#: src/contexts/Network.jsx:53
|
||||
msgid "Cannot find resource."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/NotifyAndRedirect.jsx:23
|
||||
msgid "Cannot find route {0}."
|
||||
msgstr ""
|
||||
|
||||
#: src/App.jsx:109
|
||||
#: src/components/CardCloseButton.jsx:22
|
||||
#: src/components/CardCloseButton.jsx:23
|
||||
#: src/components/CardCloseButton.jsx:34
|
||||
#: src/components/Lookup/Lookup.jsx:162
|
||||
#: src/pages/Organizations/screens/OrganizationAdd.jsx:67
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/ExpandCollapse/ExpandCollapse.jsx:34
|
||||
msgid "Collapse"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/About.jsx:55
|
||||
#~ msgid "Copyright 2018 Red Hat, Inc."
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/components/About.jsx:55
|
||||
msgid "Copyright 2019 Red Hat, Inc."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/Organizations.jsx:39
|
||||
#: src/pages/Organizations/Organizations.jsx:25
|
||||
msgid "Create New Organization"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/InstanceGroupsLookup.jsx:50
|
||||
#: src/pages/Organizations/screens/Organization/OrganizationDetail.jsx:83
|
||||
#: src/pages/Organizations/screens/OrganizationsList.jsx:164
|
||||
msgid "Created"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:173
|
||||
#: src/pages/CredentialTypes.jsx:19
|
||||
msgid "Credential Types"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:126
|
||||
#: src/pages/Credentials.jsx:19
|
||||
msgid "Credentials"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:29
|
||||
msgid "Current page"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:95
|
||||
#: src/pages/Dashboard.jsx:19
|
||||
msgid "Dashboard"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/PaginatedDataList/ToolbarDeleteButton.jsx:95
|
||||
#: src/components/PaginatedDataList/ToolbarDeleteButton.jsx:119
|
||||
#: src/components/PaginatedDataList/ToolbarDeleteButton.jsx:143
|
||||
#: src/pages/Organizations/components/DeleteRoleConfirmationModal.jsx:42
|
||||
msgid "Delete"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/PaginatedDataList/ToolbarDeleteButton.jsx:132
|
||||
msgid "Delete {0}"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/PaginatedDataList/ToolbarDeleteButton.jsx:131
|
||||
msgid "Delete {itemName}"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationForm.jsx:113
|
||||
#: src/pages/Organizations/screens/Organization/OrganizationDetail.jsx:75
|
||||
msgid "Description"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/Organizations.jsx:42
|
||||
#: src/pages/Organizations/screens/Organization/Organization.jsx:123
|
||||
msgid "Details"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/screens/Organization/OrganizationDetail.jsx:110
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/Organizations.jsx:41
|
||||
msgid "Edit Details"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/ExpandCollapse/ExpandCollapse.jsx:44
|
||||
msgid "Expand"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/NotificationsList/NotificationListItem.jsx:82
|
||||
msgid "Failure"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:170
|
||||
#~ msgid "First"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:24
|
||||
msgid "Go to first page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:26
|
||||
msgid "Go to last page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:27
|
||||
msgid "Go to next page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:25
|
||||
msgid "Go to previous page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/PageHeaderToolbar.jsx:80
|
||||
msgid "Help"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/DeleteRoleConfirmationModal.jsx:54
|
||||
msgid "If you {0} want to remove access for this particular user, please remove them from the team."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/PageHeaderToolbar.jsx:66
|
||||
msgid "Info"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:188
|
||||
#: src/pages/InstanceGroups.jsx:19
|
||||
#: src/pages/Organizations/components/InstanceGroupsLookup.jsx:24
|
||||
#: src/pages/Organizations/components/InstanceGroupsLookup.jsx:42
|
||||
#: src/pages/Organizations/screens/Organization/OrganizationDetail.jsx:93
|
||||
msgid "Instance Groups"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:193
|
||||
msgid "Integrations"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Login.jsx:94
|
||||
msgid "Invalid username or password. Please try again."
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:136
|
||||
#: src/pages/Inventories.jsx:19
|
||||
msgid "Inventories"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:141
|
||||
#: src/pages/InventoryScripts.jsx:19
|
||||
msgid "Inventory Scripts"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:142
|
||||
#~ msgid "Items Per Page"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:22
|
||||
msgid "Items per page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:162
|
||||
#~ msgid "Items {itemMin} – {itemMax} of {count}"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/index.jsx:100
|
||||
#: src/index.jsx:209
|
||||
#: src/pages/Jobs.jsx:19
|
||||
msgid "Jobs"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/JobsSettings.jsx:19
|
||||
msgid "Jobs Settings"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:213
|
||||
#~ msgid "Last"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/pages/Organizations/screens/Organization/OrganizationDetail.jsx:87
|
||||
msgid "Last Modified"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/screens/Organization/OrganizationAccess.jsx:175
|
||||
msgid "Last Name"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:224
|
||||
#: src/pages/License.jsx:19
|
||||
msgid "License"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AddRole/SelectResourceStep.jsx:89
|
||||
msgid "Loading..."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/PageHeaderToolbar.jsx:120
|
||||
msgid "Logout"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:183
|
||||
#: src/pages/ManagementJobs.jsx:19
|
||||
msgid "Management Jobs"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationListItem.jsx:91
|
||||
msgid "Members"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/InstanceGroupsLookup.jsx:49
|
||||
#: src/pages/Organizations/screens/OrganizationsList.jsx:163
|
||||
msgid "Modified"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:110
|
||||
#: src/pages/Portal.jsx:19
|
||||
msgid "My View"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AddRole/AddResourceRole.jsx:134
|
||||
#: src/components/PaginatedDataList/PaginatedDataList.jsx:99
|
||||
#: src/pages/Organizations/components/InstanceGroupsLookup.jsx:48
|
||||
#: src/pages/Organizations/components/OrganizationAccessItem.jsx:100
|
||||
#: src/pages/Organizations/components/OrganizationForm.jsx:105
|
||||
#: src/pages/Organizations/screens/Organization/OrganizationAccess.jsx:173
|
||||
#: src/pages/Organizations/screens/Organization/OrganizationDetail.jsx:71
|
||||
#: src/pages/Organizations/screens/OrganizationsList.jsx:162
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:204
|
||||
#~ msgid "Next"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/components/PaginatedDataList/PaginatedDataList.jsx:119
|
||||
msgid "No {0} Found"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/NotificationTemplates.jsx:19
|
||||
msgid "Notification Templates"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:178
|
||||
#: src/pages/Organizations/Organizations.jsx:45
|
||||
#: src/pages/Organizations/screens/Organization/Organization.jsx:130
|
||||
msgid "Notifications"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/views/Organization.add.jsx:79
|
||||
#~ msgid "Organization Add"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/pages/Organizations/screens/Organization/Organization.jsx:144
|
||||
msgid "Organization detail tabs"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:152
|
||||
#: src/pages/Organizations/Organizations.jsx:38
|
||||
#: src/pages/Organizations/Organizations.jsx:24
|
||||
msgid "Organizations"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/views/Organizations.list.jsx:218
|
||||
#~ msgid "Organizations List"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:190
|
||||
#~ msgid "Page"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:189
|
||||
#~ msgid "Page <0/> of {pageCount}"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:193
|
||||
#~ msgid "Page Number"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:30
|
||||
msgid "Pagination"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Login.jsx:92
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:158
|
||||
#~ msgid "Per Page"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/components/PaginatedDataList/PaginatedDataList.jsx:122
|
||||
msgid "Please add {0} to populate this list"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/PaginatedDataList/PaginatedDataList.jsx:136
|
||||
#: src/components/PaginatedDataList/PaginatedDataList.jsx:199
|
||||
#~ msgid "Please add {0} {itemName} to populate this list"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/App.jsx:203
|
||||
#~ msgid "Portal Mode"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:179
|
||||
#~ msgid "Previous"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/index.jsx:88
|
||||
msgid "Primary Navigation"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:131
|
||||
#: src/pages/Projects.jsx:19
|
||||
msgid "Projects"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/DeleteRoleConfirmationModal.jsx:28
|
||||
msgid "Remove {0} Access"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:117
|
||||
msgid "Resources"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AddRole/AddResourceRole.jsx:214
|
||||
#: src/components/FormActionGroup/FormActionGroup.jsx:24
|
||||
#: src/components/FormActionGroup/FormActionGroup.jsx:24
|
||||
#: src/components/Lookup/Lookup.jsx:161
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:105
|
||||
#: src/pages/Schedules.jsx:19
|
||||
msgid "Schedules"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Search/Search.jsx:138
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Search/Search.jsx:131
|
||||
msgid "Search text input"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:28
|
||||
msgid "Select"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AnsibleSelect/AnsibleSelect.jsx:28
|
||||
msgid "Select Input"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AddRole/AddResourceRole.jsx:153
|
||||
msgid "Select Users Or Teams"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/PaginatedDataList/ToolbarDeleteButton.jsx:97
|
||||
msgid "Select a row to delete"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/DataListToolbar/DataListToolbar.jsx:108
|
||||
msgid "Select all"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AddRole/AddResourceRole.jsx:172
|
||||
msgid "Select items from list"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationForm.jsx:141
|
||||
msgid "Select the Instance Groups for this Organization to run on."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Lookup/Lookup.jsx:157
|
||||
msgid "Select {header}"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AddRole/AddResourceRole.jsx:181
|
||||
#: src/components/AddRole/AddResourceRole.jsx:192
|
||||
#: src/components/AddRole/AddResourceRole.jsx:209
|
||||
#: src/components/AddRole/SelectRoleStep.jsx:29
|
||||
#: src/components/Lookup/Lookup.jsx:187
|
||||
msgid "Selected"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:200
|
||||
msgid "Settings"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Sort/Sort.jsx:135
|
||||
msgid "Sort"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/NotificationsList/NotificationListItem.jsx:70
|
||||
msgid "Successful"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:214
|
||||
msgid "System"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/SystemSettings.jsx:19
|
||||
msgid "System Settings"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/DeleteRoleConfirmationModal.jsx:28
|
||||
msgid "Team"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationAccessItem.jsx:122
|
||||
msgid "Team Roles"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AddRole/AddResourceRole.jsx:163
|
||||
#: src/index.jsx:162
|
||||
#: src/pages/Organizations/Organizations.jsx:44
|
||||
#: src/pages/Organizations/components/OrganizationListItem.jsx:99
|
||||
#: src/pages/Organizations/screens/Organization/Organization.jsx:125
|
||||
#: src/pages/Teams.jsx:19
|
||||
msgid "Teams"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:121
|
||||
#: src/pages/Templates.jsx:19
|
||||
msgid "Templates"
|
||||
msgstr ""
|
||||
|
||||
#: src/util/validators.jsx:6
|
||||
msgid "This field must not be blank"
|
||||
msgstr ""
|
||||
|
||||
#: src/util/validators.jsx:16
|
||||
msgid "This field must not exceed {max} characters"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/NotificationsList/NotificationListItem.jsx:90
|
||||
msgid "Toggle notification failure"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/NotificationsList/NotificationListItem.jsx:78
|
||||
msgid "Toggle notification success"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AnsibleSelect/AnsibleSelect.jsx:35
|
||||
msgid "Use Default {label}"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/DeleteRoleConfirmationModal.jsx:28
|
||||
msgid "User"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/PageHeaderToolbar.jsx:113
|
||||
msgid "User Details"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:219
|
||||
msgid "User Interface"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/UISettings.jsx:19
|
||||
msgid "User Interface Settings"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationAccessItem.jsx:112
|
||||
msgid "User Roles"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AddRole/AddResourceRole.jsx:130
|
||||
#: src/pages/Login.jsx:91
|
||||
#: src/pages/Organizations/screens/Organization/OrganizationAccess.jsx:174
|
||||
msgid "Username"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/AddRole/AddResourceRole.jsx:158
|
||||
#: src/index.jsx:157
|
||||
#: src/pages/Users.jsx:19
|
||||
msgid "Users"
|
||||
msgstr ""
|
||||
|
||||
#: src/index.jsx:91
|
||||
msgid "Views"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Login.jsx:86
|
||||
msgid "Welcome to Ansible {brandName}! Please Sign In."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/PaginatedDataList/ToolbarDeleteButton.jsx:90
|
||||
msgid "You do not have permission to delete the following {0}: {itemsUnableToDelete}"
|
||||
msgstr ""
|
||||
|
||||
#: src/contexts/Network.jsx:40
|
||||
msgid "You have been logged out."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationDetail.jsx:56
|
||||
#~ msgid "add {currentTab}"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationDetail.jsx:45
|
||||
#~ msgid "adding {currentTab}"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/components/PaginatedDataList/ToolbarDeleteButton.jsx:148
|
||||
msgid "cancel delete"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/PaginatedDataList/ToolbarDeleteButton.jsx:140
|
||||
msgid "confirm delete"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationDetail.jsx:38
|
||||
#~ msgid "confirm removal of {currentTab}/cancel and go back to {currentTab} view."
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationDetail.jsx:60
|
||||
#~ msgid "delete {currentTab}"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationDetail.jsx:36
|
||||
#~ msgid "deleting {currentTab} association with orgs"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationEdit.jsx:20
|
||||
#~ msgid "edit view"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/components/Lookup/Lookup.jsx:128
|
||||
#: src/components/Pagination/Pagination.jsx:20
|
||||
msgid "items"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:198
|
||||
#~ msgid "of {pageCount}"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:21
|
||||
msgid "pages"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:23
|
||||
msgid "per page"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationEdit.jsx:22
|
||||
#~ msgid "save/cancel and go back to view"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationDetail.jsx:47
|
||||
#~ msgid "save/cancel and go back to {currentTab} view"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationListItem.jsx:29
|
||||
#~ msgid "select organization {itemId}"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationDetail.jsx:82
|
||||
#~ msgid "{0}"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/components/PaginatedDataList/PaginatedDataList.jsx:135
|
||||
msgid "{0} List"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/Organizations/components/OrganizationDetail.jsx:54
|
||||
#~ msgid "{currentTab} detail view"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/components/Pagination/Pagination.jsx:163
|
||||
#~ msgid "{itemMin} - {itemMax} of {count}"
|
||||
#~ msgstr ""
|
7
awx/ui_next/dist/index.html
vendored
Normal file
7
awx/ui_next/dist/index.html
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="/bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
232
awx/ui_next/images/brand-logo.svg
Normal file
232
awx/ui_next/images/brand-logo.svg
Normal file
@ -0,0 +1,232 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 500 500" style="enable-background:new 0 0 500 500;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{display:none;}
|
||||
.st1{display:inline;fill:#ED1C24;}
|
||||
.st2{fill:#42210B;}
|
||||
.st3{fill:#FFFFFF;}
|
||||
.st4{fill:#C69C6D;stroke:#8C6239;stroke-width:5;stroke-miterlimit:10;}
|
||||
.st5{fill:#FFFFFF;stroke:#42210B;stroke-width:3;stroke-miterlimit:10;}
|
||||
.st6{fill:#ED1C24;stroke:#8C6239;stroke-width:5;stroke-miterlimit:10;}
|
||||
.st7{fill:#A67C52;}
|
||||
.st8{fill:#ED1C24;}
|
||||
</style>
|
||||
<g class="st0">
|
||||
<path class="st1" d="M319.8,169.3c1.5-14.2,13.7-27.2,29.9-31.9c-13.1,1.5-27.3-1.7-36-10c-8.7-8.3-10-21.9-1.4-30.1
|
||||
c-12,6.7-28.1,8.1-41.4,3.4c-13.3-4.6-23.5-15.1-26.2-26.9c-2-8.8,0-17.9,2-26.7c-6.2,9.4-17.6,17.3-30.5,17.3
|
||||
c-12.9,0.1-25.7-10.2-22.9-20.7c-5.5,7.8-11.4,15.9-21,20.2c-9.5,4.3-23.7,2.7-28.2-5.5c-1.6,10.8-7.5,22-19.1,27
|
||||
c-9,3.9-21.5,2.2-28-3.8c5.7,11.4,4.3,25.3-4.1,35.6c-9.9,12.2-29.1,18.6-46.4,15.6c14.7,7.2,28.5,17.7,32.1,31.5
|
||||
c3.7,13.8-7.1,30.7-24.1,31.7c13.6,3.1,28,7.4,35.6,17.2c7.6,9.8,2.9,26.4-11.1,28c12.8-2.6,27.4,3.9,31.9,14.2
|
||||
c4.1,9.5-0.9,20.9-10.9,26.5c18.6-8.9,41-17.1,59.6-8.8c13.9,6.2,20.8,21.6,15.1,33.8c10.4-10.6,23-21.3,39.2-23.5
|
||||
c12.8-1.8,27.5,4.6,31.9,14.1c-0.3-12.7,6.1-25.5,17.5-34c13.8-10.3,34.4-14,52-9.2c-11.1-7.8-14.9-22-8.9-33
|
||||
c6-11,21.3-18,35.7-16.2C327.5,198.1,318.3,183.5,319.8,169.3z"/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st2" d="M179.7,297.3c-10.1,3.2-20.3,6-30.6,8.4c-10.7,2.5-21.7,5-32.8,5.1C96,311.1,79.9,297.2,60,296.1
|
||||
c-5.8-0.3-5.8,8.7,0,9c9.9,0.5,18.9,5.1,27.9,8.8c9.8,4,19.6,6.3,30.2,5.9c21.5-0.8,43.5-7.4,64-13.8
|
||||
C187.6,304.3,185.2,295.6,179.7,297.3L179.7,297.3z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st2" d="M322.2,194.8c17.9-8,36-18.5,44.3-37.2c4.2-9.3,6-19.2,7.2-29.3c1.5-11.7,2.5-23.4,3.7-35.2
|
||||
c0.6-5.8-8.4-5.7-9,0c-1.1,10.3-2.1,20.6-3.3,30.9c-1.1,9.7-2.5,19.7-6.4,28.7c-7.5,17.5-24.6,26.8-41.2,34.2
|
||||
C312.4,189.4,316.9,197.2,322.2,194.8L322.2,194.8z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
|
||||
<ellipse transform="matrix(0.5541 -0.8324 0.8324 0.5541 -219.4917 376.0051)" class="st2" cx="241.2" cy="392.9" rx="65.5" ry="33.7"/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st3" d="M224.1,442.5c22-11.5,38.7-31,47.1-54.3c2-5.5-6.7-7.8-8.7-2.4c-7.6,21.1-23.1,38.5-43,48.9
|
||||
C214.4,437.4,218.9,445.1,224.1,442.5L224.1,442.5z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
|
||||
<ellipse transform="matrix(0.9684 -0.2494 0.2494 0.9684 -66.4734 109.0276)" class="st2" cx="397" cy="316.8" rx="63.9" ry="32.9"/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st3" d="M363.8,341.5c28.3,7,58.7-0.8,80.2-20.5c4.3-3.9-2.1-10.3-6.4-6.4c-19.1,17.5-46.4,24.4-71.5,18.2
|
||||
C360.5,331.5,358.1,340.1,363.8,341.5L363.8,341.5z"/>
|
||||
</g>
|
||||
</g>
|
||||
<path class="st4" d="M156.9,96c-25.4,4.5-32.9,20.2-45,46.9c-20.2,44.4,2,90.3,5.6,97.5c18.4,36.5,42.3,36.8,60,80.6
|
||||
c8.6,21.2,4.6,25.2,13.1,37.5c20.4,29.2,63.7,36.1,91.9,33.8c40.3-3.3,91.5-28.8,108.8-82.5c17.1-53.2-6-112.1-41.2-131.2
|
||||
c-25.3-13.7-44.9-0.5-71.2-20.6c-21.6-16.5-18.4-33.1-37.5-48.8C227.9,98.1,203.7,87.7,156.9,96z"/>
|
||||
<ellipse transform="matrix(0.6622 -0.7494 0.7494 0.6622 65.2068 309.6339)" class="st2" cx="376" cy="82.5" rx="21" ry="15.5"/>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st3" d="M379.8,75.3c0.8,0.2-0.6-0.4-0.1-0.1c0.2,0.1,0.3,0.2,0.5,0.3c0.4,0.2-0.6-0.7-0.1-0.1
|
||||
c0.1,0.1,0.7,0.8,0.2,0.2c-0.4-0.5,0,0,0.1,0.1c0.4,0.7,0,0.2,0-0.2c0,0.1,0.1,0.4,0.2,0.5c0.3,0.9-0.1-1,0-0.1
|
||||
c0.1,2.3,2,4.6,4.5,4.5c2.3-0.1,4.6-2,4.5-4.5c-0.3-4.4-3-8.1-7.3-9.4c-2.2-0.7-5,0.8-5.5,3.1C376.1,72.2,377.4,74.5,379.8,75.3
|
||||
L379.8,75.3z"/>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<ellipse transform="matrix(0.9999 -1.433736e-02 1.433736e-02 0.9999 -4.303 0.8051)" class="st2" cx="54" cy="300.5" rx="21" ry="15.5"/>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st3" d="M52.2,297.5c1.1-0.3,1.4-0.4,2.5,0c0.8,0.3,1.3,0.7,2,1.7c1.5,1.9,4.8,1.6,6.4,0c1.9-1.9,1.5-4.4,0-6.4
|
||||
c-3.1-3.9-8.6-5.4-13.3-4C44.3,290.5,46.7,299.2,52.2,297.5L52.2,297.5z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st2" d="M149.3,108.8c4.9-10.8-1.3-24.2-12.9-26.9c-1.9-0.4-2.7,2.4-0.8,2.9c9.6,2.3,15.3,13.5,11.2,22.5
|
||||
C145.9,109,148.5,110.5,149.3,108.8L149.3,108.8z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st2" d="M141.2,112.3c2.4-9.4-5.4-19.3-15.2-19c-1.9,0.1-1.9,3.1,0,3c7.8-0.2,14.2,7.6,12.3,15.2
|
||||
C137.8,113.4,140.7,114.2,141.2,112.3L141.2,112.3z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st2" d="M132.6,118c-1.1-8.3-10.9-13.4-18.2-9.1c-1.7,1-0.2,3.6,1.5,2.6c5.2-3,12.9,0.4,13.7,6.5
|
||||
C129.8,119.9,132.8,119.9,132.6,118L132.6,118z"/>
|
||||
</g>
|
||||
</g>
|
||||
<path class="st5" d="M215.5,166.5l34-73c0,0,35,0,46,21c7.5,14.3,8,39,8,39L215.5,166.5z"/>
|
||||
<path class="st5" d="M208.2,170.5l-79.5-12.7c0,0-19.6,29-8.4,49.9c7.6,14.2,27.8,28.5,27.8,28.5L208.2,170.5z"/>
|
||||
<path class="st2" d="M210.5,164.5l33-74c0,0-2.5-5.5-8-7s-12,0-12,0L210.5,164.5z"/>
|
||||
<path class="st2" d="M207.4,165.3l-73.1-35c0,0-5.6,2.4-7.2,7.8c-1.6,5.5-0.3,12-0.3,12L207.4,165.3z"/>
|
||||
<path d="M215.5,166.5L234,127c0,0,17-6,25.5,7.5c8.6,13.6-3.5,25.5-3.5,25.5L215.5,166.5z"/>
|
||||
<path d="M206.7,170.9l-29.6,32c0,0-18,0.5-22-14.9c-4-15.6,11.1-23.2,11.1-23.2L206.7,170.9z"/>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st3" d="M243.4,139.1c-0.6,0.2-0.7,0.3-0.4,0.2c0.3-0.1,0.2-0.1-0.5,0.1c0.7,0-0.3,0-0.4-0.1c0.1,0,0.3,0.1,0.4,0.1
|
||||
c0.3,0.1,0.2,0-0.4-0.2c0,0,0.6,0.3,0.6,0.3c0.5,0.2-0.9-0.6-0.1-0.1c0.6,0.4-0.3-0.5-0.1-0.1c0.3,0.5-0.3-1-0.1-0.2
|
||||
c0.2,0.8,0-1,0-0.1c0,2.4,2.1,4.6,4.5,4.5c2.5-0.1,4.5-2,4.5-4.5c0-3-1.6-5.7-4.1-7.3c-2.6-1.7-5.6-1.6-8.4-0.4
|
||||
c-2.2,0.9-2.8,4.3-1.6,6.2C238.7,139.7,241,140.1,243.4,139.1L243.4,139.1z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st3" d="M173.5,176.4c-0.5-0.3-0.1,0,0.2,0.1c-0.7-0.6,0.3,0.5,0.1,0c-0.3-0.5,0.4,0.8,0.1,0.2
|
||||
c-0.4-0.8,0.2,0.2,0,0.1c0,0,0-0.6,0-0.6c-0.1,0.1-0.1,1,0,0.3c-0.1,0.2-0.1,0.3-0.2,0.5c0.2-0.3,0.2-0.4,0-0.1
|
||||
c-0.2,0.2-0.2,0.3-0.1,0.1c0.2-0.2,0.1-0.2-0.3,0.2c1.9-1.4,3-4,1.6-6.2c-1.2-1.9-4.1-3.1-6.2-1.6c-2.4,1.7-4,4.3-3.9,7.4
|
||||
c0.1,3,1.6,5.7,4.1,7.3c2,1.2,5,0.5,6.2-1.6C176.3,180.4,175.7,177.7,173.5,176.4L173.5,176.4z"/>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<ellipse transform="matrix(0.862 -0.5069 0.5069 0.862 -88.3186 186.5516)" class="st6" cx="298.5" cy="255.5" rx="79.5" ry="68.5"/>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st7" d="M173.6,109.8c-2.1,2-3.9,4.6-3.6,7.6c0.3,3.5,2.8,6.6,6.6,6.7c6,0.2,11.5-7.7,8.2-13c-1-1.7-3.1-3.1-5.2-3
|
||||
c-1.7,0.1-3.1,0.8-4.4,1.9c-2,1.8-2.8,5.2-1.9,7.7c2.4,6.6,11.8,5.9,13.8-0.7c0.7-2.5-0.9-5.6-3.5-6.2c-2.7-0.6-5.4,0.8-6.2,3.5
|
||||
c0.6-2.1,3.1-2.6,4.6-1c0.8,0.9,1,1.8,0.8,2.8c0.2-0.5,0.1-0.4-0.1,0.3c-0.4,0.7-1,1.2-1.8,1.4c-0.9,0-1.8,0-2.7,0
|
||||
c-1.8-0.6-2.5-1.6-2.3-3.1c-0.1-0.4-0.1-0.7,0.1-1c0.2-0.3,0.1-0.3-0.1,0.1c0.1-0.1,0.2-0.2,0.3-0.4c-0.2,0.3-0.5,0.5-0.7,0.8
|
||||
c-0.1,0.1-0.2,0.2-0.3,0.3c-0.3,0.2-0.2,0.2,0.1-0.1c1.3,0.2,2.6,0.4,3.9,0.6c0.2,0.4,0.5,0.9,0.7,1.3c0.2,0.6-0.2,0.9-0.2,1.4
|
||||
c0,0.4,0.4-0.5-0.1,0.1c0.3-0.4,0.6-0.7,1-1c1.9-1.8,2-5.3,0-7.1C178.7,107.9,175.6,107.8,173.6,109.8L173.6,109.8z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st7" d="M151.2,248.6c-5.7,7,1.7,16.9,10,13.3c3.4-1.5,6.3-5,6.3-8.9c0-4.2-2.7-7.6-7-7.8c-3.1-0.1-5.8,3.3-4.8,6.3
|
||||
c1.2,3.4,3.7,6.1,7.3,7c2.6,0.6,5.4-0.8,6.2-3.5c0.7-2.5-0.9-5.5-3.5-6.2c-1.7-0.4,0,0.1-0.2,0.1c-0.4,0-0.4-0.8-0.1-0.1
|
||||
c-1.6,2.1-3.2,4.2-4.8,6.3c-2.4-0.1-2.8-1.1-3-2.6c0.1,0.7-0.1,0.2,0.1-0.1c0.7-0.9-0.5,0.5,0,0c-0.5,0.5-0.3,0.1-0.2,0.2
|
||||
c0.1,0,0.6,0,0.7,0c0.4,0.1,0.5,0.4,0.8,0.6c0.2,0.3,0.2,0.2-0.1-0.2c0.1,0.1,0.1,0.3,0.2,0.4c0,1,0.1,1.1-0.7,2.1
|
||||
c1.7-2.1,2-5,0-7.1C156.5,246.8,152.9,246.5,151.2,248.6L151.2,248.6z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st7" d="M204.1,205.7c0.8,4.8,5.3,8.6,10.1,8.6c5.1,0,9.5-3.9,10.3-8.9c0.7-4.4-0.2-12.1-5.3-13.6
|
||||
c-2.7-0.8-5.2,0.5-7,2.4c-1.1,1.2-1.5,1.7-3.1,1.2c0.7,2.8,1.5,5.6,2.2,8.4c0.2-0.2-0.5,0.2-0.5,0.2c6.3,1.4,8.9-8.2,2.7-9.6
|
||||
c-3.5-0.8-6.6,0-9.3,2.4c-3,2.6-1.1,7.2,2.2,8.4c2.6,0.9,5.5,0.8,8-0.2c1.3-0.5,2.4-1.2,3.4-2.1c0.4-0.3,0.7-0.6,1-1
|
||||
c0.2-0.3,0.4-0.5,0.6-0.7c0.4-0.4,0.3-0.4-0.5,0.3c-0.9,0-1.8,0-2.7,0c0.2,0.1,0.3,0.1,0.5,0.2c-0.7-0.4-1.5-0.9-2.2-1.3
|
||||
c0.1,0.2,0.3,0.3,0.4,0.5c-0.4-0.7-0.9-1.5-1.3-2.2c0.4,1.2,0.8,2.5,1,3.7c0,0.4,0,0.8,0,1.2c0,0.5-0.5,0.9,0,0.4
|
||||
c-0.8,0.6-0.9,0.2-1.1-0.9c-0.4-2.7-3.8-4.1-6.2-3.5C204.7,200.3,203.7,203,204.1,205.7L204.1,205.7z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st7" d="M265.9,179.6c0.2,0.4,0.5,0.9,0.7,1.3c0.6,1.1,1.8,2,3,2.3c1.2,0.3,2.8,0.2,3.9-0.5c1.1-0.7,2-1.7,2.3-3
|
||||
c0.3-1.4,0.1-2.6-0.5-3.9c-0.2-0.4-0.5-0.9-0.7-1.3c-0.6-1.1-1.8-2-3-2.3c-1.2-0.3-2.8-0.2-3.9,0.5c-1.1,0.7-2,1.7-2.3,3
|
||||
C265.1,177.1,265.3,178.3,265.9,179.6L265.9,179.6z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st7" d="M200.4,295.8c-6.1,1.6-8.1,8.6-5,13.7c2.8,4.7,9.1,7.2,14.3,5.4c4.9-1.7,7.8-7.1,6.3-12.2
|
||||
c-0.8-2.7-2.7-4.8-5.3-5.8c-1.4-0.5-2.8-0.7-4.2-0.8c-0.1,0-0.9-0.1-0.9-0.1c0.2-0.4,1.2,2.5,0.9,0.7c0,0.9,0,1.8,0,2.7
|
||||
c-0.1,0.1-0.1,0.1-0.2,0.2c3.1-5.6-5.5-10.7-8.6-5c-1.7,3-1.1,6.6,1.4,9c1.3,1.2,2.8,2,4.5,2.3c0.8,0.1,1.6,0.2,2.4,0.3
|
||||
c0.4,0,0.7,0,1.1,0.1c0.2,0.1,0.1,0.1-0.2-0.1c0,0.1-0.6-0.5-0.6-0.5c-0.1-0.1-0.1-0.2,0-0.3c0.1-0.3,0.1-0.1-0.1,0.5
|
||||
c-0.3-0.1,0.7-0.2-0.3-0.3c-0.9-0.1-1.1-0.6-1.8-0.9c0,0-0.2-0.3-0.3-0.3c0.3,0-0.8,1.2-0.8,1.2
|
||||
C209.3,303.8,206.6,294.2,200.4,295.8L200.4,295.8z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st7" d="M244.8,355.3c-4-6.2-11.2-2.3-12,3.9c-0.8,5.9,1.8,12,6.5,15.6c4.5,3.5,11.5,4.9,16.7,2.1
|
||||
c6.4-3.3,5.4-9.8,4.9-15.9c-0.5-6.3-1.9-12-9.5-12.1c-5.1-0.1-13.1,0.2-14.5,6.4c-1.2,5.4,2.5,12.8,8.2,13.8
|
||||
c6.2,1.1,11.2-5.5,7.8-11c-2.2-3.5-8.1-3.1-9.1,1.2c-1.1,4.4,0.5,8,4.1,10.6c5.2,3.8,10.2-4.8,5-8.6c0.2,0.2,0.4,0.5,0.5,0.7
|
||||
c-3,0.4-6.1,0.8-9.1,1.2c-0.4-0.7,3.4-3.1,2.9-4.8c-0.8-2.6-1.7,1.4-1.9,1.1c0,0.1,5.2-0.1,5.6-0.4c0.7,0.1,0.8-0.1,0.2-0.6
|
||||
c-0.4-0.7-0.5-0.8-0.4-0.3c-0.2,0.3,0.2,1.9,0.2,2.3c0.2,2,0.3,4,0.5,5.9c0.1,1.6,0.4,1.7-1.1,2c-1.3,0.2-2.9-0.3-4-0.9
|
||||
c-1.4-0.8-2.5-2-3.1-3.5c-0.3-0.7-0.4-1.3-0.5-2c0-0.3-0.1-0.7,0-1c0.2-1.9-1.1-1.5-3.8,1.2c-1-0.8-2-1.5-3-2.3
|
||||
c0.1,0.2,0.2,0.4,0.4,0.6C239.6,365.7,248.3,360.7,244.8,355.3L244.8,355.3z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st7" d="M336.5,337.4c-2.4-1.5-5.1-2.5-7.9-1.8c-2.7,0.7-4.9,3.2-5.3,6c-0.9,6.4,6.3,8.3,11.2,8.4
|
||||
c4.8,0.1,10.6-2.4,10.9-7.9c0.2-5.6-5.5-9.6-10.6-6.9c-5.7,3-0.7,11.6,5,8.6c-0.1,0.1-0.2,0.1-0.3,0.2c-0.9,0-1.8,0-2.7,0
|
||||
c-2.1-0.4-1.4-4.8-0.3-4.3c0,0-1.3,0.3-1.3,0.3c-0.6,0-1.2,0-1.8-0.1c-0.5-0.1-1-0.2-1.5-0.4c-1.2-0.5-1-0.2,0.6,0.7
|
||||
c0.2,0.8,0.5,1.7,0.7,2.5c-3.4,1.1-4.4,1.9-2.8,2.7c0.4,0.2,0.7,0.4,1.1,0.7C336.9,349.6,341.9,340.9,336.5,337.4L336.5,337.4z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<path class="st3" d="M224.3,256.5L252,273v-40l32,20v-38l28,17l4-28l23,12l-3-24c0,0-14-8-35.5-6.4c-11.6,0.9-24.3,6.8-33.5,11.4
|
||||
c-14,7-23.7,18.9-31.2,29.1C227,238,224.3,256.5,224.3,256.5z"/>
|
||||
<path class="st3" d="M372.9,248.9l-28.8-14.5l2.9,39.9l-33.3-17.7l2.7,37.9l-29.1-15l-2,28.2l-23.8-10.3l4.7,23.7
|
||||
c0,0,14.5,7,35.9,3.8c11.5-1.7,23.7-8.5,32.6-13.8c13.5-8,22.3-20.5,29-31.2C371.5,267.5,372.9,248.9,372.9,248.9z"/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st8" d="M235.2,121.6c8.5-3.1,23.2-0.1,27.8,8.4c2.3,4.4,4.5,9.9,4.5,14.9c0.1,5.5-2.7,10.5-5.3,15.3
|
||||
c-1.5,2.8,2.8,5.4,4.3,2.5c3.1-5.8,6.3-11.9,6-18.7c-0.3-6-2.8-12.8-5.9-17.9c-6-9.5-22.6-13.1-32.7-9.4
|
||||
C230.9,117.8,232.2,122.7,235.2,121.6L235.2,121.6z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st8" d="M241.1,110.5c11.6-2.3,25.6,2.3,32.2,12.4c6.6,10.2,6.1,22.8,3.1,34.2c-1.3,5,6.4,7.1,7.7,2.1
|
||||
c3.8-14.3,3.8-30.3-5.5-42.6c-8.9-11.7-25.5-16.6-39.6-13.8C233.9,103.8,236.1,111.5,241.1,110.5L241.1,110.5z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st8" d="M245.4,97.5c7.8-1.8,15.5,0,22.9,2.8c7.2,2.7,15,6.1,20.3,11.8c10.7,11.7,9.5,29.3,8.7,44
|
||||
c-0.3,6.4,9.7,6.4,10,0c1-17.9,1.2-38.5-12.7-52.1c-6.4-6.3-15.3-10.2-23.6-13.3c-9.1-3.4-18.6-4.9-28.2-2.8
|
||||
C236.5,89.2,239.1,98.9,245.4,97.5L245.4,97.5z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st8" d="M155.8,158.5c-13.1,4.8-14.2,21.6-10.1,33.1c4.3,12,15.2,20.6,28.2,20.5c3.2,0,3.2-5,0-5
|
||||
c-9.9,0.1-18.6-5.9-22.6-14.9c-3.9-8.6-5.2-24.8,5.8-28.9C160.2,162.3,158.9,157.4,155.8,158.5L155.8,158.5z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st8" d="M164.1,216.5c-11.4-2.2-18.8-11.4-22.7-21.9c-3.6-9.6-7.7-25.3,1.2-33.1c3.9-3.4-1.8-9-5.7-5.7
|
||||
c-11.3,9.9-7.9,28.5-3.3,40.9c4.8,13,14.1,24.7,28.3,27.5C167,225.2,169.1,217.5,164.1,216.5L164.1,216.5z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st8" d="M152,231.7c-27.3-13.3-38.1-46.5-23.3-73.2c3.1-5.6-5.5-10.7-8.6-5c-17.3,31.2-5.3,71.1,26.9,86.9
|
||||
C152.7,243.1,157.8,234.5,152,231.7L152,231.7z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 12 KiB |
41
awx/ui_next/jest.config.js
Normal file
41
awx/ui_next/jest.config.js
Normal file
@ -0,0 +1,41 @@
|
||||
module.exports = {
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.{js,jsx}'
|
||||
],
|
||||
coveragePathIgnorePatterns: [
|
||||
'<rootDir>/src/locales',
|
||||
'index.js'
|
||||
],
|
||||
moduleNameMapper: {
|
||||
'\\.(css|scss|less)$': '<rootDir>/__mocks__/styleMock.js',
|
||||
'^@api(.*)$': '<rootDir>/src/api$1',
|
||||
'^@components(.*)$': '<rootDir>/src/components$1',
|
||||
'^@contexts(.*)$': '<rootDir>/src/contexts$1',
|
||||
'^@screens(.*)$': '<rootDir>/src/screens$1',
|
||||
'^@util(.*)$': '<rootDir>/src/util$1',
|
||||
'^@types(.*)$': '<rootDir>/src/types$1',
|
||||
'^@testUtils(.*)$': '<rootDir>/testUtils$1',
|
||||
},
|
||||
setupFiles: [
|
||||
'@nteract/mockument'
|
||||
],
|
||||
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
|
||||
snapshotSerializers: [
|
||||
"enzyme-to-json/serializer"
|
||||
],
|
||||
testMatch: [
|
||||
'<rootDir>/**/*.test.{js,jsx}'
|
||||
],
|
||||
testEnvironment: 'jsdom',
|
||||
testURL: 'http://127.0.0.1:3001',
|
||||
transform: {
|
||||
'^.+\\.(js|jsx)$': 'babel-jest',
|
||||
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/__mocks__/fileMock.js',
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
'[/\\\\]node_modules[/\\\\].+\\.(?!(axios)/)(js|jsx)$'
|
||||
],
|
||||
watchPathIgnorePatterns: [
|
||||
'<rootDir>/node_modules'
|
||||
]
|
||||
};
|
9
awx/ui_next/jest.setup.js
Normal file
9
awx/ui_next/jest.setup.js
Normal file
@ -0,0 +1,9 @@
|
||||
require('@babel/polyfill');
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const asyncFlush = () => new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
const enzyme = require('enzyme');
|
||||
const Adapter = require('enzyme-adapter-react-16');
|
||||
|
||||
enzyme.configure({ adapter: new Adapter() });
|
17193
awx/ui_next/package-lock.json
generated
Normal file
17193
awx/ui_next/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
74
awx/ui_next/package.json
Normal file
74
awx/ui_next/package.json
Normal file
@ -0,0 +1,74 @@
|
||||
{
|
||||
"name": "awx-react",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.jsx",
|
||||
"scripts": {
|
||||
"start": "webpack-dev-server --config ./webpack.config.js --mode development",
|
||||
"test": "jest --coverage",
|
||||
"test-watch": "jest --watch",
|
||||
"lint": "eslint --ext .js --ext .jsx .",
|
||||
"add-locale": "lingui add-locale",
|
||||
"extract-strings": "lingui extract",
|
||||
"compile-strings": "lingui compile",
|
||||
"prettier": "prettier --write \"src/**/*.{js,jsx,scss}\""
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "Apache",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.2.0",
|
||||
"@babel/plugin-proposal-class-properties": "^7.1.0",
|
||||
"@babel/polyfill": "^7.0.0",
|
||||
"@babel/preset-env": "^7.1.0",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@lingui/cli": "^2.7.4",
|
||||
"@lingui/macro": "^2.7.2",
|
||||
"@nteract/mockument": "^1.0.4",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-jest": "^24.7.1",
|
||||
"babel-loader": "^8.0.4",
|
||||
"babel-plugin-macros": "^2.4.2",
|
||||
"babel-plugin-styled-components": "^1.10.0",
|
||||
"css-loader": "^1.0.0",
|
||||
"enzyme": "^3.9.0",
|
||||
"enzyme-adapter-react-16": "^1.12.1",
|
||||
"enzyme-to-json": "^3.3.5",
|
||||
"eslint": "^5.6.0",
|
||||
"eslint-config-airbnb": "^17.1.0",
|
||||
"eslint-config-prettier": "^5.0.0",
|
||||
"eslint-import-resolver-webpack": "0.11.1",
|
||||
"eslint-plugin-import": "^2.14.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.1.1",
|
||||
"eslint-plugin-react": "^7.11.1",
|
||||
"file-loader": "^2.0.0",
|
||||
"history": "^4.9.0",
|
||||
"jest": "^24.7.1",
|
||||
"node-sass": "^4.12.0",
|
||||
"prettier": "^1.18.2",
|
||||
"react-hot-loader": "^4.3.3",
|
||||
"sass-loader": "^7.1.0",
|
||||
"style-loader": "^0.23.0",
|
||||
"webpack": "^4.23.1",
|
||||
"webpack-cli": "^3.0.8",
|
||||
"webpack-dev-server": "^3.1.14"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lingui/react": "^2.7.2",
|
||||
"@patternfly/patternfly": "^2.7.0",
|
||||
"@patternfly/react-core": "^3.16.14",
|
||||
"@patternfly/react-icons": "^3.7.5",
|
||||
"@patternfly/react-tokens": "^2.3.3",
|
||||
"axios": "^0.18.0",
|
||||
"codemirror": "^5.47.0",
|
||||
"formik": "^1.5.1",
|
||||
"js-yaml": "^3.13.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.4.1",
|
||||
"react-codemirror2": "^6.0.0",
|
||||
"react-dom": "^16.4.1",
|
||||
"react-router-dom": "^4.3.1",
|
||||
"styled-components": "^4.2.0"
|
||||
}
|
||||
}
|
196
awx/ui_next/src/App.jsx
Normal file
196
awx/ui_next/src/App.jsx
Normal file
@ -0,0 +1,196 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { global_breakpoint_md } from '@patternfly/react-tokens';
|
||||
import {
|
||||
Nav,
|
||||
NavList,
|
||||
Page,
|
||||
PageHeader as PFPageHeader,
|
||||
PageSidebar,
|
||||
} from '@patternfly/react-core';
|
||||
import styled from 'styled-components';
|
||||
import { t } from '@lingui/macro';
|
||||
import { withI18n } from '@lingui/react';
|
||||
|
||||
import { ConfigAPI, MeAPI, RootAPI } from '@api';
|
||||
import About from '@components/About';
|
||||
import AlertModal from '@components/AlertModal';
|
||||
import NavExpandableGroup from '@components/NavExpandableGroup';
|
||||
import BrandLogo from '@components/BrandLogo';
|
||||
import PageHeaderToolbar from '@components/PageHeaderToolbar';
|
||||
import ErrorDetail from '@components/ErrorDetail';
|
||||
import { ConfigProvider } from '@contexts/Config';
|
||||
|
||||
const PageHeader = styled(PFPageHeader)`
|
||||
& .pf-c-page__header-brand-link {
|
||||
color: inherit;
|
||||
|
||||
&:hover {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
& svg {
|
||||
height: 76px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
class App extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
// initialize with a closed navbar if window size is small
|
||||
const isNavOpen =
|
||||
typeof window !== 'undefined' &&
|
||||
window.innerWidth >= parseInt(global_breakpoint_md.value, 10);
|
||||
|
||||
this.state = {
|
||||
ansible_version: null,
|
||||
custom_virtualenvs: null,
|
||||
me: null,
|
||||
version: null,
|
||||
isAboutModalOpen: false,
|
||||
isNavOpen,
|
||||
configError: null,
|
||||
};
|
||||
|
||||
this.handleLogout = this.handleLogout.bind(this);
|
||||
this.handleAboutClose = this.handleAboutClose.bind(this);
|
||||
this.handleAboutOpen = this.handleAboutOpen.bind(this);
|
||||
this.handleNavToggle = this.handleNavToggle.bind(this);
|
||||
this.handleConfigErrorClose = this.handleConfigErrorClose.bind(this);
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
await this.loadConfig();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
async handleLogout() {
|
||||
await RootAPI.logout();
|
||||
window.location.replace('/#/login');
|
||||
}
|
||||
|
||||
handleAboutOpen() {
|
||||
this.setState({ isAboutModalOpen: true });
|
||||
}
|
||||
|
||||
handleAboutClose() {
|
||||
this.setState({ isAboutModalOpen: false });
|
||||
}
|
||||
|
||||
handleNavToggle() {
|
||||
this.setState(({ isNavOpen }) => ({ isNavOpen: !isNavOpen }));
|
||||
}
|
||||
|
||||
handleConfigErrorClose() {
|
||||
this.setState({
|
||||
configError: null,
|
||||
});
|
||||
}
|
||||
|
||||
async loadConfig() {
|
||||
try {
|
||||
const [configRes, meRes] = await Promise.all([
|
||||
ConfigAPI.read(),
|
||||
MeAPI.read(),
|
||||
]);
|
||||
const {
|
||||
data: { ansible_version, custom_virtualenvs, version },
|
||||
} = configRes;
|
||||
const {
|
||||
data: {
|
||||
results: [me],
|
||||
},
|
||||
} = meRes;
|
||||
|
||||
this.setState({ ansible_version, custom_virtualenvs, version, me });
|
||||
} catch (err) {
|
||||
this.setState({ configError: err });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
ansible_version,
|
||||
custom_virtualenvs,
|
||||
isAboutModalOpen,
|
||||
isNavOpen,
|
||||
me,
|
||||
version,
|
||||
configError,
|
||||
} = this.state;
|
||||
const {
|
||||
i18n,
|
||||
render = () => {},
|
||||
routeGroups = [],
|
||||
navLabel = '',
|
||||
} = this.props;
|
||||
|
||||
const header = (
|
||||
<PageHeader
|
||||
showNavToggle
|
||||
onNavToggle={this.handleNavToggle}
|
||||
logo={<BrandLogo />}
|
||||
logoProps={{ href: '/' }}
|
||||
toolbar={
|
||||
<PageHeaderToolbar
|
||||
loggedInUser={me}
|
||||
isAboutDisabled={!version}
|
||||
onAboutClick={this.handleAboutOpen}
|
||||
onLogoutClick={this.handleLogout}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
const sidebar = (
|
||||
<PageSidebar
|
||||
isNavOpen={isNavOpen}
|
||||
nav={
|
||||
<Nav aria-label={navLabel}>
|
||||
<NavList>
|
||||
{routeGroups.map(({ groupId, groupTitle, routes }) => (
|
||||
<NavExpandableGroup
|
||||
key={groupId}
|
||||
groupId={groupId}
|
||||
groupTitle={groupTitle}
|
||||
routes={routes}
|
||||
/>
|
||||
))}
|
||||
</NavList>
|
||||
</Nav>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Page usecondensed="True" header={header} sidebar={sidebar}>
|
||||
<ConfigProvider
|
||||
value={{ ansible_version, custom_virtualenvs, me, version }}
|
||||
>
|
||||
{render({ routeGroups })}
|
||||
</ConfigProvider>
|
||||
</Page>
|
||||
<About
|
||||
ansible_version={ansible_version}
|
||||
version={version}
|
||||
isOpen={isAboutModalOpen}
|
||||
onClose={this.handleAboutClose}
|
||||
/>
|
||||
<AlertModal
|
||||
isOpen={configError}
|
||||
variant="danger"
|
||||
title={i18n._(t`Error!`)}
|
||||
onClose={this.handleConfigErrorClose}
|
||||
>
|
||||
{i18n._(t`Failed to retrieve configuration.`)}
|
||||
<ErrorDetail error={configError} />
|
||||
</AlertModal>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { App as _App };
|
||||
export default withI18n()(App);
|
121
awx/ui_next/src/App.test.jsx
Normal file
121
awx/ui_next/src/App.test.jsx
Normal file
@ -0,0 +1,121 @@
|
||||
import React from 'react';
|
||||
|
||||
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
||||
import { ConfigAPI, MeAPI, RootAPI } from '@api';
|
||||
import { asyncFlush } from '../jest.setup';
|
||||
|
||||
import App from './App';
|
||||
|
||||
jest.mock('./api');
|
||||
|
||||
describe('<App />', () => {
|
||||
const ansible_version = '111';
|
||||
const custom_virtualenvs = [];
|
||||
const version = '222';
|
||||
|
||||
beforeEach(() => {
|
||||
ConfigAPI.read = () =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
ansible_version,
|
||||
custom_virtualenvs,
|
||||
version,
|
||||
},
|
||||
});
|
||||
MeAPI.read = () => Promise.resolve({ data: { results: [{}] } });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('expected content is rendered', () => {
|
||||
const appWrapper = mountWithContexts(
|
||||
<App
|
||||
routeGroups={[
|
||||
{
|
||||
groupTitle: 'Group One',
|
||||
groupId: 'group_one',
|
||||
routes: [
|
||||
{ title: 'Foo', path: '/foo' },
|
||||
{ title: 'Bar', path: '/bar' },
|
||||
],
|
||||
},
|
||||
{
|
||||
groupTitle: 'Group Two',
|
||||
groupId: 'group_two',
|
||||
routes: [{ title: 'Fiz', path: '/fiz' }],
|
||||
},
|
||||
]}
|
||||
render={({ routeGroups }) =>
|
||||
routeGroups.map(({ groupId }) => <div key={groupId} id={groupId} />)
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
// page components
|
||||
expect(appWrapper.length).toBe(1);
|
||||
expect(appWrapper.find('PageHeader').length).toBe(1);
|
||||
expect(appWrapper.find('PageSidebar').length).toBe(1);
|
||||
|
||||
// sidebar groups and route links
|
||||
expect(appWrapper.find('NavExpandableGroup').length).toBe(2);
|
||||
expect(appWrapper.find('a[href="/#/foo"]').length).toBe(1);
|
||||
expect(appWrapper.find('a[href="/#/bar"]').length).toBe(1);
|
||||
expect(appWrapper.find('a[href="/#/fiz"]').length).toBe(1);
|
||||
|
||||
// inline render
|
||||
expect(appWrapper.find('#group_one').length).toBe(1);
|
||||
expect(appWrapper.find('#group_two').length).toBe(1);
|
||||
});
|
||||
|
||||
test('opening the about modal renders prefetched config data', async done => {
|
||||
const wrapper = mountWithContexts(<App />);
|
||||
wrapper.update();
|
||||
|
||||
// open about modal
|
||||
const aboutDropdown = 'Dropdown QuestionCircleIcon';
|
||||
const aboutButton = 'DropdownItem li button';
|
||||
const aboutModalContent = 'AboutModalBoxContent';
|
||||
const aboutModalClose = 'button[aria-label="Close Dialog"]';
|
||||
|
||||
await waitForElement(wrapper, aboutDropdown);
|
||||
wrapper.find(aboutDropdown).simulate('click');
|
||||
|
||||
const button = await waitForElement(
|
||||
wrapper,
|
||||
aboutButton,
|
||||
el => !el.props().disabled
|
||||
);
|
||||
button.simulate('click');
|
||||
|
||||
// check about modal content
|
||||
const content = await waitForElement(wrapper, aboutModalContent);
|
||||
expect(content.find('dd').text()).toContain(ansible_version);
|
||||
expect(content.find('pre').text()).toContain(`< AWX ${version} >`);
|
||||
|
||||
// close about modal
|
||||
wrapper.find(aboutModalClose).simulate('click');
|
||||
expect(wrapper.find(aboutModalContent)).toHaveLength(0);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
test('handleNavToggle sets state.isNavOpen to opposite', () => {
|
||||
const appWrapper = mountWithContexts(<App />).find('App');
|
||||
|
||||
const { handleNavToggle } = appWrapper.instance();
|
||||
[true, false, true, false, true].forEach(expected => {
|
||||
expect(appWrapper.state().isNavOpen).toBe(expected);
|
||||
handleNavToggle();
|
||||
});
|
||||
});
|
||||
|
||||
test('onLogout makes expected call to api client', async done => {
|
||||
const appWrapper = mountWithContexts(<App />).find('App');
|
||||
appWrapper.instance().handleLogout();
|
||||
await asyncFlush();
|
||||
expect(RootAPI.logout).toHaveBeenCalledTimes(1);
|
||||
done();
|
||||
});
|
||||
});
|
34
awx/ui_next/src/RootProvider.jsx
Normal file
34
awx/ui_next/src/RootProvider.jsx
Normal file
@ -0,0 +1,34 @@
|
||||
import React, { Component } from 'react';
|
||||
import { I18nProvider } from '@lingui/react';
|
||||
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
|
||||
import ja from '../build/locales/ja/messages';
|
||||
import en from '../build/locales/en/messages';
|
||||
|
||||
export function getLanguage(nav) {
|
||||
const language =
|
||||
(nav.languages && nav.languages[0]) || nav.language || nav.userLanguage;
|
||||
const languageWithoutRegionCode = language.toLowerCase().split(/[_-]+/)[0];
|
||||
|
||||
return languageWithoutRegionCode;
|
||||
}
|
||||
|
||||
class RootProvider extends Component {
|
||||
render() {
|
||||
const { children } = this.props;
|
||||
|
||||
const catalogs = { en, ja };
|
||||
const language = getLanguage(navigator);
|
||||
|
||||
return (
|
||||
<HashRouter>
|
||||
<I18nProvider language={language} catalogs={catalogs}>
|
||||
{children}
|
||||
</I18nProvider>
|
||||
</HashRouter>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default RootProvider;
|
18
awx/ui_next/src/RootProvider.test.jsx
Normal file
18
awx/ui_next/src/RootProvider.test.jsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { getLanguage } from './RootProvider';
|
||||
|
||||
describe('RootProvider.jsx', () => {
|
||||
test('getLanguage returns the expected language code', () => {
|
||||
expect(getLanguage({ languages: ['es-US'] })).toEqual('es');
|
||||
expect(
|
||||
getLanguage({
|
||||
languages: ['es-US'],
|
||||
language: 'fr-FR',
|
||||
userLanguage: 'en-US',
|
||||
})
|
||||
).toEqual('es');
|
||||
expect(getLanguage({ language: 'fr-FR', userLanguage: 'en-US' })).toEqual(
|
||||
'fr'
|
||||
);
|
||||
expect(getLanguage({ userLanguage: 'en-US' })).toEqual('en');
|
||||
});
|
||||
});
|
43
awx/ui_next/src/api/Base.js
Normal file
43
awx/ui_next/src/api/Base.js
Normal file
@ -0,0 +1,43 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const defaultHttp = axios.create({
|
||||
xsrfCookieName: 'csrftoken',
|
||||
xsrfHeaderName: 'X-CSRFToken',
|
||||
});
|
||||
|
||||
class Base {
|
||||
constructor(http = defaultHttp, baseURL) {
|
||||
this.http = http;
|
||||
this.baseUrl = baseURL;
|
||||
}
|
||||
|
||||
create(data) {
|
||||
return this.http.post(this.baseUrl, data);
|
||||
}
|
||||
|
||||
destroy(id) {
|
||||
return this.http.delete(`${this.baseUrl}${id}/`);
|
||||
}
|
||||
|
||||
read(params = {}) {
|
||||
return this.http.get(this.baseUrl, { params });
|
||||
}
|
||||
|
||||
readDetail(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/`);
|
||||
}
|
||||
|
||||
readOptions() {
|
||||
return this.http.options(this.baseUrl);
|
||||
}
|
||||
|
||||
replace(id, data) {
|
||||
return this.http.put(`${this.baseUrl}${id}/`, data);
|
||||
}
|
||||
|
||||
update(id, data) {
|
||||
return this.http.patch(`${this.baseUrl}${id}/`, data);
|
||||
}
|
||||
}
|
||||
|
||||
export default Base;
|
105
awx/ui_next/src/api/Base.test.jsx
Normal file
105
awx/ui_next/src/api/Base.test.jsx
Normal file
@ -0,0 +1,105 @@
|
||||
import Base from './Base';
|
||||
|
||||
describe('Base', () => {
|
||||
const createPromise = () => Promise.resolve();
|
||||
const mockBaseURL = '/api/v2/organizations/';
|
||||
const mockHttp = {
|
||||
delete: jest.fn(createPromise),
|
||||
get: jest.fn(createPromise),
|
||||
options: jest.fn(createPromise),
|
||||
patch: jest.fn(createPromise),
|
||||
post: jest.fn(createPromise),
|
||||
put: jest.fn(createPromise),
|
||||
};
|
||||
|
||||
const BaseAPI = new Base(mockHttp, mockBaseURL);
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('create calls http method with expected data', async done => {
|
||||
const data = { name: 'test ' };
|
||||
await BaseAPI.create(data);
|
||||
|
||||
expect(mockHttp.post).toHaveBeenCalledTimes(1);
|
||||
expect(mockHttp.post.mock.calls[0][1]).toEqual(data);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
test('destroy calls http method with expected data', async done => {
|
||||
const resourceId = 1;
|
||||
await BaseAPI.destroy(resourceId);
|
||||
|
||||
expect(mockHttp.delete).toHaveBeenCalledTimes(1);
|
||||
expect(mockHttp.delete.mock.calls[0][0]).toEqual(
|
||||
`${mockBaseURL}${resourceId}/`
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
test('read calls http method with expected data', async done => {
|
||||
const defaultParams = {};
|
||||
const testParams = { foo: 'bar' };
|
||||
|
||||
await BaseAPI.read(testParams);
|
||||
await BaseAPI.read();
|
||||
|
||||
expect(mockHttp.get).toHaveBeenCalledTimes(2);
|
||||
expect(mockHttp.get.mock.calls[0][1]).toEqual({ params: testParams });
|
||||
expect(mockHttp.get.mock.calls[1][1]).toEqual({ params: defaultParams });
|
||||
done();
|
||||
});
|
||||
|
||||
test('readDetail calls http method with expected data', async done => {
|
||||
const resourceId = 1;
|
||||
|
||||
await BaseAPI.readDetail(resourceId);
|
||||
|
||||
expect(mockHttp.get).toHaveBeenCalledTimes(1);
|
||||
expect(mockHttp.get.mock.calls[0][0]).toEqual(
|
||||
`${mockBaseURL}${resourceId}/`
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
||||
test('readOptions calls http method with expected data', async done => {
|
||||
await BaseAPI.readOptions();
|
||||
|
||||
expect(mockHttp.options).toHaveBeenCalledTimes(1);
|
||||
expect(mockHttp.options.mock.calls[0][0]).toEqual(`${mockBaseURL}`);
|
||||
done();
|
||||
});
|
||||
|
||||
test('replace calls http method with expected data', async done => {
|
||||
const resourceId = 1;
|
||||
const data = { name: 'test ' };
|
||||
|
||||
await BaseAPI.replace(resourceId, data);
|
||||
|
||||
expect(mockHttp.put).toHaveBeenCalledTimes(1);
|
||||
expect(mockHttp.put.mock.calls[0][0]).toEqual(
|
||||
`${mockBaseURL}${resourceId}/`
|
||||
);
|
||||
expect(mockHttp.put.mock.calls[0][1]).toEqual(data);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
test('update calls http method with expected data', async done => {
|
||||
const resourceId = 1;
|
||||
const data = { name: 'test ' };
|
||||
|
||||
await BaseAPI.update(resourceId, data);
|
||||
|
||||
expect(mockHttp.patch).toHaveBeenCalledTimes(1);
|
||||
expect(mockHttp.patch.mock.calls[0][0]).toEqual(
|
||||
`${mockBaseURL}${resourceId}/`
|
||||
);
|
||||
expect(mockHttp.patch.mock.calls[0][1]).toEqual(data);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
40
awx/ui_next/src/api/index.js
Normal file
40
awx/ui_next/src/api/index.js
Normal file
@ -0,0 +1,40 @@
|
||||
import Config from './models/Config';
|
||||
import InstanceGroups from './models/InstanceGroups';
|
||||
import JobTemplates from './models/JobTemplates';
|
||||
import Jobs from './models/Jobs';
|
||||
import Me from './models/Me';
|
||||
import Organizations from './models/Organizations';
|
||||
import Root from './models/Root';
|
||||
import Teams from './models/Teams';
|
||||
import UnifiedJobTemplates from './models/UnifiedJobTemplates';
|
||||
import UnifiedJobs from './models/UnifiedJobs';
|
||||
import Users from './models/Users';
|
||||
import WorkflowJobTemplates from './models/WorkflowJobTemplates';
|
||||
|
||||
const ConfigAPI = new Config();
|
||||
const InstanceGroupsAPI = new InstanceGroups();
|
||||
const JobTemplatesAPI = new JobTemplates();
|
||||
const JobsAPI = new Jobs();
|
||||
const MeAPI = new Me();
|
||||
const OrganizationsAPI = new Organizations();
|
||||
const RootAPI = new Root();
|
||||
const TeamsAPI = new Teams();
|
||||
const UnifiedJobTemplatesAPI = new UnifiedJobTemplates();
|
||||
const UnifiedJobsAPI = new UnifiedJobs();
|
||||
const UsersAPI = new Users();
|
||||
const WorkflowJobTemplatesAPI = new WorkflowJobTemplates();
|
||||
|
||||
export {
|
||||
ConfigAPI,
|
||||
InstanceGroupsAPI,
|
||||
JobTemplatesAPI,
|
||||
JobsAPI,
|
||||
MeAPI,
|
||||
OrganizationsAPI,
|
||||
RootAPI,
|
||||
TeamsAPI,
|
||||
UnifiedJobTemplatesAPI,
|
||||
UnifiedJobsAPI,
|
||||
UsersAPI,
|
||||
WorkflowJobTemplatesAPI,
|
||||
};
|
23
awx/ui_next/src/api/mixins/InstanceGroups.mixin.js
Normal file
23
awx/ui_next/src/api/mixins/InstanceGroups.mixin.js
Normal file
@ -0,0 +1,23 @@
|
||||
const InstanceGroupsMixin = parent =>
|
||||
class extends parent {
|
||||
readInstanceGroups(resourceId, params = {}) {
|
||||
return this.http.get(`${this.baseUrl}${resourceId}/instance_groups/`, {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
associateInstanceGroup(resourceId, instanceGroupId) {
|
||||
return this.http.post(`${this.baseUrl}${resourceId}/instance_groups/`, {
|
||||
id: instanceGroupId,
|
||||
});
|
||||
}
|
||||
|
||||
disassociateInstanceGroup(resourceId, instanceGroupId) {
|
||||
return this.http.post(`${this.baseUrl}${resourceId}/instance_groups/`, {
|
||||
id: instanceGroupId,
|
||||
disassociate: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default InstanceGroupsMixin;
|
102
awx/ui_next/src/api/mixins/Notifications.mixin.js
Normal file
102
awx/ui_next/src/api/mixins/Notifications.mixin.js
Normal file
@ -0,0 +1,102 @@
|
||||
const NotificationsMixin = parent =>
|
||||
class extends parent {
|
||||
readNotificationTemplates(id, params = {}) {
|
||||
return this.http.get(`${this.baseUrl}${id}/notification_templates/`, {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
readNotificationTemplatesSuccess(id, params = {}) {
|
||||
return this.http.get(
|
||||
`${this.baseUrl}${id}/notification_templates_success/`,
|
||||
{ params }
|
||||
);
|
||||
}
|
||||
|
||||
readNotificationTemplatesError(id, params = {}) {
|
||||
return this.http.get(
|
||||
`${this.baseUrl}${id}/notification_templates_error/`,
|
||||
{ params }
|
||||
);
|
||||
}
|
||||
|
||||
associateNotificationTemplatesSuccess(resourceId, notificationId) {
|
||||
return this.http.post(
|
||||
`${this.baseUrl}${resourceId}/notification_templates_success/`,
|
||||
{ id: notificationId }
|
||||
);
|
||||
}
|
||||
|
||||
disassociateNotificationTemplatesSuccess(resourceId, notificationId) {
|
||||
return this.http.post(
|
||||
`${this.baseUrl}${resourceId}/notification_templates_success/`,
|
||||
{ id: notificationId, disassociate: true }
|
||||
);
|
||||
}
|
||||
|
||||
associateNotificationTemplatesError(resourceId, notificationId) {
|
||||
return this.http.post(
|
||||
`${this.baseUrl}${resourceId}/notification_templates_error/`,
|
||||
{ id: notificationId }
|
||||
);
|
||||
}
|
||||
|
||||
disassociateNotificationTemplatesError(resourceId, notificationId) {
|
||||
return this.http.post(
|
||||
`${this.baseUrl}${resourceId}/notification_templates_error/`,
|
||||
{ id: notificationId, disassociate: true }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a helper method meant to simplify setting the "on" or "off" status of
|
||||
* a related notification.
|
||||
*
|
||||
* @param[resourceId] - id of the base resource
|
||||
* @param[notificationId] - id of the notification
|
||||
* @param[notificationType] - the type of notification, options are "success" and "error"
|
||||
* @param[associationState] - Boolean for associating or disassociating,
|
||||
* options are true or false
|
||||
*/
|
||||
// eslint-disable-next-line max-len
|
||||
updateNotificationTemplateAssociation(
|
||||
resourceId,
|
||||
notificationId,
|
||||
notificationType,
|
||||
associationState
|
||||
) {
|
||||
if (notificationType === 'success' && associationState === true) {
|
||||
return this.associateNotificationTemplatesSuccess(
|
||||
resourceId,
|
||||
notificationId
|
||||
);
|
||||
}
|
||||
|
||||
if (notificationType === 'success' && associationState === false) {
|
||||
return this.disassociateNotificationTemplatesSuccess(
|
||||
resourceId,
|
||||
notificationId
|
||||
);
|
||||
}
|
||||
|
||||
if (notificationType === 'error' && associationState === true) {
|
||||
return this.associateNotificationTemplatesError(
|
||||
resourceId,
|
||||
notificationId
|
||||
);
|
||||
}
|
||||
|
||||
if (notificationType === 'error' && associationState === false) {
|
||||
return this.disassociateNotificationTemplatesError(
|
||||
resourceId,
|
||||
notificationId
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Unsupported notificationType, associationState combination: ${notificationType}, ${associationState}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default NotificationsMixin;
|
10
awx/ui_next/src/api/models/Config.js
Normal file
10
awx/ui_next/src/api/models/Config.js
Normal file
@ -0,0 +1,10 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class Config extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/config/';
|
||||
}
|
||||
}
|
||||
|
||||
export default Config;
|
10
awx/ui_next/src/api/models/InstanceGroups.js
Normal file
10
awx/ui_next/src/api/models/InstanceGroups.js
Normal file
@ -0,0 +1,10 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class InstanceGroups extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/instance_groups/';
|
||||
}
|
||||
}
|
||||
|
||||
export default InstanceGroups;
|
22
awx/ui_next/src/api/models/JobTemplates.js
Normal file
22
awx/ui_next/src/api/models/JobTemplates.js
Normal file
@ -0,0 +1,22 @@
|
||||
import Base from '../Base';
|
||||
import InstanceGroupsMixin from '../mixins/InstanceGroups.mixin';
|
||||
|
||||
class JobTemplates extends InstanceGroupsMixin(Base) {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/job_templates/';
|
||||
|
||||
this.launch = this.launch.bind(this);
|
||||
this.readLaunch = this.readLaunch.bind(this);
|
||||
}
|
||||
|
||||
launch(id, data) {
|
||||
return this.http.post(`${this.baseUrl}${id}/launch/`, data);
|
||||
}
|
||||
|
||||
readLaunch(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/launch/`);
|
||||
}
|
||||
}
|
||||
|
||||
export default JobTemplates;
|
10
awx/ui_next/src/api/models/Jobs.js
Normal file
10
awx/ui_next/src/api/models/Jobs.js
Normal file
@ -0,0 +1,10 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class Jobs extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/jobs/';
|
||||
}
|
||||
}
|
||||
|
||||
export default Jobs;
|
10
awx/ui_next/src/api/models/Me.js
Normal file
10
awx/ui_next/src/api/models/Me.js
Normal file
@ -0,0 +1,10 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class Me extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/me/';
|
||||
}
|
||||
}
|
||||
|
||||
export default Me;
|
20
awx/ui_next/src/api/models/Organizations.js
Normal file
20
awx/ui_next/src/api/models/Organizations.js
Normal file
@ -0,0 +1,20 @@
|
||||
import Base from '../Base';
|
||||
import NotificationsMixin from '../mixins/Notifications.mixin';
|
||||
import InstanceGroupsMixin from '../mixins/InstanceGroups.mixin';
|
||||
|
||||
class Organizations extends InstanceGroupsMixin(NotificationsMixin(Base)) {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/organizations/';
|
||||
}
|
||||
|
||||
readAccessList(id, params = {}) {
|
||||
return this.http.get(`${this.baseUrl}${id}/access_list/`, { params });
|
||||
}
|
||||
|
||||
readTeams(id, params = {}) {
|
||||
return this.http.get(`${this.baseUrl}${id}/teams/`, { params });
|
||||
}
|
||||
}
|
||||
|
||||
export default Organizations;
|
51
awx/ui_next/src/api/models/Organizations.test.jsx
Normal file
51
awx/ui_next/src/api/models/Organizations.test.jsx
Normal file
@ -0,0 +1,51 @@
|
||||
import Organizations from './Organizations';
|
||||
import { describeNotificationMixin } from '../../../testUtils/apiReusable';
|
||||
|
||||
describe('OrganizationsAPI', () => {
|
||||
const orgId = 1;
|
||||
const searchParams = { foo: 'bar' };
|
||||
const createPromise = () => Promise.resolve();
|
||||
const mockHttp = { get: jest.fn(createPromise) };
|
||||
|
||||
const OrganizationsAPI = new Organizations(mockHttp);
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('read access list calls get with expected params', async done => {
|
||||
await OrganizationsAPI.readAccessList(orgId);
|
||||
await OrganizationsAPI.readAccessList(orgId, searchParams);
|
||||
|
||||
expect(mockHttp.get).toHaveBeenCalledTimes(2);
|
||||
expect(mockHttp.get.mock.calls[0]).toContainEqual(
|
||||
`/api/v2/organizations/${orgId}/access_list/`,
|
||||
{ params: {} }
|
||||
);
|
||||
expect(mockHttp.get.mock.calls[1]).toContainEqual(
|
||||
`/api/v2/organizations/${orgId}/access_list/`,
|
||||
{ params: searchParams }
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
test('read teams calls get with expected params', async done => {
|
||||
await OrganizationsAPI.readTeams(orgId);
|
||||
await OrganizationsAPI.readTeams(orgId, searchParams);
|
||||
|
||||
expect(mockHttp.get).toHaveBeenCalledTimes(2);
|
||||
expect(mockHttp.get.mock.calls[0]).toContainEqual(
|
||||
`/api/v2/organizations/${orgId}/teams/`,
|
||||
{ params: {} }
|
||||
);
|
||||
expect(mockHttp.get.mock.calls[1]).toContainEqual(
|
||||
`/api/v2/organizations/${orgId}/teams/`,
|
||||
{ params: searchParams }
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describeNotificationMixin(Organizations, 'Organizations[NotificationsMixin]');
|
30
awx/ui_next/src/api/models/Root.js
Normal file
30
awx/ui_next/src/api/models/Root.js
Normal file
@ -0,0 +1,30 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class Root extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/';
|
||||
this.redirectURL = '/api/v2/config/';
|
||||
}
|
||||
|
||||
async login(username, password, redirect = this.redirectURL) {
|
||||
const loginUrl = `${this.baseUrl}login/`;
|
||||
const un = encodeURIComponent(username);
|
||||
const pw = encodeURIComponent(password);
|
||||
const next = encodeURIComponent(redirect);
|
||||
|
||||
const data = `username=${un}&password=${pw}&next=${next}`;
|
||||
const headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
|
||||
|
||||
await this.http.get(loginUrl, { headers });
|
||||
const response = await this.http.post(loginUrl, data, { headers });
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
logout() {
|
||||
return this.http.get(`${this.baseUrl}logout/`);
|
||||
}
|
||||
}
|
||||
|
||||
export default Root;
|
52
awx/ui_next/src/api/models/Root.test.jsx
Normal file
52
awx/ui_next/src/api/models/Root.test.jsx
Normal file
@ -0,0 +1,52 @@
|
||||
import Root from './Root';
|
||||
|
||||
describe('RootAPI', () => {
|
||||
const createPromise = () => Promise.resolve();
|
||||
const mockHttp = {
|
||||
get: jest.fn(createPromise),
|
||||
post: jest.fn(createPromise),
|
||||
};
|
||||
|
||||
const RootAPI = new Root(mockHttp);
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('login calls get and post with expected content headers', async done => {
|
||||
const headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
|
||||
|
||||
await RootAPI.login('username', 'password');
|
||||
|
||||
expect(mockHttp.get).toHaveBeenCalledTimes(1);
|
||||
expect(mockHttp.get.mock.calls[0]).toContainEqual({ headers });
|
||||
|
||||
expect(mockHttp.post).toHaveBeenCalledTimes(1);
|
||||
expect(mockHttp.post.mock.calls[0]).toContainEqual({ headers });
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
test('login sends expected data', async done => {
|
||||
await RootAPI.login('foo', 'bar');
|
||||
await RootAPI.login('foo', 'bar', 'baz');
|
||||
|
||||
expect(mockHttp.post).toHaveBeenCalledTimes(2);
|
||||
expect(mockHttp.post.mock.calls[0]).toContainEqual(
|
||||
'username=foo&password=bar&next=%2Fapi%2Fv2%2Fconfig%2F'
|
||||
);
|
||||
expect(mockHttp.post.mock.calls[1]).toContainEqual(
|
||||
'username=foo&password=bar&next=baz'
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
test('logout calls expected http method', async done => {
|
||||
await RootAPI.logout();
|
||||
|
||||
expect(mockHttp.get).toHaveBeenCalledTimes(1);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
21
awx/ui_next/src/api/models/Teams.js
Normal file
21
awx/ui_next/src/api/models/Teams.js
Normal file
@ -0,0 +1,21 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class Teams extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/teams/';
|
||||
}
|
||||
|
||||
associateRole(teamId, roleId) {
|
||||
return this.http.post(`${this.baseUrl}${teamId}/roles/`, { id: roleId });
|
||||
}
|
||||
|
||||
disassociateRole(teamId, roleId) {
|
||||
return this.http.post(`${this.baseUrl}${teamId}/roles/`, {
|
||||
id: roleId,
|
||||
disassociate: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default Teams;
|
41
awx/ui_next/src/api/models/Teams.test.jsx
Normal file
41
awx/ui_next/src/api/models/Teams.test.jsx
Normal file
@ -0,0 +1,41 @@
|
||||
import Teams from './Teams';
|
||||
|
||||
describe('TeamsAPI', () => {
|
||||
const teamId = 1;
|
||||
const roleId = 7;
|
||||
const createPromise = () => Promise.resolve();
|
||||
const mockHttp = { post: jest.fn(createPromise) };
|
||||
|
||||
const TeamsAPI = new Teams(mockHttp);
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('associate role calls post with expected params', async done => {
|
||||
await TeamsAPI.associateRole(teamId, roleId);
|
||||
|
||||
expect(mockHttp.post).toHaveBeenCalledTimes(1);
|
||||
expect(mockHttp.post.mock.calls[0]).toContainEqual(
|
||||
`/api/v2/teams/${teamId}/roles/`,
|
||||
{ id: roleId }
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
test('read teams calls post with expected params', async done => {
|
||||
await TeamsAPI.disassociateRole(teamId, roleId);
|
||||
|
||||
expect(mockHttp.post).toHaveBeenCalledTimes(1);
|
||||
expect(mockHttp.post.mock.calls[0]).toContainEqual(
|
||||
`/api/v2/teams/${teamId}/roles/`,
|
||||
{
|
||||
id: roleId,
|
||||
disassociate: true,
|
||||
}
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
10
awx/ui_next/src/api/models/UnifiedJobTemplates.js
Normal file
10
awx/ui_next/src/api/models/UnifiedJobTemplates.js
Normal file
@ -0,0 +1,10 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class UnifiedJobTemplates extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/unified_job_templates/';
|
||||
}
|
||||
}
|
||||
|
||||
export default UnifiedJobTemplates;
|
10
awx/ui_next/src/api/models/UnifiedJobs.js
Normal file
10
awx/ui_next/src/api/models/UnifiedJobs.js
Normal file
@ -0,0 +1,10 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class UnifiedJobs extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/unified_jobs/';
|
||||
}
|
||||
}
|
||||
|
||||
export default UnifiedJobs;
|
21
awx/ui_next/src/api/models/Users.js
Normal file
21
awx/ui_next/src/api/models/Users.js
Normal file
@ -0,0 +1,21 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class Users extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/users/';
|
||||
}
|
||||
|
||||
associateRole(userId, roleId) {
|
||||
return this.http.post(`${this.baseUrl}${userId}/roles/`, { id: roleId });
|
||||
}
|
||||
|
||||
disassociateRole(userId, roleId) {
|
||||
return this.http.post(`${this.baseUrl}${userId}/roles/`, {
|
||||
id: roleId,
|
||||
disassociate: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default Users;
|
41
awx/ui_next/src/api/models/Users.test.jsx
Normal file
41
awx/ui_next/src/api/models/Users.test.jsx
Normal file
@ -0,0 +1,41 @@
|
||||
import Users from './Users';
|
||||
|
||||
describe('UsersAPI', () => {
|
||||
const userId = 1;
|
||||
const roleId = 7;
|
||||
const createPromise = () => Promise.resolve();
|
||||
const mockHttp = { post: jest.fn(createPromise) };
|
||||
|
||||
const UsersAPI = new Users(mockHttp);
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('associate role calls post with expected params', async done => {
|
||||
await UsersAPI.associateRole(userId, roleId);
|
||||
|
||||
expect(mockHttp.post).toHaveBeenCalledTimes(1);
|
||||
expect(mockHttp.post.mock.calls[0]).toContainEqual(
|
||||
`/api/v2/users/${userId}/roles/`,
|
||||
{ id: roleId }
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
test('read users calls post with expected params', async done => {
|
||||
await UsersAPI.disassociateRole(userId, roleId);
|
||||
|
||||
expect(mockHttp.post).toHaveBeenCalledTimes(1);
|
||||
expect(mockHttp.post.mock.calls[0]).toContainEqual(
|
||||
`/api/v2/users/${userId}/roles/`,
|
||||
{
|
||||
id: roleId,
|
||||
disassociate: true,
|
||||
}
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
10
awx/ui_next/src/api/models/WorkflowJobTemplates.js
Normal file
10
awx/ui_next/src/api/models/WorkflowJobTemplates.js
Normal file
@ -0,0 +1,10 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class WorkflowJobTemplates extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/workflow_job_templates/';
|
||||
}
|
||||
}
|
||||
|
||||
export default WorkflowJobTemplates;
|
275
awx/ui_next/src/app.scss
Normal file
275
awx/ui_next/src/app.scss
Normal file
@ -0,0 +1,275 @@
|
||||
// https://github.com/patternfly/patternfly-react/issues/1294
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
//
|
||||
// sidebar overrides
|
||||
//
|
||||
|
||||
.pf-c-page__sidebar {
|
||||
--pf-c-page__sidebar--md--Width: 255px;
|
||||
|
||||
.pf-c-nav {
|
||||
overflow-y: auto;
|
||||
|
||||
.pf-c-nav__section {
|
||||
--pf-c-nav__section--MarginTop: 8px;
|
||||
}
|
||||
|
||||
.pf-c-nav__section + .pf-c-nav__section {
|
||||
--pf-c-nav__section--MarginTop: 8px;
|
||||
}
|
||||
|
||||
.pf-c-nav__simple-list .pf-c-nav__link {
|
||||
--pf-c-nav__simple-list-link--PaddingBottom: 6px;
|
||||
--pf-c-nav__simple-list-link--PaddingTop: 6px;
|
||||
}
|
||||
|
||||
.pf-c-nav__section-title {
|
||||
--pf-c-nav__section-title--PaddingLeft: 24px;
|
||||
}
|
||||
|
||||
.pf-c-nav__link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 64px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// data list overrides
|
||||
//
|
||||
|
||||
.pf-c-data-list {
|
||||
--pf-global--target-size--MinHeight: 32px;
|
||||
--pf-global--target-size--MinWidth: 32px;
|
||||
--pf-global--FontSize--md: 14px;
|
||||
|
||||
.pf-c-badge:not(:last-child),
|
||||
.pf-c-switch:not(:last-child) {
|
||||
margin-right: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.pf-c-data-list__item-row {
|
||||
--pf-c-data-list__item-row--PaddingRight: 20px;
|
||||
--pf-c-data-list__item-row--PaddingLeft: 20px;
|
||||
}
|
||||
|
||||
.pf-c-data-list__item-content {
|
||||
--pf-c-data-list__item-content--PaddingBottom: 16px;
|
||||
|
||||
min-height: 59px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pf-c-data-list__item-control {
|
||||
--pf-c-data-list__item-control--PaddingTop: 16px;
|
||||
--pf-c-data-list__item-control--MarginRight: 8px;
|
||||
--pf-c-data-list__item-control--PaddingBottom: 16px;
|
||||
}
|
||||
|
||||
.pf-c-data-list__item {
|
||||
--pf-c-data-list__item--PaddingLeft: 20px;
|
||||
--pf-c-data-list__item--PaddingRight: 20px;
|
||||
}
|
||||
|
||||
.pf-c-data-list__cell {
|
||||
--pf-c-data-list__cell--PaddingTop: 16px;
|
||||
--pf-c-data-list__cell-cell--PaddingTop: 16px;
|
||||
|
||||
&.pf-c-data-list__cell--divider {
|
||||
--pf-c-data-list__cell-cell--MarginRight: 0;
|
||||
--pf-c-data-list__cell--PaddingTop: 12px;
|
||||
flex-grow: 0;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// pf modal overrides
|
||||
//
|
||||
|
||||
.awx-c-modal.pf-c-modal-box {
|
||||
margin: 0;
|
||||
padding: 24px;
|
||||
width: 600px;
|
||||
|
||||
.pf-c-modal-box__body {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.pf-c-modal-box__footer > .pf-c-button:not(:last-child) {
|
||||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.pf-c-modal-box__footer {
|
||||
--pf-c-modal-box__footer--PaddingTop: 20px;
|
||||
--pf-c-modal-box__footer--PaddingRight: 20px;
|
||||
--pf-c-modal-box__footer--PaddingBottom: 20px;
|
||||
--pf-c-modal-box__footer--PaddingLeft: 20px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.pf-c-modal-box__header {
|
||||
--pf-c-modal-box__header--PaddingTop: 10px;
|
||||
--pf-c-modal-box__header--PaddingRight: 0;
|
||||
--pf-c-modal-box__header--PaddingBottom: 0;
|
||||
--pf-c-modal-box__header--PaddingLeft: 20px;
|
||||
}
|
||||
|
||||
.pf-c-modal-box__body {
|
||||
--pf-c-modal-box__body--PaddingLeft: 20px;
|
||||
--pf-c-modal-box__body--PaddingRight: 20px;
|
||||
--pf-c-modal-box__body--PaddingBottom: 5px;
|
||||
}
|
||||
|
||||
//
|
||||
// pf tooltip overrides
|
||||
//
|
||||
|
||||
.pf-c-tooltip__content {
|
||||
--pf-c-tooltip__content--PaddingTop: 0.71rem;
|
||||
--pf-c-tooltip__content--PaddingRight: 0.71rem;
|
||||
--pf-c-tooltip__content--PaddingBottom: 0.71rem;
|
||||
--pf-c-tooltip__content--PaddingLeft: 0.71rem;
|
||||
}
|
||||
// higher specificity needed to override PF styles added dynamically to page
|
||||
.pf-c-tooltip .pf-c-tooltip__content {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
//
|
||||
// pf empty state overrides
|
||||
//
|
||||
|
||||
.pf-c-empty-state {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
//
|
||||
// assorted custom component styles
|
||||
// note that these should be given a consistent prefix
|
||||
// and bem style, as well as moved into component-based scss files
|
||||
//
|
||||
|
||||
.awx-lookup .pf-c-form-control {
|
||||
--pf-c-form-control--Height: 90px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.at-c-listCardBody {
|
||||
--pf-c-card__footer--PaddingX: 0;
|
||||
--pf-c-card__footer--PaddingY: 0;
|
||||
--pf-c-card__body--PaddingX: 0;
|
||||
--pf-c-card__body--PaddingY: 0;
|
||||
}
|
||||
|
||||
.awx-c-card {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
//
|
||||
// PF Alert notification component overrides
|
||||
//
|
||||
|
||||
.pf-c-alert__title {
|
||||
--pf-c-alert__title--PaddingTop: 20px;
|
||||
--pf-c-alert__title--PaddingRight: 20px;
|
||||
--pf-c-alert__title--PaddingBottom: 20px;
|
||||
--pf-c-alert__title--PaddingLeft: 20px;
|
||||
}
|
||||
|
||||
.pf-c-alert__description {
|
||||
--pf-c-alert__description--PaddingRight: 20px;
|
||||
--pf-c-alert__description--PaddingBottom: 20px;
|
||||
--pf-c-alert__description--PaddingLeft: 20px;
|
||||
}
|
||||
|
||||
.pf-c-alert {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.pf-c-alert__icon {
|
||||
--pf-c-alert__icon--Color: white;
|
||||
}
|
||||
|
||||
.at-u-textRight {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
//
|
||||
// AlertModal styles
|
||||
//
|
||||
|
||||
.at-c-alertModal.pf-c-modal-box {
|
||||
border: 0;
|
||||
border-left: 56px solid black;
|
||||
|
||||
.at-c-alertModal__icon {
|
||||
position: absolute;
|
||||
font-size: 23px;
|
||||
top: 28px;
|
||||
left: -39px;
|
||||
}
|
||||
}
|
||||
|
||||
.at-c-alertModal--warning.pf-c-modal-box {
|
||||
border-color: var(--pf-global--warning-color--100);
|
||||
|
||||
.pf-c-title {
|
||||
color: var(--pf-global--warning-color--200);
|
||||
}
|
||||
|
||||
.at-c-alertModal__icon {
|
||||
color: var(--pf-global--warning-color--200);
|
||||
}
|
||||
}
|
||||
|
||||
.at-c-alertModal--danger.pf-c-modal-box {
|
||||
border-color: var(--pf-global--danger-color--100);
|
||||
|
||||
.pf-c-title {
|
||||
color: var(--pf-global--danger-color--200);
|
||||
}
|
||||
|
||||
.at-c-alertModal__icon {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.at-c-alertModal--info.pf-c-modal-box {
|
||||
border-color: var(--pf-global--info-color--100);
|
||||
|
||||
.pf-c-title {
|
||||
color: var(--pf-global--info-color--200);
|
||||
}
|
||||
|
||||
.at-c-alertModal__icon {
|
||||
color: var(--pf-global--info-color--200);
|
||||
}
|
||||
}
|
||||
|
||||
.at-c-alertModal--success.pf-c-modal-box {
|
||||
border-color: var(--pf-global--success-color--100);
|
||||
|
||||
.pf-c-title {
|
||||
color: var(--pf-global--success-color--200);
|
||||
}
|
||||
|
||||
.at-c-alertModal__icon {
|
||||
color: var(--pf-global--success-color--200);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// LoginModal overrides
|
||||
//
|
||||
|
||||
.pf-m-error p.pf-c-form__helper-text {
|
||||
color: var(--pf-global--danger-color--100);
|
||||
}
|
90
awx/ui_next/src/components/About/About.jsx
Normal file
90
awx/ui_next/src/components/About/About.jsx
Normal file
@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
AboutModal,
|
||||
TextContent,
|
||||
TextList,
|
||||
TextListItem,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import { BrandName } from '../../variables';
|
||||
import brandLogoImg from '../../../images/brand-logo.svg';
|
||||
|
||||
class About extends React.Component {
|
||||
static createSpeechBubble(version) {
|
||||
let text = `${BrandName} ${version}`;
|
||||
let top = '';
|
||||
let bottom = '';
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
top += '_';
|
||||
bottom += '-';
|
||||
}
|
||||
|
||||
top = ` __${top}__ \n`;
|
||||
text = `< ${text} >\n`;
|
||||
bottom = ` --${bottom}-- `;
|
||||
|
||||
return top + text + bottom;
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.createSpeechBubble = this.constructor.createSpeechBubble.bind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { ansible_version, version, isOpen, onClose, i18n } = this.props;
|
||||
|
||||
const speechBubble = this.createSpeechBubble(version);
|
||||
|
||||
return (
|
||||
<AboutModal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
productName={`Ansible ${BrandName}`}
|
||||
trademark={i18n._(t`Copyright 2019 Red Hat, Inc.`)}
|
||||
brandImageSrc={brandLogoImg}
|
||||
brandImageAlt={i18n._(t`Brand Image`)}
|
||||
>
|
||||
<pre>
|
||||
{speechBubble}
|
||||
{`
|
||||
\\
|
||||
\\ ^__^
|
||||
(oo)\\_______
|
||||
(__) A )\\
|
||||
||----w |
|
||||
|| ||
|
||||
`}
|
||||
</pre>
|
||||
<TextContent>
|
||||
<TextList component="dl">
|
||||
<TextListItem component="dt">
|
||||
{i18n._(t`Ansible Version`)}
|
||||
</TextListItem>
|
||||
<TextListItem component="dd">{ansible_version}</TextListItem>
|
||||
</TextList>
|
||||
</TextContent>
|
||||
</AboutModal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
About.propTypes = {
|
||||
ansible_version: PropTypes.string,
|
||||
isOpen: PropTypes.bool,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
version: PropTypes.string,
|
||||
};
|
||||
|
||||
About.defaultProps = {
|
||||
ansible_version: null,
|
||||
isOpen: false,
|
||||
version: null,
|
||||
};
|
||||
|
||||
export default withI18n()(About);
|
22
awx/ui_next/src/components/About/About.test.jsx
Normal file
22
awx/ui_next/src/components/About/About.test.jsx
Normal file
@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||
import About from './About';
|
||||
|
||||
describe('<About />', () => {
|
||||
let aboutWrapper;
|
||||
let closeButton;
|
||||
const onClose = jest.fn();
|
||||
test('initially renders without crashing', () => {
|
||||
aboutWrapper = mountWithContexts(<About isOpen onClose={onClose} />);
|
||||
expect(aboutWrapper.length).toBe(1);
|
||||
aboutWrapper.unmount();
|
||||
});
|
||||
|
||||
test('close button calls onClose handler', () => {
|
||||
aboutWrapper = mountWithContexts(<About isOpen onClose={onClose} />);
|
||||
closeButton = aboutWrapper.find('AboutModalBoxCloseButton Button');
|
||||
closeButton.simulate('click');
|
||||
expect(onClose).toBeCalled();
|
||||
aboutWrapper.unmount();
|
||||
});
|
||||
});
|
1
awx/ui_next/src/components/About/index.js
Normal file
1
awx/ui_next/src/components/About/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './About';
|
250
awx/ui_next/src/components/AddRole/AddResourceRole.jsx
Normal file
250
awx/ui_next/src/components/AddRole/AddResourceRole.jsx
Normal file
@ -0,0 +1,250 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Wizard } from '@patternfly/react-core';
|
||||
import SelectResourceStep from './SelectResourceStep';
|
||||
import SelectRoleStep from './SelectRoleStep';
|
||||
import SelectableCard from './SelectableCard';
|
||||
import { TeamsAPI, UsersAPI } from '../../api';
|
||||
|
||||
const readUsers = async queryParams =>
|
||||
UsersAPI.read(Object.assign(queryParams, { is_superuser: false }));
|
||||
|
||||
const readTeams = async queryParams => TeamsAPI.read(queryParams);
|
||||
|
||||
class AddResourceRole extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
selectedResource: null,
|
||||
selectedResourceRows: [],
|
||||
selectedRoleRows: [],
|
||||
currentStepId: 1,
|
||||
};
|
||||
|
||||
this.handleResourceCheckboxClick = this.handleResourceCheckboxClick.bind(
|
||||
this
|
||||
);
|
||||
this.handleResourceSelect = this.handleResourceSelect.bind(this);
|
||||
this.handleRoleCheckboxClick = this.handleRoleCheckboxClick.bind(this);
|
||||
this.handleWizardNext = this.handleWizardNext.bind(this);
|
||||
this.handleWizardSave = this.handleWizardSave.bind(this);
|
||||
}
|
||||
|
||||
handleResourceCheckboxClick(user) {
|
||||
const { selectedResourceRows } = this.state;
|
||||
|
||||
const selectedIndex = selectedResourceRows.findIndex(
|
||||
selectedRow => selectedRow.id === user.id
|
||||
);
|
||||
|
||||
if (selectedIndex > -1) {
|
||||
selectedResourceRows.splice(selectedIndex, 1);
|
||||
this.setState({ selectedResourceRows });
|
||||
} else {
|
||||
this.setState(prevState => ({
|
||||
selectedResourceRows: [...prevState.selectedResourceRows, user],
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
handleRoleCheckboxClick(role) {
|
||||
const { selectedRoleRows } = this.state;
|
||||
|
||||
const selectedIndex = selectedRoleRows.findIndex(
|
||||
selectedRow => selectedRow.id === role.id
|
||||
);
|
||||
|
||||
if (selectedIndex > -1) {
|
||||
selectedRoleRows.splice(selectedIndex, 1);
|
||||
this.setState({ selectedRoleRows });
|
||||
} else {
|
||||
this.setState(prevState => ({
|
||||
selectedRoleRows: [...prevState.selectedRoleRows, role],
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
handleResourceSelect(resourceType) {
|
||||
this.setState({
|
||||
selectedResource: resourceType,
|
||||
selectedResourceRows: [],
|
||||
selectedRoleRows: [],
|
||||
});
|
||||
}
|
||||
|
||||
handleWizardNext(step) {
|
||||
this.setState({
|
||||
currentStepId: step.id,
|
||||
});
|
||||
}
|
||||
|
||||
async handleWizardSave() {
|
||||
const { onSave } = this.props;
|
||||
const {
|
||||
selectedResourceRows,
|
||||
selectedRoleRows,
|
||||
selectedResource,
|
||||
} = this.state;
|
||||
|
||||
try {
|
||||
const roleRequests = [];
|
||||
|
||||
for (let i = 0; i < selectedResourceRows.length; i++) {
|
||||
for (let j = 0; j < selectedRoleRows.length; j++) {
|
||||
if (selectedResource === 'users') {
|
||||
roleRequests.push(
|
||||
UsersAPI.associateRole(
|
||||
selectedResourceRows[i].id,
|
||||
selectedRoleRows[j].id
|
||||
)
|
||||
);
|
||||
} else if (selectedResource === 'teams') {
|
||||
roleRequests.push(
|
||||
TeamsAPI.associateRole(
|
||||
selectedResourceRows[i].id,
|
||||
selectedRoleRows[j].id
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(roleRequests);
|
||||
onSave();
|
||||
} catch (err) {
|
||||
// TODO: handle this error
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
selectedResource,
|
||||
selectedResourceRows,
|
||||
selectedRoleRows,
|
||||
currentStepId,
|
||||
} = this.state;
|
||||
const { onClose, roles, i18n } = this.props;
|
||||
|
||||
const userColumns = [
|
||||
{ name: i18n._(t`Username`), key: 'username', isSortable: true },
|
||||
];
|
||||
|
||||
const teamColumns = [
|
||||
{ name: i18n._(t`Name`), key: 'name', isSortable: true },
|
||||
];
|
||||
|
||||
let wizardTitle = '';
|
||||
|
||||
switch (selectedResource) {
|
||||
case 'users':
|
||||
wizardTitle = i18n._(t`Add User Roles`);
|
||||
break;
|
||||
case 'teams':
|
||||
wizardTitle = i18n._(t`Add Team Roles`);
|
||||
break;
|
||||
default:
|
||||
wizardTitle = i18n._(t`Add Roles`);
|
||||
}
|
||||
|
||||
const steps = [
|
||||
{
|
||||
id: 1,
|
||||
name: i18n._(t`Select Users Or Teams`),
|
||||
component: (
|
||||
<div style={{ display: 'flex' }}>
|
||||
<SelectableCard
|
||||
isSelected={selectedResource === 'users'}
|
||||
label={i18n._(t`Users`)}
|
||||
onClick={() => this.handleResourceSelect('users')}
|
||||
/>
|
||||
<SelectableCard
|
||||
isSelected={selectedResource === 'teams'}
|
||||
label={i18n._(t`Teams`)}
|
||||
onClick={() => this.handleResourceSelect('teams')}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
enableNext: selectedResource !== null,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: i18n._(t`Select items from list`),
|
||||
component: (
|
||||
<Fragment>
|
||||
{selectedResource === 'users' && (
|
||||
<SelectResourceStep
|
||||
columns={userColumns}
|
||||
displayKey="username"
|
||||
onRowClick={this.handleResourceCheckboxClick}
|
||||
onSearch={readUsers}
|
||||
selectedLabel={i18n._(t`Selected`)}
|
||||
selectedResourceRows={selectedResourceRows}
|
||||
sortedColumnKey="username"
|
||||
itemName="user"
|
||||
/>
|
||||
)}
|
||||
{selectedResource === 'teams' && (
|
||||
<SelectResourceStep
|
||||
columns={teamColumns}
|
||||
onRowClick={this.handleResourceCheckboxClick}
|
||||
onSearch={readTeams}
|
||||
selectedLabel={i18n._(t`Selected`)}
|
||||
selectedResourceRows={selectedResourceRows}
|
||||
itemName="team"
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
),
|
||||
enableNext: selectedResourceRows.length > 0,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: i18n._(t`Apply roles`),
|
||||
component: (
|
||||
<SelectRoleStep
|
||||
onRolesClick={this.handleRoleCheckboxClick}
|
||||
roles={roles}
|
||||
selectedListKey={selectedResource === 'users' ? 'username' : 'name'}
|
||||
selectedListLabel={i18n._(t`Selected`)}
|
||||
selectedResourceRows={selectedResourceRows}
|
||||
selectedRoleRows={selectedRoleRows}
|
||||
/>
|
||||
),
|
||||
nextButtonText: i18n._(t`Save`),
|
||||
enableNext: selectedRoleRows.length > 0,
|
||||
},
|
||||
];
|
||||
|
||||
const currentStep = steps.find(step => step.id === currentStepId);
|
||||
|
||||
// TODO: somehow internationalize steps and currentStep.nextButtonText
|
||||
return (
|
||||
<Wizard
|
||||
style={{ overflow: 'scroll' }}
|
||||
isOpen
|
||||
onNext={this.handleWizardNext}
|
||||
onClose={onClose}
|
||||
onSave={this.handleWizardSave}
|
||||
steps={steps}
|
||||
title={wizardTitle}
|
||||
nextButtonText={currentStep.nextButtonText || undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddResourceRole.propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
roles: PropTypes.shape(),
|
||||
};
|
||||
|
||||
AddResourceRole.defaultProps = {
|
||||
roles: {},
|
||||
};
|
||||
|
||||
export { AddResourceRole as _AddResourceRole };
|
||||
export default withI18n()(AddResourceRole);
|
219
awx/ui_next/src/components/AddRole/AddResourceRole.test.jsx
Normal file
219
awx/ui_next/src/components/AddRole/AddResourceRole.test.jsx
Normal file
@ -0,0 +1,219 @@
|
||||
/* eslint-disable react/jsx-pascal-case */
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||
import AddResourceRole, { _AddResourceRole } from './AddResourceRole';
|
||||
import { TeamsAPI, UsersAPI } from '../../api';
|
||||
|
||||
jest.mock('../../api');
|
||||
|
||||
describe('<_AddResourceRole />', () => {
|
||||
UsersAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
count: 2,
|
||||
results: [{ id: 1, username: 'foo' }, { id: 2, username: 'bar' }],
|
||||
},
|
||||
});
|
||||
const roles = {
|
||||
admin_role: {
|
||||
description: 'Can manage all aspects of the organization',
|
||||
id: 1,
|
||||
name: 'Admin',
|
||||
},
|
||||
execute_role: {
|
||||
description: 'May run any executable resources in the organization',
|
||||
id: 2,
|
||||
name: 'Execute',
|
||||
},
|
||||
};
|
||||
test('initially renders without crashing', () => {
|
||||
shallow(
|
||||
<_AddResourceRole
|
||||
onClose={() => {}}
|
||||
onSave={() => {}}
|
||||
roles={roles}
|
||||
i18n={{ _: val => val.toString() }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
test('handleRoleCheckboxClick properly updates state', () => {
|
||||
const wrapper = shallow(
|
||||
<_AddResourceRole
|
||||
onClose={() => {}}
|
||||
onSave={() => {}}
|
||||
roles={roles}
|
||||
i18n={{ _: val => val.toString() }}
|
||||
/>
|
||||
);
|
||||
wrapper.setState({
|
||||
selectedRoleRows: [
|
||||
{
|
||||
description: 'Can manage all aspects of the organization',
|
||||
name: 'Admin',
|
||||
id: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
wrapper.instance().handleRoleCheckboxClick({
|
||||
description: 'Can manage all aspects of the organization',
|
||||
name: 'Admin',
|
||||
id: 1,
|
||||
});
|
||||
expect(wrapper.state('selectedRoleRows')).toEqual([]);
|
||||
wrapper.instance().handleRoleCheckboxClick({
|
||||
description: 'Can manage all aspects of the organization',
|
||||
name: 'Admin',
|
||||
id: 1,
|
||||
});
|
||||
expect(wrapper.state('selectedRoleRows')).toEqual([
|
||||
{
|
||||
description: 'Can manage all aspects of the organization',
|
||||
name: 'Admin',
|
||||
id: 1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
test('handleResourceCheckboxClick properly updates state', () => {
|
||||
const wrapper = shallow(
|
||||
<_AddResourceRole
|
||||
onClose={() => {}}
|
||||
onSave={() => {}}
|
||||
roles={roles}
|
||||
i18n={{ _: val => val.toString() }}
|
||||
/>
|
||||
);
|
||||
wrapper.setState({
|
||||
selectedResourceRows: [
|
||||
{
|
||||
id: 1,
|
||||
username: 'foobar',
|
||||
},
|
||||
],
|
||||
});
|
||||
wrapper.instance().handleResourceCheckboxClick({
|
||||
id: 1,
|
||||
username: 'foobar',
|
||||
});
|
||||
expect(wrapper.state('selectedResourceRows')).toEqual([]);
|
||||
wrapper.instance().handleResourceCheckboxClick({
|
||||
id: 1,
|
||||
username: 'foobar',
|
||||
});
|
||||
expect(wrapper.state('selectedResourceRows')).toEqual([
|
||||
{
|
||||
id: 1,
|
||||
username: 'foobar',
|
||||
},
|
||||
]);
|
||||
});
|
||||
test('clicking user/team cards updates state', () => {
|
||||
const spy = jest.spyOn(_AddResourceRole.prototype, 'handleResourceSelect');
|
||||
const wrapper = mountWithContexts(
|
||||
<AddResourceRole onClose={() => {}} onSave={() => {}} roles={roles} />,
|
||||
{ context: { network: { handleHttpError: () => {} } } }
|
||||
).find('AddResourceRole');
|
||||
const selectableCardWrapper = wrapper.find('SelectableCard');
|
||||
expect(selectableCardWrapper.length).toBe(2);
|
||||
selectableCardWrapper.first().simulate('click');
|
||||
expect(spy).toHaveBeenCalledWith('users');
|
||||
expect(wrapper.state('selectedResource')).toBe('users');
|
||||
selectableCardWrapper.at(1).simulate('click');
|
||||
expect(spy).toHaveBeenCalledWith('teams');
|
||||
expect(wrapper.state('selectedResource')).toBe('teams');
|
||||
});
|
||||
test('handleResourceSelect clears out selected lists and sets selectedResource', () => {
|
||||
const wrapper = shallow(
|
||||
<_AddResourceRole
|
||||
onClose={() => {}}
|
||||
onSave={() => {}}
|
||||
roles={roles}
|
||||
i18n={{ _: val => val.toString() }}
|
||||
/>
|
||||
);
|
||||
wrapper.setState({
|
||||
selectedResource: 'teams',
|
||||
selectedResourceRows: [
|
||||
{
|
||||
id: 1,
|
||||
username: 'foobar',
|
||||
},
|
||||
],
|
||||
selectedRoleRows: [
|
||||
{
|
||||
description: 'Can manage all aspects of the organization',
|
||||
id: 1,
|
||||
name: 'Admin',
|
||||
},
|
||||
],
|
||||
});
|
||||
wrapper.instance().handleResourceSelect('users');
|
||||
expect(wrapper.state()).toEqual({
|
||||
selectedResource: 'users',
|
||||
selectedResourceRows: [],
|
||||
selectedRoleRows: [],
|
||||
currentStepId: 1,
|
||||
});
|
||||
wrapper.instance().handleResourceSelect('teams');
|
||||
expect(wrapper.state()).toEqual({
|
||||
selectedResource: 'teams',
|
||||
selectedResourceRows: [],
|
||||
selectedRoleRows: [],
|
||||
currentStepId: 1,
|
||||
});
|
||||
});
|
||||
test('handleWizardSave makes correct api calls, calls onSave when done', async () => {
|
||||
const handleSave = jest.fn();
|
||||
const wrapper = mountWithContexts(
|
||||
<AddResourceRole onClose={() => {}} onSave={handleSave} roles={roles} />,
|
||||
{ context: { network: { handleHttpError: () => {} } } }
|
||||
).find('AddResourceRole');
|
||||
wrapper.setState({
|
||||
selectedResource: 'users',
|
||||
selectedResourceRows: [
|
||||
{
|
||||
id: 1,
|
||||
username: 'foobar',
|
||||
},
|
||||
],
|
||||
selectedRoleRows: [
|
||||
{
|
||||
description: 'Can manage all aspects of the organization',
|
||||
id: 1,
|
||||
name: 'Admin',
|
||||
},
|
||||
{
|
||||
description: 'May run any executable resources in the organization',
|
||||
id: 2,
|
||||
name: 'Execute',
|
||||
},
|
||||
],
|
||||
});
|
||||
await wrapper.instance().handleWizardSave();
|
||||
expect(UsersAPI.associateRole).toHaveBeenCalledTimes(2);
|
||||
expect(handleSave).toHaveBeenCalled();
|
||||
wrapper.setState({
|
||||
selectedResource: 'teams',
|
||||
selectedResourceRows: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'foobar',
|
||||
},
|
||||
],
|
||||
selectedRoleRows: [
|
||||
{
|
||||
description: 'Can manage all aspects of the organization',
|
||||
id: 1,
|
||||
name: 'Admin',
|
||||
},
|
||||
{
|
||||
description: 'May run any executable resources in the organization',
|
||||
id: 2,
|
||||
name: 'Execute',
|
||||
},
|
||||
],
|
||||
});
|
||||
await wrapper.instance().handleWizardSave();
|
||||
expect(TeamsAPI.associateRole).toHaveBeenCalledTimes(2);
|
||||
expect(handleSave).toHaveBeenCalled();
|
||||
});
|
||||
});
|
49
awx/ui_next/src/components/AddRole/CheckboxCard.jsx
Normal file
49
awx/ui_next/src/components/AddRole/CheckboxCard.jsx
Normal file
@ -0,0 +1,49 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Checkbox } from '@patternfly/react-core';
|
||||
|
||||
class CheckboxCard extends Component {
|
||||
render() {
|
||||
const { name, description, isSelected, onSelect, itemId } = this.props;
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
border: '1px solid var(--pf-global--BorderColor--200)',
|
||||
borderRadius: 'var(--pf-global--BorderRadius--sm)',
|
||||
padding: '10px',
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onChange={onSelect}
|
||||
aria-label={name}
|
||||
id={`checkbox-card-${itemId}`}
|
||||
label={
|
||||
<Fragment>
|
||||
<div style={{ fontWeight: 'bold' }}>{name}</div>
|
||||
<div>{description}</div>
|
||||
</Fragment>
|
||||
}
|
||||
value={itemId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CheckboxCard.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
description: PropTypes.string,
|
||||
isSelected: PropTypes.bool,
|
||||
onSelect: PropTypes.func,
|
||||
itemId: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
CheckboxCard.defaultProps = {
|
||||
description: '',
|
||||
isSelected: false,
|
||||
onSelect: null,
|
||||
};
|
||||
|
||||
export default CheckboxCard;
|
11
awx/ui_next/src/components/AddRole/CheckboxCard.test.jsx
Normal file
11
awx/ui_next/src/components/AddRole/CheckboxCard.test.jsx
Normal file
@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import CheckboxCard from './CheckboxCard';
|
||||
|
||||
describe('<CheckboxCard />', () => {
|
||||
let wrapper;
|
||||
test('initially renders without crashing', () => {
|
||||
wrapper = shallow(<CheckboxCard name="Foobar" itemId={5} />);
|
||||
expect(wrapper.length).toBe(1);
|
||||
});
|
||||
});
|
147
awx/ui_next/src/components/AddRole/SelectResourceStep.jsx
Normal file
147
awx/ui_next/src/components/AddRole/SelectResourceStep.jsx
Normal file
@ -0,0 +1,147 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import PaginatedDataList from '../PaginatedDataList';
|
||||
import DataListToolbar from '../DataListToolbar';
|
||||
import CheckboxListItem from '../CheckboxListItem';
|
||||
import SelectedList from '../SelectedList';
|
||||
import { getQSConfig, parseNamespacedQueryString } from '../../util/qs';
|
||||
|
||||
class SelectResourceStep extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isInitialized: false,
|
||||
count: null,
|
||||
error: false,
|
||||
resources: [],
|
||||
};
|
||||
|
||||
this.qsConfig = getQSConfig('resource', {
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
order_by: props.sortedColumnKey,
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.readResourceList();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { location } = this.props;
|
||||
if (location !== prevProps.location) {
|
||||
this.readResourceList();
|
||||
}
|
||||
}
|
||||
|
||||
async readResourceList() {
|
||||
const { onSearch, location } = this.props;
|
||||
const queryParams = parseNamespacedQueryString(
|
||||
this.qsConfig,
|
||||
location.search
|
||||
);
|
||||
|
||||
this.setState({
|
||||
isLoading: true,
|
||||
error: false,
|
||||
});
|
||||
try {
|
||||
const { data } = await onSearch(queryParams);
|
||||
const { count, results } = data;
|
||||
|
||||
this.setState({
|
||||
resources: results,
|
||||
count,
|
||||
isInitialized: true,
|
||||
isLoading: false,
|
||||
error: false,
|
||||
});
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
error: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isInitialized, isLoading, count, error, resources } = this.state;
|
||||
|
||||
const {
|
||||
columns,
|
||||
displayKey,
|
||||
onRowClick,
|
||||
selectedLabel,
|
||||
selectedResourceRows,
|
||||
itemName,
|
||||
i18n,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{isLoading && <div>{i18n._(t`Loading...`)}</div>}
|
||||
{isInitialized && (
|
||||
<Fragment>
|
||||
{selectedResourceRows.length > 0 && (
|
||||
<SelectedList
|
||||
displayKey={displayKey}
|
||||
label={selectedLabel}
|
||||
onRemove={onRowClick}
|
||||
selected={selectedResourceRows}
|
||||
showOverflowAfter={5}
|
||||
/>
|
||||
)}
|
||||
<PaginatedDataList
|
||||
items={resources}
|
||||
itemCount={count}
|
||||
itemName={itemName}
|
||||
qsConfig={this.qsConfig}
|
||||
toolbarColumns={columns}
|
||||
renderItem={item => (
|
||||
<CheckboxListItem
|
||||
isSelected={selectedResourceRows.some(i => i.id === item.id)}
|
||||
itemId={item.id}
|
||||
key={item.id}
|
||||
name={item[displayKey]}
|
||||
onSelect={() => onRowClick(item)}
|
||||
/>
|
||||
)}
|
||||
renderToolbar={props => (
|
||||
<DataListToolbar {...props} alignToolbarLeft />
|
||||
)}
|
||||
showPageSizeOptions={false}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
{error ? <div>error</div> : ''}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectResourceStep.propTypes = {
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
displayKey: PropTypes.string,
|
||||
onRowClick: PropTypes.func,
|
||||
onSearch: PropTypes.func.isRequired,
|
||||
selectedLabel: PropTypes.string,
|
||||
selectedResourceRows: PropTypes.arrayOf(PropTypes.object),
|
||||
sortedColumnKey: PropTypes.string,
|
||||
itemName: PropTypes.string,
|
||||
};
|
||||
|
||||
SelectResourceStep.defaultProps = {
|
||||
displayKey: 'name',
|
||||
onRowClick: () => {},
|
||||
selectedLabel: null,
|
||||
selectedResourceRows: [],
|
||||
sortedColumnKey: 'name',
|
||||
itemName: 'item',
|
||||
};
|
||||
|
||||
export { SelectResourceStep as _SelectResourceStep };
|
||||
export default withI18n()(withRouter(SelectResourceStep));
|
121
awx/ui_next/src/components/AddRole/SelectResourceStep.test.jsx
Normal file
121
awx/ui_next/src/components/AddRole/SelectResourceStep.test.jsx
Normal file
@ -0,0 +1,121 @@
|
||||
import React from 'react';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { shallow } from 'enzyme';
|
||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||
import { sleep } from '../../../testUtils/testUtils';
|
||||
import SelectResourceStep from './SelectResourceStep';
|
||||
|
||||
describe('<SelectResourceStep />', () => {
|
||||
const columns = [{ name: 'Username', key: 'username', isSortable: true }];
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
test('initially renders without crashing', () => {
|
||||
shallow(
|
||||
<SelectResourceStep
|
||||
columns={columns}
|
||||
displayKey="username"
|
||||
onRowClick={() => {}}
|
||||
onSearch={() => {}}
|
||||
sortedColumnKey="username"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
test('fetches resources on mount', async () => {
|
||||
const handleSearch = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
count: 2,
|
||||
results: [
|
||||
{ id: 1, username: 'foo', url: 'item/1' },
|
||||
{ id: 2, username: 'bar', url: 'item/2' },
|
||||
],
|
||||
},
|
||||
});
|
||||
mountWithContexts(
|
||||
<SelectResourceStep
|
||||
columns={columns}
|
||||
displayKey="username"
|
||||
onRowClick={() => {}}
|
||||
onSearch={handleSearch}
|
||||
sortedColumnKey="username"
|
||||
/>
|
||||
);
|
||||
expect(handleSearch).toHaveBeenCalledWith({
|
||||
order_by: 'username',
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
});
|
||||
});
|
||||
|
||||
test('readResourceList properly adds rows to state', async () => {
|
||||
const selectedResourceRows = [{ id: 1, username: 'foo', url: 'item/1' }];
|
||||
const handleSearch = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
count: 2,
|
||||
results: [
|
||||
{ id: 1, username: 'foo', url: 'item/1' },
|
||||
{ id: 2, username: 'bar', url: 'item/2' },
|
||||
],
|
||||
},
|
||||
});
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: [
|
||||
'/organizations/1/access?resource.page=1&resource.order_by=-username',
|
||||
],
|
||||
});
|
||||
const wrapper = await mountWithContexts(
|
||||
<SelectResourceStep
|
||||
columns={columns}
|
||||
displayKey="username"
|
||||
onRowClick={() => {}}
|
||||
onSearch={handleSearch}
|
||||
selectedResourceRows={selectedResourceRows}
|
||||
sortedColumnKey="username"
|
||||
/>,
|
||||
{
|
||||
context: { router: { history, route: { location: history.location } } },
|
||||
}
|
||||
).find('SelectResourceStep');
|
||||
await wrapper.instance().readResourceList();
|
||||
expect(handleSearch).toHaveBeenCalledWith({
|
||||
order_by: '-username',
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
});
|
||||
expect(wrapper.state('resources')).toEqual([
|
||||
{ id: 1, username: 'foo', url: 'item/1' },
|
||||
{ id: 2, username: 'bar', url: 'item/2' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('clicking on row fires callback with correct params', async () => {
|
||||
const handleRowClick = jest.fn();
|
||||
const data = {
|
||||
count: 2,
|
||||
results: [
|
||||
{ id: 1, username: 'foo', url: 'item/1' },
|
||||
{ id: 2, username: 'bar', url: 'item/2' },
|
||||
],
|
||||
};
|
||||
const wrapper = mountWithContexts(
|
||||
<SelectResourceStep
|
||||
columns={columns}
|
||||
displayKey="username"
|
||||
onRowClick={handleRowClick}
|
||||
onSearch={() => ({ data })}
|
||||
selectedResourceRows={[]}
|
||||
sortedColumnKey="username"
|
||||
/>
|
||||
);
|
||||
await sleep(0);
|
||||
wrapper.update();
|
||||
const checkboxListItemWrapper = wrapper.find('CheckboxListItem');
|
||||
expect(checkboxListItemWrapper.length).toBe(2);
|
||||
checkboxListItemWrapper
|
||||
.first()
|
||||
.find('input[type="checkbox"]')
|
||||
.simulate('change', { target: { checked: true } });
|
||||
expect(handleRowClick).toHaveBeenCalledWith(data.results[0]);
|
||||
});
|
||||
});
|
78
awx/ui_next/src/components/AddRole/SelectRoleStep.jsx
Normal file
78
awx/ui_next/src/components/AddRole/SelectRoleStep.jsx
Normal file
@ -0,0 +1,78 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
import CheckboxCard from './CheckboxCard';
|
||||
import SelectedList from '../SelectedList';
|
||||
|
||||
class RolesStep extends React.Component {
|
||||
render() {
|
||||
const {
|
||||
onRolesClick,
|
||||
roles,
|
||||
selectedListKey,
|
||||
selectedListLabel,
|
||||
selectedResourceRows,
|
||||
selectedRoleRows,
|
||||
i18n,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div>
|
||||
{selectedResourceRows.length > 0 && (
|
||||
<SelectedList
|
||||
displayKey={selectedListKey}
|
||||
isReadOnly
|
||||
label={selectedListLabel || i18n._(t`Selected`)}
|
||||
selected={selectedResourceRows}
|
||||
showOverflowAfter={5}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gap: '20px 20px',
|
||||
marginTop: '20px',
|
||||
}}
|
||||
>
|
||||
{Object.keys(roles).map(role => (
|
||||
<CheckboxCard
|
||||
description={roles[role].description}
|
||||
itemId={roles[role].id}
|
||||
isSelected={selectedRoleRows.some(
|
||||
item => item.id === roles[role].id
|
||||
)}
|
||||
key={roles[role].id}
|
||||
name={roles[role].name}
|
||||
onSelect={() => onRolesClick(roles[role])}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RolesStep.propTypes = {
|
||||
onRolesClick: PropTypes.func,
|
||||
roles: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
selectedListKey: PropTypes.string,
|
||||
selectedListLabel: PropTypes.string,
|
||||
selectedResourceRows: PropTypes.arrayOf(PropTypes.object),
|
||||
selectedRoleRows: PropTypes.arrayOf(PropTypes.object),
|
||||
};
|
||||
|
||||
RolesStep.defaultProps = {
|
||||
onRolesClick: () => {},
|
||||
selectedListKey: 'name',
|
||||
selectedListLabel: null,
|
||||
selectedResourceRows: [],
|
||||
selectedRoleRows: [],
|
||||
};
|
||||
|
||||
export default withI18n()(RolesStep);
|
64
awx/ui_next/src/components/AddRole/SelectRoleStep.test.jsx
Normal file
64
awx/ui_next/src/components/AddRole/SelectRoleStep.test.jsx
Normal file
@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||
import SelectRoleStep from './SelectRoleStep';
|
||||
|
||||
describe('<SelectRoleStep />', () => {
|
||||
let wrapper;
|
||||
const roles = {
|
||||
project_admin_role: {
|
||||
id: 1,
|
||||
name: 'Project Admin',
|
||||
description: 'Can manage all projects of the organization',
|
||||
},
|
||||
execute_role: {
|
||||
id: 2,
|
||||
name: 'Execute',
|
||||
description: 'May run any executable resources in the organization',
|
||||
},
|
||||
};
|
||||
const selectedRoles = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Project Admin',
|
||||
description: 'Can manage all projects of the organization',
|
||||
},
|
||||
];
|
||||
const selectedResourceRows = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'foo',
|
||||
},
|
||||
];
|
||||
test('initially renders without crashing', () => {
|
||||
wrapper = shallow(
|
||||
<SelectRoleStep
|
||||
roles={roles}
|
||||
selectedResourceRows={selectedResourceRows}
|
||||
selectedRoleRows={selectedRoles}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.length).toBe(1);
|
||||
wrapper.unmount();
|
||||
});
|
||||
test('clicking role fires onRolesClick callback', () => {
|
||||
const onRolesClick = jest.fn();
|
||||
wrapper = mountWithContexts(
|
||||
<SelectRoleStep
|
||||
onRolesClick={onRolesClick}
|
||||
roles={roles}
|
||||
selectedResourceRows={selectedResourceRows}
|
||||
selectedRoleRows={selectedRoles}
|
||||
/>
|
||||
);
|
||||
const CheckboxCards = wrapper.find('CheckboxCard');
|
||||
expect(CheckboxCards.length).toBe(2);
|
||||
CheckboxCards.first().prop('onSelect')();
|
||||
expect(onRolesClick).toBeCalledWith({
|
||||
id: 1,
|
||||
name: 'Project Admin',
|
||||
description: 'Can manage all projects of the organization',
|
||||
});
|
||||
wrapper.unmount();
|
||||
});
|
||||
});
|
64
awx/ui_next/src/components/AddRole/SelectableCard.jsx
Normal file
64
awx/ui_next/src/components/AddRole/SelectableCard.jsx
Normal file
@ -0,0 +1,64 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const SelectableItem = styled.div`
|
||||
min-width: 200px;
|
||||
border: 1px solid var(--pf-global--BorderColor--200);
|
||||
border-radius: var(--pf-global--BorderRadius--sm);
|
||||
border: 1px solid;
|
||||
border-color: ${props =>
|
||||
props.isSelected
|
||||
? 'var(--pf-global--active-color--100)'
|
||||
: 'var(--pf-global--BorderColor--200)'};
|
||||
margin-right: 20px;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const Indicator = styled.div`
|
||||
display: flex;
|
||||
flex: 0 0 5px;
|
||||
background-color: ${props =>
|
||||
props.isSelected ? 'var(--pf-global--active-color--100)' : null};
|
||||
`;
|
||||
|
||||
const Label = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
`;
|
||||
|
||||
class SelectableCard extends Component {
|
||||
render() {
|
||||
const { label, onClick, isSelected } = this.props;
|
||||
|
||||
return (
|
||||
<SelectableItem
|
||||
onClick={onClick}
|
||||
onKeyPress={onClick}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
isSelected={isSelected}
|
||||
>
|
||||
<Indicator isSelected={isSelected} />
|
||||
<Label>{label}</Label>
|
||||
</SelectableItem>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectableCard.propTypes = {
|
||||
label: PropTypes.string,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
};
|
||||
|
||||
SelectableCard.defaultProps = {
|
||||
label: '',
|
||||
isSelected: false,
|
||||
};
|
||||
|
||||
export default SelectableCard;
|
18
awx/ui_next/src/components/AddRole/SelectableCard.test.jsx
Normal file
18
awx/ui_next/src/components/AddRole/SelectableCard.test.jsx
Normal file
@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import SelectableCard from './SelectableCard';
|
||||
|
||||
describe('<SelectableCard />', () => {
|
||||
let wrapper;
|
||||
const onClick = jest.fn();
|
||||
test('initially renders without crashing when not selected', () => {
|
||||
wrapper = shallow(<SelectableCard onClick={onClick} />);
|
||||
expect(wrapper.length).toBe(1);
|
||||
wrapper.unmount();
|
||||
});
|
||||
test('initially renders without crashing when selected', () => {
|
||||
wrapper = shallow(<SelectableCard isSelected onClick={onClick} />);
|
||||
expect(wrapper.length).toBe(1);
|
||||
wrapper.unmount();
|
||||
});
|
||||
});
|
5
awx/ui_next/src/components/AddRole/index.js
Normal file
5
awx/ui_next/src/components/AddRole/index.js
Normal file
@ -0,0 +1,5 @@
|
||||
export { default as AddResourceRole } from './AddResourceRole';
|
||||
export { default as CheckboxCard } from './CheckboxCard';
|
||||
export { default as SelectableCard } from './SelectableCard';
|
||||
export { default as SelectResourceStep } from './SelectResourceStep';
|
||||
export { default as SelectRoleStep } from './SelectRoleStep';
|
41
awx/ui_next/src/components/AlertModal/AlertModal.jsx
Normal file
41
awx/ui_next/src/components/AlertModal/AlertModal.jsx
Normal file
@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Modal } from '@patternfly/react-core';
|
||||
|
||||
import {
|
||||
ExclamationTriangleIcon,
|
||||
ExclamationCircleIcon,
|
||||
InfoCircleIcon,
|
||||
CheckCircleIcon,
|
||||
} from '@patternfly/react-icons';
|
||||
|
||||
const getIcon = variant => {
|
||||
let icon;
|
||||
if (variant === 'warning') {
|
||||
icon = <ExclamationTriangleIcon className="at-c-alertModal__icon" />;
|
||||
} else if (variant === 'danger') {
|
||||
icon = <ExclamationCircleIcon className="at-c-alertModal__icon" />;
|
||||
}
|
||||
if (variant === 'info') {
|
||||
icon = <InfoCircleIcon className="at-c-alertModal__icon" />;
|
||||
}
|
||||
if (variant === 'success') {
|
||||
icon = <CheckCircleIcon className="at-c-alertModal__icon" />;
|
||||
}
|
||||
return icon;
|
||||
};
|
||||
|
||||
export default ({ variant, children, ...props }) => {
|
||||
const { isOpen = null } = props;
|
||||
props.isOpen = Boolean(isOpen);
|
||||
return (
|
||||
<Modal
|
||||
className={`awx-c-modal${variant &&
|
||||
` at-c-alertModal at-c-alertModal--${variant}`}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{getIcon(variant)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
11
awx/ui_next/src/components/AlertModal/AlertModal.test.jsx
Normal file
11
awx/ui_next/src/components/AlertModal/AlertModal.test.jsx
Normal file
@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import AlertModal from './AlertModal';
|
||||
|
||||
describe('AlertModal', () => {
|
||||
test('renders the expected content', () => {
|
||||
const wrapper = mount(<AlertModal title="Danger!" />);
|
||||
expect(wrapper).toHaveLength(1);
|
||||
});
|
||||
});
|
1
awx/ui_next/src/components/AlertModal/index.js
Normal file
1
awx/ui_next/src/components/AlertModal/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './AlertModal';
|
52
awx/ui_next/src/components/AnsibleSelect/AnsibleSelect.jsx
Normal file
52
awx/ui_next/src/components/AnsibleSelect/AnsibleSelect.jsx
Normal file
@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { FormSelect, FormSelectOption } from '@patternfly/react-core';
|
||||
|
||||
class AnsibleSelect extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onSelectChange = this.onSelectChange.bind(this);
|
||||
}
|
||||
|
||||
onSelectChange(val, event) {
|
||||
const { onChange, name } = this.props;
|
||||
event.target.name = name;
|
||||
onChange(event, val);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { value, data, i18n } = this.props;
|
||||
|
||||
return (
|
||||
<FormSelect
|
||||
value={value}
|
||||
onChange={this.onSelectChange}
|
||||
aria-label={i18n._(t`Select Input`)}
|
||||
>
|
||||
{data.map(datum => (
|
||||
<FormSelectOption
|
||||
key={datum.key}
|
||||
value={datum.value}
|
||||
label={datum.label}
|
||||
isDisabled={datum.isDisabled}
|
||||
/>
|
||||
))}
|
||||
</FormSelect>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AnsibleSelect.defaultProps = {
|
||||
data: [],
|
||||
};
|
||||
|
||||
AnsibleSelect.propTypes = {
|
||||
data: PropTypes.arrayOf(PropTypes.object),
|
||||
onChange: PropTypes.func.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export { AnsibleSelect as _AnsibleSelect };
|
||||
export default withI18n()(AnsibleSelect);
|
@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||
import AnsibleSelect, { _AnsibleSelect } from './AnsibleSelect';
|
||||
|
||||
const mockData = [
|
||||
{
|
||||
key: 'baz',
|
||||
label: 'Baz',
|
||||
value: '/venv/baz/',
|
||||
},
|
||||
{
|
||||
key: 'default',
|
||||
label: 'Default',
|
||||
value: '/venv/ansible/',
|
||||
},
|
||||
];
|
||||
|
||||
describe('<AnsibleSelect />', () => {
|
||||
test('initially renders succesfully', async () => {
|
||||
mountWithContexts(
|
||||
<AnsibleSelect
|
||||
value="foo"
|
||||
name="bar"
|
||||
onChange={() => {}}
|
||||
data={mockData}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
test('calls "onSelectChange" on dropdown select change', () => {
|
||||
const spy = jest.spyOn(_AnsibleSelect.prototype, 'onSelectChange');
|
||||
const wrapper = mountWithContexts(
|
||||
<AnsibleSelect
|
||||
value="foo"
|
||||
name="bar"
|
||||
onChange={() => {}}
|
||||
data={mockData}
|
||||
/>
|
||||
);
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
wrapper.find('select').simulate('change');
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('Returns correct select options', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<AnsibleSelect
|
||||
value="foo"
|
||||
name="bar"
|
||||
onChange={() => {}}
|
||||
data={mockData}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('FormSelect')).toHaveLength(1);
|
||||
expect(wrapper.find('FormSelectOption')).toHaveLength(2);
|
||||
});
|
||||
});
|
1
awx/ui_next/src/components/AnsibleSelect/index.js
Normal file
1
awx/ui_next/src/components/AnsibleSelect/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './AnsibleSelect';
|
20
awx/ui_next/src/components/Background/Background.jsx
Normal file
20
awx/ui_next/src/components/Background/Background.jsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React, { Fragment } from 'react';
|
||||
|
||||
import { BackgroundImage, BackgroundImageSrc } from '@patternfly/react-core';
|
||||
import bgFilter from '@patternfly/patternfly/assets/images/background-filter.svg';
|
||||
|
||||
const backgroundImageConfig = {
|
||||
[BackgroundImageSrc.xs]: '/assets/images/pfbg_576.jpg',
|
||||
[BackgroundImageSrc.xs2x]: '/assets/images/pfbg_576@2x.jpg',
|
||||
[BackgroundImageSrc.sm]: '/assets/images/pfbg_768.jpg',
|
||||
[BackgroundImageSrc.sm2x]: '/assets/images/pfbg_768@2x.jpg',
|
||||
[BackgroundImageSrc.lg]: '/assets/images/pfbg_2000.jpg',
|
||||
[BackgroundImageSrc.filter]: `${bgFilter}#image_overlay`,
|
||||
};
|
||||
|
||||
export default ({ children }) => (
|
||||
<Fragment>
|
||||
<BackgroundImage src={backgroundImageConfig} />
|
||||
{children}
|
||||
</Fragment>
|
||||
);
|
17
awx/ui_next/src/components/Background/Background.test.jsx
Normal file
17
awx/ui_next/src/components/Background/Background.test.jsx
Normal file
@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import Background from './Background';
|
||||
|
||||
describe('Background', () => {
|
||||
test('renders the expected content', () => {
|
||||
const wrapper = mount(
|
||||
<Background>
|
||||
<div id="test" />
|
||||
</Background>
|
||||
);
|
||||
expect(wrapper).toHaveLength(1);
|
||||
expect(wrapper.find('BackgroundImage')).toHaveLength(1);
|
||||
expect(wrapper.find('#test')).toHaveLength(1);
|
||||
});
|
||||
});
|
1
awx/ui_next/src/components/Background/index.js
Normal file
1
awx/ui_next/src/components/Background/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './Background';
|
374
awx/ui_next/src/components/BrandLogo/BrandLogo.jsx
Normal file
374
awx/ui_next/src/components/BrandLogo/BrandLogo.jsx
Normal file
@ -0,0 +1,374 @@
|
||||
import React, { Component } from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
import styled from 'styled-components';
|
||||
|
||||
const ST0 = styled.g`
|
||||
display: none;
|
||||
`;
|
||||
|
||||
const ST1 = styled.path`
|
||||
display: inline;
|
||||
fill: #ed1c24;
|
||||
`;
|
||||
|
||||
const ST2 = styled.path`
|
||||
fill: #42210b;
|
||||
`;
|
||||
|
||||
const ST3 = styled.path`
|
||||
fill: #ffffff;
|
||||
`;
|
||||
|
||||
const ST4 = styled.path`
|
||||
fill: #c69c6d;
|
||||
stroke: #8c6239;
|
||||
stroke-width: 5;
|
||||
stroke-miterlimit: 10;
|
||||
`;
|
||||
|
||||
const ST5 = styled.path`
|
||||
fill: #ffffff;
|
||||
stroke: #42210b;
|
||||
stroke-width: 3;
|
||||
stroke-miterlimit: 10;
|
||||
`;
|
||||
|
||||
const ST6 = styled.ellipse`
|
||||
fill: #ed1c24;
|
||||
stroke: #8c6239;
|
||||
stroke-width: 5;
|
||||
stroke-miterlimit: 10;
|
||||
`;
|
||||
|
||||
const ST7 = styled.path`
|
||||
fill: #a67c52;
|
||||
`;
|
||||
|
||||
const ST8 = styled.path`
|
||||
fill: #ed1c24;
|
||||
`;
|
||||
|
||||
const ST9 = styled.ellipse`
|
||||
fill: #42210b;
|
||||
`;
|
||||
|
||||
class BrandLogo extends Component {
|
||||
render() {
|
||||
const { i18n } = this.props;
|
||||
return (
|
||||
<svg
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 500 500"
|
||||
xmlSpace="preserve"
|
||||
>
|
||||
<title>{i18n._(t`AWX Logo`)}</title>
|
||||
<ST0>
|
||||
<ST1
|
||||
d="M319.8,169.3c1.5-14.2,13.7-27.2,29.9-31.9c-13.1,1.5-27.3-1.7-36-10c-8.7-8.3-10-21.9-1.4-30.1
|
||||
c-12,6.7-28.1,8.1-41.4,3.4c-13.3-4.6-23.5-15.1-26.2-26.9c-2-8.8,0-17.9,2-26.7c-6.2,9.4-17.6,17.3-30.5,17.3
|
||||
c-12.9,0.1-25.7-10.2-22.9-20.7c-5.5,7.8-11.4,15.9-21,20.2c-9.5,4.3-23.7,2.7-28.2-5.5c-1.6,10.8-7.5,22-19.1,27
|
||||
c-9,3.9-21.5,2.2-28-3.8c5.7,11.4,4.3,25.3-4.1,35.6c-9.9,12.2-29.1,18.6-46.4,15.6c14.7,7.2,28.5,17.7,32.1,31.5
|
||||
c3.7,13.8-7.1,30.7-24.1,31.7c13.6,3.1,28,7.4,35.6,17.2c7.6,9.8,2.9,26.4-11.1,28c12.8-2.6,27.4,3.9,31.9,14.2
|
||||
c4.1,9.5-0.9,20.9-10.9,26.5c18.6-8.9,41-17.1,59.6-8.8c13.9,6.2,20.8,21.6,15.1,33.8c10.4-10.6,23-21.3,39.2-23.5
|
||||
c12.8-1.8,27.5,4.6,31.9,14.1c-0.3-12.7,6.1-25.5,17.5-34c13.8-10.3,34.4-14,52-9.2c-11.1-7.8-14.9-22-8.9-33
|
||||
c6-11,21.3-18,35.7-16.2C327.5,198.1,318.3,183.5,319.8,169.3z"
|
||||
/>
|
||||
</ST0>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<ST2
|
||||
d="M179.7,297.3c-10.1,3.2-20.3,6-30.6,8.4c-10.7,2.5-21.7,5-32.8,5.1C96,311.1,79.9,297.2,60,296.1
|
||||
c-5.8-0.3-5.8,8.7,0,9c9.9,0.5,18.9,5.1,27.9,8.8c9.8,4,19.6,6.3,30.2,5.9c21.5-0.8,43.5-7.4,64-13.8
|
||||
C187.6,304.3,185.2,295.6,179.7,297.3L179.7,297.3z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<ST2
|
||||
d="M322.2,194.8c17.9-8,36-18.5,44.3-37.2c4.2-9.3,6-19.2,7.2-29.3c1.5-11.7,2.5-23.4,3.7-35.2
|
||||
c0.6-5.8-8.4-5.7-9,0c-1.1,10.3-2.1,20.6-3.3,30.9c-1.1,9.7-2.5,19.7-6.4,28.7c-7.5,17.5-24.6,26.8-41.2,34.2
|
||||
C312.4,189.4,316.9,197.2,322.2,194.8L322.2,194.8z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<ST9
|
||||
transform="matrix(0.5541 -0.8324 0.8324 0.5541 -219.4917 376.0051)"
|
||||
cx="241.2"
|
||||
cy="392.9"
|
||||
rx="65.5"
|
||||
ry="33.7"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<ST3
|
||||
d="M224.1,442.5c22-11.5,38.7-31,47.1-54.3c2-5.5-6.7-7.8-8.7-2.4c-7.6,21.1-23.1,38.5-43,48.9
|
||||
C214.4,437.4,218.9,445.1,224.1,442.5L224.1,442.5z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<ST9
|
||||
transform="matrix(0.9684 -0.2494 0.2494 0.9684 -66.4734 109.0276)"
|
||||
cx="397"
|
||||
cy="316.8"
|
||||
rx="63.9"
|
||||
ry="32.9"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<ST3
|
||||
d="M363.8,341.5c28.3,7,58.7-0.8,80.2-20.5c4.3-3.9-2.1-10.3-6.4-6.4c-19.1,17.5-46.4,24.4-71.5,18.2
|
||||
C360.5,331.5,358.1,340.1,363.8,341.5L363.8,341.5z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<ST4
|
||||
d="M156.9,96c-25.4,4.5-32.9,20.2-45,46.9c-20.2,44.4,2,90.3,5.6,97.5c18.4,36.5,42.3,36.8,60,80.6
|
||||
c8.6,21.2,4.6,25.2,13.1,37.5c20.4,29.2,63.7,36.1,91.9,33.8c40.3-3.3,91.5-28.8,108.8-82.5c17.1-53.2-6-112.1-41.2-131.2
|
||||
c-25.3-13.7-44.9-0.5-71.2-20.6c-21.6-16.5-18.4-33.1-37.5-48.8C227.9,98.1,203.7,87.7,156.9,96z"
|
||||
/>
|
||||
<ST9
|
||||
transform="matrix(0.6622 -0.7494 0.7494 0.6622 65.2068 309.6339)"
|
||||
cx="376"
|
||||
cy="82.5"
|
||||
rx="21"
|
||||
ry="15.5"
|
||||
/>
|
||||
<g>
|
||||
<g>
|
||||
<ST3
|
||||
d="M379.8,75.3c0.8,0.2-0.6-0.4-0.1-0.1c0.2,0.1,0.3,0.2,0.5,0.3c0.4,0.2-0.6-0.7-0.1-0.1
|
||||
c0.1,0.1,0.7,0.8,0.2,0.2c-0.4-0.5,0,0,0.1,0.1c0.4,0.7,0,0.2,0-0.2c0,0.1,0.1,0.4,0.2,0.5c0.3,0.9-0.1-1,0-0.1
|
||||
c0.1,2.3,2,4.6,4.5,4.5c2.3-0.1,4.6-2,4.5-4.5c-0.3-4.4-3-8.1-7.3-9.4c-2.2-0.7-5,0.8-5.5,3.1C376.1,72.2,377.4,74.5,379.8,75.3
|
||||
L379.8,75.3z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<ST9
|
||||
transform="matrix(0.9999 -1.433736e-02 1.433736e-02 0.9999 -4.303 0.8051)"
|
||||
cx="54"
|
||||
cy="300.5"
|
||||
rx="21"
|
||||
ry="15.5"
|
||||
/>
|
||||
<g>
|
||||
<g>
|
||||
<ST3
|
||||
d="M52.2,297.5c1.1-0.3,1.4-0.4,2.5,0c0.8,0.3,1.3,0.7,2,1.7c1.5,1.9,4.8,1.6,6.4,0c1.9-1.9,1.5-4.4,0-6.4
|
||||
c-3.1-3.9-8.6-5.4-13.3-4C44.3,290.5,46.7,299.2,52.2,297.5L52.2,297.5z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<ST2
|
||||
d="M149.3,108.8c4.9-10.8-1.3-24.2-12.9-26.9c-1.9-0.4-2.7,2.4-0.8,2.9c9.6,2.3,15.3,13.5,11.2,22.5
|
||||
C145.9,109,148.5,110.5,149.3,108.8L149.3,108.8z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<ST2
|
||||
d="M141.2,112.3c2.4-9.4-5.4-19.3-15.2-19c-1.9,0.1-1.9,3.1,0,3c7.8-0.2,14.2,7.6,12.3,15.2
|
||||
C137.8,113.4,140.7,114.2,141.2,112.3L141.2,112.3z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<ST2
|
||||
d="M132.6,118c-1.1-8.3-10.9-13.4-18.2-9.1c-1.7,1-0.2,3.6,1.5,2.6c5.2-3,12.9,0.4,13.7,6.5
|
||||
C129.8,119.9,132.8,119.9,132.6,118L132.6,118z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<ST5 d="M215.5,166.5l34-73c0,0,35,0,46,21c7.5,14.3,8,39,8,39L215.5,166.5z" />
|
||||
<ST5 d="M208.2,170.5l-79.5-12.7c0,0-19.6,29-8.4,49.9c7.6,14.2,27.8,28.5,27.8,28.5L208.2,170.5z" />
|
||||
<ST2 d="M210.5,164.5l33-74c0,0-2.5-5.5-8-7s-12,0-12,0L210.5,164.5z" />
|
||||
<ST2 d="M207.4,165.3l-73.1-35c0,0-5.6,2.4-7.2,7.8c-1.6,5.5-0.3,12-0.3,12L207.4,165.3z" />
|
||||
<path d="M215.5,166.5L234,127c0,0,17-6,25.5,7.5c8.6,13.6-3.5,25.5-3.5,25.5L215.5,166.5z" />
|
||||
<path d="M206.7,170.9l-29.6,32c0,0-18,0.5-22-14.9c-4-15.6,11.1-23.2,11.1-23.2L206.7,170.9z" />
|
||||
<g>
|
||||
<g>
|
||||
<ST3
|
||||
d="M243.4,139.1c-0.6,0.2-0.7,0.3-0.4,0.2c0.3-0.1,0.2-0.1-0.5,0.1c0.7,0-0.3,0-0.4-0.1c0.1,0,0.3,0.1,0.4,0.1
|
||||
c0.3,0.1,0.2,0-0.4-0.2c0,0,0.6,0.3,0.6,0.3c0.5,0.2-0.9-0.6-0.1-0.1c0.6,0.4-0.3-0.5-0.1-0.1c0.3,0.5-0.3-1-0.1-0.2
|
||||
c0.2,0.8,0-1,0-0.1c0,2.4,2.1,4.6,4.5,4.5c2.5-0.1,4.5-2,4.5-4.5c0-3-1.6-5.7-4.1-7.3c-2.6-1.7-5.6-1.6-8.4-0.4
|
||||
c-2.2,0.9-2.8,4.3-1.6,6.2C238.7,139.7,241,140.1,243.4,139.1L243.4,139.1z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<ST3
|
||||
d="M173.5,176.4c-0.5-0.3-0.1,0,0.2,0.1c-0.7-0.6,0.3,0.5,0.1,0c-0.3-0.5,0.4,0.8,0.1,0.2
|
||||
c-0.4-0.8,0.2,0.2,0,0.1c0,0,0-0.6,0-0.6c-0.1,0.1-0.1,1,0,0.3c-0.1,0.2-0.1,0.3-0.2,0.5c0.2-0.3,0.2-0.4,0-0.1
|
||||
c-0.2,0.2-0.2,0.3-0.1,0.1c0.2-0.2,0.1-0.2-0.3,0.2c1.9-1.4,3-4,1.6-6.2c-1.2-1.9-4.1-3.1-6.2-1.6c-2.4,1.7-4,4.3-3.9,7.4
|
||||
c0.1,3,1.6,5.7,4.1,7.3c2,1.2,5,0.5,6.2-1.6C176.3,180.4,175.7,177.7,173.5,176.4L173.5,176.4z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<ST6
|
||||
transform="matrix(0.862 -0.5069 0.5069 0.862 -88.3186 186.5516)"
|
||||
cx="298.5"
|
||||
cy="255.5"
|
||||
rx="79.5"
|
||||
ry="68.5"
|
||||
/>
|
||||
<g>
|
||||
<g>
|
||||
<ST7
|
||||
d="M173.6,109.8c-2.1,2-3.9,4.6-3.6,7.6c0.3,3.5,2.8,6.6,6.6,6.7c6,0.2,11.5-7.7,8.2-13c-1-1.7-3.1-3.1-5.2-3
|
||||
c-1.7,0.1-3.1,0.8-4.4,1.9c-2,1.8-2.8,5.2-1.9,7.7c2.4,6.6,11.8,5.9,13.8-0.7c0.7-2.5-0.9-5.6-3.5-6.2c-2.7-0.6-5.4,0.8-6.2,3.5
|
||||
c0.6-2.1,3.1-2.6,4.6-1c0.8,0.9,1,1.8,0.8,2.8c0.2-0.5,0.1-0.4-0.1,0.3c-0.4,0.7-1,1.2-1.8,1.4c-0.9,0-1.8,0-2.7,0
|
||||
c-1.8-0.6-2.5-1.6-2.3-3.1c-0.1-0.4-0.1-0.7,0.1-1c0.2-0.3,0.1-0.3-0.1,0.1c0.1-0.1,0.2-0.2,0.3-0.4c-0.2,0.3-0.5,0.5-0.7,0.8
|
||||
c-0.1,0.1-0.2,0.2-0.3,0.3c-0.3,0.2-0.2,0.2,0.1-0.1c1.3,0.2,2.6,0.4,3.9,0.6c0.2,0.4,0.5,0.9,0.7,1.3c0.2,0.6-0.2,0.9-0.2,1.4
|
||||
c0,0.4,0.4-0.5-0.1,0.1c0.3-0.4,0.6-0.7,1-1c1.9-1.8,2-5.3,0-7.1C178.7,107.9,175.6,107.8,173.6,109.8L173.6,109.8z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<ST7
|
||||
d="M151.2,248.6c-5.7,7,1.7,16.9,10,13.3c3.4-1.5,6.3-5,6.3-8.9c0-4.2-2.7-7.6-7-7.8c-3.1-0.1-5.8,3.3-4.8,6.3
|
||||
c1.2,3.4,3.7,6.1,7.3,7c2.6,0.6,5.4-0.8,6.2-3.5c0.7-2.5-0.9-5.5-3.5-6.2c-1.7-0.4,0,0.1-0.2,0.1c-0.4,0-0.4-0.8-0.1-0.1
|
||||
c-1.6,2.1-3.2,4.2-4.8,6.3c-2.4-0.1-2.8-1.1-3-2.6c0.1,0.7-0.1,0.2,0.1-0.1c0.7-0.9-0.5,0.5,0,0c-0.5,0.5-0.3,0.1-0.2,0.2
|
||||
c0.1,0,0.6,0,0.7,0c0.4,0.1,0.5,0.4,0.8,0.6c0.2,0.3,0.2,0.2-0.1-0.2c0.1,0.1,0.1,0.3,0.2,0.4c0,1,0.1,1.1-0.7,2.1
|
||||
c1.7-2.1,2-5,0-7.1C156.5,246.8,152.9,246.5,151.2,248.6L151.2,248.6z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<ST7
|
||||
d="M204.1,205.7c0.8,4.8,5.3,8.6,10.1,8.6c5.1,0,9.5-3.9,10.3-8.9c0.7-4.4-0.2-12.1-5.3-13.6
|
||||
c-2.7-0.8-5.2,0.5-7,2.4c-1.1,1.2-1.5,1.7-3.1,1.2c0.7,2.8,1.5,5.6,2.2,8.4c0.2-0.2-0.5,0.2-0.5,0.2c6.3,1.4,8.9-8.2,2.7-9.6
|
||||
c-3.5-0.8-6.6,0-9.3,2.4c-3,2.6-1.1,7.2,2.2,8.4c2.6,0.9,5.5,0.8,8-0.2c1.3-0.5,2.4-1.2,3.4-2.1c0.4-0.3,0.7-0.6,1-1
|
||||
c0.2-0.3,0.4-0.5,0.6-0.7c0.4-0.4,0.3-0.4-0.5,0.3c-0.9,0-1.8,0-2.7,0c0.2,0.1,0.3,0.1,0.5,0.2c-0.7-0.4-1.5-0.9-2.2-1.3
|
||||
c0.1,0.2,0.3,0.3,0.4,0.5c-0.4-0.7-0.9-1.5-1.3-2.2c0.4,1.2,0.8,2.5,1,3.7c0,0.4,0,0.8,0,1.2c0,0.5-0.5,0.9,0,0.4
|
||||
c-0.8,0.6-0.9,0.2-1.1-0.9c-0.4-2.7-3.8-4.1-6.2-3.5C204.7,200.3,203.7,203,204.1,205.7L204.1,205.7z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<ST7
|
||||
d="M265.9,179.6c0.2,0.4,0.5,0.9,0.7,1.3c0.6,1.1,1.8,2,3,2.3c1.2,0.3,2.8,0.2,3.9-0.5c1.1-0.7,2-1.7,2.3-3
|
||||
c0.3-1.4,0.1-2.6-0.5-3.9c-0.2-0.4-0.5-0.9-0.7-1.3c-0.6-1.1-1.8-2-3-2.3c-1.2-0.3-2.8-0.2-3.9,0.5c-1.1,0.7-2,1.7-2.3,3
|
||||
C265.1,177.1,265.3,178.3,265.9,179.6L265.9,179.6z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<ST7
|
||||
d="M200.4,295.8c-6.1,1.6-8.1,8.6-5,13.7c2.8,4.7,9.1,7.2,14.3,5.4c4.9-1.7,7.8-7.1,6.3-12.2
|
||||
c-0.8-2.7-2.7-4.8-5.3-5.8c-1.4-0.5-2.8-0.7-4.2-0.8c-0.1,0-0.9-0.1-0.9-0.1c0.2-0.4,1.2,2.5,0.9,0.7c0,0.9,0,1.8,0,2.7
|
||||
c-0.1,0.1-0.1,0.1-0.2,0.2c3.1-5.6-5.5-10.7-8.6-5c-1.7,3-1.1,6.6,1.4,9c1.3,1.2,2.8,2,4.5,2.3c0.8,0.1,1.6,0.2,2.4,0.3
|
||||
c0.4,0,0.7,0,1.1,0.1c0.2,0.1,0.1,0.1-0.2-0.1c0,0.1-0.6-0.5-0.6-0.5c-0.1-0.1-0.1-0.2,0-0.3c0.1-0.3,0.1-0.1-0.1,0.5
|
||||
c-0.3-0.1,0.7-0.2-0.3-0.3c-0.9-0.1-1.1-0.6-1.8-0.9c0,0-0.2-0.3-0.3-0.3c0.3,0-0.8,1.2-0.8,1.2
|
||||
C209.3,303.8,206.6,294.2,200.4,295.8L200.4,295.8z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<ST7
|
||||
d="M244.8,355.3c-4-6.2-11.2-2.3-12,3.9c-0.8,5.9,1.8,12,6.5,15.6c4.5,3.5,11.5,4.9,16.7,2.1
|
||||
c6.4-3.3,5.4-9.8,4.9-15.9c-0.5-6.3-1.9-12-9.5-12.1c-5.1-0.1-13.1,0.2-14.5,6.4c-1.2,5.4,2.5,12.8,8.2,13.8
|
||||
c6.2,1.1,11.2-5.5,7.8-11c-2.2-3.5-8.1-3.1-9.1,1.2c-1.1,4.4,0.5,8,4.1,10.6c5.2,3.8,10.2-4.8,5-8.6c0.2,0.2,0.4,0.5,0.5,0.7
|
||||
c-3,0.4-6.1,0.8-9.1,1.2c-0.4-0.7,3.4-3.1,2.9-4.8c-0.8-2.6-1.7,1.4-1.9,1.1c0,0.1,5.2-0.1,5.6-0.4c0.7,0.1,0.8-0.1,0.2-0.6
|
||||
c-0.4-0.7-0.5-0.8-0.4-0.3c-0.2,0.3,0.2,1.9,0.2,2.3c0.2,2,0.3,4,0.5,5.9c0.1,1.6,0.4,1.7-1.1,2c-1.3,0.2-2.9-0.3-4-0.9
|
||||
c-1.4-0.8-2.5-2-3.1-3.5c-0.3-0.7-0.4-1.3-0.5-2c0-0.3-0.1-0.7,0-1c0.2-1.9-1.1-1.5-3.8,1.2c-1-0.8-2-1.5-3-2.3
|
||||
c0.1,0.2,0.2,0.4,0.4,0.6C239.6,365.7,248.3,360.7,244.8,355.3L244.8,355.3z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<ST7
|
||||
d="M336.5,337.4c-2.4-1.5-5.1-2.5-7.9-1.8c-2.7,0.7-4.9,3.2-5.3,6c-0.9,6.4,6.3,8.3,11.2,8.4
|
||||
c4.8,0.1,10.6-2.4,10.9-7.9c0.2-5.6-5.5-9.6-10.6-6.9c-5.7,3-0.7,11.6,5,8.6c-0.1,0.1-0.2,0.1-0.3,0.2c-0.9,0-1.8,0-2.7,0
|
||||
c-2.1-0.4-1.4-4.8-0.3-4.3c0,0-1.3,0.3-1.3,0.3c-0.6,0-1.2,0-1.8-0.1c-0.5-0.1-1-0.2-1.5-0.4c-1.2-0.5-1-0.2,0.6,0.7
|
||||
c0.2,0.8,0.5,1.7,0.7,2.5c-3.4,1.1-4.4,1.9-2.8,2.7c0.4,0.2,0.7,0.4,1.1,0.7C336.9,349.6,341.9,340.9,336.5,337.4L336.5,337.4z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<ST3
|
||||
d="M224.3,256.5L252,273v-40l32,20v-38l28,17l4-28l23,12l-3-24c0,0-14-8-35.5-6.4c-11.6,0.9-24.3,6.8-33.5,11.4
|
||||
c-14,7-23.7,18.9-31.2,29.1C227,238,224.3,256.5,224.3,256.5z"
|
||||
/>
|
||||
<ST3
|
||||
d="M372.9,248.9l-28.8-14.5l2.9,39.9l-33.3-17.7l2.7,37.9l-29.1-15l-2,28.2l-23.8-10.3l4.7,23.7
|
||||
c0,0,14.5,7,35.9,3.8c11.5-1.7,23.7-8.5,32.6-13.8c13.5-8,22.3-20.5,29-31.2C371.5,267.5,372.9,248.9,372.9,248.9z"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<ST8
|
||||
d="M235.2,121.6c8.5-3.1,23.2-0.1,27.8,8.4c2.3,4.4,4.5,9.9,4.5,14.9c0.1,5.5-2.7,10.5-5.3,15.3
|
||||
c-1.5,2.8,2.8,5.4,4.3,2.5c3.1-5.8,6.3-11.9,6-18.7c-0.3-6-2.8-12.8-5.9-17.9c-6-9.5-22.6-13.1-32.7-9.4
|
||||
C230.9,117.8,232.2,122.7,235.2,121.6L235.2,121.6z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<ST8
|
||||
d="M241.1,110.5c11.6-2.3,25.6,2.3,32.2,12.4c6.6,10.2,6.1,22.8,3.1,34.2c-1.3,5,6.4,7.1,7.7,2.1
|
||||
c3.8-14.3,3.8-30.3-5.5-42.6c-8.9-11.7-25.5-16.6-39.6-13.8C233.9,103.8,236.1,111.5,241.1,110.5L241.1,110.5z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<ST8
|
||||
d="M245.4,97.5c7.8-1.8,15.5,0,22.9,2.8c7.2,2.7,15,6.1,20.3,11.8c10.7,11.7,9.5,29.3,8.7,44
|
||||
c-0.3,6.4,9.7,6.4,10,0c1-17.9,1.2-38.5-12.7-52.1c-6.4-6.3-15.3-10.2-23.6-13.3c-9.1-3.4-18.6-4.9-28.2-2.8
|
||||
C236.5,89.2,239.1,98.9,245.4,97.5L245.4,97.5z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<ST8
|
||||
d="M155.8,158.5c-13.1,4.8-14.2,21.6-10.1,33.1c4.3,12,15.2,20.6,28.2,20.5c3.2,0,3.2-5,0-5
|
||||
c-9.9,0.1-18.6-5.9-22.6-14.9c-3.9-8.6-5.2-24.8,5.8-28.9C160.2,162.3,158.9,157.4,155.8,158.5L155.8,158.5z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<ST8
|
||||
d="M164.1,216.5c-11.4-2.2-18.8-11.4-22.7-21.9c-3.6-9.6-7.7-25.3,1.2-33.1c3.9-3.4-1.8-9-5.7-5.7
|
||||
c-11.3,9.9-7.9,28.5-3.3,40.9c4.8,13,14.1,24.7,28.3,27.5C167,225.2,169.1,217.5,164.1,216.5L164.1,216.5z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<ST8
|
||||
d="M152,231.7c-27.3-13.3-38.1-46.5-23.3-73.2c3.1-5.6-5.5-10.7-8.6-5c-17.3,31.2-5.3,71.1,26.9,86.9
|
||||
C152.7,243.1,157.8,234.5,152,231.7L152,231.7z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withI18n()(BrandLogo);
|
22
awx/ui_next/src/components/BrandLogo/BrandLogo.test.jsx
Normal file
22
awx/ui_next/src/components/BrandLogo/BrandLogo.test.jsx
Normal file
@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||
import BrandLogo from './BrandLogo';
|
||||
|
||||
let logoWrapper;
|
||||
let brandLogoElem;
|
||||
let svgElem;
|
||||
|
||||
const findChildren = () => {
|
||||
brandLogoElem = logoWrapper.find('BrandLogo');
|
||||
svgElem = logoWrapper.find('svg');
|
||||
};
|
||||
|
||||
describe('<BrandLogo />', () => {
|
||||
test('initially renders without crashing', () => {
|
||||
logoWrapper = mountWithContexts(<BrandLogo />);
|
||||
findChildren();
|
||||
expect(logoWrapper.length).toBe(1);
|
||||
expect(brandLogoElem.length).toBe(1);
|
||||
expect(svgElem.length).toBe(1);
|
||||
});
|
||||
});
|
1
awx/ui_next/src/components/BrandLogo/index.js
Normal file
1
awx/ui_next/src/components/BrandLogo/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './BrandLogo';
|
81
awx/ui_next/src/components/Breadcrumbs/Breadcrumbs.jsx
Normal file
81
awx/ui_next/src/components/Breadcrumbs/Breadcrumbs.jsx
Normal file
@ -0,0 +1,81 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
PageSection as PFPageSection,
|
||||
PageSectionVariants,
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbHeading as PFBreadcrumbHeading,
|
||||
} from '@patternfly/react-core';
|
||||
import { Link, Route, withRouter } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const PageSection = styled(PFPageSection)`
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
`;
|
||||
|
||||
const BreadcrumbHeading = styled(PFBreadcrumbHeading)`
|
||||
--pf-c-breadcrumb__heading--FontSize: 20px;
|
||||
line-height: 24px;
|
||||
flex: 100%;
|
||||
`;
|
||||
|
||||
const Breadcrumbs = ({ breadcrumbConfig }) => {
|
||||
const { light } = PageSectionVariants;
|
||||
|
||||
return (
|
||||
<PageSection variant={light}>
|
||||
<Breadcrumb>
|
||||
<Route
|
||||
path="/:path"
|
||||
render={props => (
|
||||
<Crumb breadcrumbConfig={breadcrumbConfig} {...props} />
|
||||
)}
|
||||
/>
|
||||
</Breadcrumb>
|
||||
</PageSection>
|
||||
);
|
||||
};
|
||||
|
||||
const Crumb = ({ breadcrumbConfig, match }) => {
|
||||
const crumb = breadcrumbConfig[match.url];
|
||||
|
||||
let crumbElement = (
|
||||
<BreadcrumbItem key={match.url}>
|
||||
<Link to={match.url}>{crumb}</Link>
|
||||
</BreadcrumbItem>
|
||||
);
|
||||
|
||||
if (match.isExact) {
|
||||
crumbElement = (
|
||||
<BreadcrumbHeading key="breadcrumb-heading">{crumb}</BreadcrumbHeading>
|
||||
);
|
||||
}
|
||||
|
||||
if (!crumb) {
|
||||
crumbElement = null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{crumbElement}
|
||||
<Route
|
||||
path={`${match.url}/:path`}
|
||||
render={props => (
|
||||
<Crumb breadcrumbConfig={breadcrumbConfig} {...props} />
|
||||
)}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
Breadcrumbs.propTypes = {
|
||||
breadcrumbConfig: PropTypes.objectOf(PropTypes.string).isRequired,
|
||||
};
|
||||
|
||||
Crumb.propTypes = {
|
||||
breadcrumbConfig: PropTypes.objectOf(PropTypes.string).isRequired,
|
||||
};
|
||||
|
||||
export default withRouter(Breadcrumbs);
|
66
awx/ui_next/src/components/Breadcrumbs/Breadcrumbs.test.jsx
Normal file
66
awx/ui_next/src/components/Breadcrumbs/Breadcrumbs.test.jsx
Normal file
@ -0,0 +1,66 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import Breadcrumbs from './Breadcrumbs';
|
||||
|
||||
describe('<Breadcrumb />', () => {
|
||||
let breadcrumbWrapper;
|
||||
let breadcrumb;
|
||||
let breadcrumbItem;
|
||||
let breadcrumbHeading;
|
||||
|
||||
const config = {
|
||||
'/foo': 'Foo',
|
||||
'/foo/1': 'One',
|
||||
'/foo/1/bar': 'Bar',
|
||||
'/foo/1/bar/fiz': 'Fiz',
|
||||
};
|
||||
|
||||
const findChildren = () => {
|
||||
breadcrumb = breadcrumbWrapper.find('Breadcrumb');
|
||||
breadcrumbItem = breadcrumbWrapper.find('BreadcrumbItem');
|
||||
breadcrumbHeading = breadcrumbWrapper.find('BreadcrumbHeading');
|
||||
};
|
||||
|
||||
test('initially renders succesfully', () => {
|
||||
breadcrumbWrapper = mount(
|
||||
<MemoryRouter initialEntries={['/foo/1/bar']} initialIndex={0}>
|
||||
<Breadcrumbs breadcrumbConfig={config} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
findChildren();
|
||||
|
||||
expect(breadcrumb).toHaveLength(1);
|
||||
expect(breadcrumbItem).toHaveLength(2);
|
||||
expect(breadcrumbHeading).toHaveLength(1);
|
||||
expect(breadcrumbItem.first().text()).toBe('Foo');
|
||||
expect(breadcrumbItem.last().text()).toBe('One');
|
||||
expect(breadcrumbHeading.text()).toBe('Bar');
|
||||
breadcrumbWrapper.unmount();
|
||||
});
|
||||
|
||||
test('renders breadcrumb items defined in breadcrumbConfig', () => {
|
||||
const routes = [
|
||||
['/fo', 0],
|
||||
['/foo', 0],
|
||||
['/foo/1', 1],
|
||||
['/foo/baz', 1],
|
||||
['/foo/1/bar', 2],
|
||||
['/foo/1/bar/fiz', 3],
|
||||
];
|
||||
|
||||
routes.forEach(([location, crumbLength]) => {
|
||||
breadcrumbWrapper = mount(
|
||||
<MemoryRouter initialEntries={[location]}>
|
||||
<Breadcrumbs breadcrumbConfig={config} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
expect(breadcrumbWrapper.find('BreadcrumbItem')).toHaveLength(
|
||||
crumbLength
|
||||
);
|
||||
breadcrumbWrapper.unmount();
|
||||
});
|
||||
});
|
||||
});
|
1
awx/ui_next/src/components/Breadcrumbs/index.js
Normal file
1
awx/ui_next/src/components/Breadcrumbs/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './Breadcrumbs';
|
28
awx/ui_next/src/components/ButtonGroup.jsx
Normal file
28
awx/ui_next/src/components/ButtonGroup.jsx
Normal file
@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Group = styled.div`
|
||||
display: inline-flex;
|
||||
|
||||
& > .pf-c-button:not(:last-child) {
|
||||
&,
|
||||
&::after {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
& > .pf-c-button:not(:first-child) {
|
||||
&,
|
||||
&::after {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function ButtonGroup({ children }) {
|
||||
return <Group>{children}</Group>;
|
||||
}
|
||||
|
||||
export default ButtonGroup;
|
@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { string } from 'prop-types';
|
||||
import { Link as RRLink } from 'react-router-dom';
|
||||
import { Button } from '@patternfly/react-core';
|
||||
import { TimesIcon } from '@patternfly/react-icons';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Link = styled(RRLink)`
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 4px;
|
||||
color: var(--pf-c-button--m-plain--Color);
|
||||
`;
|
||||
|
||||
function CardCloseButton({ linkTo, i18n, i18nHash, ...props }) {
|
||||
if (linkTo) {
|
||||
return (
|
||||
<Link
|
||||
className="pf-c-button"
|
||||
aria-label={i18n._(t`Close`)}
|
||||
title={i18n._(t`Close`)}
|
||||
to={linkTo}
|
||||
{...props}
|
||||
>
|
||||
<TimesIcon />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Button variant="plain" aria-label={i18n._(t`Close`)} {...props}>
|
||||
<TimesIcon />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
CardCloseButton.propTypes = {
|
||||
linkTo: string,
|
||||
};
|
||||
CardCloseButton.defaultProps = {
|
||||
linkTo: null,
|
||||
};
|
||||
|
||||
export default withI18n()(CardCloseButton);
|
@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||
import CardCloseButton from './CardCloseButton';
|
||||
|
||||
describe('<CardCloseButton>', () => {
|
||||
test('should render close button', () => {
|
||||
const wrapper = mountWithContexts(<CardCloseButton />);
|
||||
const button = wrapper.find('Button');
|
||||
expect(button).toHaveLength(1);
|
||||
expect(button.prop('variant')).toBe('plain');
|
||||
expect(button.prop('aria-label')).toBe('Close');
|
||||
expect(wrapper.find('Link')).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should render close link when `linkTo` prop provided', () => {
|
||||
const wrapper = mountWithContexts(<CardCloseButton linkTo="/foo" />);
|
||||
expect(wrapper.find('Button')).toHaveLength(0);
|
||||
const link = wrapper.find('Link');
|
||||
expect(link).toHaveLength(1);
|
||||
expect(link.prop('to')).toEqual('/foo');
|
||||
expect(link.prop('aria-label')).toEqual('Close');
|
||||
});
|
||||
});
|
1
awx/ui_next/src/components/CardCloseButton/index.js
Normal file
1
awx/ui_next/src/components/CardCloseButton/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './CardCloseButton';
|
@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
DataListItem,
|
||||
DataListItemRow,
|
||||
DataListItemCells,
|
||||
DataListCheck,
|
||||
DataListCell,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import VerticalSeparator from '../VerticalSeparator';
|
||||
|
||||
const CheckboxListItem = ({ itemId, name, isSelected, onSelect }) => (
|
||||
<DataListItem key={itemId} aria-labelledby={`check-action-item-${itemId}`}>
|
||||
<DataListItemRow>
|
||||
<DataListCheck
|
||||
id={`selected-${itemId}`}
|
||||
checked={isSelected}
|
||||
onChange={onSelect}
|
||||
aria-labelledby={`check-action-item-${itemId}`}
|
||||
value={itemId}
|
||||
/>
|
||||
<DataListItemCells
|
||||
dataListCells={[
|
||||
<DataListCell key="divider" className="pf-c-data-list__cell--divider">
|
||||
<VerticalSeparator />
|
||||
</DataListCell>,
|
||||
<DataListCell key="name">
|
||||
<label
|
||||
id={`check-action-item-${itemId}`}
|
||||
htmlFor={`selected-${itemId}`}
|
||||
className="check-action-item"
|
||||
>
|
||||
<b>{name}</b>
|
||||
</label>
|
||||
</DataListCell>,
|
||||
]}
|
||||
/>
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
);
|
||||
|
||||
CheckboxListItem.propTypes = {
|
||||
itemId: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
isSelected: PropTypes.bool.isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default CheckboxListItem;
|
@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import CheckboxListItem from './CheckboxListItem';
|
||||
|
||||
describe('CheckboxListItem', () => {
|
||||
test('renders the expected content', () => {
|
||||
const wrapper = mount(
|
||||
<CheckboxListItem
|
||||
itemId={1}
|
||||
name="Buzz"
|
||||
isSelected={false}
|
||||
onSelect={() => {}}
|
||||
/>
|
||||
);
|
||||
expect(wrapper).toHaveLength(1);
|
||||
});
|
||||
});
|
1
awx/ui_next/src/components/CheckboxListItem/index.js
Normal file
1
awx/ui_next/src/components/CheckboxListItem/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './CheckboxListItem';
|
19
awx/ui_next/src/components/Chip/Chip.jsx
Normal file
19
awx/ui_next/src/components/Chip/Chip.jsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { Chip } from '@patternfly/react-core';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export default styled(Chip)`
|
||||
--pf-c-chip--m-read-only--PaddingTop: 3px;
|
||||
--pf-c-chip--m-read-only--PaddingRight: 8px;
|
||||
--pf-c-chip--m-read-only--PaddingBottom: 3px;
|
||||
--pf-c-chip--m-read-only--PaddingLeft: 8px;
|
||||
|
||||
& > .pf-c-button {
|
||||
padding: 3px 8px;
|
||||
}
|
||||
|
||||
${props =>
|
||||
props.isOverflowChip &&
|
||||
`
|
||||
padding: 0;
|
||||
`}
|
||||
`;
|
11
awx/ui_next/src/components/Chip/Chip.test.jsx
Normal file
11
awx/ui_next/src/components/Chip/Chip.test.jsx
Normal file
@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import Chip from './Chip';
|
||||
|
||||
describe('Chip', () => {
|
||||
test('renders the expected content', () => {
|
||||
const wrapper = mount(<Chip />);
|
||||
expect(wrapper).toHaveLength(1);
|
||||
});
|
||||
});
|
42
awx/ui_next/src/components/Chip/ChipGroup.jsx
Normal file
42
awx/ui_next/src/components/Chip/ChipGroup.jsx
Normal file
@ -0,0 +1,42 @@
|
||||
import React, { useState } from 'react';
|
||||
import { number } from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
import Chip from './Chip';
|
||||
|
||||
const ChipGroup = ({ children, className, showOverflowAfter, ...props }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(!showOverflowAfter);
|
||||
const toggleIsOpen = () => setIsExpanded(!isExpanded);
|
||||
|
||||
const mappedChildren = React.Children.map(children, c =>
|
||||
React.cloneElement(c, { component: 'li' })
|
||||
);
|
||||
const showOverflowToggle =
|
||||
showOverflowAfter && children.length > showOverflowAfter;
|
||||
const numToShow = isExpanded
|
||||
? children.length
|
||||
: Math.min(showOverflowAfter, children.length);
|
||||
const expandedText = 'Show Less';
|
||||
const collapsedText = `${children.length - showOverflowAfter} more`;
|
||||
|
||||
return (
|
||||
<ul className={`pf-c-chip-group ${className}`} {...props}>
|
||||
{mappedChildren.slice(0, numToShow)}
|
||||
{showOverflowToggle && (
|
||||
<Chip isOverflowChip onClick={toggleIsOpen} component="li">
|
||||
{isExpanded ? expandedText : collapsedText}
|
||||
</Chip>
|
||||
)}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
ChipGroup.propTypes = {
|
||||
showOverflowAfter: number,
|
||||
};
|
||||
ChipGroup.defaultProps = {
|
||||
showOverflowAfter: null,
|
||||
};
|
||||
|
||||
export default styled(ChipGroup)`
|
||||
--pf-c-chip-group--c-chip--MarginRight: 10px;
|
||||
--pf-c-chip-group--c-chip--MarginBottom: 10px;
|
||||
`;
|
74
awx/ui_next/src/components/Chip/ChipGroup.test.jsx
Normal file
74
awx/ui_next/src/components/Chip/ChipGroup.test.jsx
Normal file
@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||
import { ChipGroup, Chip } from '.';
|
||||
|
||||
describe('<ChipGroup />', () => {
|
||||
test('should render all chips', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<ChipGroup>
|
||||
<Chip>One</Chip>
|
||||
<Chip>Two</Chip>
|
||||
<Chip>Three</Chip>
|
||||
<Chip>Four</Chip>
|
||||
<Chip>Five</Chip>
|
||||
<Chip>Six</Chip>
|
||||
</ChipGroup>
|
||||
);
|
||||
expect(wrapper.find(Chip)).toHaveLength(6);
|
||||
expect(wrapper.find('li')).toHaveLength(6);
|
||||
});
|
||||
|
||||
test('should render show more toggle', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<ChipGroup showOverflowAfter={5}>
|
||||
<Chip>One</Chip>
|
||||
<Chip>Two</Chip>
|
||||
<Chip>Three</Chip>
|
||||
<Chip>Four</Chip>
|
||||
<Chip>Five</Chip>
|
||||
<Chip>Six</Chip>
|
||||
<Chip>Seven</Chip>
|
||||
</ChipGroup>
|
||||
);
|
||||
expect(wrapper.find(Chip)).toHaveLength(6);
|
||||
const toggle = wrapper.find(Chip).at(5);
|
||||
expect(toggle.prop('isOverflowChip')).toBe(true);
|
||||
expect(toggle.text()).toEqual('2 more');
|
||||
});
|
||||
|
||||
test('should render show less toggle', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<ChipGroup showOverflowAfter={5}>
|
||||
<Chip>One</Chip>
|
||||
<Chip>Two</Chip>
|
||||
<Chip>Three</Chip>
|
||||
<Chip>Four</Chip>
|
||||
<Chip>Five</Chip>
|
||||
<Chip>Six</Chip>
|
||||
<Chip>Seven</Chip>
|
||||
</ChipGroup>
|
||||
);
|
||||
expect(wrapper.find(Chip)).toHaveLength(6);
|
||||
const toggle = wrapper.find(Chip).at(5);
|
||||
expect(toggle.prop('isOverflowChip')).toBe(true);
|
||||
act(() => {
|
||||
toggle.prop('onClick')();
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find(Chip)).toHaveLength(8);
|
||||
expect(
|
||||
wrapper
|
||||
.find(Chip)
|
||||
.at(7)
|
||||
.text()
|
||||
).toEqual('Show Less');
|
||||
act(() => {
|
||||
const toggle2 = wrapper.find(Chip).at(7);
|
||||
expect(toggle2.prop('isOverflowChip')).toBe(true);
|
||||
toggle2.prop('onClick')();
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find(Chip)).toHaveLength(6);
|
||||
});
|
||||
});
|
2
awx/ui_next/src/components/Chip/index.js
Normal file
2
awx/ui_next/src/components/Chip/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
export { default as ChipGroup } from './ChipGroup';
|
||||
export { default as Chip } from './Chip';
|
@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
import { oneOf, bool, number, string, func } from 'prop-types';
|
||||
import { Controlled as ReactCodeMirror } from 'react-codemirror2';
|
||||
import styled from 'styled-components';
|
||||
import 'codemirror/mode/javascript/javascript';
|
||||
import 'codemirror/mode/yaml/yaml';
|
||||
import 'codemirror/mode/jinja2/jinja2';
|
||||
import 'codemirror/lib/codemirror.css';
|
||||
|
||||
const LINE_HEIGHT = 24;
|
||||
const PADDING = 12;
|
||||
|
||||
const CodeMirror = styled(ReactCodeMirror)`
|
||||
&& {
|
||||
height: initial;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
& > .CodeMirror {
|
||||
height: ${props => props.rows * LINE_HEIGHT + PADDING}px;
|
||||
font-family: var(--pf-global--FontFamily--monospace);
|
||||
}
|
||||
|
||||
${props =>
|
||||
props.hasErrors &&
|
||||
`
|
||||
&& {
|
||||
--pf-c-form-control--PaddingRight: var(--pf-c-form-control--invalid--PaddingRight);
|
||||
--pf-c-form-control--BorderBottomColor: var(--pf-c-form-control--invalid--BorderBottomColor);
|
||||
padding-right: 24px;
|
||||
padding-bottom: var(--pf-c-form-control--invalid--PaddingBottom);
|
||||
background: var(--pf-c-form-control--invalid--Background);
|
||||
border-bottom-width: var(--pf-c-form-control--invalid--BorderBottomWidth);
|
||||
}`}
|
||||
`;
|
||||
|
||||
function CodeMirrorInput({ value, onChange, mode, readOnly, hasErrors, rows }) {
|
||||
return (
|
||||
<CodeMirror
|
||||
className="pf-c-form-control"
|
||||
value={value}
|
||||
onBeforeChange={(editor, data, val) => onChange(val)}
|
||||
mode={mode}
|
||||
hasErrors={hasErrors}
|
||||
options={{
|
||||
smartIndent: false,
|
||||
lineNumbers: true,
|
||||
readOnly,
|
||||
}}
|
||||
rows={rows}
|
||||
/>
|
||||
);
|
||||
}
|
||||
CodeMirrorInput.propTypes = {
|
||||
value: string.isRequired,
|
||||
onChange: func.isRequired,
|
||||
mode: oneOf(['javascript', 'yaml', 'jinja2']).isRequired,
|
||||
readOnly: bool,
|
||||
hasErrors: bool,
|
||||
rows: number,
|
||||
};
|
||||
CodeMirrorInput.defaultProps = {
|
||||
readOnly: false,
|
||||
rows: 6,
|
||||
hasErrors: false,
|
||||
};
|
||||
|
||||
export default CodeMirrorInput;
|
@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import CodeMirrorInput from './CodeMirrorInput';
|
||||
|
||||
describe('CodeMirrorInput', () => {
|
||||
beforeEach(() => {
|
||||
document.body.createTextRange = jest.fn();
|
||||
});
|
||||
|
||||
it('should trigger onChange prop', () => {
|
||||
const onChange = jest.fn();
|
||||
const wrapper = mount(
|
||||
<CodeMirrorInput value="---\n" onChange={onChange} mode="yaml" />
|
||||
);
|
||||
const codemirror = wrapper.find('Controlled');
|
||||
expect(codemirror.prop('mode')).toEqual('yaml');
|
||||
expect(codemirror.prop('options').readOnly).toEqual(false);
|
||||
codemirror.prop('onBeforeChange')(null, null, 'newvalue');
|
||||
expect(onChange).toHaveBeenCalledWith('newvalue');
|
||||
});
|
||||
|
||||
it('should render in read only mode', () => {
|
||||
const onChange = jest.fn();
|
||||
const wrapper = mount(
|
||||
<CodeMirrorInput value="---\n" onChange={onChange} mode="yaml" readOnly />
|
||||
);
|
||||
const codemirror = wrapper.find('Controlled');
|
||||
expect(codemirror.prop('options').readOnly).toEqual(true);
|
||||
});
|
||||
});
|
101
awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx
Normal file
101
awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx
Normal file
@ -0,0 +1,101 @@
|
||||
import React, { useState } from 'react';
|
||||
import { string, bool } from 'prop-types';
|
||||
import { Field } from 'formik';
|
||||
import { Button, Split, SplitItem } from '@patternfly/react-core';
|
||||
import styled from 'styled-components';
|
||||
import ButtonGroup from '../ButtonGroup';
|
||||
import CodeMirrorInput from './CodeMirrorInput';
|
||||
import { yamlToJson, jsonToYaml } from '../../util/yaml';
|
||||
|
||||
const YAML_MODE = 'yaml';
|
||||
const JSON_MODE = 'javascript';
|
||||
|
||||
const SmallButton = styled(Button)`
|
||||
padding: 3px 8px;
|
||||
font-size: var(--pf-global--FontSize--xs);
|
||||
`;
|
||||
|
||||
function VariablesField({ id, name, label, readOnly }) {
|
||||
const [mode, setMode] = useState(YAML_MODE);
|
||||
|
||||
return (
|
||||
<Field
|
||||
name={name}
|
||||
render={({ field, form }) => (
|
||||
<div className="pf-c-form__group">
|
||||
<Split gutter="sm">
|
||||
<SplitItem>
|
||||
<label htmlFor={id} className="pf-c-form__label">
|
||||
{label}
|
||||
</label>
|
||||
</SplitItem>
|
||||
<SplitItem>
|
||||
<ButtonGroup>
|
||||
<SmallButton
|
||||
onClick={() => {
|
||||
if (mode === YAML_MODE) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
form.setFieldValue(name, jsonToYaml(field.value));
|
||||
setMode(YAML_MODE);
|
||||
} catch (err) {
|
||||
form.setFieldError(name, err.message);
|
||||
}
|
||||
}}
|
||||
variant={mode === YAML_MODE ? 'primary' : 'secondary'}
|
||||
>
|
||||
YAML
|
||||
</SmallButton>
|
||||
<SmallButton
|
||||
onClick={() => {
|
||||
if (mode === JSON_MODE) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
form.setFieldValue(name, yamlToJson(field.value));
|
||||
setMode(JSON_MODE);
|
||||
} catch (err) {
|
||||
form.setFieldError(name, err.message);
|
||||
}
|
||||
}}
|
||||
variant={mode === JSON_MODE ? 'primary' : 'secondary'}
|
||||
>
|
||||
JSON
|
||||
</SmallButton>
|
||||
</ButtonGroup>
|
||||
</SplitItem>
|
||||
</Split>
|
||||
<CodeMirrorInput
|
||||
mode={mode}
|
||||
readOnly={readOnly}
|
||||
{...field}
|
||||
onChange={value => {
|
||||
form.setFieldValue(name, value);
|
||||
}}
|
||||
hasErrors={!!form.errors[field.name]}
|
||||
/>
|
||||
{form.errors[field.name] ? (
|
||||
<div
|
||||
className="pf-c-form__helper-text pf-m-error"
|
||||
aria-live="polite"
|
||||
>
|
||||
{form.errors[field.name]}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
VariablesField.propTypes = {
|
||||
id: string.isRequired,
|
||||
name: string.isRequired,
|
||||
label: string.isRequired,
|
||||
readOnly: bool,
|
||||
};
|
||||
VariablesField.defaultProps = {
|
||||
readOnly: false,
|
||||
};
|
||||
|
||||
export default VariablesField;
|
@ -0,0 +1,100 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { Formik } from 'formik';
|
||||
import { sleep } from '../../../testUtils/testUtils';
|
||||
import VariablesField from './VariablesField';
|
||||
|
||||
describe('VariablesField', () => {
|
||||
beforeEach(() => {
|
||||
document.body.createTextRange = jest.fn();
|
||||
});
|
||||
|
||||
it('should render code mirror input', () => {
|
||||
const value = '---\n';
|
||||
const wrapper = mount(
|
||||
<Formik
|
||||
initialValues={{ variables: value }}
|
||||
render={() => (
|
||||
<VariablesField id="the-field" name="variables" label="Variables" />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
const codemirror = wrapper.find('Controlled');
|
||||
expect(codemirror.prop('value')).toEqual(value);
|
||||
});
|
||||
|
||||
it('should render yaml/json toggles', () => {
|
||||
const value = '---\n';
|
||||
const wrapper = mount(
|
||||
<Formik
|
||||
initialValues={{ variables: value }}
|
||||
render={() => (
|
||||
<VariablesField id="the-field" name="variables" label="Variables" />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
const buttons = wrapper.find('Button');
|
||||
expect(buttons).toHaveLength(2);
|
||||
expect(buttons.at(0).prop('variant')).toEqual('primary');
|
||||
expect(buttons.at(1).prop('variant')).toEqual('secondary');
|
||||
|
||||
buttons.at(1).simulate('click');
|
||||
wrapper.update(0);
|
||||
expect(wrapper.find('CodeMirrorInput').prop('mode')).toEqual('javascript');
|
||||
const buttons2 = wrapper.find('Button');
|
||||
expect(buttons2.at(0).prop('variant')).toEqual('secondary');
|
||||
expect(buttons2.at(1).prop('variant')).toEqual('primary');
|
||||
buttons2.at(0).simulate('click');
|
||||
wrapper.update(0);
|
||||
expect(wrapper.find('CodeMirrorInput').prop('mode')).toEqual('yaml');
|
||||
});
|
||||
|
||||
it('should set Formik error if yaml is invalid', () => {
|
||||
const value = '---\nfoo bar\n';
|
||||
const wrapper = mount(
|
||||
<Formik
|
||||
initialValues={{ variables: value }}
|
||||
render={() => (
|
||||
<VariablesField id="the-field" name="variables" label="Variables" />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
wrapper
|
||||
.find('Button')
|
||||
.at(1)
|
||||
.simulate('click');
|
||||
wrapper.update();
|
||||
|
||||
const field = wrapper.find('CodeMirrorInput');
|
||||
expect(field.prop('hasErrors')).toEqual(true);
|
||||
expect(wrapper.find('.pf-m-error')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should submit value through Formik', async () => {
|
||||
const value = '---\nfoo: bar\n';
|
||||
const handleSubmit = jest.fn();
|
||||
const wrapper = mount(
|
||||
<Formik
|
||||
initialValues={{ variables: value }}
|
||||
onSubmit={handleSubmit}
|
||||
render={formik => (
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<VariablesField id="the-field" name="variables" label="Variables" />
|
||||
<button type="submit" id="submit">
|
||||
Submit
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
wrapper.find('CodeMirrorInput').prop('onChange')('---\nnewval: changed');
|
||||
wrapper.find('form').simulate('submit');
|
||||
await sleep(1);
|
||||
await sleep(1);
|
||||
|
||||
expect(handleSubmit).toHaveBeenCalled();
|
||||
expect(handleSubmit.mock.calls[0][0]).toEqual({
|
||||
variables: '---\nnewval: changed',
|
||||
});
|
||||
});
|
||||
});
|
4
awx/ui_next/src/components/CodeMirrorInput/index.js
Normal file
4
awx/ui_next/src/components/CodeMirrorInput/index.js
Normal file
@ -0,0 +1,4 @@
|
||||
import CodeMirrorInput from './CodeMirrorInput';
|
||||
|
||||
export default CodeMirrorInput;
|
||||
export { default as VariablesField } from './VariablesField';
|
21
awx/ui_next/src/components/ContentEmpty/ContentEmpty.jsx
Normal file
21
awx/ui_next/src/components/ContentEmpty/ContentEmpty.jsx
Normal file
@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import {
|
||||
Title,
|
||||
EmptyState,
|
||||
EmptyStateIcon,
|
||||
EmptyStateBody,
|
||||
} from '@patternfly/react-core';
|
||||
import { CubesIcon } from '@patternfly/react-icons';
|
||||
|
||||
const ContentEmpty = ({ i18n, title = '', message = '' }) => (
|
||||
<EmptyState>
|
||||
<EmptyStateIcon icon={CubesIcon} />
|
||||
<Title size="lg">{title || i18n._(t`No items found.`)}</Title>
|
||||
<EmptyStateBody>{message}</EmptyStateBody>
|
||||
</EmptyState>
|
||||
);
|
||||
|
||||
export { ContentEmpty as _ContentEmpty };
|
||||
export default withI18n()(ContentEmpty);
|
@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||
|
||||
import ContentEmpty from './ContentEmpty';
|
||||
|
||||
describe('ContentEmpty', () => {
|
||||
test('renders the expected content', () => {
|
||||
const wrapper = mountWithContexts(<ContentEmpty />);
|
||||
expect(wrapper).toHaveLength(1);
|
||||
});
|
||||
});
|
1
awx/ui_next/src/components/ContentEmpty/index.js
Normal file
1
awx/ui_next/src/components/ContentEmpty/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './ContentEmpty';
|
38
awx/ui_next/src/components/ContentError/ContentError.jsx
Normal file
38
awx/ui_next/src/components/ContentError/ContentError.jsx
Normal file
@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import {
|
||||
Title,
|
||||
EmptyState as PFEmptyState,
|
||||
EmptyStateIcon,
|
||||
EmptyStateBody,
|
||||
} from '@patternfly/react-core';
|
||||
import { ExclamationTriangleIcon } from '@patternfly/react-icons';
|
||||
|
||||
import ErrorDetail from '@components/ErrorDetail';
|
||||
|
||||
const EmptyState = styled(PFEmptyState)`
|
||||
width: var(--pf-c-empty-state--m-lg--MaxWidth);
|
||||
`;
|
||||
|
||||
class ContentError extends React.Component {
|
||||
render() {
|
||||
const { error, i18n } = this.props;
|
||||
return (
|
||||
<EmptyState>
|
||||
<EmptyStateIcon icon={ExclamationTriangleIcon} />
|
||||
<Title size="lg">{i18n._(t`Something went wrong...`)}</Title>
|
||||
<EmptyStateBody>
|
||||
{i18n._(
|
||||
t`There was an error loading this content. Please reload the page.`
|
||||
)}
|
||||
</EmptyStateBody>
|
||||
{error && <ErrorDetail error={error} />}
|
||||
</EmptyState>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { ContentError as _ContentError };
|
||||
export default withI18n()(ContentError);
|
@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||
|
||||
import ContentError from './ContentError';
|
||||
|
||||
describe('ContentError', () => {
|
||||
test('renders the expected content', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<ContentError
|
||||
error={
|
||||
new Error({
|
||||
response: {
|
||||
config: {
|
||||
method: 'post',
|
||||
},
|
||||
data: 'An error occurred',
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
expect(wrapper).toHaveLength(1);
|
||||
});
|
||||
});
|
1
awx/ui_next/src/components/ContentError/index.js
Normal file
1
awx/ui_next/src/components/ContentError/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './ContentError';
|
14
awx/ui_next/src/components/ContentLoading/ContentLoading.jsx
Normal file
14
awx/ui_next/src/components/ContentLoading/ContentLoading.jsx
Normal file
@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { EmptyState, EmptyStateBody } from '@patternfly/react-core';
|
||||
|
||||
// TODO: Better loading state - skeleton lines / spinner, etc.
|
||||
const ContentLoading = ({ i18n }) => (
|
||||
<EmptyState>
|
||||
<EmptyStateBody>{i18n._(t`Loading...`)}</EmptyStateBody>
|
||||
</EmptyState>
|
||||
);
|
||||
|
||||
export { ContentLoading as _ContentLoading };
|
||||
export default withI18n()(ContentLoading);
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user