Merge ui and ui_next in one dir
Merge ui and ui_next in one dir See: https://github.com/ansible/awx/issues/10676 Update django .po files Update django .po files Run `awx-manage makemessages`.
@@ -1,10 +0,0 @@
|
||||
jest.*.js
|
||||
webpack.*.js
|
||||
|
||||
etc
|
||||
coverage
|
||||
build
|
||||
node_modules
|
||||
dist
|
||||
images
|
||||
instrumented
|
||||
@@ -1,146 +0,0 @@
|
||||
{
|
||||
"parser": "babel-eslint",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"jsx": true,
|
||||
"modules": true
|
||||
}
|
||||
},
|
||||
"plugins": ["react-hooks", "jsx-a11y", "i18next"],
|
||||
"extends": [
|
||||
"airbnb",
|
||||
"prettier",
|
||||
"plugin:jsx-a11y/strict",
|
||||
"plugin:i18next/recommended"
|
||||
],
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "16.5.2"
|
||||
},
|
||||
"import/resolver": {
|
||||
"node": {
|
||||
"paths": ["src"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"jest": true
|
||||
},
|
||||
"globals": {
|
||||
"window": true
|
||||
},
|
||||
"rules": {
|
||||
"i18next/no-literal-string": [
|
||||
2,
|
||||
{
|
||||
"markupOnly": true,
|
||||
"ignoreAttribute": [
|
||||
"dateFieldName",
|
||||
"timeFieldName",
|
||||
"to",
|
||||
"streamType",
|
||||
"path",
|
||||
"component",
|
||||
"variant",
|
||||
"key",
|
||||
"position",
|
||||
"promptName",
|
||||
"color",
|
||||
"promptId",
|
||||
"headingLevel",
|
||||
"size",
|
||||
"target",
|
||||
"autoComplete",
|
||||
"trigger",
|
||||
"from",
|
||||
"name",
|
||||
"fieldId",
|
||||
"css",
|
||||
"gutter",
|
||||
"dataCy",
|
||||
"tooltipMaxWidth",
|
||||
"mode",
|
||||
"aria-labelledby",
|
||||
"aria-hidden",
|
||||
"aria-controls",
|
||||
"aria-pressed",
|
||||
"sortKey",
|
||||
"ouiaId",
|
||||
"credentialTypeNamespace",
|
||||
"link",
|
||||
"value",
|
||||
"credentialTypeKind",
|
||||
"linkTo",
|
||||
"scrollToAlignment",
|
||||
"displayKey",
|
||||
"sortedColumnKey",
|
||||
"maxHeight",
|
||||
"role",
|
||||
"aria-haspopup",
|
||||
"dropDirection",
|
||||
"resizeOrientation",
|
||||
"src",
|
||||
"theme",
|
||||
"gridColumns",
|
||||
"rows",
|
||||
"href",
|
||||
"modifier",
|
||||
"data-cy",
|
||||
"fieldName"
|
||||
],
|
||||
"ignore": ["Ansible", "Tower", "JSON", "YAML", "lg", "hh:mm AM/PM"],
|
||||
"ignoreComponent": [
|
||||
"AboutModal",
|
||||
"code",
|
||||
"Omit",
|
||||
"PotentialLink",
|
||||
"TypeRedirect",
|
||||
"Radio",
|
||||
"RunOnRadio",
|
||||
"NodeTypeLetter",
|
||||
"SelectableItem",
|
||||
"Dash",
|
||||
"Plural"
|
||||
],
|
||||
"ignoreCallee": ["describe"]
|
||||
}
|
||||
],
|
||||
"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/jsx-props-no-spreading":["off"],
|
||||
"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",
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"react-hooks/exhaustive-deps": "warn",
|
||||
"react/jsx-filename-extension": "off"
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
{"catalogs":[{
|
||||
"path": "<rootDir>/locales/{locale}/messages",
|
||||
"include": ["<rootDir>"],
|
||||
"exclude": ["**/node_modules/**"]
|
||||
}],
|
||||
"compileNamespace": "cjs",
|
||||
"extractBabelOptions": {},
|
||||
"compilerBabelOptions": {},
|
||||
"fallbackLocales": { default: "en"},
|
||||
"format": "po",
|
||||
"locales": ["en","es","fr","nl","zh","ja","zu"],
|
||||
"orderBy": "messageId",
|
||||
"pseudoLocale": "zu",
|
||||
"rootDir": "./src",
|
||||
"runtimeConfigModule": ["@lingui/core", "i18n"],
|
||||
"sourceLocale": "en",
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
engine-strict = true
|
||||
@@ -1,2 +0,0 @@
|
||||
build
|
||||
src/locales
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true
|
||||
}
|
||||
@@ -1,363 +0,0 @@
|
||||
# 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
|
||||
|
||||
- [Ansible AWX UI With PatternFly](#ansible-awx-ui-with-patternfly)
|
||||
- [Table of contents](#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)
|
||||
- [Forms](#forms)
|
||||
- [Working with React](#working-with-react)
|
||||
- [App structure](#app-structure)
|
||||
- [Patterns](#patterns)
|
||||
- [Bootstrapping the application (root src/ files)](#bootstrapping-the-application-root-src-files)
|
||||
- [Naming files](#naming-files)
|
||||
- [Naming components that use the context api](#naming-components-that-use-the-context-api)
|
||||
- [Class constructors vs Class properties](#class-constructors-vs-class-properties)
|
||||
- [Binding](#binding)
|
||||
- [Typechecking with PropTypes](#typechecking-with-proptypes)
|
||||
- [Custom Hooks](#custom-hooks)
|
||||
- [Naming Functions](#naming-functions)
|
||||
- [Default State Initialization](#default-state-initialization)
|
||||
- [Testing components that use contexts](#testing-components-that-use-contexts)
|
||||
- [Internationalization](#internationalization)
|
||||
- [Marking strings for translation and replacement in the UI](#marking-strings-for-translation-and-replacement-in-the-ui)
|
||||
- [Setting up .po files to give to translation team](#setting-up-po-files-to-give-to-translation-team)
|
||||
- [Marking an issue to be translated](#marking-an-issue-to-be-translated)
|
||||
|
||||
## 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
|
||||
- We use a [code formatter](https://prettier.io/). Before adding a new commit or opening a PR, please apply the formatter using `npm run prettier`
|
||||
- We adopt the following code style guide:
|
||||
- functions should adopt camelCase
|
||||
- constructors/classes should adopt PascalCase
|
||||
- constants to be exported should adopt UPPERCASE
|
||||
- For strings, we adopt the `sentence capitalization` since it is a [Patternfly style guide](https://www.patternfly.org/v4/design-guidelines/content/grammar-and-terminology#capitalization).
|
||||
|
||||
## 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 14.x LTS
|
||||
- NPM 6.x LTS
|
||||
|
||||
Run the following to install all the dependencies:
|
||||
|
||||
```bash
|
||||
(host) $ npm 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:
|
||||
|
||||
```javascript
|
||||
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:
|
||||
|
||||
```javascript
|
||||
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();
|
||||
});
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
**Test Attributes** -
|
||||
It should be noted that the `dataCy` prop, as well as its equivalent attribute `data-cy`, are used as flags for any UI test that wants to avoid relying on brittle CSS selectors such as `nth-of-type()`.
|
||||
|
||||
## 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.
|
||||
|
||||
## Forms
|
||||
|
||||
Our forms should have a known, consistent, and fully-resolved starting state before it is possible for a user, keyboard-mouse, screen reader, or automated test to interact with them. If multiple network calls are needed to populate a form, resolve them all before displaying the form or showing a content error. When multiple requests are needed to create or update the resources represented by a form, resolve them all before transitioning the ui to a success or failure state.
|
||||
|
||||
## 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.
|
||||
- **/hooks** - Custom react [hooks](https://reactjs.org/docs/hooks-custom.html)
|
||||
- **/locales** - [Internationalization](#internationalization) config and source files.
|
||||
- **/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.
|
||||
|
||||
### Patterns
|
||||
|
||||
- A **screen** shouldn't import from another screen. If a component _needs_ to be shared between two or more screens, it is a generic and should be moved to `src/components`.
|
||||
|
||||
#### 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.js**
|
||||
- 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.js**
|
||||
- Sets standard page layout, about modal, and root dialog modal.
|
||||
- **RootProvider.js**
|
||||
- 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.js`, and its tests would be defined in `FooBar.test.js`.
|
||||
|
||||
#### 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.js`.
|
||||
|
||||
**Component naming and conventions** - In order to provide a consistent interface with react-router and [lingui](https://lingui.js.org/), 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,
|
||||
};
|
||||
```
|
||||
|
||||
### Custom Hooks
|
||||
|
||||
There are currently a few custom hooks:
|
||||
|
||||
1. [useRequest](https://github.com/ansible/awx/blob/devel/awx/ui_next/src/util/useRequest.js#L21) encapsulates main actions related to requests.
|
||||
2. [useDismissableError](https://github.com/ansible/awx/blob/devel/awx/ui_next/src/util/useRequest.js#L71) provides controls for "dismissing" an error message.
|
||||
3. [useDeleteItems](https://github.com/ansible/awx/blob/devel/awx/ui_next/src/util/useRequest.js#L98) handles deletion of items from a paginated item list.
|
||||
4. [useSelected](https://github.com/ansible/awx/blob/devel/awx/ui_next/src/util/useSelected.js#L14) provides a way to read and update a selected list.
|
||||
|
||||
### 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.js`. 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:
|
||||
|
||||
```javascript
|
||||
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:
|
||||
|
||||
```javascript
|
||||
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 library 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 translated. 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 t template tag function from the @lingui/macro package.
|
||||
- wrap your string using the following format: `` t`String to be translated` ``
|
||||
|
||||
**Note:** If you have a variable string with text that needs translating, you must wrap it in `` t`${variable} string` `` where it is defined. Then you must run `npm run extract-strings` to generate new `.po` files and submit those files along with your pull request.
|
||||
|
||||
**Note:** We try to avoid the `I18n` consumer, or `i18nMark` function 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.
|
||||
|
||||
**Note:** Pluralization can be complicated so it is best to allow lingui handle cases where we have a string that may need to be pluralized based on number of items, or count. In that case lingui provides a `<Plural>` component, and a `plural()` function. When adding or updating strings in a `<Plural/>` tag you must run `npm run extra-strings` and submit the new `.po` files with your pull request. See documentation [here](https://lingui.js.org/guides/plurals.html?highlight=pluralization).
|
||||
|
||||
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. When updating strings that are used by `<Plural>` or `plural()` you will need to run this command to get the strings to render properly. This command will create `.po` files for each of the supported languages that will need to be committed with your PR.
|
||||
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.js).
|
||||
5. Change the language in your browser and reload the page. You should see your specified translations in place of English strings.
|
||||
|
||||
### Marking an issue to be translated
|
||||
|
||||
1. Issues marked with `component:I10n` should not be closed after the issue was fixed.
|
||||
2. Remove the label `state:needs_devel`.
|
||||
3. Add the label `state:pending_translations`. At this point, the translations will be batch translated by a maintainer, creating relevant entries in the PO files. Then after those translations have been merged, the issue can be closed.
|
||||
@@ -1,16 +0,0 @@
|
||||
FROM node:14
|
||||
ARG NPMRC_FILE=.npmrc
|
||||
ENV NPMRC_FILE=${NPMRC_FILE}
|
||||
ARG TARGET='https://awx:8043'
|
||||
ENV TARGET=${TARGET}
|
||||
ENV CI=true
|
||||
WORKDIR /ui_next
|
||||
ADD public public
|
||||
ADD package.json package.json
|
||||
ADD package-lock.json package-lock.json
|
||||
ADD .linguirc .linguirc
|
||||
COPY ${NPMRC_FILE} .npmrc
|
||||
RUN npm install
|
||||
ADD src src
|
||||
EXPOSE 3001
|
||||
CMD [ "npm", "start" ]
|
||||
@@ -1,115 +0,0 @@
|
||||
# AWX-PF
|
||||
|
||||
## Requirements
|
||||
- node 14.x LTS, npm 6.x LTS, make, git
|
||||
|
||||
## Development
|
||||
The API development server will need to be running. See [CONTRIBUTING.md](../../CONTRIBUTING.md).
|
||||
|
||||
```shell
|
||||
# install
|
||||
npm --prefix=awx/ui_next install
|
||||
|
||||
# Start the ui development server. While running, the ui will be reachable
|
||||
# at https://127.0.0.1:3001 and updated automatically when code changes.
|
||||
npm --prefix=awx/ui_next start
|
||||
```
|
||||
|
||||
### Build for the Development Containers
|
||||
If you just want to build a ui for the container-based awx development
|
||||
environment, use these make targets:
|
||||
|
||||
```shell
|
||||
# The ui will be reachable at https://localhost:8043 or
|
||||
# http://localhost:8013
|
||||
make ui-devel
|
||||
|
||||
# clean up
|
||||
make clean-ui
|
||||
```
|
||||
|
||||
### Using an External Server
|
||||
If you normally run awx on an external host/server (in this example, `awx.local`),
|
||||
you'll need use the `TARGET` environment variable when starting the ui development
|
||||
server:
|
||||
|
||||
```shell
|
||||
TARGET='https://awx.local:8043' npm --prefix awx/ui_next start
|
||||
```
|
||||
|
||||
## Testing
|
||||
```shell
|
||||
# run code formatting check
|
||||
npm --prefix awx/ui_next run prettier-check
|
||||
|
||||
# run lint checks
|
||||
npm --prefix awx/ui_next run lint
|
||||
|
||||
# run all unit tests
|
||||
npm --prefix awx/ui_next run test
|
||||
|
||||
# run a single test (in this case the login page test):
|
||||
npm --prefix awx/ui_next test -- src/screens/Login/Login.test.jsx
|
||||
|
||||
# start the test watcher and run tests on files that you've changed
|
||||
npm --prefix awx/ui_next run test-watch
|
||||
|
||||
# start the tests and get the coverage report after the tests have completed
|
||||
npm --prefix awx/ui_next run test -- --coverage
|
||||
```
|
||||
#### Note:
|
||||
- Once the test watcher is up and running you can hit `a` to run all the tests.
|
||||
- All commands are run on your host machine and not in the api development containers.
|
||||
|
||||
|
||||
## Updating Dependencies
|
||||
It is not uncommon to run the ui development tooling outside of the awx development
|
||||
container. That said, dependencies should always be modified from within the
|
||||
container to ensure consistency.
|
||||
|
||||
```shell
|
||||
# make sure the awx development container is running and open a shell
|
||||
docker exec -it tools_awx_1 bash
|
||||
|
||||
# start with a fresh install of the current dependencies
|
||||
(tools_awx_1)$ make clean-ui && npm --prefix=awx/ui_next ci
|
||||
|
||||
# add an exact development dependency
|
||||
(tools_awx_1)$ npm --prefix awx/ui_next install --save-dev --save-exact dev-package@1.2.3
|
||||
|
||||
# add an exact production dependency
|
||||
(tools_awx_1)$ npm --prefix awx/ui_next install --save --save-exact prod-package@1.23
|
||||
|
||||
# remove a development dependency
|
||||
(tools_awx_1)$ npm --prefix awx/ui_next uninstall --save-dev dev-package
|
||||
|
||||
# remove a production dependency
|
||||
(tools_awx_1)$ npm --prefix awx/ui_next uninstall --save prod-package
|
||||
|
||||
# exit the container
|
||||
(tools_awx_1)$ exit
|
||||
|
||||
# add the updated package.json and package-lock.json files to scm
|
||||
git add awx/ui_next_next/package.json awx/ui_next_next/package-lock.json
|
||||
```
|
||||
#### Note:
|
||||
- Building the ui can use up a lot of resources. If you're running docker for mac or similar
|
||||
virtualization, the default memory limit may not be enough and you should increase it.
|
||||
|
||||
## Building for Production
|
||||
```shell
|
||||
# built files are placed in awx/ui_next/build
|
||||
npm --prefix awx/ui_next run build
|
||||
```
|
||||
|
||||
## CI Container
|
||||
|
||||
To run:
|
||||
|
||||
```shell
|
||||
cd awx/awx/ui_next
|
||||
docker build -t awx-ui-next .
|
||||
docker run --name tools_ui_next_1 --network tools_default --link 'tools_awx_1:awx' -e TARGET="https://awx:8043" -p '3001:3001' --rm -v $(pwd)/src:/ui_next/src awx-ui-next
|
||||
```
|
||||
|
||||
**Note:** This is for CI, test systems, zuul, etc. For local development, see [usage](https://github.com/ansible/awx/blob/devel/awx/ui_next/README.md#Development)
|
||||
@@ -1,389 +0,0 @@
|
||||
# Simple Search
|
||||
|
||||
## UX Considerations
|
||||
|
||||
Historically, the code that powers search in the AngularJS version of the AWX UI is very complex and prone to bugs. In order to reduce that complexity, we've made some UX decisions to help make the code easier to maintain.
|
||||
|
||||
**ALL query params namespaced and in url bar**
|
||||
|
||||
This includes lists that aren't necessarily hyperlinked, like lookup lists. The reason behind this is so we can treat the url bar as the source of truth for queries always. Any params that have both a key AND value that is in the defaultParams section of the qs config are stripped out of the search string (see "Encoding for UI vs. API" for more info on this point)
|
||||
|
||||
**Django fuzzy search (`?search=`) is not accessible outside of "advanced search"**
|
||||
|
||||
In current smart search typing a term with no key utilizes `?search=` i.e. for "foo" tag, `?search=foo` is given. `?search=` looks on a static list of field name "guesses" (such as name, description, etc.), as well as specific fields as defined for each endpoint (for example, the events endpoint looks for a "stdout" field as well). Due to the fact a key will always be present on the left-hand of simple search, it doesn't make sense to use `?search=` as the default.
|
||||
|
||||
We may allow passing of `?search=` through our future advanced search interface. Some details that were gathered in planning phases about `?search=` that might be helpful in the future:
|
||||
- `?search=` tags are OR'd together (union is returned).
|
||||
- `?search=foo&name=bar` returns items that have a name field of bar (not case insensitive) AND some text field with foo on it
|
||||
- `?search=foo&search=bar&name=baz` returns (foo in name OR foo in description OR ...) AND (bar in name OR bar in description OR ...) AND (baz in name)
|
||||
- similarly `?related__search=` looks on the static list of "guesses" for models related to the endpoint. The specific fields are not "searched" for `?related__search=`.
|
||||
- `?related__search=` not currently used in awx ui
|
||||
|
||||
**A note on clicking a tag to putting it back into the search bar**
|
||||
|
||||
This was brought up as a nice to have when we were discussing our initial implementation of search in the new application. Since there isn't a way we would be able to know if the user created the tag from the simple or advanced search interface, we wouldn't know where to put it back. This breaks our idea of using the query params as the exclusive source of truth, so we've decided against implementing it for now.
|
||||
|
||||
## Tasklist
|
||||
|
||||
### DONE
|
||||
|
||||
- DONE update handleSearch to follow handleSort param
|
||||
- DONE update qsConfig columns to utilize isSearchable bool (just like isSortable bool)
|
||||
- DONE enter keydown in text search bar to search
|
||||
- DONE get decoded params and write test
|
||||
- DONE make list header component
|
||||
- DONE make filter component
|
||||
- DONE make filters show up for empty list
|
||||
- DONE make clear all button
|
||||
- DONE styling of FilterTags component
|
||||
- DONE clear out text input after tag has been made
|
||||
- DONE deal with duplicate key tags being added/removed in qs util file
|
||||
- DONE deal with widgetry changing between one dropdown option to the left of search and many
|
||||
- DONE bug: figure out why ?name=org returning just org not “org 2”
|
||||
- DONE update contrib file to have the first section with updated text as is in this pr description.
|
||||
- DONE rebase with latest awx-pf changes
|
||||
- DONE styling of search bar
|
||||
- DONE make filter and list header tests
|
||||
- DONE change api paramsSerializer to handle duplicate key stuff
|
||||
- DONE update qs update function to be smaller, simple param functions, as opposed to one big one with a lot of params
|
||||
- DONE add search filter removal test for qs.
|
||||
- DONE remove button for search tags of duplicate keys are broken, fix that
|
||||
|
||||
### TODO pre-holiday break
|
||||
- Update COLUMNS to SORT_COLUMNS and SEARCH_COLUMNS
|
||||
- Update to using new PF Toolbar component (currently an experimental component)
|
||||
- Change the right-hand input based on the type of key selected on the left-hand side. In addition to text input, for our MVP we will support:
|
||||
- number input
|
||||
- select input (multiple-choice configured from UI or Options)
|
||||
- Update the following lists to have the following keys:
|
||||
|
||||
**Jobs list** (signed off earlier in chat)
|
||||
- Name (which is also the name of the job template) - search is ?name=jt
|
||||
- Job ID - search is ?id=13
|
||||
- Label name - search is ?labels__name=foo
|
||||
- Job type (dropdown on right with the different types) ?type = job
|
||||
- Created by (username) - search is ?created_by__username=admin
|
||||
- Status - search (dropdown on right with different statuses) is ?status=successful
|
||||
|
||||
Instances of jobs list include:
|
||||
- Jobs list
|
||||
- Host completed jobs list
|
||||
- JT completed jobs list
|
||||
|
||||
**Organization list**
|
||||
- Name - search is ?name=org
|
||||
- ? Team name (of a team in the org) - search is ?teams__name=ansible
|
||||
- ? Username (of a user in the org) - search is ?users__username=johndoe
|
||||
|
||||
Instances of orgs list include:
|
||||
- Orgs list
|
||||
- User orgs list
|
||||
- Lookup on Project
|
||||
- Lookup on Credential
|
||||
- Lookup on Inventory
|
||||
- User access add wizard list
|
||||
- Team access add wizard list
|
||||
|
||||
**Instance Groups list**
|
||||
- Name - search is ?name=ig
|
||||
- ? is_container_group boolean choice (doesn't work right now in API but will soon) - search is ?is_container_group=true
|
||||
- ? credential name - search is ?credentials__name=kubey
|
||||
|
||||
Instance of instance groups list include:
|
||||
- Lookup on Org
|
||||
- Lookup on JT
|
||||
- Lookup on Inventory
|
||||
|
||||
**Users list**
|
||||
- Username - search is ?username=johndoe
|
||||
- First Name - search is ?first_name=John
|
||||
- Last Name - search is ?last_name=Doe
|
||||
- ? (if not superfluous, would not include on Team users list) Team Name - search is ?teams__name=team_of_john_does (note API issue: User has no field named "teams")
|
||||
- ? (only for access or permissions list) Role Name - search is ?roles__name=Admin (note API issue: Role has no field "name")
|
||||
- ? (if not superfluous, would not include on Organization users list) ORg Name - search is ?organizations__name=org_of_jhn_does
|
||||
|
||||
Instance of user lists include:
|
||||
- User list
|
||||
- Org user list
|
||||
- Access list for Org, JT, Project, Credential, Inventory, User and Team
|
||||
- Access list for JT
|
||||
- Access list Project
|
||||
- Access list for Credential
|
||||
- Access list for Inventory
|
||||
- Access list for User
|
||||
- Access list for Team
|
||||
- Team add users list
|
||||
- Users list in access wizard (to add new roles for a particular list) for Org
|
||||
- Users list in access wizard (to add new roles for a particular list) for JT
|
||||
- Users list in access wizard (to add new roles for a particular list) for Project
|
||||
- Users list in access wizard (to add new roles for a particular list) for Credential
|
||||
- Users list in access wizard (to add new roles for a particular list) for Inventory
|
||||
|
||||
**Teams list**
|
||||
- Name - search is ?name=teamname
|
||||
- ? Username (of a user in the team) - search is ?users__username=johndoe
|
||||
- ? (if not superfluous, would not include on Organizations teams list) Org Name - search is ?organizations__name=org_of_john_does
|
||||
|
||||
Instance of team lists include:
|
||||
- Team list
|
||||
- Org team list
|
||||
- User team list
|
||||
- Team list in access wizard (to add new roles for a particular list) for Org
|
||||
- Team list in access wizard (to add new roles for a particular list) for JT
|
||||
- Team list in access wizard (to add new roles for a particular list) for Project
|
||||
- Team list in access wizard (to add new roles for a particular list) for Credential
|
||||
- Team list in access wizard (to add new roles for a particular list) for Inventory
|
||||
|
||||
**Credentials list**
|
||||
- Name
|
||||
- ? Type (dropdown on right with different types)
|
||||
- ? Created by (username)
|
||||
- ? Modified by (username)
|
||||
|
||||
Instance of credential lists include:
|
||||
- Credential list
|
||||
- Lookup for JT
|
||||
- Lookup for Project
|
||||
- User access add wizard list
|
||||
- Team access add wizard list
|
||||
|
||||
**Projects list**
|
||||
- Name - search is ?name=proj
|
||||
- ? Type (dropdown on right with different types) - search is scm_type=git
|
||||
- ? SCM URL - search is ?scm_url=github.com/ansible/test-playbooks
|
||||
- ? Created by (username) - search is ?created_by__username=admin
|
||||
- ? Modified by (username) - search is ?modified_by__username=admin
|
||||
|
||||
Instance of project lists include:
|
||||
- Project list
|
||||
- Lookup for JT
|
||||
- User access add wizard list
|
||||
- Team access add wizard list
|
||||
|
||||
**Templates list**
|
||||
- Name - search is ?name=cleanup
|
||||
- ? Type (dropdown on right with different types) - search is ?type=playbook_run
|
||||
- ? Playbook name - search is ?job_template__playbook=debug.yml
|
||||
- ? Created by (username) - search is ?created_by__username=admin
|
||||
- ? Modified by (username) - search is ?modified_by__username=admin
|
||||
|
||||
Instance of template lists include:
|
||||
- Template list
|
||||
- Project Templates list
|
||||
|
||||
**Inventories list**
|
||||
- Name - search is ?name=inv
|
||||
- ? Created by (username) - search is ?created_by__username=admin
|
||||
- ? Modified by (username) - search is ?modified_by__username=admin
|
||||
|
||||
Instance of inventory lists include:
|
||||
- Inventory list
|
||||
- Lookup for JT
|
||||
- User access add wizard list
|
||||
- Team access add wizard list
|
||||
|
||||
**Groups list**
|
||||
- Name - search is ?name=group_name
|
||||
- ? Created by (username) - search is ?created_by__username=admin
|
||||
- ? Modified by (username) - search is ?modified_by__username=admin
|
||||
|
||||
Instance of group lists include:
|
||||
- Group list
|
||||
|
||||
**Hosts list**
|
||||
- Name - search is ?name=hostname
|
||||
- ? Created by (username) - search is ?created_by__username=admin
|
||||
- ? Modified by (username) - search is ?modified_by__username=admin
|
||||
|
||||
Instance of host lists include:
|
||||
- Host list
|
||||
|
||||
**Notifications list**
|
||||
- Name - search is ?name=notification_template_name
|
||||
- ? Type (dropdown on right with different types) - search is ?type=slack
|
||||
- ? Created by (username) - search is ?created_by__username=admin
|
||||
- ? Modified by (username) - search is ?modified_by__username=admin
|
||||
|
||||
Instance of notification lists include:
|
||||
- Org notification list
|
||||
- JT notification list
|
||||
- Project notification list
|
||||
|
||||
### TODO backlog
|
||||
- Change the right-hand input based on the type of key selected on the left-hand side. We will eventually want to support:
|
||||
- lookup input (selection of particular resources, based on API list endpoints)
|
||||
- date picker input
|
||||
- Update the following lists to have the following keys:
|
||||
- Update all __name and __username related field search-based keys to be type-ahead lookup based searches
|
||||
|
||||
## Code Details
|
||||
|
||||
### Search component
|
||||
|
||||
The component looks like this:
|
||||
|
||||
```
|
||||
<Search
|
||||
qsConfig={qsConfig}
|
||||
columns={columns}
|
||||
onSearch={onSearch}
|
||||
/>
|
||||
```
|
||||
|
||||
**qsConfig** is used to get namespace so that multiple lists can be on the page. When tags are modified they append namespace to query params. The qsConfig is also used to get "type" of fields in order to correctly parse values as int or date as it is translating.
|
||||
|
||||
**columns** are passed as an array, as defined in the screen where the list is located. You pass a bool `isDefault` to indicate that should be the key that shows up in the left-hand dropdown as default in the UI. If you don't pass any columns, a default of `isDefault=true` will be added to a name column, which is nearly universally shared throughout the models of awx.
|
||||
|
||||
There is a type attribute that can be `'string'`, `'number'` or `'choice'` (and in the future, `'date'` and `'lookup'`), which will change the type of input on the right-hand side of the search bar. For a key that has a set number of choices, you will pass a choices attribute, which is an array in the format choices: [{label: 'Foo', value: 'foo'}]
|
||||
|
||||
**onSearch** calls the `mergeParams` qs util in order to add new tags to the queryset. mergeParams is used so that we can support duplicate keys (see mergeParams vs. replaceParams for more info).
|
||||
|
||||
### ListHeader component
|
||||
|
||||
`DataListToolbar`, `EmptyListControls`, and `FilterTags` components were created or moved to a new sub-component of `PaginatedDataList`, `ListHeader`. This allowed us to consolidate the logic between both lists with data (which need to show search, sort, any search tags currently active, and actions) as well as empty lists (which need to show search tags currently active so they can be removed, potentially getting you back to a "list-has-data" state, as well as a subset of options still valid, such as "add").
|
||||
|
||||
The ability to search and remove filters, as well as sort the list is handled through callbacks which are passed from functions defined in `ListHeader`. These are the following:
|
||||
|
||||
- `handleSort(key, direction)` - use key and direction of sort to change the order_by value in the queryset
|
||||
- `handleSearch(key, value)` - use key and value to push a new value to the param
|
||||
- `handleRemove(key, value)` - use key and value to remove a value to the param
|
||||
- `handleRemoveAll()` - remove all non-default params
|
||||
|
||||
All of these functions act on the react-router history using the `pushHistoryState` function. This causes the query params in the url to update, which in turn triggers change handlers that will re-fetch data for the lists.
|
||||
|
||||
**a note on sort_columns and search_columns**
|
||||
|
||||
We have split out column configuration into separate search and sort column array props--these are passed to the search and sort columns. Both accept an isDefault prop for one of the items in the array to be the default option selected when going to the page. Sort column items can pass an isNumeric boolean in order to chnage the iconography of the sort UI element. Search column items can pass type and if applicable choices, in order to configure the right-hand side of the search bar.
|
||||
|
||||
### FilterTags component
|
||||
|
||||
Similar to the way the list grabs data based on changes to the react-router params, the `FilterTags` component updates when new params are added. This component is a fairly straight-forward map (only slightly complex, because it needed to do a nested map over any values with duplicate keys that were represented by an inner-array). Both key and value are displayed for the tag.
|
||||
|
||||
### qs utility
|
||||
|
||||
The qs (queryset) utility is used to make the search speak the language of the REST API. The main functions of the utilities are to:
|
||||
- add, replace and remove filters
|
||||
- translate filters as url params (for linking and maintaining state), in-memory representation (as JS objects), and params that Django REST Framework understands.
|
||||
|
||||
More info in the below sections:
|
||||
|
||||
#### Encoding for UI vs. API
|
||||
|
||||
For the UI url params, we want to only encode those params that aren't defaults, as the default behavior was defined through configuration and we don't need these in the url as a source of truth. For the API, we need to pass these params so that they are taken into account when the response is built.
|
||||
|
||||
#### mergeParams vs. replaceParams
|
||||
|
||||
**mergeParams** is used to suppport putting values with the same key
|
||||
|
||||
From a UX perspective, we wanted to be able to support searching on the same key multiple times (i.e. searching for things like `?foo=bar&foo=baz`). We do this by creating an array of all values. i.e.:
|
||||
|
||||
```
|
||||
{
|
||||
foo: ['bar', 'baz']
|
||||
}
|
||||
```
|
||||
|
||||
Concatenating terms in this way gives you the intersection of both terms (i.e. foo must be "bar" and "baz"). This is helpful for the most-common type of searching, substring (`__icontains`) searches. This will increase filtering, allowing the user to drill-down into the list as terms are added.
|
||||
|
||||
**replaceParams** is used to support sorting, setting page_size, etc. These params only allow one choice, and we need to replace a particular key's value if one is passed.
|
||||
|
||||
#### Working with REST API
|
||||
|
||||
The REST API is coupled with the qs util through the `paramsSerializer`, due to the fact we need axios to support the array for duplicate key values in the object representation of the params to pass to the get request. This is done where axios is configured in the Base.js file, so all requests and request types should support our array syntax for duplicate keys automatically.
|
||||
|
||||
# Advanced Search - this section is a mess, update eventually
|
||||
|
||||
**a note on typing in a smart search query**
|
||||
|
||||
In order to not support a special "language" or "syntax" for crafting the query like we have now (and is the cause of a large amount of bugs), we will not support the old way of typing in a filter like in the current implementation of search.
|
||||
|
||||
Since all search bars are represented in the url, for users who want to input a string to filter results in a single step, typing directly in the url to achieve the filter is acceptable.
|
||||
|
||||
# Advanced search notes
|
||||
|
||||
Current thinking is Advanced Search will be post-3.6, or at least late 3.6 after awx features and "simple search" with the left dropdown and right input for the above phase 1 lists.
|
||||
|
||||
That being said, we want to plan it out so we make sure the infrastructure of how we set up adding/removing tags, what shows up in the url bar, etc. all doesn't have to be redone.
|
||||
|
||||
Users will get to advanced search with a button to the right of search bar. When selected type-ahead key thing opens, left dropdown of search bar goes away, and x is given to get back to regular search (this is in the mockups)
|
||||
|
||||
It is okay to only make this typing representation available initially (i.e. they start doing stuff with the type-ahead and the phases, no more typing in to make a query that way).
|
||||
|
||||
when you click through or type in the search bar for the various phases of crafting the query ("not", "related resource project", "related resource key name", "value foo") which might be represented in the top bar as a series of tags that can be added and removed before submitting the tag.
|
||||
|
||||
We will try to form options data from a static file. Because options data is static, we may be able to generate and store as a static file of some sort (that we can use for managing smart search). Alan had ideas around this. If we do this it will mean we don't have to make a ton of requests as we craft smart search filters. It sounds like the cli may start using something similar.
|
||||
|
||||
## Smart search flow
|
||||
|
||||
Smart search will be able to craft the tag through various states. Note that the phases don't necessarily need to be completed in sequential order.
|
||||
|
||||
PHASE 1: prefix operators
|
||||
|
||||
**TODO: Double check there's no reason we need to include or__ and chain__ and can just do not__**
|
||||
|
||||
- not__
|
||||
- or__
|
||||
- chain__
|
||||
|
||||
how these work:
|
||||
|
||||
To exclude results matching certain criteria, prefix the field parameter with not__:
|
||||
|
||||
?not__field=value
|
||||
By default, all query string filters are AND'ed together, so only the results matching all filters will be returned. To combine results matching any one of multiple criteria, prefix each query string parameter with or__:
|
||||
|
||||
?or__field=value&or__field=othervalue
|
||||
?or__not__field=value&or__field=othervalue
|
||||
(Added in Ansible Tower 1.4.5) The default AND filtering applies all filters simultaneously to each related object being filtered across database relationships. The chain filter instead applies filters separately for each related object. To use, prefix the query string parameter with chain__:
|
||||
|
||||
?chain__related__field=value&chain__related__field2=othervalue
|
||||
?chain__not__related__field=value&chain__related__field2=othervalue
|
||||
If the first query above were written as ?related__field=value&related__field2=othervalue, it would return only the primary objects where the same related object satisfied both conditions. As written using the chain filter, it would return the intersection of primary objects matching each condition.
|
||||
|
||||
PHASE 2: related fields, given by array, where __search is appended to them, i.e.
|
||||
|
||||
```
|
||||
"related_search_fields": [
|
||||
"credentials__search",
|
||||
"labels__search",
|
||||
"created_by__search",
|
||||
"modified_by__search",
|
||||
"notification_templates__search",
|
||||
"custom_inventory_scripts__search",
|
||||
"notification_templates_error__search",
|
||||
"notification_templates_success__search",
|
||||
"notification_templates_any__search",
|
||||
"teams__search",
|
||||
"projects__search",
|
||||
"inventories__search",
|
||||
"applications__search",
|
||||
"workflows__search",
|
||||
"instance_groups__search"
|
||||
],
|
||||
```
|
||||
|
||||
PHASE 3: keys, give by object key names for data.actions.GET
|
||||
- type is given for each key which we could use to help craft the value
|
||||
|
||||
PHASE 4: after key postfix operators can be
|
||||
|
||||
**TODO: will need to figure out which ones we support**
|
||||
|
||||
- exact: Exact match (default lookup if not specified).
|
||||
- iexact: Case-insensitive version of exact.
|
||||
- contains: Field contains value.
|
||||
- icontains: Case-insensitive version of contains.
|
||||
- startswith: Field starts with value.
|
||||
- istartswith: Case-insensitive version of startswith.
|
||||
- endswith: Field ends with value.
|
||||
- iendswith: Case-insensitive version of endswith.
|
||||
- regex: Field matches the given regular expression.
|
||||
- iregex: Case-insensitive version of regex.
|
||||
- gt: Greater than comparison.
|
||||
- gte: Greater than or equal to comparison.
|
||||
- lt: Less than comparison.
|
||||
- lte: Less than or equal to comparison.
|
||||
- isnull: Check whether the given field or related object is null; expects a boolean value.
|
||||
- in: Check whether the given field's value is present in the list provided; expects a list of items.
|
||||
|
||||
PHASE 5: The value. Based on options, we can give hints or validation based on type of value (like number fields don't accept "foo" or whatever)
|
||||
@@ -1,9 +0,0 @@
|
||||
# Django
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
class UINextConfig(AppConfig):
|
||||
|
||||
name = 'awx.ui_next'
|
||||
verbose_name = _('UI_Next')
|
||||
@@ -1,27 +0,0 @@
|
||||
# Application Architecture
|
||||
|
||||
## Local Storage Integration
|
||||
The `useStorage` hook integrates with the browser's localStorage api.
|
||||
It accepts a localStorage key as its only argument and returns a state
|
||||
variable and setter function for that state variable. The hook enables
|
||||
bidirectional data transfer between tabs via an event listener that
|
||||
is registered with the Web Storage api.
|
||||
|
||||
|
||||

|
||||
|
||||
The `useStorage` hook currently lives in the `AppContainer` component. It
|
||||
can be relocated to a more general location should and if the need
|
||||
ever arise
|
||||
|
||||
## Session Expiration
|
||||
Session timeout state is communicated to the client in the HTTP(S)
|
||||
response headers. Every HTTP(S) response is intercepted to read the
|
||||
session expiration time before being passed into the rest of the
|
||||
application. A timeout date is computed from the intercepted HTTP(S)
|
||||
headers and is pushed into local storage, where it can be read using
|
||||
standard Web Storage apis or other utilities, such as `useStorage`.
|
||||
|
||||
|
||||

|
||||
|
||||
|
Before Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 57 KiB |
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src"
|
||||
}
|
||||
}
|
||||
47982
awx/ui_next/package-lock.json
generated
@@ -1,107 +0,0 @@
|
||||
{
|
||||
"name": "ui_next",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": "14.x"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lingui/react": "3.9.0",
|
||||
"@patternfly/patternfly": "^4.102.1",
|
||||
"@patternfly/react-core": "4.121.1",
|
||||
"@patternfly/react-icons": "4.7.22",
|
||||
"@patternfly/react-table": "^4.19.15",
|
||||
"ace-builds": "^1.4.12",
|
||||
"ansi-to-html": "0.7.0",
|
||||
"axios": "^0.21.1",
|
||||
"babel-plugin-macros": "^3.0.1",
|
||||
"codemirror": "^5.47.0",
|
||||
"d3": "6.7.0",
|
||||
"dagre": "^0.8.4",
|
||||
"formik": "^2.1.2",
|
||||
"has-ansi": "4.0.0",
|
||||
"html-entities": "2.3.2",
|
||||
"js-yaml": "^3.13.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.13.1",
|
||||
"react-ace": "^9.3.0",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-error-boundary": "^3.1.3",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-virtualized": "^9.21.1",
|
||||
"rrule": "^2.6.4",
|
||||
"sanitize-html": "2.4.0",
|
||||
"styled-components": "5.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/polyfill": "^7.8.7",
|
||||
"@cypress/instrument-cra": "^1.4.0",
|
||||
"@lingui/cli": "^3.7.1",
|
||||
"@lingui/loader": "^3.8.3",
|
||||
"@lingui/macro": "^3.7.1",
|
||||
"@nteract/mockument": "^1.0.4",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"enzyme": "^3.10.0",
|
||||
"enzyme-adapter-react-16": "^1.14.0",
|
||||
"enzyme-to-json": "^3.3.5",
|
||||
"eslint": "7.30.0",
|
||||
"eslint-config-airbnb": "18.2.1",
|
||||
"eslint-config-prettier": "8.3.0",
|
||||
"eslint-import-resolver-webpack": "0.11.1",
|
||||
"eslint-plugin-i18next": "^5.0.0",
|
||||
"eslint-plugin-import": "^2.14.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-react": "^7.11.1",
|
||||
"eslint-plugin-react-hooks": "4.2.0",
|
||||
"http-proxy-middleware": "^1.0.3",
|
||||
"jest-websocket-mock": "^2.0.2",
|
||||
"mock-socket": "^9.0.3",
|
||||
"prettier": "2.3.2",
|
||||
"react-scripts": "^4.0.3"
|
||||
},
|
||||
"scripts": {
|
||||
"prelint": "lingui compile",
|
||||
"prestart": "lingui compile",
|
||||
"prestart-instrumented": "lingui compile",
|
||||
"pretest": "lingui compile",
|
||||
"pretest-watch": "lingui compile",
|
||||
"start": "ESLINT_NO_DEV_ERRORS=true PORT=3001 HTTPS=true DANGEROUSLY_DISABLE_HOST_CHECK=true react-scripts start",
|
||||
"start-instrumented": "ESLINT_NO_DEV_ERRORS=true DEBUG=instrument-cra PORT=3001 HTTPS=true DANGEROUSLY_DISABLE_HOST_CHECK=true react-scripts -r @cypress/instrument-cra start",
|
||||
"build": "INLINE_RUNTIME_CHUNK=false react-scripts build",
|
||||
"test": "TZ='UTC' react-scripts test --watchAll=false",
|
||||
"test-watch": "TZ='UTC' react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"lint": "eslint --ext .js --ext .jsx .",
|
||||
"add-locale": "lingui add-locale",
|
||||
"extract-strings": "lingui extract",
|
||||
"extract-template": "lingui extract-template",
|
||||
"compile-strings": "lingui compile",
|
||||
"prettier": "prettier --write \"src/**/*.{js,jsx,scss}\"",
|
||||
"prettier-check": "prettier --check \"src/**/*.{js,jsx,scss}\""
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"jest": {
|
||||
"snapshotSerializers": [
|
||||
"enzyme-to-json/serializer"
|
||||
],
|
||||
"collectCoverageFrom": [
|
||||
"src/**/*.{js,jsx}",
|
||||
"testUtils/**/*.{js,jsx}"
|
||||
],
|
||||
"coveragePathIgnorePatterns": [
|
||||
"<rootDir>/src/locales",
|
||||
"index.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
<!-- There's multiple layers of templating in this file:
|
||||
|
||||
* "< ... >" with % symbols are ejs templates used by react-scripts at build time. These
|
||||
templates are mainly used to check whether or not we're building a ui for production
|
||||
versus one that will be sent from the ui dev server. Since this type of template is
|
||||
applied at build time, it can be used to conditionally render the others.
|
||||
|
||||
* "% ... %" are templates used by the react-scripts dev server when serving the ui from
|
||||
port 3001. These are applied at runtime and only work for development mode.
|
||||
|
||||
* "{ ... }" with % symbols and "{{ ... }}" are django templates that only run for
|
||||
production builds (e.g port 8043) when serving the ui from a webserver.
|
||||
|
||||
-->
|
||||
<% if (process.env.NODE_ENV === 'production') { %>
|
||||
{% load static %}
|
||||
<% } %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<% if (process.env.NODE_ENV === 'production') { %>
|
||||
<script nonce="{{ csp_nonce }}" type="text/javascript">
|
||||
window.NONCE_ID = '{{ csp_nonce }}';
|
||||
</script>
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; connect-src 'self' ws: wss:; style-src 'self' 'unsafe-inline'; script-src 'self' 'nonce-{{ csp_nonce }}' *.pendo.io; img-src 'self' *.pendo.io data:;"
|
||||
/>
|
||||
<link rel="shortcut icon" href="{% static 'media/favicon.ico' %}" />
|
||||
<% } else { %>
|
||||
<link rel="shortcut icon" href="%PUBLIC_URL%/static/media/favicon.ico" />
|
||||
<% } %>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="AWX"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<% if (process.env.NODE_ENV === 'production') { %>
|
||||
<style nonce="{{ csp_nonce }}">.app{height: 100%;}</style><div id="app" class="app"></div>
|
||||
<% } else { %>
|
||||
<div id="app" style="height: 100%"></div>
|
||||
<% } %>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,40 +0,0 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<title data-cy="migration-title">{{ title }}</title>
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; connect-src 'self' ws: wss:; style-src 'self' 'unsafe-inline'; script-src 'self' 'nonce-{{ csp_nonce }}' *.pendo.io; img-src 'self' *.pendo.io data:;"
|
||||
/>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link href="{% static 'css/fonts/assets/RedHatDisplay/RedHatDisplay-Medium.woff' %}" rel="stylesheet" type="application/font-woff" media="all"/>
|
||||
<link href="{% static 'css/fonts/assets/RedHatText/RedHatText-Regular.woff' %}" rel="stylesheet" type="application/font-woff" media="all"/>
|
||||
<link href="{% static 'css/patternfly.min.css' %}" rel="stylesheet" type="text/css" media="all"/>
|
||||
<script nonce="{{ csp_nonce }}">
|
||||
setInterval(function() {
|
||||
window.location = '/';
|
||||
}, 10000);
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="pf-l-bullseye pf-m-gutter">
|
||||
<div class="pf-l-bullseye__item">
|
||||
<div class="pf-l-bullseye">
|
||||
<img src="{% static 'media/logo-black.svg' %}" width="300px" alt={{image_alt}} />
|
||||
</div>
|
||||
<div class="pf-l-bullseye">
|
||||
<span class="pf-c-spinner" role="progressbar" aria-valuetext={{aria_spinner}}>
|
||||
<span class="pf-c-spinner__clipper"></span>
|
||||
<span class="pf-c-spinner__lead-ball"></span>
|
||||
<span class="pf-c-spinner__tail-ball"></span>
|
||||
</span>
|
||||
</div>
|
||||
<h2 data-cy="migration-message-upgrade" class="pf-l-bullseye pf-c-title pf-m-2xl ws-heading ws-title ws-h2">{{message_upgrade}}</h2>
|
||||
<h2 class="pf-l-bullseye pf-c-title pf-m-2xl ws-heading ws-title ws-h2">{{message_refresh}}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"BRAND_NAME": "Ansible AWX",
|
||||
"COMPONENT_NAME": "",
|
||||
"PENDO_API_KEY": ""
|
||||
}
|
||||
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 76 KiB |
@@ -1,232 +0,0 @@
|
||||
<?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>
|
||||
|
Before Width: | Height: | Size: 12 KiB |
@@ -1,232 +0,0 @@
|
||||
<?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>
|
||||
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 270 KiB |
|
Before Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 191 KiB |
|
Before Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 354 KiB |
@@ -1,182 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
useRouteMatch,
|
||||
useLocation,
|
||||
HashRouter,
|
||||
Route,
|
||||
Switch,
|
||||
Redirect,
|
||||
useHistory,
|
||||
} from 'react-router-dom';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { I18nProvider } from '@lingui/react';
|
||||
import { i18n } from '@lingui/core';
|
||||
import { Card, PageSection } from '@patternfly/react-core';
|
||||
import {
|
||||
ConfigProvider,
|
||||
useAuthorizedPath,
|
||||
useUserProfile,
|
||||
} from 'contexts/Config';
|
||||
import { SessionProvider, useSession } from 'contexts/Session';
|
||||
import AppContainer from 'components/AppContainer';
|
||||
import Background from 'components/Background';
|
||||
import ContentError from 'components/ContentError';
|
||||
import NotFound from 'screens/NotFound';
|
||||
import Login from 'screens/Login';
|
||||
import { isAuthenticated } from 'util/auth';
|
||||
import { getLanguageWithoutRegionCode } from 'util/language';
|
||||
import Metrics from 'screens/Metrics';
|
||||
import SubscriptionEdit from 'screens/Setting/Subscription/SubscriptionEdit';
|
||||
import { RootAPI } from 'api';
|
||||
import { dynamicActivate, locales } from './i18nLoader';
|
||||
import getRouteConfig from './routeConfig';
|
||||
import { SESSION_REDIRECT_URL } from './constants';
|
||||
|
||||
function ErrorFallback({ error }) {
|
||||
return (
|
||||
<PageSection>
|
||||
<Card>
|
||||
<ContentError error={error} />
|
||||
</Card>
|
||||
</PageSection>
|
||||
);
|
||||
}
|
||||
|
||||
const RenderAppContainer = () => {
|
||||
const userProfile = useUserProfile();
|
||||
const navRouteConfig = getRouteConfig(userProfile);
|
||||
|
||||
return (
|
||||
<AppContainer navRouteConfig={navRouteConfig}>
|
||||
<AuthorizedRoutes routeConfig={navRouteConfig} />
|
||||
</AppContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const AuthorizedRoutes = ({ routeConfig }) => {
|
||||
const isAuthorized = useAuthorizedPath();
|
||||
const match = useRouteMatch();
|
||||
|
||||
if (!isAuthorized) {
|
||||
return (
|
||||
<Switch>
|
||||
<ProtectedRoute
|
||||
key="/subscription_management"
|
||||
path="/subscription_management"
|
||||
>
|
||||
<PageSection>
|
||||
<Card>
|
||||
<SubscriptionEdit />
|
||||
</Card>
|
||||
</PageSection>
|
||||
</ProtectedRoute>
|
||||
<Route path="*">
|
||||
<Redirect to="/subscription_management" />
|
||||
</Route>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
{routeConfig
|
||||
.flatMap(({ routes }) => routes)
|
||||
.map(({ path, screen: Screen }) => (
|
||||
<ProtectedRoute key={path} path={path}>
|
||||
<Screen match={match} />
|
||||
</ProtectedRoute>
|
||||
))
|
||||
.concat(
|
||||
<ProtectedRoute key="metrics" path="/metrics">
|
||||
<Metrics />
|
||||
</ProtectedRoute>,
|
||||
<ProtectedRoute key="not-found" path="*">
|
||||
<NotFound />
|
||||
</ProtectedRoute>
|
||||
)}
|
||||
</Switch>
|
||||
);
|
||||
};
|
||||
|
||||
const ProtectedRoute = ({ children, ...rest }) => {
|
||||
const { authRedirectTo, setAuthRedirectTo } = useSession();
|
||||
const { pathname } = useLocation();
|
||||
|
||||
if (isAuthenticated(document.cookie)) {
|
||||
return (
|
||||
<Route {...rest}>
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||
{children}
|
||||
</ErrorBoundary>
|
||||
</Route>
|
||||
);
|
||||
}
|
||||
|
||||
setAuthRedirectTo(authRedirectTo === '/logout' ? '/' : pathname);
|
||||
|
||||
return <Redirect to="/login" />;
|
||||
};
|
||||
|
||||
function App() {
|
||||
const history = useHistory();
|
||||
const { hash, search, pathname } = useLocation();
|
||||
let language = getLanguageWithoutRegionCode(navigator);
|
||||
if (!Object.keys(locales).includes(language)) {
|
||||
// If there isn't a string catalog available for the browser's
|
||||
// preferred language, default to one that has strings.
|
||||
language = 'en';
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
dynamicActivate(language);
|
||||
}, [language]);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchBrandName() {
|
||||
const {
|
||||
data: { BRAND_NAME },
|
||||
} = await RootAPI.readAssetVariables();
|
||||
|
||||
document.title = BRAND_NAME;
|
||||
}
|
||||
fetchBrandName();
|
||||
}, []);
|
||||
|
||||
const redirectURL = window.sessionStorage.getItem(SESSION_REDIRECT_URL);
|
||||
if (redirectURL) {
|
||||
window.sessionStorage.removeItem(SESSION_REDIRECT_URL);
|
||||
if (redirectURL !== '/' || redirectURL !== '/home')
|
||||
history.replace(redirectURL);
|
||||
}
|
||||
|
||||
return (
|
||||
<I18nProvider i18n={i18n}>
|
||||
<Background>
|
||||
<SessionProvider>
|
||||
<Switch>
|
||||
<Route exact strict path="/*/">
|
||||
<Redirect to={`${pathname.slice(0, -1)}${search}${hash}`} />
|
||||
</Route>
|
||||
<Route path="/login">
|
||||
<Login isAuthenticated={isAuthenticated} />
|
||||
</Route>
|
||||
<Route exact path="/">
|
||||
<Redirect to="/home" />
|
||||
</Route>
|
||||
<ProtectedRoute>
|
||||
<ConfigProvider>
|
||||
<RenderAppContainer />
|
||||
</ConfigProvider>
|
||||
</ProtectedRoute>
|
||||
</Switch>
|
||||
</SessionProvider>
|
||||
</Background>
|
||||
</I18nProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default () => (
|
||||
<HashRouter>
|
||||
<App />
|
||||
</HashRouter>
|
||||
);
|
||||
@@ -1,35 +0,0 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { RootAPI } from 'api';
|
||||
import * as SessionContext from 'contexts/Session';
|
||||
import { mountWithContexts } from '../testUtils/enzymeHelpers';
|
||||
import App from './App';
|
||||
|
||||
jest.mock('./api');
|
||||
|
||||
describe('<App />', () => {
|
||||
beforeEach(() => {
|
||||
RootAPI.readAssetVariables.mockResolvedValue({
|
||||
data: {
|
||||
BRAND_NAME: 'AWX',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('renders ok', async () => {
|
||||
const contextValues = {
|
||||
setAuthRedirectTo: jest.fn(),
|
||||
isSessionExpired: false,
|
||||
};
|
||||
jest
|
||||
.spyOn(SessionContext, 'useSession')
|
||||
.mockImplementation(() => contextValues);
|
||||
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<App />);
|
||||
});
|
||||
expect(wrapper.length).toBe(1);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
});
|
||||
@@ -1,69 +0,0 @@
|
||||
import axios from 'axios';
|
||||
import { encodeQueryString } from 'util/qs';
|
||||
import debounce from 'util/debounce';
|
||||
import { SESSION_TIMEOUT_KEY } from '../constants';
|
||||
|
||||
const updateStorage = debounce((key, val) => {
|
||||
window.localStorage.setItem(key, val);
|
||||
window.dispatchEvent(new Event('storage'));
|
||||
}, 500);
|
||||
|
||||
const defaultHttp = axios.create({
|
||||
xsrfCookieName: 'csrftoken',
|
||||
xsrfHeaderName: 'X-CSRFToken',
|
||||
paramsSerializer(params) {
|
||||
return encodeQueryString(params);
|
||||
},
|
||||
});
|
||||
|
||||
defaultHttp.interceptors.response.use((response) => {
|
||||
const timeout = response?.headers['session-timeout'];
|
||||
if (timeout) {
|
||||
const timeoutDate = new Date().getTime() + timeout * 1000;
|
||||
updateStorage(SESSION_TIMEOUT_KEY, String(timeoutDate));
|
||||
}
|
||||
return response;
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
copy(id, data) {
|
||||
return this.http.post(`${this.baseUrl}${id}/copy/`, data);
|
||||
}
|
||||
}
|
||||
|
||||
export default Base;
|
||||
@@ -1,106 +0,0 @@
|
||||
import Base from './Base';
|
||||
|
||||
describe('Base', () => {
|
||||
const mockBaseURL = '/api/v2/organizations/';
|
||||
|
||||
let BaseAPI;
|
||||
let mockHttp;
|
||||
|
||||
beforeEach(() => {
|
||||
const createPromise = () => Promise.resolve();
|
||||
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),
|
||||
};
|
||||
BaseAPI = new Base(mockHttp, mockBaseURL);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test('create calls http method with expected data', async () => {
|
||||
const data = { name: 'test ' };
|
||||
await BaseAPI.create(data);
|
||||
|
||||
expect(mockHttp.post).toHaveBeenCalledTimes(1);
|
||||
expect(mockHttp.post.mock.calls[0][1]).toEqual(data);
|
||||
});
|
||||
|
||||
test('destroy calls http method with expected data', async () => {
|
||||
const resourceId = 1;
|
||||
await BaseAPI.destroy(resourceId);
|
||||
|
||||
expect(mockHttp.delete).toHaveBeenCalledTimes(1);
|
||||
expect(mockHttp.delete.mock.calls[0][0]).toEqual(
|
||||
`${mockBaseURL}${resourceId}/`
|
||||
);
|
||||
});
|
||||
|
||||
test('read calls http method with expected data', async () => {
|
||||
const testParams = { foo: 'bar' };
|
||||
const testParamsDuplicates = { foo: ['bar', 'baz'] };
|
||||
|
||||
await BaseAPI.read(testParams);
|
||||
await BaseAPI.read();
|
||||
await BaseAPI.read(testParamsDuplicates);
|
||||
|
||||
expect(mockHttp.get).toHaveBeenCalledTimes(3);
|
||||
expect(mockHttp.get.mock.calls[0][0]).toEqual(`${mockBaseURL}`);
|
||||
expect(mockHttp.get.mock.calls[0][1]).toEqual({ params: { foo: 'bar' } });
|
||||
expect(mockHttp.get.mock.calls[1][0]).toEqual(`${mockBaseURL}`);
|
||||
expect(mockHttp.get.mock.calls[1][1]).toEqual({ params: undefined });
|
||||
expect(mockHttp.get.mock.calls[2][0]).toEqual(`${mockBaseURL}`);
|
||||
expect(mockHttp.get.mock.calls[2][1]).toEqual({
|
||||
params: { foo: ['bar', 'baz'] },
|
||||
});
|
||||
});
|
||||
|
||||
test('readDetail calls http method with expected data', async () => {
|
||||
const resourceId = 1;
|
||||
|
||||
await BaseAPI.readDetail(resourceId);
|
||||
|
||||
expect(mockHttp.get).toHaveBeenCalledTimes(1);
|
||||
expect(mockHttp.get.mock.calls[0][0]).toEqual(
|
||||
`${mockBaseURL}${resourceId}/`
|
||||
);
|
||||
});
|
||||
|
||||
test('readOptions calls http method with expected data', async () => {
|
||||
await BaseAPI.readOptions();
|
||||
|
||||
expect(mockHttp.options).toHaveBeenCalledTimes(1);
|
||||
expect(mockHttp.options.mock.calls[0][0]).toEqual(`${mockBaseURL}`);
|
||||
});
|
||||
|
||||
test('replace calls http method with expected data', async () => {
|
||||
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);
|
||||
});
|
||||
|
||||
test('update calls http method with expected data', async () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -1,136 +0,0 @@
|
||||
import ActivityStream from './models/ActivityStream';
|
||||
import AdHocCommands from './models/AdHocCommands';
|
||||
import Applications from './models/Applications';
|
||||
import Auth from './models/Auth';
|
||||
import Config from './models/Config';
|
||||
import CredentialInputSources from './models/CredentialInputSources';
|
||||
import CredentialTypes from './models/CredentialTypes';
|
||||
import Credentials from './models/Credentials';
|
||||
import Dashboard from './models/Dashboard';
|
||||
import ExecutionEnvironments from './models/ExecutionEnvironments';
|
||||
import Groups from './models/Groups';
|
||||
import Hosts from './models/Hosts';
|
||||
import InstanceGroups from './models/InstanceGroups';
|
||||
import Instances from './models/Instances';
|
||||
import Inventories from './models/Inventories';
|
||||
import InventoryScripts from './models/InventoryScripts';
|
||||
import InventorySources from './models/InventorySources';
|
||||
import InventoryUpdates from './models/InventoryUpdates';
|
||||
import JobTemplates from './models/JobTemplates';
|
||||
import Jobs from './models/Jobs';
|
||||
import Labels from './models/Labels';
|
||||
import Me from './models/Me';
|
||||
import Metrics from './models/Metrics';
|
||||
import NotificationTemplates from './models/NotificationTemplates';
|
||||
import Notifications from './models/Notifications';
|
||||
import Organizations from './models/Organizations';
|
||||
import ProjectUpdates from './models/ProjectUpdates';
|
||||
import Projects from './models/Projects';
|
||||
import Roles from './models/Roles';
|
||||
import Root from './models/Root';
|
||||
import Schedules from './models/Schedules';
|
||||
import Settings from './models/Settings';
|
||||
import SystemJobs from './models/SystemJobs';
|
||||
import SystemJobTemplates from './models/SystemJobTemplates';
|
||||
import Teams from './models/Teams';
|
||||
import Tokens from './models/Tokens';
|
||||
import UnifiedJobTemplates from './models/UnifiedJobTemplates';
|
||||
import UnifiedJobs from './models/UnifiedJobs';
|
||||
import Users from './models/Users';
|
||||
import WorkflowApprovals from './models/WorkflowApprovals';
|
||||
import WorkflowApprovalTemplates from './models/WorkflowApprovalTemplates';
|
||||
import WorkflowJobTemplateNodes from './models/WorkflowJobTemplateNodes';
|
||||
import WorkflowJobTemplates from './models/WorkflowJobTemplates';
|
||||
import WorkflowJobs from './models/WorkflowJobs';
|
||||
|
||||
const ActivityStreamAPI = new ActivityStream();
|
||||
const AdHocCommandsAPI = new AdHocCommands();
|
||||
const ApplicationsAPI = new Applications();
|
||||
const AuthAPI = new Auth();
|
||||
const ConfigAPI = new Config();
|
||||
const CredentialInputSourcesAPI = new CredentialInputSources();
|
||||
const CredentialTypesAPI = new CredentialTypes();
|
||||
const CredentialsAPI = new Credentials();
|
||||
const DashboardAPI = new Dashboard();
|
||||
const ExecutionEnvironmentsAPI = new ExecutionEnvironments();
|
||||
const GroupsAPI = new Groups();
|
||||
const HostsAPI = new Hosts();
|
||||
const InstanceGroupsAPI = new InstanceGroups();
|
||||
const InstancesAPI = new Instances();
|
||||
const InventoriesAPI = new Inventories();
|
||||
const InventoryScriptsAPI = new InventoryScripts();
|
||||
const InventorySourcesAPI = new InventorySources();
|
||||
const InventoryUpdatesAPI = new InventoryUpdates();
|
||||
const JobTemplatesAPI = new JobTemplates();
|
||||
const JobsAPI = new Jobs();
|
||||
const LabelsAPI = new Labels();
|
||||
const MeAPI = new Me();
|
||||
const MetricsAPI = new Metrics();
|
||||
const NotificationTemplatesAPI = new NotificationTemplates();
|
||||
const NotificationsAPI = new Notifications();
|
||||
const OrganizationsAPI = new Organizations();
|
||||
const ProjectUpdatesAPI = new ProjectUpdates();
|
||||
const ProjectsAPI = new Projects();
|
||||
const RolesAPI = new Roles();
|
||||
const RootAPI = new Root();
|
||||
const SchedulesAPI = new Schedules();
|
||||
const SettingsAPI = new Settings();
|
||||
const SystemJobsAPI = new SystemJobs();
|
||||
const SystemJobTemplatesAPI = new SystemJobTemplates();
|
||||
const TeamsAPI = new Teams();
|
||||
const TokensAPI = new Tokens();
|
||||
const UnifiedJobTemplatesAPI = new UnifiedJobTemplates();
|
||||
const UnifiedJobsAPI = new UnifiedJobs();
|
||||
const UsersAPI = new Users();
|
||||
const WorkflowApprovalsAPI = new WorkflowApprovals();
|
||||
const WorkflowApprovalTemplatesAPI = new WorkflowApprovalTemplates();
|
||||
const WorkflowJobTemplateNodesAPI = new WorkflowJobTemplateNodes();
|
||||
const WorkflowJobTemplatesAPI = new WorkflowJobTemplates();
|
||||
const WorkflowJobsAPI = new WorkflowJobs();
|
||||
|
||||
export {
|
||||
ActivityStreamAPI,
|
||||
AdHocCommandsAPI,
|
||||
ApplicationsAPI,
|
||||
AuthAPI,
|
||||
ConfigAPI,
|
||||
CredentialInputSourcesAPI,
|
||||
CredentialTypesAPI,
|
||||
CredentialsAPI,
|
||||
DashboardAPI,
|
||||
ExecutionEnvironmentsAPI,
|
||||
GroupsAPI,
|
||||
HostsAPI,
|
||||
InstanceGroupsAPI,
|
||||
InstancesAPI,
|
||||
InventoriesAPI,
|
||||
InventoryScriptsAPI,
|
||||
InventorySourcesAPI,
|
||||
InventoryUpdatesAPI,
|
||||
JobTemplatesAPI,
|
||||
JobsAPI,
|
||||
LabelsAPI,
|
||||
MeAPI,
|
||||
MetricsAPI,
|
||||
NotificationTemplatesAPI,
|
||||
NotificationsAPI,
|
||||
OrganizationsAPI,
|
||||
ProjectUpdatesAPI,
|
||||
ProjectsAPI,
|
||||
RolesAPI,
|
||||
RootAPI,
|
||||
SchedulesAPI,
|
||||
SettingsAPI,
|
||||
SystemJobsAPI,
|
||||
SystemJobTemplatesAPI,
|
||||
TeamsAPI,
|
||||
TokensAPI,
|
||||
UnifiedJobTemplatesAPI,
|
||||
UnifiedJobsAPI,
|
||||
UsersAPI,
|
||||
WorkflowApprovalsAPI,
|
||||
WorkflowApprovalTemplatesAPI,
|
||||
WorkflowJobTemplateNodesAPI,
|
||||
WorkflowJobTemplatesAPI,
|
||||
WorkflowJobsAPI,
|
||||
};
|
||||
@@ -1,44 +0,0 @@
|
||||
function isEqual(array1, array2) {
|
||||
return (
|
||||
array1.length === array2.length &&
|
||||
array1.every((element, index) => element.id === array2[index].id)
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
async orderInstanceGroups(resourceId, current, original) {
|
||||
/* eslint-disable no-await-in-loop, no-restricted-syntax */
|
||||
// Resolve Promises sequentially to maintain order and avoid race condition
|
||||
if (!isEqual(current, original)) {
|
||||
for (const group of original) {
|
||||
await this.disassociateInstanceGroup(resourceId, group.id);
|
||||
}
|
||||
for (const group of current) {
|
||||
await this.associateInstanceGroup(resourceId, group.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
/* eslint-enable no-await-in-loop, no-restricted-syntax */
|
||||
};
|
||||
|
||||
export default InstanceGroupsMixin;
|
||||
@@ -1,12 +0,0 @@
|
||||
const LaunchUpdateMixin = (parent) =>
|
||||
class extends parent {
|
||||
launchUpdate(id, data) {
|
||||
return this.http.post(`${this.baseUrl}${id}/update/`, data);
|
||||
}
|
||||
|
||||
readLaunchUpdate(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/update/`);
|
||||
}
|
||||
};
|
||||
|
||||
export default LaunchUpdateMixin;
|
||||
@@ -1,170 +0,0 @@
|
||||
const NotificationsMixin = (parent) =>
|
||||
class extends parent {
|
||||
readOptionsNotificationTemplates(id) {
|
||||
return this.http.options(`${this.baseUrl}${id}/notification_templates/`);
|
||||
}
|
||||
|
||||
readNotificationTemplates(id, params) {
|
||||
return this.http.get(
|
||||
`${this.baseUrl}${id}/notification_templates/`,
|
||||
params
|
||||
);
|
||||
}
|
||||
|
||||
readNotificationTemplatesStarted(id, params) {
|
||||
return this.http.get(
|
||||
`${this.baseUrl}${id}/notification_templates_started/`,
|
||||
{ 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 }
|
||||
);
|
||||
}
|
||||
|
||||
associateNotificationTemplatesStarted(resourceId, notificationId) {
|
||||
return this.http.post(
|
||||
`${this.baseUrl}${resourceId}/notification_templates_started/`,
|
||||
{ id: notificationId }
|
||||
);
|
||||
}
|
||||
|
||||
disassociateNotificationTemplatesStarted(resourceId, notificationId) {
|
||||
return this.http.post(
|
||||
`${this.baseUrl}${resourceId}/notification_templates_started/`,
|
||||
{ id: notificationId, disassociate: true }
|
||||
);
|
||||
}
|
||||
|
||||
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" 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"
|
||||
*/
|
||||
associateNotificationTemplate(
|
||||
resourceId,
|
||||
notificationId,
|
||||
notificationType
|
||||
) {
|
||||
if (notificationType === 'approvals') {
|
||||
return this.associateNotificationTemplatesApprovals(
|
||||
resourceId,
|
||||
notificationId
|
||||
);
|
||||
}
|
||||
|
||||
if (notificationType === 'started') {
|
||||
return this.associateNotificationTemplatesStarted(
|
||||
resourceId,
|
||||
notificationId
|
||||
);
|
||||
}
|
||||
|
||||
if (notificationType === 'success') {
|
||||
return this.associateNotificationTemplatesSuccess(
|
||||
resourceId,
|
||||
notificationId
|
||||
);
|
||||
}
|
||||
|
||||
if (notificationType === 'error') {
|
||||
return this.associateNotificationTemplatesError(
|
||||
resourceId,
|
||||
notificationId
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Unsupported notificationType for association: ${notificationType}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a helper method meant to simplify setting the "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"
|
||||
*/
|
||||
disassociateNotificationTemplate(
|
||||
resourceId,
|
||||
notificationId,
|
||||
notificationType
|
||||
) {
|
||||
if (notificationType === 'approvals') {
|
||||
return this.disassociateNotificationTemplatesApprovals(
|
||||
resourceId,
|
||||
notificationId
|
||||
);
|
||||
}
|
||||
|
||||
if (notificationType === 'started') {
|
||||
return this.disassociateNotificationTemplatesStarted(
|
||||
resourceId,
|
||||
notificationId
|
||||
);
|
||||
}
|
||||
|
||||
if (notificationType === 'success') {
|
||||
return this.disassociateNotificationTemplatesSuccess(
|
||||
resourceId,
|
||||
notificationId
|
||||
);
|
||||
}
|
||||
|
||||
if (notificationType === 'error') {
|
||||
return this.disassociateNotificationTemplatesError(
|
||||
resourceId,
|
||||
notificationId
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Unsupported notificationType for disassociation: ${notificationType}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default NotificationsMixin;
|
||||
@@ -1,48 +0,0 @@
|
||||
const Runnable = (parent) =>
|
||||
class extends parent {
|
||||
jobEventSlug = '/events/';
|
||||
|
||||
cancel(id) {
|
||||
const endpoint = `${this.baseUrl}${id}/cancel/`;
|
||||
|
||||
return this.http.post(endpoint);
|
||||
}
|
||||
|
||||
launchUpdate(id, data) {
|
||||
const endpoint = `${this.baseUrl}${id}/update/`;
|
||||
|
||||
return this.http.post(endpoint, data);
|
||||
}
|
||||
|
||||
readLaunchUpdate(id) {
|
||||
const endpoint = `${this.baseUrl}${id}/update/`;
|
||||
|
||||
return this.http.get(endpoint);
|
||||
}
|
||||
|
||||
readEvents(id, params = {}) {
|
||||
const endpoint = `${this.baseUrl}${id}${this.jobEventSlug}`;
|
||||
|
||||
return this.http.get(endpoint, { params });
|
||||
}
|
||||
|
||||
readEventOptions(id) {
|
||||
const endpoint = `${this.baseUrl}${id}${this.jobEventSlug}`;
|
||||
|
||||
return this.http.options(endpoint);
|
||||
}
|
||||
|
||||
readRelaunch(id) {
|
||||
const endpoint = `${this.baseUrl}${id}/relaunch/`;
|
||||
|
||||
return this.http.get(endpoint);
|
||||
}
|
||||
|
||||
relaunch(id, data) {
|
||||
const endpoint = `${this.baseUrl}${id}/relaunch/`;
|
||||
|
||||
return this.http.post(endpoint, data);
|
||||
}
|
||||
};
|
||||
|
||||
export default Runnable;
|
||||
@@ -1,16 +0,0 @@
|
||||
const SchedulesMixin = (parent) =>
|
||||
class extends parent {
|
||||
createSchedule(id, data) {
|
||||
return this.http.post(`${this.baseUrl}${id}/schedules/`, data);
|
||||
}
|
||||
|
||||
readSchedules(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/schedules/`, { params });
|
||||
}
|
||||
|
||||
readScheduleOptions(id) {
|
||||
return this.http.options(`${this.baseUrl}${id}/schedules/`);
|
||||
}
|
||||
};
|
||||
|
||||
export default SchedulesMixin;
|
||||
@@ -1,10 +0,0 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class ActivityStream extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/activity_stream/';
|
||||
}
|
||||
}
|
||||
|
||||
export default ActivityStream;
|
||||
@@ -1,15 +0,0 @@
|
||||
import Base from '../Base';
|
||||
import RunnableMixin from '../mixins/Runnable.mixin';
|
||||
|
||||
class AdHocCommands extends RunnableMixin(Base) {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/ad_hoc_commands/';
|
||||
}
|
||||
|
||||
readCredentials(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/credentials/`);
|
||||
}
|
||||
}
|
||||
|
||||
export default AdHocCommands;
|
||||
@@ -1,20 +0,0 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class Applications extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/applications/';
|
||||
}
|
||||
|
||||
readTokens(appId, params) {
|
||||
return this.http.get(`${this.baseUrl}${appId}/tokens/`, {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
readTokenOptions(appId) {
|
||||
return this.http.options(`${this.baseUrl}${appId}/tokens/`);
|
||||
}
|
||||
}
|
||||
|
||||
export default Applications;
|
||||
@@ -1,10 +0,0 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class Auth extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/auth/';
|
||||
}
|
||||
}
|
||||
|
||||
export default Auth;
|
||||
@@ -1,22 +0,0 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class Config extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/config/';
|
||||
this.read = this.read.bind(this);
|
||||
}
|
||||
|
||||
readSubscriptions(username, password) {
|
||||
return this.http.post(`${this.baseUrl}subscriptions/`, {
|
||||
subscriptions_username: username,
|
||||
subscriptions_password: password,
|
||||
});
|
||||
}
|
||||
|
||||
attach(data) {
|
||||
return this.http.post(`${this.baseUrl}attach/`, data);
|
||||
}
|
||||
}
|
||||
|
||||
export default Config;
|
||||
@@ -1,10 +0,0 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class CredentialInputSources extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/credential_input_sources/';
|
||||
}
|
||||
}
|
||||
|
||||
export default CredentialInputSources;
|
||||
@@ -1,36 +0,0 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class CredentialTypes extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/credential_types/';
|
||||
}
|
||||
|
||||
async loadAllTypes(
|
||||
acceptableKinds = ['machine', 'cloud', 'net', 'ssh', 'vault', 'kubernetes']
|
||||
) {
|
||||
const pageSize = 200;
|
||||
// The number of credential types a user can have is unlimited. In practice, it is unlikely for
|
||||
// users to have more than a page at the maximum request size.
|
||||
const {
|
||||
data: { next, results },
|
||||
} = await this.read({ page_size: pageSize });
|
||||
let nextResults = [];
|
||||
if (next) {
|
||||
const { data } = await this.read({
|
||||
page_size: pageSize,
|
||||
page: 2,
|
||||
});
|
||||
nextResults = data.results;
|
||||
}
|
||||
return results
|
||||
.concat(nextResults)
|
||||
.filter((type) => acceptableKinds.includes(type.kind));
|
||||
}
|
||||
|
||||
test(id, data) {
|
||||
return this.http.post(`${this.baseUrl}${id}/test/`, data);
|
||||
}
|
||||
}
|
||||
|
||||
export default CredentialTypes;
|
||||
@@ -1,68 +0,0 @@
|
||||
import CredentialTypes from './CredentialTypes';
|
||||
|
||||
const typesData = [
|
||||
{ id: 1, kind: 'machine' },
|
||||
{ id: 2, kind: 'cloud' },
|
||||
];
|
||||
|
||||
describe('CredentialTypesAPI', () => {
|
||||
test('should load all types', async () => {
|
||||
const getPromise = () =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
results: typesData,
|
||||
},
|
||||
});
|
||||
const mockHttp = { get: jest.fn(getPromise) };
|
||||
const CredentialTypesAPI = new CredentialTypes(mockHttp);
|
||||
|
||||
const types = await CredentialTypesAPI.loadAllTypes();
|
||||
|
||||
expect(mockHttp.get).toHaveBeenCalledTimes(1);
|
||||
expect(mockHttp.get.mock.calls[0]).toEqual([
|
||||
`/api/v2/credential_types/`,
|
||||
{ params: { page_size: 200 } },
|
||||
]);
|
||||
expect(types).toEqual(typesData);
|
||||
});
|
||||
|
||||
test('should load all types (2 pages)', async () => {
|
||||
const getPromise = () =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
results: typesData,
|
||||
next: 2,
|
||||
},
|
||||
});
|
||||
const mockHttp = { get: jest.fn(getPromise) };
|
||||
const CredentialTypesAPI = new CredentialTypes(mockHttp);
|
||||
|
||||
const types = await CredentialTypesAPI.loadAllTypes();
|
||||
|
||||
expect(mockHttp.get).toHaveBeenCalledTimes(2);
|
||||
expect(mockHttp.get.mock.calls[0]).toEqual([
|
||||
`/api/v2/credential_types/`,
|
||||
{ params: { page_size: 200 } },
|
||||
]);
|
||||
expect(mockHttp.get.mock.calls[1]).toEqual([
|
||||
`/api/v2/credential_types/`,
|
||||
{ params: { page_size: 200, page: 2 } },
|
||||
]);
|
||||
expect(types).toHaveLength(4);
|
||||
});
|
||||
|
||||
test('should filter by acceptable kinds', async () => {
|
||||
const getPromise = () =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
results: typesData,
|
||||
},
|
||||
});
|
||||
const mockHttp = { get: jest.fn(getPromise) };
|
||||
const CredentialTypesAPI = new CredentialTypes(mockHttp);
|
||||
|
||||
const types = await CredentialTypesAPI.loadAllTypes(['machine']);
|
||||
|
||||
expect(types).toEqual([typesData[0]]);
|
||||
});
|
||||
});
|
||||
@@ -1,62 +0,0 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class Credentials extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/credentials/';
|
||||
|
||||
this.readAccessList = this.readAccessList.bind(this);
|
||||
this.readAccessOptions = this.readAccessOptions.bind(this);
|
||||
this.readInputSources = this.readInputSources.bind(this);
|
||||
}
|
||||
|
||||
readAccessList(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/access_list/`, {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
readAccessOptions(id) {
|
||||
return this.http.options(`${this.baseUrl}${id}/access_list/`);
|
||||
}
|
||||
|
||||
readInputSources(id) {
|
||||
const maxRequests = 5;
|
||||
let requestCounter = 0;
|
||||
const fetchInputSources = async (pageNo = 1, inputSources = []) => {
|
||||
try {
|
||||
requestCounter++;
|
||||
const { data } = await this.http.get(
|
||||
`${this.baseUrl}${id}/input_sources/`,
|
||||
{
|
||||
params: {
|
||||
page: pageNo,
|
||||
page_size: 200,
|
||||
},
|
||||
}
|
||||
);
|
||||
if (data?.next && requestCounter <= maxRequests) {
|
||||
return fetchInputSources(
|
||||
pageNo + 1,
|
||||
inputSources.concat(data.results)
|
||||
);
|
||||
}
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
results: inputSources.concat(data.results),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
return fetchInputSources();
|
||||
}
|
||||
|
||||
test(id, data) {
|
||||
return this.http.post(`${this.baseUrl}${id}/test/`, data);
|
||||
}
|
||||
}
|
||||
|
||||
export default Credentials;
|
||||
@@ -1,16 +0,0 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class Dashboard extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/dashboard/';
|
||||
}
|
||||
|
||||
readJobGraph(params) {
|
||||
return this.http.get(`${this.baseUrl}graphs/jobs/`, {
|
||||
params,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default Dashboard;
|
||||
@@ -1,20 +0,0 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class ExecutionEnvironments extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/execution_environments/';
|
||||
}
|
||||
|
||||
readUnifiedJobTemplates(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/unified_job_templates/`, {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
readUnifiedJobTemplateOptions(id) {
|
||||
return this.http.options(`${this.baseUrl}${id}/unified_job_templates/`);
|
||||
}
|
||||
}
|
||||
|
||||
export default ExecutionEnvironments;
|
||||
@@ -1,59 +0,0 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class Groups extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/groups/';
|
||||
|
||||
this.associateHost = this.associateHost.bind(this);
|
||||
this.createHost = this.createHost.bind(this);
|
||||
this.readAllHosts = this.readAllHosts.bind(this);
|
||||
this.disassociateHost = this.disassociateHost.bind(this);
|
||||
}
|
||||
|
||||
associateHost(id, hostId) {
|
||||
return this.http.post(`${this.baseUrl}${id}/hosts/`, {
|
||||
id: hostId,
|
||||
});
|
||||
}
|
||||
|
||||
createHost(id, data) {
|
||||
return this.http.post(`${this.baseUrl}${id}/hosts/`, data);
|
||||
}
|
||||
|
||||
readAllHosts(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/all_hosts/`, {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
disassociateHost(id, host) {
|
||||
return this.http.post(`${this.baseUrl}${id}/hosts/`, {
|
||||
id: host.id,
|
||||
disassociate: true,
|
||||
});
|
||||
}
|
||||
|
||||
readChildren(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/children/`, { params });
|
||||
}
|
||||
|
||||
associateChildGroup(id, childId) {
|
||||
return this.http.post(`${this.baseUrl}${id}/children/`, { id: childId });
|
||||
}
|
||||
|
||||
disassociateChildGroup(id, childId) {
|
||||
return this.http.post(`${this.baseUrl}${id}/children/`, {
|
||||
disassociate: id,
|
||||
id: childId,
|
||||
});
|
||||
}
|
||||
|
||||
readPotentialGroups(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/potential_children/`, {
|
||||
params,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default Groups;
|
||||
@@ -1,39 +0,0 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class Hosts extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/hosts/';
|
||||
|
||||
this.readFacts = this.readFacts.bind(this);
|
||||
this.readAllGroups = this.readAllGroups.bind(this);
|
||||
this.readGroupsOptions = this.readGroupsOptions.bind(this);
|
||||
this.associateGroup = this.associateGroup.bind(this);
|
||||
this.disassociateGroup = this.disassociateGroup.bind(this);
|
||||
}
|
||||
|
||||
readFacts(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/ansible_facts/`);
|
||||
}
|
||||
|
||||
readAllGroups(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/all_groups/`, { params });
|
||||
}
|
||||
|
||||
readGroupsOptions(id) {
|
||||
return this.http.options(`${this.baseUrl}${id}/all_groups/`);
|
||||
}
|
||||
|
||||
associateGroup(id, groupId) {
|
||||
return this.http.post(`${this.baseUrl}${id}/groups/`, { id: groupId });
|
||||
}
|
||||
|
||||
disassociateGroup(id, group) {
|
||||
return this.http.post(`${this.baseUrl}${id}/groups/`, {
|
||||
id: group.id,
|
||||
disassociate: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default Hosts;
|
||||
@@ -1,41 +0,0 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class InstanceGroups extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/instance_groups/';
|
||||
|
||||
this.associateInstance = this.associateInstance.bind(this);
|
||||
this.disassociateInstance = this.disassociateInstance.bind(this);
|
||||
this.readInstanceOptions = this.readInstanceOptions.bind(this);
|
||||
this.readInstances = this.readInstances.bind(this);
|
||||
this.readJobs = this.readJobs.bind(this);
|
||||
}
|
||||
|
||||
associateInstance(instanceGroupId, instanceId) {
|
||||
return this.http.post(`${this.baseUrl}${instanceGroupId}/instances/`, {
|
||||
id: instanceId,
|
||||
});
|
||||
}
|
||||
|
||||
disassociateInstance(instanceGroupId, instanceId) {
|
||||
return this.http.post(`${this.baseUrl}${instanceGroupId}/instances/`, {
|
||||
id: instanceId,
|
||||
disassociate: true,
|
||||
});
|
||||
}
|
||||
|
||||
readInstances(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/instances/`, { params });
|
||||
}
|
||||
|
||||
readInstanceOptions(id) {
|
||||
return this.http.options(`${this.baseUrl}${id}/instances/`);
|
||||
}
|
||||
|
||||
readJobs(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/jobs/`);
|
||||
}
|
||||
}
|
||||
|
||||
export default InstanceGroups;
|
||||
@@ -1,10 +0,0 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class Instances extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/instances/';
|
||||
}
|
||||
}
|
||||
|
||||
export default Instances;
|
||||
@@ -1,121 +0,0 @@
|
||||
import Base from '../Base';
|
||||
import InstanceGroupsMixin from '../mixins/InstanceGroups.mixin';
|
||||
|
||||
class Inventories extends InstanceGroupsMixin(Base) {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/inventories/';
|
||||
|
||||
this.readAccessList = this.readAccessList.bind(this);
|
||||
this.readAccessOptions = this.readAccessOptions.bind(this);
|
||||
this.readHosts = this.readHosts.bind(this);
|
||||
this.readHostDetail = this.readHostDetail.bind(this);
|
||||
this.readGroups = this.readGroups.bind(this);
|
||||
this.readGroupsOptions = this.readGroupsOptions.bind(this);
|
||||
this.promoteGroup = this.promoteGroup.bind(this);
|
||||
}
|
||||
|
||||
readAccessList(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/access_list/`, {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
readAccessOptions(id) {
|
||||
return this.http.options(`${this.baseUrl}${id}/access_list/`);
|
||||
}
|
||||
|
||||
createHost(id, data) {
|
||||
return this.http.post(`${this.baseUrl}${id}/hosts/`, data);
|
||||
}
|
||||
|
||||
readHosts(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/hosts/`, {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
async readHostDetail(inventoryId, hostId) {
|
||||
const {
|
||||
data: { results },
|
||||
} = await this.http.get(
|
||||
`${this.baseUrl}${inventoryId}/hosts/?id=${hostId}`
|
||||
);
|
||||
|
||||
if (Array.isArray(results) && results.length) {
|
||||
return results[0];
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`How did you get here? Host not found for Inventory ID: ${inventoryId}`
|
||||
);
|
||||
}
|
||||
|
||||
readGroups(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/groups/`, {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
readGroupsOptions(id) {
|
||||
return this.http.options(`${this.baseUrl}${id}/groups/`);
|
||||
}
|
||||
|
||||
readHostsOptions(id) {
|
||||
return this.http.options(`${this.baseUrl}${id}/hosts/`);
|
||||
}
|
||||
|
||||
promoteGroup(inventoryId, groupId) {
|
||||
return this.http.post(`${this.baseUrl}${inventoryId}/groups/`, {
|
||||
id: groupId,
|
||||
disassociate: true,
|
||||
});
|
||||
}
|
||||
|
||||
readSources(inventoryId, params) {
|
||||
return this.http.get(`${this.baseUrl}${inventoryId}/inventory_sources/`, {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
updateSources(inventoryId) {
|
||||
return this.http.get(
|
||||
`${this.baseUrl}${inventoryId}/update_inventory_sources/`
|
||||
);
|
||||
}
|
||||
|
||||
async readSourceDetail(inventoryId, sourceId) {
|
||||
const {
|
||||
data: { results },
|
||||
} = await this.http.get(
|
||||
`${this.baseUrl}${inventoryId}/inventory_sources/?id=${sourceId}`
|
||||
);
|
||||
|
||||
if (Array.isArray(results) && results.length) {
|
||||
return results[0];
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`How did you get here? Source not found for Inventory ID: ${inventoryId}`
|
||||
);
|
||||
}
|
||||
|
||||
syncAllSources(inventoryId) {
|
||||
return this.http.post(
|
||||
`${this.baseUrl}${inventoryId}/update_inventory_sources/`
|
||||
);
|
||||
}
|
||||
|
||||
readAdHocOptions(inventoryId) {
|
||||
return this.http.options(`${this.baseUrl}${inventoryId}/ad_hoc_commands/`);
|
||||
}
|
||||
|
||||
launchAdHocCommands(inventoryId, values) {
|
||||
return this.http.post(
|
||||
`${this.baseUrl}${inventoryId}/ad_hoc_commands/`,
|
||||
values
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Inventories;
|
||||
@@ -1,10 +0,0 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class InventoryScripts extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/inventory_scripts/';
|
||||
}
|
||||
}
|
||||
|
||||
export default InventoryScripts;
|
||||
@@ -1,41 +0,0 @@
|
||||
import Base from '../Base';
|
||||
import NotificationsMixin from '../mixins/Notifications.mixin';
|
||||
import LaunchUpdateMixin from '../mixins/LaunchUpdate.mixin';
|
||||
import SchedulesMixin from '../mixins/Schedules.mixin';
|
||||
|
||||
class InventorySources extends LaunchUpdateMixin(
|
||||
NotificationsMixin(SchedulesMixin(Base))
|
||||
) {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/inventory_sources/';
|
||||
|
||||
this.createSchedule = this.createSchedule.bind(this);
|
||||
this.createSyncStart = this.createSyncStart.bind(this);
|
||||
this.destroyGroups = this.destroyGroups.bind(this);
|
||||
this.destroyHosts = this.destroyHosts.bind(this);
|
||||
}
|
||||
|
||||
createSyncStart(sourceId, extraVars) {
|
||||
return this.http.post(`${this.baseUrl}${sourceId}/update/`, {
|
||||
extra_vars: extraVars,
|
||||
});
|
||||
}
|
||||
|
||||
readGroups(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/groups/`);
|
||||
}
|
||||
|
||||
readHosts(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/hosts/`);
|
||||
}
|
||||
|
||||
destroyGroups(id) {
|
||||
return this.http.delete(`${this.baseUrl}${id}/groups/`);
|
||||
}
|
||||
|
||||
destroyHosts(id) {
|
||||
return this.http.delete(`${this.baseUrl}${id}/hosts/`);
|
||||
}
|
||||
}
|
||||
export default InventorySources;
|
||||
@@ -1,19 +0,0 @@
|
||||
import Base from '../Base';
|
||||
import RunnableMixin from '../mixins/Runnable.mixin';
|
||||
|
||||
class InventoryUpdates extends RunnableMixin(Base) {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/inventory_updates/';
|
||||
this.createSyncCancel = this.createSyncCancel.bind(this);
|
||||
}
|
||||
|
||||
createSyncCancel(sourceId) {
|
||||
return this.http.post(`${this.baseUrl}${sourceId}/cancel/`);
|
||||
}
|
||||
|
||||
readCredentials(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/credentials/`);
|
||||
}
|
||||
}
|
||||
export default InventoryUpdates;
|
||||
@@ -1,106 +0,0 @@
|
||||
import Base from '../Base';
|
||||
import NotificationsMixin from '../mixins/Notifications.mixin';
|
||||
import InstanceGroupsMixin from '../mixins/InstanceGroups.mixin';
|
||||
import SchedulesMixin from '../mixins/Schedules.mixin';
|
||||
|
||||
class JobTemplates extends SchedulesMixin(
|
||||
InstanceGroupsMixin(NotificationsMixin(Base))
|
||||
) {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/job_templates/';
|
||||
|
||||
this.createSchedule = this.createSchedule.bind(this);
|
||||
this.launch = this.launch.bind(this);
|
||||
this.readLaunch = this.readLaunch.bind(this);
|
||||
this.associateLabel = this.associateLabel.bind(this);
|
||||
this.disassociateLabel = this.disassociateLabel.bind(this);
|
||||
this.readCredentials = this.readCredentials.bind(this);
|
||||
this.readAccessList = this.readAccessList.bind(this);
|
||||
this.readAccessOptions = this.readAccessOptions.bind(this);
|
||||
this.readWebhookKey = this.readWebhookKey.bind(this);
|
||||
}
|
||||
|
||||
launch(id, data) {
|
||||
return this.http.post(`${this.baseUrl}${id}/launch/`, data);
|
||||
}
|
||||
|
||||
readTemplateOptions(id) {
|
||||
return this.http.options(`${this.baseUrl}${id}/`);
|
||||
}
|
||||
|
||||
readLaunch(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/launch/`);
|
||||
}
|
||||
|
||||
associateLabel(id, label, orgId) {
|
||||
return this.http.post(`${this.baseUrl}${id}/labels/`, {
|
||||
name: label.name,
|
||||
organization: orgId,
|
||||
});
|
||||
}
|
||||
|
||||
disassociateLabel(id, label) {
|
||||
return this.http.post(`${this.baseUrl}${id}/labels/`, {
|
||||
id: label.id,
|
||||
disassociate: true,
|
||||
});
|
||||
}
|
||||
|
||||
readCredentials(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/credentials/`, {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
associateCredentials(id, credentialId) {
|
||||
return this.http.post(`${this.baseUrl}${id}/credentials/`, {
|
||||
id: credentialId,
|
||||
});
|
||||
}
|
||||
|
||||
disassociateCredentials(id, credentialId) {
|
||||
return this.http.post(`${this.baseUrl}${id}/credentials/`, {
|
||||
id: credentialId,
|
||||
disassociate: true,
|
||||
});
|
||||
}
|
||||
|
||||
readAccessList(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/access_list/`, {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
readAccessOptions(id) {
|
||||
return this.http.options(`${this.baseUrl}${id}/access_list/`);
|
||||
}
|
||||
|
||||
readScheduleList(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/schedules/`, {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
readSurvey(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/survey_spec/`);
|
||||
}
|
||||
|
||||
updateSurvey(id, survey) {
|
||||
return this.http.post(`${this.baseUrl}${id}/survey_spec/`, survey);
|
||||
}
|
||||
|
||||
destroySurvey(id) {
|
||||
return this.http.delete(`${this.baseUrl}${id}/survey_spec/`);
|
||||
}
|
||||
|
||||
readWebhookKey(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/webhook_key/`);
|
||||
}
|
||||
|
||||
updateWebhookKey(id) {
|
||||
return this.http.post(`${this.baseUrl}${id}/webhook_key/`);
|
||||
}
|
||||
}
|
||||
|
||||
export default JobTemplates;
|
||||
@@ -1,24 +0,0 @@
|
||||
import Base from '../Base';
|
||||
import RunnableMixin from '../mixins/Runnable.mixin';
|
||||
|
||||
class Jobs extends RunnableMixin(Base) {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/jobs/';
|
||||
this.jobEventSlug = '/job_events/';
|
||||
}
|
||||
|
||||
cancel(id) {
|
||||
return this.http.post(`${this.baseUrl}${id}/cancel/`);
|
||||
}
|
||||
|
||||
readCredentials(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/credentials/`);
|
||||
}
|
||||
|
||||
readDetail(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/`);
|
||||
}
|
||||
}
|
||||
|
||||
export default Jobs;
|
||||
@@ -1,10 +0,0 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class Labels extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/labels/';
|
||||
}
|
||||
}
|
||||
|
||||
export default Labels;
|
||||
@@ -1,10 +0,0 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class Me extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/me/';
|
||||
}
|
||||
}
|
||||
|
||||
export default Me;
|
||||
@@ -1,9 +0,0 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class Metrics extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/metrics/';
|
||||
}
|
||||
}
|
||||
export default Metrics;
|
||||
@@ -1,14 +0,0 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class NotificationTemplates extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/notification_templates/';
|
||||
}
|
||||
|
||||
test(id) {
|
||||
return this.http.post(`${this.baseUrl}${id}/test/`);
|
||||
}
|
||||
}
|
||||
|
||||
export default NotificationTemplates;
|
||||
@@ -1,10 +0,0 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class Notifications extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/notifications/';
|
||||
}
|
||||
}
|
||||
|
||||
export default Notifications;
|
||||
@@ -1,82 +0,0 @@
|
||||
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 });
|
||||
}
|
||||
|
||||
readAccessOptions(id) {
|
||||
return this.http.options(`${this.baseUrl}${id}/access_list/`);
|
||||
}
|
||||
|
||||
readTeams(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/teams/`, { params });
|
||||
}
|
||||
|
||||
readTeamsOptions(id) {
|
||||
return this.http.options(`${this.baseUrl}${id}/teams/`);
|
||||
}
|
||||
|
||||
readGalaxyCredentials(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/galaxy_credentials/`, {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
readExecutionEnvironments(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/execution_environments/`, {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
readExecutionEnvironmentsOptions(id) {
|
||||
return this.http.options(`${this.baseUrl}${id}/execution_environments/`);
|
||||
}
|
||||
|
||||
createUser(id, data) {
|
||||
return this.http.post(`${this.baseUrl}${id}/users/`, data);
|
||||
}
|
||||
|
||||
readNotificationTemplatesApprovals(id, params) {
|
||||
return this.http.get(
|
||||
`${this.baseUrl}${id}/notification_templates_approvals/`,
|
||||
{ params }
|
||||
);
|
||||
}
|
||||
|
||||
associateNotificationTemplatesApprovals(resourceId, notificationId) {
|
||||
return this.http.post(
|
||||
`${this.baseUrl}${resourceId}/notification_templates_approvals/`,
|
||||
{ id: notificationId }
|
||||
);
|
||||
}
|
||||
|
||||
disassociateNotificationTemplatesApprovals(resourceId, notificationId) {
|
||||
return this.http.post(
|
||||
`${this.baseUrl}${resourceId}/notification_templates_approvals/`,
|
||||
{ id: notificationId, disassociate: true }
|
||||
);
|
||||
}
|
||||
|
||||
associateGalaxyCredential(resourceId, credentialId) {
|
||||
return this.http.post(`${this.baseUrl}${resourceId}/galaxy_credentials/`, {
|
||||
id: credentialId,
|
||||
});
|
||||
}
|
||||
|
||||
disassociateGalaxyCredential(resourceId, credentialId) {
|
||||
return this.http.post(`${this.baseUrl}${resourceId}/galaxy_credentials/`, {
|
||||
id: credentialId,
|
||||
disassociate: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default Organizations;
|
||||
@@ -1,62 +0,0 @@
|
||||
import { describeNotificationMixin } from '../../../testUtils/apiReusable';
|
||||
import Organizations from './Organizations';
|
||||
|
||||
describe('OrganizationsAPI', () => {
|
||||
const orgId = 1;
|
||||
let mockHttp;
|
||||
let OrganizationsAPI;
|
||||
beforeEach(() => {
|
||||
const createPromise = () => Promise.resolve();
|
||||
mockHttp = { get: jest.fn(createPromise) };
|
||||
|
||||
OrganizationsAPI = new Organizations(mockHttp);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test('read access list calls get with expected params', async () => {
|
||||
const testParams = { foo: 'bar' };
|
||||
const testParamsDuplicates = { foo: ['bar', 'baz'] };
|
||||
|
||||
const mockBaseURL = `/api/v2/organizations/${orgId}/access_list/`;
|
||||
|
||||
await OrganizationsAPI.readAccessList(orgId);
|
||||
await OrganizationsAPI.readAccessList(orgId, testParams);
|
||||
await OrganizationsAPI.readAccessList(orgId, testParamsDuplicates);
|
||||
|
||||
expect(mockHttp.get).toHaveBeenCalledTimes(3);
|
||||
expect(mockHttp.get.mock.calls[0][0]).toEqual(`${mockBaseURL}`);
|
||||
expect(mockHttp.get.mock.calls[0][1]).toEqual({ params: undefined });
|
||||
expect(mockHttp.get.mock.calls[1][0]).toEqual(`${mockBaseURL}`);
|
||||
expect(mockHttp.get.mock.calls[1][1]).toEqual({ params: { foo: 'bar' } });
|
||||
expect(mockHttp.get.mock.calls[2][0]).toEqual(`${mockBaseURL}`);
|
||||
expect(mockHttp.get.mock.calls[2][1]).toEqual({
|
||||
params: { foo: ['bar', 'baz'] },
|
||||
});
|
||||
});
|
||||
|
||||
test('read teams calls get with expected params', async () => {
|
||||
const testParams = { foo: 'bar' };
|
||||
const testParamsDuplicates = { foo: ['bar', 'baz'] };
|
||||
|
||||
const mockBaseURL = `/api/v2/organizations/${orgId}/teams/`;
|
||||
|
||||
await OrganizationsAPI.readTeams(orgId);
|
||||
await OrganizationsAPI.readTeams(orgId, testParams);
|
||||
await OrganizationsAPI.readTeams(orgId, testParamsDuplicates);
|
||||
|
||||
expect(mockHttp.get).toHaveBeenCalledTimes(3);
|
||||
expect(mockHttp.get.mock.calls[0][0]).toEqual(`${mockBaseURL}`);
|
||||
expect(mockHttp.get.mock.calls[0][1]).toEqual({ params: undefined });
|
||||
expect(mockHttp.get.mock.calls[1][0]).toEqual(`${mockBaseURL}`);
|
||||
expect(mockHttp.get.mock.calls[1][1]).toEqual({ params: { foo: 'bar' } });
|
||||
expect(mockHttp.get.mock.calls[2][0]).toEqual(`${mockBaseURL}`);
|
||||
expect(mockHttp.get.mock.calls[2][1]).toEqual({
|
||||
params: { foo: ['bar', 'baz'] },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describeNotificationMixin(Organizations, 'Organizations[NotificationsMixin]');
|
||||
@@ -1,15 +0,0 @@
|
||||
import Base from '../Base';
|
||||
import RunnableMixin from '../mixins/Runnable.mixin';
|
||||
|
||||
class ProjectUpdates extends RunnableMixin(Base) {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/project_updates/';
|
||||
}
|
||||
|
||||
readCredentials(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/credentials/`);
|
||||
}
|
||||
}
|
||||
|
||||
export default ProjectUpdates;
|
||||
@@ -1,47 +0,0 @@
|
||||
import Base from '../Base';
|
||||
import NotificationsMixin from '../mixins/Notifications.mixin';
|
||||
import LaunchUpdateMixin from '../mixins/LaunchUpdate.mixin';
|
||||
import SchedulesMixin from '../mixins/Schedules.mixin';
|
||||
|
||||
class Projects extends SchedulesMixin(
|
||||
LaunchUpdateMixin(NotificationsMixin(Base))
|
||||
) {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/projects/';
|
||||
|
||||
this.readAccessList = this.readAccessList.bind(this);
|
||||
this.readAccessOptions = this.readAccessOptions.bind(this);
|
||||
this.readInventories = this.readInventories.bind(this);
|
||||
this.readPlaybooks = this.readPlaybooks.bind(this);
|
||||
this.readSync = this.readSync.bind(this);
|
||||
this.sync = this.sync.bind(this);
|
||||
this.createSchedule = this.createSchedule.bind(this);
|
||||
}
|
||||
|
||||
readAccessList(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/access_list/`, { params });
|
||||
}
|
||||
|
||||
readAccessOptions(id) {
|
||||
return this.http.options(`${this.baseUrl}${id}/access_list/`);
|
||||
}
|
||||
|
||||
readInventories(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/inventories/`);
|
||||
}
|
||||
|
||||
readPlaybooks(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/playbooks/`);
|
||||
}
|
||||
|
||||
readSync(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/update/`);
|
||||
}
|
||||
|
||||
sync(id) {
|
||||
return this.http.post(`${this.baseUrl}${id}/update/`);
|
||||
}
|
||||
}
|
||||
|
||||
export default Projects;
|
||||
@@ -1,23 +0,0 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class Roles extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/roles/';
|
||||
}
|
||||
|
||||
disassociateUserRole(roleId, userId) {
|
||||
return this.http.post(`${this.baseUrl}${roleId}/users/`, {
|
||||
disassociate: true,
|
||||
id: userId,
|
||||
});
|
||||
}
|
||||
|
||||
disassociateTeamRole(roleId, teamId) {
|
||||
return this.http.post(`${this.baseUrl}${roleId}/teams/`, {
|
||||
disassociate: true,
|
||||
id: teamId,
|
||||
});
|
||||
}
|
||||
}
|
||||
export default Roles;
|
||||
@@ -1,38 +0,0 @@
|
||||
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/`);
|
||||
}
|
||||
|
||||
readAssetVariables() {
|
||||
// TODO: There's better ways of doing this. Build tools, scripts,
|
||||
// automation etc. should relocate this variable file to an importable
|
||||
// location in src prior to building. That said, a raw http call
|
||||
// works for now.
|
||||
return this.http.get('/static/media/default.strings.json');
|
||||
}
|
||||
}
|
||||
|
||||
export default Root;
|
||||
@@ -1,50 +0,0 @@
|
||||
import Root from './Root';
|
||||
|
||||
describe('RootAPI', () => {
|
||||
let mockHttp;
|
||||
let RootAPI;
|
||||
beforeEach(() => {
|
||||
const createPromise = () => Promise.resolve();
|
||||
mockHttp = {
|
||||
get: jest.fn(createPromise),
|
||||
post: jest.fn(createPromise),
|
||||
};
|
||||
|
||||
RootAPI = new Root(mockHttp);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('login calls get and post with expected content headers', async () => {
|
||||
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 });
|
||||
});
|
||||
|
||||
test('login sends expected data', async () => {
|
||||
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'
|
||||
);
|
||||
});
|
||||
|
||||
test('logout calls expected http method', async () => {
|
||||
await RootAPI.logout();
|
||||
|
||||
expect(mockHttp.get).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -1,35 +0,0 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class Schedules extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/schedules/';
|
||||
}
|
||||
|
||||
createPreview(data) {
|
||||
return this.http.post(`${this.baseUrl}preview/`, data);
|
||||
}
|
||||
|
||||
readCredentials(resourceId, params) {
|
||||
return this.http.get(`${this.baseUrl}${resourceId}/credentials/`, params);
|
||||
}
|
||||
|
||||
associateCredential(resourceId, credentialId) {
|
||||
return this.http.post(`${this.baseUrl}${resourceId}/credentials/`, {
|
||||
id: credentialId,
|
||||
});
|
||||
}
|
||||
|
||||
disassociateCredential(resourceId, credentialId) {
|
||||
return this.http.post(`${this.baseUrl}${resourceId}/credentials/`, {
|
||||
id: credentialId,
|
||||
disassociate: true,
|
||||
});
|
||||
}
|
||||
|
||||
readZoneInfo() {
|
||||
return this.http.get(`${this.baseUrl}zoneinfo/`);
|
||||
}
|
||||
}
|
||||
|
||||
export default Schedules;
|
||||
@@ -1,42 +0,0 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class Settings extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/settings/';
|
||||
}
|
||||
|
||||
readAllOptions() {
|
||||
return this.http.options(`${this.baseUrl}all/`);
|
||||
}
|
||||
|
||||
updateAll(data) {
|
||||
return this.http.patch(`${this.baseUrl}all/`, data);
|
||||
}
|
||||
|
||||
readAll() {
|
||||
return this.http.get(`${this.baseUrl}all/`);
|
||||
}
|
||||
|
||||
updateCategory(category, data) {
|
||||
return this.http.patch(`${this.baseUrl}${category}/`, data);
|
||||
}
|
||||
|
||||
readCategory(category) {
|
||||
return this.http.get(`${this.baseUrl}${category}/`);
|
||||
}
|
||||
|
||||
readCategoryOptions(category) {
|
||||
return this.http.options(`${this.baseUrl}${category}/`);
|
||||
}
|
||||
|
||||
createTest(category, data) {
|
||||
return this.http.post(`${this.baseUrl}${category}/test/`, data);
|
||||
}
|
||||
|
||||
revertCategory(category) {
|
||||
return this.http.delete(`${this.baseUrl}${category}/`);
|
||||
}
|
||||
}
|
||||
|
||||
export default Settings;
|
||||
@@ -1,18 +0,0 @@
|
||||
import Base from '../Base';
|
||||
import NotificationsMixin from '../mixins/Notifications.mixin';
|
||||
import SchedulesMixin from '../mixins/Schedules.mixin';
|
||||
|
||||
const Mixins = SchedulesMixin(NotificationsMixin(Base));
|
||||
|
||||
class SystemJobTemplates extends Mixins {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/system_job_templates/';
|
||||
}
|
||||
|
||||
launch(id, data) {
|
||||
return this.http.post(`${this.baseUrl}${id}/launch/`, data);
|
||||
}
|
||||
}
|
||||
|
||||
export default SystemJobTemplates;
|
||||
@@ -1,16 +0,0 @@
|
||||
import Base from '../Base';
|
||||
|
||||
import RunnableMixin from '../mixins/Runnable.mixin';
|
||||
|
||||
class SystemJobs extends RunnableMixin(Base) {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/system_jobs/';
|
||||
}
|
||||
|
||||
readCredentials(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/credentials/`);
|
||||
}
|
||||
}
|
||||
|
||||
export default SystemJobs;
|
||||
@@ -1,47 +0,0 @@
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
readRoles(teamId, params) {
|
||||
return this.http.get(`${this.baseUrl}${teamId}/roles/`, {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
readRoleOptions(teamId) {
|
||||
return this.http.options(`${this.baseUrl}${teamId}/roles/`);
|
||||
}
|
||||
|
||||
readAccessList(teamId, params) {
|
||||
return this.http.get(`${this.baseUrl}${teamId}/access_list/`, {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
readAccessOptions(id) {
|
||||
return this.http.options(`${this.baseUrl}${id}/access_list/`);
|
||||
}
|
||||
|
||||
readUsersAccessOptions(teamId) {
|
||||
return this.http.options(`${this.baseUrl}${teamId}/users/`);
|
||||
}
|
||||
}
|
||||
|
||||
export default Teams;
|
||||
@@ -1,43 +0,0 @@
|
||||
import Teams from './Teams';
|
||||
|
||||
describe('TeamsAPI', () => {
|
||||
const teamId = 1;
|
||||
const roleId = 7;
|
||||
|
||||
let TeamsAPI;
|
||||
let mockHttp;
|
||||
|
||||
beforeEach(() => {
|
||||
const createPromise = () => Promise.resolve();
|
||||
mockHttp = { post: jest.fn(createPromise) };
|
||||
|
||||
TeamsAPI = new Teams(mockHttp);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test('associate role calls post with expected params', async () => {
|
||||
await TeamsAPI.associateRole(teamId, roleId);
|
||||
|
||||
expect(mockHttp.post).toHaveBeenCalledTimes(1);
|
||||
expect(mockHttp.post.mock.calls[0]).toContainEqual(
|
||||
`/api/v2/teams/${teamId}/roles/`,
|
||||
{ id: roleId }
|
||||
);
|
||||
});
|
||||
|
||||
test('read teams calls post with expected params', async () => {
|
||||
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,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class Tokens extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/tokens/';
|
||||
}
|
||||
}
|
||||
|
||||
export default Tokens;
|
||||
@@ -1,10 +0,0 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class UnifiedJobTemplates extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/unified_job_templates/';
|
||||
}
|
||||
}
|
||||
|
||||
export default UnifiedJobTemplates;
|
||||
@@ -1,10 +0,0 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class UnifiedJobs extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/unified_jobs/';
|
||||
}
|
||||
}
|
||||
|
||||
export default UnifiedJobs;
|
||||
@@ -1,69 +0,0 @@
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
createToken(userId, data) {
|
||||
return this.http.post(`${this.baseUrl}${userId}/authorized_tokens/`, data);
|
||||
}
|
||||
|
||||
disassociateRole(userId, roleId) {
|
||||
return this.http.post(`${this.baseUrl}${userId}/roles/`, {
|
||||
id: roleId,
|
||||
disassociate: true,
|
||||
});
|
||||
}
|
||||
|
||||
readOrganizations(userId, params) {
|
||||
return this.http.get(`${this.baseUrl}${userId}/organizations/`, {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
readRoles(userId, params) {
|
||||
return this.http.get(`${this.baseUrl}${userId}/roles/`, {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
readRoleOptions(userId) {
|
||||
return this.http.options(`${this.baseUrl}${userId}/roles/`);
|
||||
}
|
||||
|
||||
readTeams(userId, params) {
|
||||
return this.http.get(`${this.baseUrl}${userId}/teams/`, {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
readTeamsOptions(userId) {
|
||||
return this.http.options(`${this.baseUrl}${userId}/teams/`);
|
||||
}
|
||||
|
||||
readTokens(userId, params) {
|
||||
return this.http.get(`${this.baseUrl}${userId}/tokens/`, {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
readAdminOfOrganizations(userId, params) {
|
||||
return this.http.get(`${this.baseUrl}${userId}/admin_of_organizations/`, {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
readTokenOptions(userId) {
|
||||
return this.http.options(`${this.baseUrl}${userId}/tokens/`);
|
||||
}
|
||||
}
|
||||
|
||||
export default Users;
|
||||
@@ -1,40 +0,0 @@
|
||||
import Users from './Users';
|
||||
|
||||
describe('UsersAPI', () => {
|
||||
const userId = 1;
|
||||
const roleId = 7;
|
||||
let UsersAPI;
|
||||
let mockHttp;
|
||||
beforeEach(() => {
|
||||
const createPromise = () => Promise.resolve();
|
||||
mockHttp = { post: jest.fn(createPromise) };
|
||||
UsersAPI = new Users(mockHttp);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test('associate role calls post with expected params', async () => {
|
||||
await UsersAPI.associateRole(userId, roleId);
|
||||
|
||||
expect(mockHttp.post).toHaveBeenCalledTimes(1);
|
||||
expect(mockHttp.post.mock.calls[0]).toContainEqual(
|
||||
`/api/v2/users/${userId}/roles/`,
|
||||
{ id: roleId }
|
||||
);
|
||||
});
|
||||
|
||||
test('read users calls post with expected params', async () => {
|
||||
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,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class WorkflowApprovalTemplates extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/workflow_approval_templates/';
|
||||
}
|
||||
}
|
||||
|
||||
export default WorkflowApprovalTemplates;
|
||||
@@ -1,18 +0,0 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class WorkflowApprovals extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/workflow_approvals/';
|
||||
}
|
||||
|
||||
approve(id) {
|
||||
return this.http.post(`${this.baseUrl}${id}/approve/`);
|
||||
}
|
||||
|
||||
deny(id) {
|
||||
return this.http.post(`${this.baseUrl}${id}/deny/`);
|
||||
}
|
||||
}
|
||||
|
||||
export default WorkflowApprovals;
|
||||
@@ -1,73 +0,0 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class WorkflowJobTemplateNodes extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/workflow_job_template_nodes/';
|
||||
}
|
||||
|
||||
createApprovalTemplate(id, data) {
|
||||
return this.http.post(
|
||||
`${this.baseUrl}${id}/create_approval_template/`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
associateSuccessNode(id, idToAssociate) {
|
||||
return this.http.post(`${this.baseUrl}${id}/success_nodes/`, {
|
||||
id: idToAssociate,
|
||||
});
|
||||
}
|
||||
|
||||
associateFailureNode(id, idToAssociate) {
|
||||
return this.http.post(`${this.baseUrl}${id}/failure_nodes/`, {
|
||||
id: idToAssociate,
|
||||
});
|
||||
}
|
||||
|
||||
associateAlwaysNode(id, idToAssociate) {
|
||||
return this.http.post(`${this.baseUrl}${id}/always_nodes/`, {
|
||||
id: idToAssociate,
|
||||
});
|
||||
}
|
||||
|
||||
disassociateSuccessNode(id, idToDissociate) {
|
||||
return this.http.post(`${this.baseUrl}${id}/success_nodes/`, {
|
||||
id: idToDissociate,
|
||||
disassociate: true,
|
||||
});
|
||||
}
|
||||
|
||||
disassociateFailuresNode(id, idToDissociate) {
|
||||
return this.http.post(`${this.baseUrl}${id}/failure_nodes/`, {
|
||||
id: idToDissociate,
|
||||
disassociate: true,
|
||||
});
|
||||
}
|
||||
|
||||
disassociateAlwaysNode(id, idToDissociate) {
|
||||
return this.http.post(`${this.baseUrl}${id}/always_nodes/`, {
|
||||
id: idToDissociate,
|
||||
disassociate: true,
|
||||
});
|
||||
}
|
||||
|
||||
readCredentials(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/credentials/`);
|
||||
}
|
||||
|
||||
associateCredentials(id, credentialId) {
|
||||
return this.http.post(`${this.baseUrl}${id}/credentials/`, {
|
||||
id: credentialId,
|
||||
});
|
||||
}
|
||||
|
||||
disassociateCredentials(id, credentialId) {
|
||||
return this.http.post(`${this.baseUrl}${id}/credentials/`, {
|
||||
id: credentialId,
|
||||
disassociate: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default WorkflowJobTemplateNodes;
|
||||
@@ -1,107 +0,0 @@
|
||||
import Base from '../Base';
|
||||
import SchedulesMixin from '../mixins/Schedules.mixin';
|
||||
import NotificationsMixin from '../mixins/Notifications.mixin';
|
||||
|
||||
class WorkflowJobTemplates extends SchedulesMixin(NotificationsMixin(Base)) {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/workflow_job_templates/';
|
||||
this.createSchedule = this.createSchedule.bind(this);
|
||||
}
|
||||
|
||||
readWebhookKey(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/webhook_key/`);
|
||||
}
|
||||
|
||||
readWorkflowJobTemplateOptions(id) {
|
||||
return this.http.options(`${this.baseUrl}${id}/`);
|
||||
}
|
||||
|
||||
updateWebhookKey(id) {
|
||||
return this.http.post(`${this.baseUrl}${id}/webhook_key/`);
|
||||
}
|
||||
|
||||
associateLabel(id, label, orgId) {
|
||||
return this.http.post(`${this.baseUrl}${id}/labels/`, {
|
||||
name: label.name,
|
||||
organization: orgId,
|
||||
});
|
||||
}
|
||||
|
||||
createNode(id, data) {
|
||||
return this.http.post(`${this.baseUrl}${id}/workflow_nodes/`, data);
|
||||
}
|
||||
|
||||
disassociateLabel(id, label) {
|
||||
return this.http.post(`${this.baseUrl}${id}/labels/`, {
|
||||
id: label.id,
|
||||
disassociate: true,
|
||||
});
|
||||
}
|
||||
|
||||
launch(id, data) {
|
||||
return this.http.post(`${this.baseUrl}${id}/launch/`, data);
|
||||
}
|
||||
|
||||
readLaunch(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/launch/`);
|
||||
}
|
||||
|
||||
readNodes(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/workflow_nodes/`, {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
readAccessList(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/access_list/`, {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
readAccessOptions(id) {
|
||||
return this.http.options(`${this.baseUrl}${id}/access_list/`);
|
||||
}
|
||||
|
||||
readSurvey(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/survey_spec/`);
|
||||
}
|
||||
|
||||
updateSurvey(id, survey) {
|
||||
return this.http.post(`${this.baseUrl}${id}/survey_spec/`, survey);
|
||||
}
|
||||
|
||||
destroySurvey(id) {
|
||||
return this.http.delete(`${this.baseUrl}${id}/survey_spec/`);
|
||||
}
|
||||
|
||||
readNotificationTemplatesApprovals(id, params) {
|
||||
return this.http.get(
|
||||
`${this.baseUrl}${id}/notification_templates_approvals/`,
|
||||
{
|
||||
params,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
associateNotificationTemplatesApprovals(resourceId, notificationId) {
|
||||
return this.http.post(
|
||||
`${this.baseUrl}${resourceId}/notification_templates_approvals/`,
|
||||
{
|
||||
id: notificationId,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
disassociateNotificationTemplatesApprovals(resourceId, notificationId) {
|
||||
return this.http.post(
|
||||
`${this.baseUrl}${resourceId}/notification_templates_approvals/`,
|
||||
{
|
||||
id: notificationId,
|
||||
disassociate: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default WorkflowJobTemplates;
|
||||
@@ -1,19 +0,0 @@
|
||||
import Base from '../Base';
|
||||
import RunnableMixin from '../mixins/Runnable.mixin';
|
||||
|
||||
class WorkflowJobs extends RunnableMixin(Base) {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/workflow_jobs/';
|
||||
}
|
||||
|
||||
readNodes(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/workflow_nodes/`, { params });
|
||||
}
|
||||
|
||||
readCredentials(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/credentials/`);
|
||||
}
|
||||
}
|
||||
|
||||
export default WorkflowJobs;
|
||||
@@ -1,7 +0,0 @@
|
||||
.pf-c-select__toggle:before {
|
||||
border-top: var(--pf-c-select__toggle--before--BorderTopWidth) solid var(--pf-c-select__toggle--before--BorderTopColor);
|
||||
border-right: var(--pf-c-select__toggle--before--BorderRightWidth) solid var(--pf-c-select__toggle--before--BorderRightColor);
|
||||
border-bottom: var(--pf-c-select__toggle--before--BorderBottomWidth) solid var(--pf-c-select__toggle--before--BorderBottomColor);
|
||||
border-left: var(--pf-c-select__toggle--before--BorderLeftWidth) solid var(--pf-c-select__toggle--before--BorderLeftColor);
|
||||
}
|
||||
/* https://github.com/patternfly/patternfly-react/issues/5650 */
|
||||
@@ -1,70 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { t } from '@lingui/macro';
|
||||
import { AboutModal } from '@patternfly/react-core';
|
||||
import useBrandName from 'hooks/useBrandName';
|
||||
|
||||
function About({ version, isOpen, onClose }) {
|
||||
const {
|
||||
current: { brandName, componentName },
|
||||
} = useBrandName();
|
||||
const createSpeechBubble = () => {
|
||||
let text =
|
||||
componentName !== ''
|
||||
? `${brandName} ${componentName} ${version}`
|
||||
: `${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;
|
||||
};
|
||||
|
||||
const speechBubble = createSpeechBubble();
|
||||
const copyright = t`Copyright`;
|
||||
const redHatInc = t`Red Hat, Inc.`;
|
||||
|
||||
return (
|
||||
<AboutModal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
productName={brandName}
|
||||
trademark={`${copyright} ${new Date().getFullYear()} ${redHatInc}`}
|
||||
brandImageSrc="/static/media/logo-header.svg"
|
||||
brandImageAlt={t`Brand Image`}
|
||||
>
|
||||
<pre>
|
||||
{speechBubble}
|
||||
{`
|
||||
\\
|
||||
\\ ^__^
|
||||
(oo)\\_______
|
||||
(__) A )\\
|
||||
||----w |
|
||||
|| ||
|
||||
`}
|
||||
</pre>
|
||||
</AboutModal>
|
||||
);
|
||||
}
|
||||
|
||||
About.propTypes = {
|
||||
isOpen: PropTypes.bool,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
version: PropTypes.string,
|
||||
};
|
||||
|
||||
About.defaultProps = {
|
||||
isOpen: false,
|
||||
version: null,
|
||||
};
|
||||
|
||||
export default About;
|
||||
@@ -1,23 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import About from './About';
|
||||
|
||||
jest.mock('../../hooks/useBrandName', () => ({
|
||||
__esModule: true,
|
||||
default: () => ({
|
||||
current: { brandName: 'AWX', componentName: '' },
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('<About />', () => {
|
||||
test('should render AboutModal', () => {
|
||||
const onClose = jest.fn();
|
||||
const wrapper = shallow(<About isOpen onClose={onClose} />);
|
||||
|
||||
const modal = wrapper.find('AboutModal');
|
||||
expect(modal).toHaveLength(1);
|
||||
expect(modal.prop('onClose')).toEqual(onClose);
|
||||
expect(modal.prop('productName')).toEqual('AWX');
|
||||
expect(modal.prop('isOpen')).toEqual(true);
|
||||
});
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from './About';
|
||||
@@ -1,168 +0,0 @@
|
||||
import React, { useCallback, useEffect, useState, useContext } from 'react';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, DropdownItem, Tooltip } from '@patternfly/react-core';
|
||||
|
||||
import useRequest, { useDismissableError } from 'hooks/useRequest';
|
||||
import { InventoriesAPI, CredentialTypesAPI } from 'api';
|
||||
|
||||
import { KebabifiedContext } from 'contexts/Kebabified';
|
||||
import AlertModal from '../AlertModal';
|
||||
import ErrorDetail from '../ErrorDetail';
|
||||
import AdHocCommandsWizard from './AdHocCommandsWizard';
|
||||
import ContentError from '../ContentError';
|
||||
|
||||
function AdHocCommands({
|
||||
adHocItems,
|
||||
hasListItems,
|
||||
onLaunchLoading,
|
||||
moduleOptions,
|
||||
}) {
|
||||
const history = useHistory();
|
||||
const { id } = useParams();
|
||||
|
||||
const [isWizardOpen, setIsWizardOpen] = useState(false);
|
||||
const { isKebabified, onKebabModalChange } = useContext(KebabifiedContext);
|
||||
|
||||
const verbosityOptions = [
|
||||
{ value: '0', key: '0', label: t`0 (Normal)` },
|
||||
{ value: '1', key: '1', label: t`1 (Verbose)` },
|
||||
{ value: '2', key: '2', label: t`2 (More Verbose)` },
|
||||
{ value: '3', key: '3', label: t`3 (Debug)` },
|
||||
{ value: '4', key: '4', label: t`4 (Connection Debug)` },
|
||||
];
|
||||
useEffect(() => {
|
||||
if (isKebabified) {
|
||||
onKebabModalChange(isWizardOpen);
|
||||
}
|
||||
}, [isKebabified, isWizardOpen, onKebabModalChange]);
|
||||
|
||||
const {
|
||||
result: { credentialTypeId, organizationId },
|
||||
request: fetchData,
|
||||
error: fetchError,
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const [{ data }, cred] = await Promise.all([
|
||||
InventoriesAPI.readDetail(id),
|
||||
CredentialTypesAPI.read({ namespace: 'ssh' }),
|
||||
]);
|
||||
return {
|
||||
credentialTypeId: cred.data.results[0].id,
|
||||
organizationId: data.organization,
|
||||
};
|
||||
}, [id]),
|
||||
{ organizationId: null }
|
||||
);
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
const {
|
||||
isLoading: isLaunchLoading,
|
||||
error: launchError,
|
||||
request: launchAdHocCommands,
|
||||
} = useRequest(
|
||||
useCallback(
|
||||
async (values) => {
|
||||
const { data } = await InventoriesAPI.launchAdHocCommands(id, values);
|
||||
history.push(`/jobs/command/${data.id}/output`);
|
||||
},
|
||||
|
||||
[id, history]
|
||||
)
|
||||
);
|
||||
|
||||
const { error, dismissError } = useDismissableError(
|
||||
launchError || fetchError
|
||||
);
|
||||
|
||||
const handleSubmit = async (values) => {
|
||||
const { credential, execution_environment, ...remainingValues } = values;
|
||||
const newCredential = credential[0].id;
|
||||
|
||||
const manipulatedValues = {
|
||||
credential: newCredential,
|
||||
execution_environment: execution_environment[0]?.id,
|
||||
...remainingValues,
|
||||
};
|
||||
await launchAdHocCommands(manipulatedValues);
|
||||
};
|
||||
useEffect(
|
||||
() => onLaunchLoading(isLaunchLoading),
|
||||
[isLaunchLoading, onLaunchLoading]
|
||||
);
|
||||
|
||||
if (error && isWizardOpen) {
|
||||
return (
|
||||
<AlertModal
|
||||
isOpen={error}
|
||||
variant="error"
|
||||
title={t`Error!`}
|
||||
onClose={() => {
|
||||
dismissError();
|
||||
setIsWizardOpen(false);
|
||||
}}
|
||||
>
|
||||
{launchError ? (
|
||||
<>
|
||||
{t`Failed to launch job.`}
|
||||
<ErrorDetail error={error} />
|
||||
</>
|
||||
) : (
|
||||
<ContentError error={error} />
|
||||
)}
|
||||
</AlertModal>
|
||||
);
|
||||
}
|
||||
return (
|
||||
// render buttons for drop down and for toolbar
|
||||
// if modal is open render the modal
|
||||
<>
|
||||
<Tooltip content={t`Run ad hoc command`}>
|
||||
{isKebabified ? (
|
||||
<DropdownItem
|
||||
key="cancel-job"
|
||||
isDisabled={!hasListItems}
|
||||
component="button"
|
||||
aria-label={t`Run Command`}
|
||||
onClick={() => setIsWizardOpen(true)}
|
||||
>
|
||||
{t`Run Command`}
|
||||
</DropdownItem>
|
||||
) : (
|
||||
<Button
|
||||
ouiaId="run-command-button"
|
||||
variant="secondary"
|
||||
aria-label={t`Run Command`}
|
||||
onClick={() => setIsWizardOpen(true)}
|
||||
isDisabled={!hasListItems}
|
||||
>
|
||||
{t`Run Command`}
|
||||
</Button>
|
||||
)}
|
||||
</Tooltip>
|
||||
|
||||
{isWizardOpen && (
|
||||
<AdHocCommandsWizard
|
||||
adHocItems={adHocItems}
|
||||
organizationId={organizationId}
|
||||
moduleOptions={moduleOptions}
|
||||
verbosityOptions={verbosityOptions}
|
||||
credentialTypeId={credentialTypeId}
|
||||
onCloseWizard={() => setIsWizardOpen(false)}
|
||||
onLaunch={handleSubmit}
|
||||
onDismissError={() => dismissError()}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
AdHocCommands.propTypes = {
|
||||
adHocItems: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
hasListItems: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default AdHocCommands;
|
||||
@@ -1,446 +0,0 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import {
|
||||
CredentialTypesAPI,
|
||||
InventoriesAPI,
|
||||
CredentialsAPI,
|
||||
ExecutionEnvironmentsAPI,
|
||||
RootAPI,
|
||||
} from 'api';
|
||||
import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
} from '../../../testUtils/enzymeHelpers';
|
||||
import AdHocCommands from './AdHocCommands';
|
||||
|
||||
jest.mock('../../api/models/CredentialTypes');
|
||||
jest.mock('../../api/models/Inventories');
|
||||
jest.mock('../../api/models/Credentials');
|
||||
jest.mock('../../api/models/ExecutionEnvironments');
|
||||
jest.mock('../../api/models/Root');
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useParams: () => ({
|
||||
id: 1,
|
||||
}),
|
||||
}));
|
||||
const credentials = [
|
||||
{ id: 1, kind: 'cloud', name: 'Cred 1', url: 'www.google.com' },
|
||||
{ id: 2, kind: 'ssh', name: 'Cred 2', url: 'www.google.com' },
|
||||
{ id: 3, kind: 'Ansible', name: 'Cred 3', url: 'www.google.com' },
|
||||
{ id: 4, kind: 'Machine', name: 'Cred 4', url: 'www.google.com' },
|
||||
{ id: 5, kind: 'Machine', name: 'Cred 5', url: 'www.google.com' },
|
||||
];
|
||||
|
||||
const adHocItems = [
|
||||
{
|
||||
name: 'Inventory 1 Org 0',
|
||||
},
|
||||
{ name: 'Inventory 2 Org 0' },
|
||||
];
|
||||
|
||||
describe('<AdHocCommands />', () => {
|
||||
beforeEach(() => {
|
||||
RootAPI.readAssetVariables.mockResolvedValue({
|
||||
data: {
|
||||
BRAND_NAME: 'AWX',
|
||||
},
|
||||
});
|
||||
CredentialTypesAPI.read.mockResolvedValue({
|
||||
data: { count: 1, results: [{ id: 1, name: 'cred' }] },
|
||||
});
|
||||
ExecutionEnvironmentsAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
results: [
|
||||
{ id: 1, name: 'EE1 1', url: 'wwww.google.com' },
|
||||
{ id: 2, name: 'EE2', url: 'wwww.google.com' },
|
||||
],
|
||||
count: 2,
|
||||
},
|
||||
});
|
||||
});
|
||||
let wrapper;
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('mounts successfully', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<AdHocCommands
|
||||
adHocItems={adHocItems}
|
||||
hasListItems
|
||||
onLaunchLoading={() => jest.fn()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
expect(wrapper.find('AdHocCommands').length).toBe(1);
|
||||
});
|
||||
|
||||
test('should open the wizard', async () => {
|
||||
InventoriesAPI.readDetail.mockResolvedValue({ data: { organization: 1 } });
|
||||
CredentialTypesAPI.read.mockResolvedValue({
|
||||
data: { results: [{ id: 1 }] },
|
||||
});
|
||||
ExecutionEnvironmentsAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
results: [
|
||||
{ id: 1, name: 'EE1 1', url: 'wwww.google.com' },
|
||||
{ id: 2, name: 'EE2', url: 'wwww.google.com' },
|
||||
],
|
||||
count: 2,
|
||||
},
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<AdHocCommands
|
||||
moduleOptions={[
|
||||
['command', 'command'],
|
||||
['foo', 'foo'],
|
||||
]}
|
||||
adHocItems={adHocItems}
|
||||
hasListItems
|
||||
onLaunchLoading={() => jest.fn()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
await waitForElement(
|
||||
wrapper,
|
||||
'button[aria-label="Run Command"]',
|
||||
(el) => el.length === 1
|
||||
);
|
||||
await act(async () =>
|
||||
wrapper.find('button[aria-label="Run Command"]').prop('onClick')()
|
||||
);
|
||||
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('AdHocCommandsWizard').length).toBe(1);
|
||||
});
|
||||
|
||||
test('should submit properly', async () => {
|
||||
InventoriesAPI.launchAdHocCommands.mockResolvedValue({ data: { id: 1 } });
|
||||
InventoriesAPI.readDetail.mockResolvedValue({
|
||||
data: { organization: 1 },
|
||||
});
|
||||
|
||||
CredentialsAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
results: credentials,
|
||||
count: 5,
|
||||
},
|
||||
});
|
||||
CredentialsAPI.readOptions.mockResolvedValue({
|
||||
data: { actions: { GET: {} } },
|
||||
});
|
||||
|
||||
ExecutionEnvironmentsAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
results: [
|
||||
{ id: 1, name: 'EE1 1', url: 'wwww.google.com' },
|
||||
{ id: 2, name: 'EE2', url: 'wwww.google.com' },
|
||||
],
|
||||
count: 2,
|
||||
},
|
||||
});
|
||||
ExecutionEnvironmentsAPI.readOptions.mockResolvedValue({
|
||||
data: { actions: { GET: {}, POST: {} } },
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<AdHocCommands
|
||||
adHocItems={adHocItems}
|
||||
hasListItems
|
||||
moduleOptions={[
|
||||
['command', 'command'],
|
||||
['foo', 'foo'],
|
||||
]}
|
||||
onLaunchLoading={() => jest.fn()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
await waitForElement(
|
||||
wrapper,
|
||||
'button[aria-label="Run Command"]',
|
||||
(el) => el.length === 1
|
||||
);
|
||||
await act(async () =>
|
||||
wrapper.find('button[aria-label="Run Command"]').prop('onClick')()
|
||||
);
|
||||
wrapper.update();
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('AnsibleSelect[name="module_name"]').prop('onChange')(
|
||||
{},
|
||||
'command'
|
||||
);
|
||||
wrapper.find('input#module_args').simulate('change', {
|
||||
target: { value: 'foo', name: 'module_args' },
|
||||
});
|
||||
wrapper.find('AnsibleSelect[name="verbosity"]').prop('onChange')({}, 1);
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
|
||||
false
|
||||
);
|
||||
await act(async () =>
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
);
|
||||
await waitForElement(wrapper, 'ContentEmpty', (el) => el.length === 0);
|
||||
|
||||
// second step of wizard
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('td#check-action-item-2').find('input').simulate('click');
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
|
||||
expect(
|
||||
wrapper.find('CheckboxListItem[label="EE2"]').prop('isSelected')
|
||||
).toBe(true);
|
||||
|
||||
await act(async () =>
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
);
|
||||
// third step of wizard
|
||||
await waitForElement(wrapper, 'ContentEmpty', (el) => el.length === 0);
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('td#check-action-item-4').find('input').simulate('click');
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
|
||||
expect(
|
||||
wrapper.find('CheckboxListItem[label="Cred 4"]').prop('isSelected')
|
||||
).toBe(true);
|
||||
|
||||
await act(async () =>
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
);
|
||||
wrapper.update();
|
||||
|
||||
// fourth step
|
||||
await act(async () =>
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
);
|
||||
|
||||
expect(InventoriesAPI.launchAdHocCommands).toBeCalledWith(1, {
|
||||
module_args: 'foo',
|
||||
diff_mode: false,
|
||||
credential: 4,
|
||||
job_type: 'run',
|
||||
become_enabled: '',
|
||||
extra_vars: '---',
|
||||
forks: 0,
|
||||
limit: 'Inventory 1 Org 0, Inventory 2 Org 0',
|
||||
module_name: 'command',
|
||||
verbosity: 1,
|
||||
execution_environment: 2,
|
||||
});
|
||||
});
|
||||
|
||||
test('should throw error on submission properly', async () => {
|
||||
InventoriesAPI.launchAdHocCommands.mockRejectedValue(
|
||||
new Error({
|
||||
response: {
|
||||
config: {
|
||||
method: 'post',
|
||||
url: '/api/v2/inventories/1/ad_hoc_commands',
|
||||
},
|
||||
data: 'An error occurred',
|
||||
status: 403,
|
||||
},
|
||||
})
|
||||
);
|
||||
InventoriesAPI.readDetail.mockResolvedValue({
|
||||
data: { organization: 1 },
|
||||
});
|
||||
CredentialTypesAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
results: [
|
||||
{
|
||||
id: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
CredentialsAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
results: credentials,
|
||||
count: 5,
|
||||
},
|
||||
});
|
||||
CredentialsAPI.readOptions.mockResolvedValue({
|
||||
data: { actions: { GET: {} } },
|
||||
});
|
||||
|
||||
ExecutionEnvironmentsAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
results: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'EE1 1',
|
||||
url: 'wwww.google.com',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'EE2',
|
||||
url: 'wwww.google.com',
|
||||
},
|
||||
],
|
||||
count: 2,
|
||||
},
|
||||
});
|
||||
ExecutionEnvironmentsAPI.readOptions.mockResolvedValue({
|
||||
data: { actions: { GET: {}, POST: {} } },
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<AdHocCommands
|
||||
adHocItems={adHocItems}
|
||||
moduleOptions={[
|
||||
['command', 'command'],
|
||||
['foo', 'foo'],
|
||||
]}
|
||||
hasListItems
|
||||
onLaunchLoading={() => jest.fn()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
await waitForElement(
|
||||
wrapper,
|
||||
'button[aria-label="Run Command"]',
|
||||
(el) => el.length === 1
|
||||
);
|
||||
await act(async () =>
|
||||
wrapper.find('button[aria-label="Run Command"]').prop('onClick')()
|
||||
);
|
||||
wrapper.update();
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('AnsibleSelect[name="module_name"]').prop('onChange')(
|
||||
{},
|
||||
'command'
|
||||
);
|
||||
wrapper.find('input#module_args').simulate('change', {
|
||||
target: {
|
||||
value: 'foo',
|
||||
name: 'module_args',
|
||||
},
|
||||
});
|
||||
wrapper.find('AnsibleSelect[name="verbosity"]').prop('onChange')({}, 1);
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
|
||||
false
|
||||
);
|
||||
|
||||
await act(async () =>
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
);
|
||||
|
||||
await waitForElement(wrapper, 'ContentEmpty', (el) => el.length === 0);
|
||||
|
||||
// second step of wizard
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('td#check-action-item-2').find('input').simulate('click');
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
|
||||
expect(
|
||||
wrapper.find('CheckboxListItem[label="EE2"]').prop('isSelected')
|
||||
).toBe(true);
|
||||
|
||||
await act(async () =>
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
);
|
||||
// third step of wizard
|
||||
await waitForElement(wrapper, 'ContentEmpty', (el) => el.length === 0);
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('td#check-action-item-4').find('input').simulate('click');
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
|
||||
expect(
|
||||
wrapper.find('CheckboxListItem[label="Cred 4"]').prop('isSelected')
|
||||
).toBe(true);
|
||||
|
||||
await act(async () =>
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
);
|
||||
wrapper.update();
|
||||
|
||||
// fourth step of wizard
|
||||
|
||||
await act(async () =>
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
);
|
||||
await waitForElement(wrapper, 'ErrorDetail', (el) => el.length > 0);
|
||||
});
|
||||
|
||||
test('should disable run command button due to lack of list items', async () => {
|
||||
InventoriesAPI.readHosts.mockResolvedValue({
|
||||
data: { results: [], count: 0 },
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<AdHocCommands
|
||||
moduleOptions={[
|
||||
['command', 'command'],
|
||||
['foo', 'foo'],
|
||||
]}
|
||||
adHocItems={adHocItems}
|
||||
hasListItems={false}
|
||||
onLaunchLoading={() => jest.fn()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
|
||||
const runCommandsButton = wrapper.find('button[aria-label="Run Command"]');
|
||||
expect(runCommandsButton.prop('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
test('should open alert modal when error on fetching data', async () => {
|
||||
InventoriesAPI.readDetail.mockRejectedValue(
|
||||
new Error({
|
||||
response: {
|
||||
config: {
|
||||
method: 'options',
|
||||
url: '/api/v2/inventories/1/',
|
||||
},
|
||||
data: 'An error occurred',
|
||||
status: 403,
|
||||
},
|
||||
})
|
||||
);
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<AdHocCommands
|
||||
moduleOptions={[
|
||||
['command', 'command'],
|
||||
['foo', 'foo'],
|
||||
]}
|
||||
adHocItems={adHocItems}
|
||||
hasListItems
|
||||
onLaunchLoading={() => jest.fn()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
await act(async () => wrapper.find('button').prop('onClick')());
|
||||
wrapper.update();
|
||||
expect(wrapper.find('ErrorDetail').length).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -1,85 +0,0 @@
|
||||
import React from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { withFormik, useFormikContext } from 'formik';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Wizard from '../Wizard';
|
||||
import useAdHocLaunchSteps from './useAdHocLaunchSteps';
|
||||
|
||||
function AdHocCommandsWizard({
|
||||
onLaunch,
|
||||
moduleOptions,
|
||||
verbosityOptions,
|
||||
onCloseWizard,
|
||||
credentialTypeId,
|
||||
organizationId,
|
||||
}) {
|
||||
const { setFieldTouched, values } = useFormikContext();
|
||||
|
||||
const { steps, validateStep, visitStep, visitAllSteps } = useAdHocLaunchSteps(
|
||||
moduleOptions,
|
||||
verbosityOptions,
|
||||
organizationId,
|
||||
credentialTypeId
|
||||
);
|
||||
|
||||
return (
|
||||
<Wizard
|
||||
style={{ overflow: 'scroll' }}
|
||||
isOpen
|
||||
onNext={(nextStep, prevStep) => {
|
||||
if (nextStep.id === 'preview') {
|
||||
visitAllSteps(setFieldTouched);
|
||||
} else {
|
||||
visitStep(prevStep.prevId, setFieldTouched);
|
||||
validateStep(nextStep.id);
|
||||
}
|
||||
}}
|
||||
onClose={() => onCloseWizard()}
|
||||
onSave={() => {
|
||||
onLaunch(values);
|
||||
}}
|
||||
onGoToStep={(nextStep, prevStep) => {
|
||||
if (nextStep.id === 'preview') {
|
||||
visitAllSteps(setFieldTouched);
|
||||
} else {
|
||||
visitStep(prevStep.prevId, setFieldTouched);
|
||||
validateStep(nextStep.id);
|
||||
}
|
||||
}}
|
||||
steps={steps}
|
||||
title={t`Run command`}
|
||||
backButtonText={t`Back`}
|
||||
cancelButtonText={t`Cancel`}
|
||||
nextButtonText={t`Next`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const FormikApp = withFormik({
|
||||
mapPropsToValues({ adHocItems, verbosityOptions }) {
|
||||
const adHocItemStrings = adHocItems.map((item) => item.name).join(', ');
|
||||
return {
|
||||
limit: adHocItemStrings || 'all',
|
||||
credential: [],
|
||||
module_args: '',
|
||||
verbosity: verbosityOptions[0].value,
|
||||
forks: 0,
|
||||
diff_mode: false,
|
||||
become_enabled: '',
|
||||
module_name: '',
|
||||
extra_vars: '---',
|
||||
job_type: 'run',
|
||||
execution_environment: '',
|
||||
};
|
||||
},
|
||||
})(AdHocCommandsWizard);
|
||||
|
||||
FormikApp.propTypes = {
|
||||
onLaunch: PropTypes.func.isRequired,
|
||||
moduleOptions: PropTypes.arrayOf(PropTypes.array).isRequired,
|
||||
verbosityOptions: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onCloseWizard: PropTypes.func.isRequired,
|
||||
credentialTypeId: PropTypes.number.isRequired,
|
||||
};
|
||||
export default FormikApp;
|
||||
@@ -1,261 +0,0 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { CredentialsAPI, ExecutionEnvironmentsAPI, RootAPI } from 'api';
|
||||
import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
} from '../../../testUtils/enzymeHelpers';
|
||||
import AdHocCommandsWizard from './AdHocCommandsWizard';
|
||||
|
||||
jest.mock('../../api/models/CredentialTypes');
|
||||
jest.mock('../../api/models/Inventories');
|
||||
jest.mock('../../api/models/Credentials');
|
||||
jest.mock('../../api/models/ExecutionEnvironments');
|
||||
jest.mock('../../api/models/Root');
|
||||
|
||||
const verbosityOptions = [
|
||||
{ value: '0', key: '0', label: '0 (Normal)' },
|
||||
{ value: '1', key: '1', label: '1 (Verbose)' },
|
||||
{ value: '2', key: '2', label: '2 (More Verbose)' },
|
||||
{ value: '3', key: '3', label: '3 (Debug)' },
|
||||
{ value: '4', key: '4', label: '4 (Connection Debug)' },
|
||||
];
|
||||
const moduleOptions = [
|
||||
['command', 'command'],
|
||||
['shell', 'shell'],
|
||||
];
|
||||
const adHocItems = [
|
||||
{ name: 'Inventory 1' },
|
||||
{ name: 'Inventory 2' },
|
||||
{ name: 'inventory 3' },
|
||||
];
|
||||
describe('<AdHocCommandsWizard/>', () => {
|
||||
let wrapper;
|
||||
const onLaunch = jest.fn();
|
||||
beforeEach(async () => {
|
||||
RootAPI.readAssetVariables.mockResolvedValue({
|
||||
data: {
|
||||
BRAND_NAME: 'AWX',
|
||||
},
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<AdHocCommandsWizard
|
||||
adHocItems={adHocItems}
|
||||
onLaunch={onLaunch}
|
||||
moduleOptions={moduleOptions}
|
||||
verbosityOptions={verbosityOptions}
|
||||
onCloseWizard={() => {}}
|
||||
credentialTypeId={1}
|
||||
organizationId={1}
|
||||
/>
|
||||
);
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should mount properly', async () => {
|
||||
expect(wrapper.find('AdHocCommandsWizard').length).toBe(1);
|
||||
});
|
||||
|
||||
test('launch button should be disabled', async () => {
|
||||
waitForElement(wrapper, 'WizardNavItem', (el) => el.length > 0);
|
||||
|
||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
|
||||
false
|
||||
);
|
||||
act(() => wrapper.find('Button[type="submit"]').prop('onClick')());
|
||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
|
||||
false
|
||||
);
|
||||
wrapper.update();
|
||||
act(() => wrapper.find('Button[type="submit"]').prop('onClick')());
|
||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
|
||||
false
|
||||
);
|
||||
wrapper.update();
|
||||
act(() => wrapper.find('Button[type="submit"]').prop('onClick')());
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('AdHocPreviewStep').prop('hasErrors')).toBe(true);
|
||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(true);
|
||||
});
|
||||
|
||||
test('launch button should become active', async () => {
|
||||
ExecutionEnvironmentsAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
results: [
|
||||
{ id: 1, name: 'EE 1', url: '' },
|
||||
{ id: 2, name: 'EE 2', url: '' },
|
||||
],
|
||||
count: 2,
|
||||
},
|
||||
});
|
||||
ExecutionEnvironmentsAPI.readOptions.mockResolvedValue({
|
||||
data: { actions: { GET: {} } },
|
||||
});
|
||||
CredentialsAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
results: [
|
||||
{ id: 1, name: 'Cred 1', url: '' },
|
||||
{ id: 2, name: 'Cred2', url: '' },
|
||||
],
|
||||
count: 2,
|
||||
},
|
||||
});
|
||||
CredentialsAPI.readOptions.mockResolvedValue({
|
||||
data: { actions: { GET: {} } },
|
||||
});
|
||||
await waitForElement(wrapper, 'WizardNavItem', (el) => el.length > 0);
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('AnsibleSelect[name="module_name"]').prop('onChange')(
|
||||
{},
|
||||
'command'
|
||||
);
|
||||
wrapper.find('input#module_args').simulate('change', {
|
||||
target: { value: 'foo', name: 'module_args' },
|
||||
});
|
||||
wrapper.find('AnsibleSelect[name="verbosity"]').prop('onChange')({}, 1);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
|
||||
false
|
||||
);
|
||||
await act(async () =>
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
);
|
||||
|
||||
wrapper.update();
|
||||
|
||||
// step 2
|
||||
|
||||
await waitForElement(wrapper, 'OptionsList', (el) => el.length > 0);
|
||||
expect(wrapper.find('CheckboxListItem').length).toBe(2);
|
||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
|
||||
false
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('td#check-action-item-1').find('input').simulate('click');
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
|
||||
expect(
|
||||
wrapper.find('CheckboxListItem[label="EE 1"]').prop('isSelected')
|
||||
).toBe(true);
|
||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
|
||||
false
|
||||
);
|
||||
|
||||
await act(async () =>
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
);
|
||||
|
||||
wrapper.update();
|
||||
// step 3
|
||||
|
||||
await waitForElement(wrapper, 'OptionsList', (el) => el.length > 0);
|
||||
expect(wrapper.find('CheckboxListItem').length).toBe(2);
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('td#check-action-item-1').find('input').simulate('click');
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
|
||||
expect(
|
||||
wrapper.find('CheckboxListItem[label="Cred 1"]').prop('isSelected')
|
||||
).toBe(true);
|
||||
|
||||
await act(async () =>
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
);
|
||||
wrapper.update();
|
||||
await act(async () =>
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
);
|
||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
|
||||
false
|
||||
);
|
||||
|
||||
expect(onLaunch).toHaveBeenCalledWith({
|
||||
become_enabled: '',
|
||||
credential: [{ id: 1, name: 'Cred 1', url: '' }],
|
||||
diff_mode: false,
|
||||
execution_environment: [{ id: 1, name: 'EE 1', url: '' }],
|
||||
extra_vars: '---',
|
||||
forks: 0,
|
||||
job_type: 'run',
|
||||
limit: 'Inventory 1, Inventory 2, inventory 3',
|
||||
module_args: 'foo',
|
||||
module_name: 'command',
|
||||
verbosity: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test('should show error in navigation bar', async () => {
|
||||
await waitForElement(wrapper, 'WizardNavItem', (el) => el.length > 0);
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('AnsibleSelect[name="module_name"]').prop('onChange')(
|
||||
{},
|
||||
'command'
|
||||
);
|
||||
wrapper.find('input#module_args').simulate('change', {
|
||||
target: { value: '', name: 'module_args' },
|
||||
});
|
||||
});
|
||||
waitForElement(wrapper, 'ExclamationCircleIcon', (el) => el.length > 0);
|
||||
});
|
||||
|
||||
test('expect credential step to throw error', async () => {
|
||||
CredentialsAPI.read.mockRejectedValue(
|
||||
new Error({
|
||||
response: {
|
||||
config: {
|
||||
method: 'get',
|
||||
url: '/api/v2/credentals',
|
||||
},
|
||||
data: 'An error occurred',
|
||||
status: 403,
|
||||
},
|
||||
})
|
||||
);
|
||||
CredentialsAPI.readOptions.mockResolvedValue({
|
||||
data: { actions: { GET: {} } },
|
||||
});
|
||||
await waitForElement(wrapper, 'WizardNavItem', (el) => el.length > 0);
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('AnsibleSelect[name="module_name"]').prop('onChange')(
|
||||
{},
|
||||
'command'
|
||||
);
|
||||
wrapper.find('input#module_args').simulate('change', {
|
||||
target: { value: 'foo', name: 'module_args' },
|
||||
});
|
||||
wrapper.find('AnsibleSelect[name="verbosity"]').prop('onChange')({}, 1);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
|
||||
false
|
||||
);
|
||||
|
||||
await act(async () =>
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
);
|
||||
|
||||
wrapper.update();
|
||||
|
||||
await act(async () =>
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
);
|
||||
|
||||
wrapper.update();
|
||||
expect(wrapper.find('ContentError').length).toBe(1);
|
||||
});
|
||||
});
|
||||