mirror of
https://github.com/ansible/awx.git
synced 2026-02-05 19:44:43 -03:30
Compare commits
119 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
923cc671db | ||
|
|
db105c21e4 | ||
|
|
372aa36207 | ||
|
|
173318764b | ||
|
|
1dd535a859 | ||
|
|
f4ef7d6927 | ||
|
|
7cbe112e4e | ||
|
|
c441db2aab | ||
|
|
fb292d9706 | ||
|
|
35a5f93182 | ||
|
|
116dc0c480 | ||
|
|
b87ba1c53d | ||
|
|
59691b71bb | ||
|
|
cc0bb3e401 | ||
|
|
7ef90bd9f4 | ||
|
|
f820c49b82 | ||
|
|
ac62d86f2a | ||
|
|
b9e67e7972 | ||
|
|
48a2ebd48c | ||
|
|
ee13ddd87d | ||
|
|
3fcf7429a3 | ||
|
|
51a8790d56 | ||
|
|
c231e4d05e | ||
|
|
987e5a084d | ||
|
|
70ac7b2920 | ||
|
|
30c060cb27 | ||
|
|
9b0a2b0b76 | ||
|
|
2f82b75748 | ||
|
|
84fcd2ff00 | ||
|
|
3bc0c53e37 | ||
|
|
bc2dbcfce8 | ||
|
|
876edf54a3 | ||
|
|
b31bf8fab1 | ||
|
|
e8b2998578 | ||
|
|
8a92a01652 | ||
|
|
705f86f8cf | ||
|
|
9ab6a6d57e | ||
|
|
791eb4c1e1 | ||
|
|
870ca29388 | ||
|
|
816518cfab | ||
|
|
9e981583a6 | ||
|
|
d6fb8d6cd7 | ||
|
|
7dbf5f7138 | ||
|
|
aaec9487e6 | ||
|
|
96fa881df1 | ||
|
|
b7057fdc3e | ||
|
|
2679c99cad | ||
|
|
ea3a8d4912 | ||
|
|
63d9cd7b57 | ||
|
|
b692bbaa12 | ||
|
|
186af73e5d | ||
|
|
fddf292d47 | ||
|
|
1180634ba7 | ||
|
|
9abdafe101 | ||
|
|
48ebcd5918 | ||
|
|
fe6d0ce9cc | ||
|
|
62dabcae63 | ||
|
|
0b63af8d4d | ||
|
|
b05ebe9623 | ||
|
|
c836fafb61 | ||
|
|
96330f608d | ||
|
|
23aaf5b3ad | ||
|
|
a3e86dcd73 | ||
|
|
81b8028ea2 | ||
|
|
a4bfb032ff | ||
|
|
2704b202bf | ||
|
|
550d9d5e42 | ||
|
|
ab2d05a07d | ||
|
|
4543f6935f | ||
|
|
78d3d6dc94 | ||
|
|
2d6ca4cbb1 | ||
|
|
e244644a1d | ||
|
|
d216457c09 | ||
|
|
20a1da61c0 | ||
|
|
bf7ab1ede7 | ||
|
|
3b6b449545 | ||
|
|
781cf531e6 | ||
|
|
9b7475247c | ||
|
|
44dc7f8d1d | ||
|
|
60eaf9e235 | ||
|
|
f5102ed24d | ||
|
|
309178e4e2 | ||
|
|
76ffdbb993 | ||
|
|
d8037618c8 | ||
|
|
e94e15977c | ||
|
|
f37951249f | ||
|
|
9191079dda | ||
|
|
fdd560747d | ||
|
|
faa5df19ca | ||
|
|
5f9326b131 | ||
|
|
8e389d40b4 | ||
|
|
e62c77e783 | ||
|
|
48b3a43ec2 | ||
|
|
5f783fd5ee | ||
|
|
e112cf93c2 | ||
|
|
d9f26a411e | ||
|
|
ea84e7a491 | ||
|
|
7fab619fed | ||
|
|
699a35b88a | ||
|
|
8095adb945 | ||
|
|
8d36712860 | ||
|
|
0db34d0498 | ||
|
|
7ab254e5e3 | ||
|
|
dd7ab459e2 | ||
|
|
33df2e8aa4 | ||
|
|
39b8fd433b | ||
|
|
c31d74100d | ||
|
|
3af89c1e2b | ||
|
|
1d35bba8c3 | ||
|
|
c3c3e24875 | ||
|
|
ab9c97b158 | ||
|
|
5e700c992d | ||
|
|
d553c37d7d | ||
|
|
8a5e89e24b | ||
|
|
f02212b1fe | ||
|
|
973facebba | ||
|
|
bca6e00e37 | ||
|
|
b562d5cc88 | ||
|
|
dfde30798e |
17
.github/BOTMETA.yml
vendored
17
.github/BOTMETA.yml
vendored
@@ -1,17 +0,0 @@
|
||||
---
|
||||
files:
|
||||
awx/ui/:
|
||||
labels: component:ui
|
||||
maintainers: $team_ui
|
||||
awx/api/:
|
||||
labels: component:api
|
||||
maintainers: $team_api
|
||||
awx/main/:
|
||||
labels: component:api
|
||||
maintainers: $team_api
|
||||
installer/:
|
||||
labels: component:installer
|
||||
|
||||
macros:
|
||||
team_api: wwitzel3 matburt chrismeyersfsu cchurch AlanCoding ryanpetrello rooftopcellist
|
||||
team_ui: jlmitch5 jaredevantabor mabashian marshmalien benthomasson jakemcdermott
|
||||
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -1 +0,0 @@
|
||||
workflows/e2e_test.yml @tiagodread @shanemcd @jakemcdermott
|
||||
26
.github/ISSUE_TEMPLATE.md
vendored
26
.github/ISSUE_TEMPLATE.md
vendored
@@ -6,17 +6,37 @@ practices regarding responsible disclosure, see
|
||||
https://www.ansible.com/security
|
||||
-->
|
||||
|
||||
|
||||
|
||||
<!--
|
||||
|
||||
PLEASE DO NOT USE A BLANK TEMPLATE IN THE AWX REPO.
|
||||
This is a legacy template used for internal testing ONLY.
|
||||
|
||||
Any issues opened will this template will be automatically closed.
|
||||
|
||||
Instead use the bug or feature request.
|
||||
|
||||
-->
|
||||
|
||||
|
||||
|
||||
##### ISSUE TYPE
|
||||
<!--- Pick one below and delete the rest: -->
|
||||
- Bug Report
|
||||
- Feature Idea
|
||||
- Documentation
|
||||
- Breaking Change
|
||||
- New or Enhanced Feature
|
||||
- Bug or Docs Fix
|
||||
|
||||
|
||||
##### COMPONENT NAME
|
||||
<!-- Pick the area of AWX for this issue, you can have multiple, delete the rest: -->
|
||||
- API
|
||||
- UI
|
||||
- Collection
|
||||
- Docs
|
||||
- CLI
|
||||
- Other
|
||||
|
||||
|
||||
##### SUMMARY
|
||||
<!-- Briefly describe the problem. -->
|
||||
|
||||
17
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
17
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,13 +1,12 @@
|
||||
---
|
||||
name: Bug Report
|
||||
description: Create a report to help us improve
|
||||
description: "🐞 Create a report to help us improve"
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Issues are for **concrete, actionable bugs and feature requests** only. For debugging help or technical support, please use:
|
||||
- The #ansible-awx channel on irc.libera.chat
|
||||
- The awx project mailing list, https://groups.google.com/forum/#!forum/awx-project
|
||||
Bug Report issues are for **concrete, actionable bugs** only.
|
||||
For debugging help or technical support, please see the [Get Involved section of our README](https://github.com/ansible/awx#get-involved)
|
||||
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
@@ -24,7 +23,7 @@ body:
|
||||
- type: textarea
|
||||
id: summary
|
||||
attributes:
|
||||
label: Summary
|
||||
label: Bug Summary
|
||||
description: Briefly describe the problem.
|
||||
validations:
|
||||
required: false
|
||||
@@ -45,6 +44,9 @@ body:
|
||||
- label: UI
|
||||
- label: API
|
||||
- label: Docs
|
||||
- label: Collection
|
||||
- label: CLI
|
||||
- label: Other
|
||||
|
||||
- type: dropdown
|
||||
id: awx-install-method
|
||||
@@ -57,9 +59,8 @@ body:
|
||||
- minikube
|
||||
- openshift
|
||||
- minishift
|
||||
- docker on linux
|
||||
- docker for mac
|
||||
- boot2docker
|
||||
- docker development environment
|
||||
- N/A
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
12
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
12
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: For debugging help or technical support
|
||||
url: https://github.com/ansible/awx#get-involved
|
||||
about: For general debugging or technical support please see the Get Involved section of our readme.
|
||||
- name: 📝 Ansible Code of Conduct
|
||||
url: https://docs.ansible.com/ansible/latest/community/code_of_conduct.html?utm_medium=github&utm_source=issue_template_chooser
|
||||
about: AWX uses the Ansible Code of Conduct; ❤ Be nice to other members of the community. ☮ Behave.
|
||||
- name: 💼 For Enterprise
|
||||
url: https://www.ansible.com/products/engine?utm_medium=github&utm_source=issue_template_chooser
|
||||
about: Red Hat offers support for the Ansible Automation Platform
|
||||
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,17 +0,0 @@
|
||||
---
|
||||
name: "✨ Feature request"
|
||||
about: Suggest an idea for this project
|
||||
|
||||
---
|
||||
<!-- Issues are for **concrete, actionable bugs and feature requests** only - if you're just asking for debugging help or technical support, please use:
|
||||
|
||||
- http://web.libera.chat/?channels=#ansible-awx
|
||||
- https://groups.google.com/forum/#!forum/awx-project
|
||||
|
||||
We have to limit this because of limited volunteer time to respond to issues! -->
|
||||
|
||||
##### ISSUE TYPE
|
||||
- Feature Idea
|
||||
|
||||
##### SUMMARY
|
||||
<!-- Briefly describe the problem or desired enhancement. -->
|
||||
42
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
42
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: ✨ Feature request
|
||||
description: Suggest an idea for this project
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Feature Request issues are for **feature requests** only.
|
||||
For debugging help or technical support, please see the [Get Involved section of our README](https://github.com/ansible/awx#get-involved)
|
||||
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: Please confirm the following
|
||||
options:
|
||||
- label: I agree to follow this project's [code of conduct](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html).
|
||||
required: true
|
||||
- label: I have checked the [current issues](https://github.com/ansible/awx/issues) for duplicates.
|
||||
required: true
|
||||
- label: I understand that AWX is open source software provided for free and that I might not receive a timely response.
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: summary
|
||||
attributes:
|
||||
label: Feature Summary
|
||||
description: Briefly describe the desired enhancement.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: components
|
||||
attributes:
|
||||
label: Select the relevant components
|
||||
options:
|
||||
- label: UI
|
||||
- label: API
|
||||
- label: Docs
|
||||
- label: Collection
|
||||
- label: CLI
|
||||
- label: Other
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
name: "\U0001F525 Security bug report"
|
||||
about: How to report security vulnerabilities
|
||||
|
||||
---
|
||||
|
||||
For all security related bugs, email security@ansible.com instead of using this issue tracker and you will receive a prompt response.
|
||||
|
||||
For more information on the Ansible community's practices regarding responsible disclosure, see https://www.ansible.com/security
|
||||
9
.github/LABEL_MAP.md
vendored
9
.github/LABEL_MAP.md
vendored
@@ -1,9 +0,0 @@
|
||||
Bug Report: type:bug
|
||||
Bugfix Pull Request: type:bug
|
||||
Feature Request: type:enhancement
|
||||
Feature Pull Request: type:enhancement
|
||||
UI: component:ui
|
||||
API: component:api
|
||||
Installer: component:installer
|
||||
Docs Pull Request: component:docs
|
||||
Documentation: component:docs
|
||||
17
.github/PULL_REQUEST_TEMPLATE.md
vendored
17
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,11 +1,3 @@
|
||||
<!--- changelog-entry
|
||||
# Fill in 'msg' below to have an entry automatically added to the next release changelog.
|
||||
# Leaving 'msg' blank will not generate a changelog entry for this PR.
|
||||
# Please ensure this is a simple (and readable) one-line string.
|
||||
---
|
||||
msg: ""
|
||||
-->
|
||||
|
||||
##### SUMMARY
|
||||
<!--- Describe the change, including rationale and design decisions -->
|
||||
|
||||
@@ -17,15 +9,18 @@ the change does.
|
||||
|
||||
##### ISSUE TYPE
|
||||
<!--- Pick one below and delete the rest: -->
|
||||
- Feature Pull Request
|
||||
- Bugfix Pull Request
|
||||
- Docs Pull Request
|
||||
- Breaking Change
|
||||
- New or Enhanced Feature
|
||||
- Bug or Docs Fix
|
||||
|
||||
##### COMPONENT NAME
|
||||
<!--- Name of the module/plugin/module/task -->
|
||||
- API
|
||||
- UI
|
||||
- Collection
|
||||
- CLI
|
||||
- Docs
|
||||
- Other
|
||||
|
||||
##### AWX VERSION
|
||||
<!--- Paste verbatim output from `make VERSION` between quotes below -->
|
||||
|
||||
8
.github/issue_labeler.yml
vendored
8
.github/issue_labeler.yml
vendored
@@ -1,12 +1,16 @@
|
||||
needs_triage:
|
||||
- '.*'
|
||||
"type:bug":
|
||||
- "Please confirm the following"
|
||||
- "Bug Summary"
|
||||
"type:enhancement":
|
||||
- "Feature Idea"
|
||||
- "Feature Summary"
|
||||
"component:ui":
|
||||
- "\\[X\\] UI"
|
||||
"component:api":
|
||||
- "\\[X\\] API"
|
||||
"component:docs":
|
||||
- "\\[X\\] Docs"
|
||||
"component:awx_collection":
|
||||
- "\\[X\\] Collection"
|
||||
"component:cli":
|
||||
- "\\[X\\] awxkit"
|
||||
|
||||
7
.github/pr_labeler.yml
vendored
7
.github/pr_labeler.yml
vendored
@@ -10,5 +10,10 @@
|
||||
"component:cli":
|
||||
- any: ["awxkit/**/*"]
|
||||
|
||||
"component:collection":
|
||||
"component:awx_collection":
|
||||
- any: ["awx_collection/**/*"]
|
||||
|
||||
"dependencies":
|
||||
- any: ["awx/ui/package.json"]
|
||||
- any: ["awx/requirements/*.txt"]
|
||||
- any: ["awx/requirements/requirements.in"]
|
||||
|
||||
94
.github/triage_replies.md
vendored
94
.github/triage_replies.md
vendored
@@ -3,47 +3,91 @@
|
||||
- Hello, we think your question is answered in our FAQ. Does this: https://www.ansible.com/products/awx-project/faq cover your question?
|
||||
- You can find the latest documentation here: https://docs.ansible.com/automation-controller/latest/html/userguide/index.html
|
||||
|
||||
## Visit our mailing list
|
||||
- Hello, your question seems like a good one to ask on our mailing list at https://groups.google.com/g/awx-project. You can also join #ansible-awx on https://libera.chat/ and ask your question there.
|
||||
|
||||
## Create an issue
|
||||
- Hello, thanks for reaching out on list. We think this merits an issue on our Github, https://github.com/ansible/awx/issues. If you could open an issue up on Github it will get tagged and integrated into our planning and workflow. All future work will be tracked there.
|
||||
|
||||
## Create a Pull Request
|
||||
- Hello, we think your idea is good, please consider contributing a PR for this, following our contributing guidelines: https://github.com/ansible/awx/blob/devel/CONTRIBUTING.md
|
||||
## PRs/Issues
|
||||
|
||||
## Give us more info
|
||||
- Hello, we'd love to help but we need a little more information about the problem you're having. Screenshots, log outputs, or any reproducers would be very helpful.
|
||||
### Visit our mailing list
|
||||
- Hello, this appears to be less of a bug report or feature request and more of a question. Could you please ask this on our mailing list? See https://github.com/ansible/awx/#get-involved for information for ways to connect with us.
|
||||
|
||||
## Receptor
|
||||
### Denied Submission
|
||||
|
||||
- Hi! \
|
||||
\
|
||||
Thanks very much for your submission to AWX. It means a lot to us that you have taken time to contribute. \
|
||||
\
|
||||
At this time we do not want to merge this PR. Our reasons for this are: \
|
||||
\
|
||||
(A) INSERT ITEM HERE \
|
||||
\
|
||||
Please know that we are always up for discussion but this project is very active. Because of this, we're unlikely to see comments made on closed PRs, and we lock them after some time. If you or anyone else has any further questions, please let us know by using any of the communication methods listed in the page below: \
|
||||
\
|
||||
https://github.com/ansible/awx/#get-involved \
|
||||
\
|
||||
In the future, sometimes starting a discussion on the development list prior to implementing a feature can make getting things included a little easier, but it is not always necessary. \
|
||||
\
|
||||
Thank you once again for this and your interest in AWX!
|
||||
|
||||
|
||||
### No Progress
|
||||
- Hi! \
|
||||
\
|
||||
Thank you very much for your submission to AWX. It means a lot to us that you have taken time to contribute. \
|
||||
\
|
||||
On this PR, changes were requested but it has been some time since then. We think this PR has merit but without the requested changes we are unable to merge it. At this time we are closing you PR. If you get time to address the changes you are welcome to open another PR or we can reopen this PR upon request if you contact us by using any of the communication methods listed in the page below: \
|
||||
\
|
||||
https://github.com/ansible/awx/#get-involved \
|
||||
\
|
||||
Thank you once again for this and your interest in AWX!
|
||||
|
||||
|
||||
|
||||
|
||||
## Common
|
||||
|
||||
### Give us more info
|
||||
- Hello, we'd love to help, but we need a little more information about the problem you're having. Screenshots, log outputs, or any reproducers would be very helpful.
|
||||
|
||||
### Code of Conduct
|
||||
- Hello. Please keep in mind that Ansible adheres to a Code of Conduct in its community spaces. The spirit of the code of conduct is to be kind, and this is your friendly reminder to be so. Please see the full code of conduct here if you have questions: https://docs.ansible.com/ansible/latest/community/code_of_conduct.html
|
||||
|
||||
|
||||
|
||||
|
||||
## Mailing List Triage
|
||||
|
||||
### Create an issue
|
||||
- Hello, thanks for reaching out on list. We think this merits an issue on our Github, https://github.com/ansible/awx/issues. If you could open an issue up on Github it will get tagged and integrated into our planning and workflow. All future work will be tracked there. Issues should include as much information as possible, including screenshots, log outputs, or any reproducers.
|
||||
|
||||
### Create a Pull Request
|
||||
- Hello, we think your idea is good! Please consider contributing a PR for this following our contributing guidelines: https://github.com/ansible/awx/blob/devel/CONTRIBUTING.md
|
||||
|
||||
### Receptor
|
||||
- You can find the receptor docs here: https://receptor.readthedocs.io/en/latest/
|
||||
- Hello, your issue seems related to receptor, could you please open an issue in the receptor repository? https://github.com/ansible/receptor. Thanks!
|
||||
- Hello, your issue seems related to receptor. Could you please open an issue in the receptor repository? https://github.com/ansible/receptor. Thanks!
|
||||
|
||||
## Ansible Engine not AWX
|
||||
### Ansible Engine not AWX
|
||||
- Hello, your question seems to be about Ansible development, not about AWX. Try asking on the Ansible-devel specific mailing list: https://groups.google.com/g/ansible-devel
|
||||
- Hello, your question seems to be about using Ansible, not about AWX. https://groups.google.com/g/ansible-project is the best place to visit for user questions about Ansible. Thanks!
|
||||
|
||||
## Ansible Galaxy not AWX
|
||||
- Hey there, that sounds like an FAQ question, did this: https://www.ansible.com/products/awx-project/faq cover your question?
|
||||
### Ansible Galaxy not AWX
|
||||
- Hey there. That sounds like an FAQ question. Did this: https://www.ansible.com/products/awx-project/faq cover your question?
|
||||
|
||||
## Contributing Guidelines
|
||||
### Contributing Guidelines
|
||||
- AWX: https://github.com/ansible/awx/blob/devel/CONTRIBUTING.md
|
||||
- AWX-Operator: https://github.com/ansible/awx-operator/blob/devel/CONTRIBUTING.md
|
||||
|
||||
## Code of Conduct
|
||||
- Hello. Please keep in mind that Ansible adheres to a Code of Conduct in its community spaces. The spirit of the code of conduct is to be kind, and this is your friendly reminder to be so. Please see the full code of conduct here if you have questions: https://docs.ansible.com/ansible/latest/community/code_of_conduct.html
|
||||
|
||||
## AWX Release
|
||||
- Hi all,\
|
||||
### AWX Release
|
||||
- Hi all, \
|
||||
\
|
||||
We're happy to announce that the next release of AWX, version 21.0.0 is now available!\
|
||||
In addition AWX Operator version 0.21.0 has also been release!\
|
||||
We're happy to announce that the next release of AWX, version <X> is now available! \
|
||||
In addition AWX Operator version <Y> has also been release! \
|
||||
\
|
||||
Please see the releases pages for more details:\
|
||||
AWX: https://github.com/ansible/awx/releases/tag/21.0.0\
|
||||
Operator: https://github.com/ansible/awx-operator/releases/tag/0.20.1\
|
||||
Please see the releases pages for more details: \
|
||||
AWX: https://github.com/ansible/awx/releases/tag/<X> \
|
||||
Operator: https://github.com/ansible/awx-operator/releases/tag/<Y> \
|
||||
\
|
||||
The AWX team.
|
||||
|
||||
## Try latest version
|
||||
- Hello, this issue pertains to an older version of AWX. Try upgrading to the lastest version and see if that resolves your issue.
|
||||
- Hello, this issue pertains to an older version of AWX. Try upgrading to the latest version and let us know if that resolves your issue.
|
||||
|
||||
2
.github/workflows/promote.yml
vendored
2
.github/workflows/promote.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
|
||||
- name: Build collection and publish to galaxy
|
||||
run: |
|
||||
COLLECTION_NAMESPACE=${{ env.collection_namespace }} make build_collection
|
||||
COLLECTION_TEMPLATE_VERSION=true COLLECTION_NAMESPACE=${{ env.collection_namespace }} make build_collection
|
||||
ansible-galaxy collection publish \
|
||||
--token=${{ secrets.GALAXY_TOKEN }} \
|
||||
awx_collection_build/${{ env.collection_namespace }}-awx-${{ github.event.release.tag_name }}.tar.gz
|
||||
|
||||
13
.github/workflows/stage.yml
vendored
13
.github/workflows/stage.yml
vendored
@@ -100,23 +100,10 @@ jobs:
|
||||
AWX_TEST_IMAGE: ${{ github.repository }}
|
||||
AWX_TEST_VERSION: ${{ github.event.inputs.version }}
|
||||
|
||||
- name: Generate changelog
|
||||
uses: shanemcd/simple-changelog-generator@v1
|
||||
id: changelog
|
||||
with:
|
||||
repo: "${{ github.repository }}"
|
||||
|
||||
- name: Write changelog to file
|
||||
run: |
|
||||
cat << 'EOF' > /tmp/awx-changelog
|
||||
${{ steps.changelog.outputs.changelog }}
|
||||
EOF
|
||||
|
||||
- name: Create draft release for AWX
|
||||
working-directory: awx
|
||||
run: |
|
||||
ansible-playbook -v tools/ansible/stage.yml \
|
||||
-e changelog_path=/tmp/awx-changelog \
|
||||
-e repo=${{ github.repository }} \
|
||||
-e awx_image=ghcr.io/${{ github.repository }} \
|
||||
-e version=${{ github.event.inputs.version }} \
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -38,7 +38,6 @@ awx/ui/build
|
||||
awx/ui/.env.local
|
||||
awx/ui/instrumented
|
||||
rsyslog.pid
|
||||
tools/prometheus
|
||||
tools/docker-compose/ansible/awx_dump.sql
|
||||
tools/docker-compose/Dockerfile
|
||||
tools/docker-compose/_build
|
||||
@@ -154,6 +153,9 @@ use_dev_supervisor.txt
|
||||
/sanity/
|
||||
/awx_collection_build/
|
||||
|
||||
# Setup for metrics gathering
|
||||
tools/prometheus/prometheus.yml
|
||||
|
||||
.idea/*
|
||||
*.unison.tmp
|
||||
*.#
|
||||
|
||||
23
Makefile
23
Makefile
@@ -17,6 +17,10 @@ KEYCLOAK ?= false
|
||||
LDAP ?= false
|
||||
# If set to true docker-compose will also start a splunk instance
|
||||
SPLUNK ?= false
|
||||
# If set to true docker-compose will also start a prometheus instance
|
||||
PROMETHEUS ?= false
|
||||
# If set to true docker-compose will also start a grafana instance
|
||||
GRAFANA ?= false
|
||||
|
||||
VENV_BASE ?= /var/lib/awx/venv
|
||||
|
||||
@@ -200,7 +204,7 @@ uwsgi: collectstatic
|
||||
--logformat "%(addr) %(method) %(uri) - %(proto) %(status)"
|
||||
|
||||
awx-autoreload:
|
||||
@/awx_devel/tools/docker-compose/awx-autoreload /awx_devel "$(DEV_RELOAD_COMMAND)"
|
||||
@/awx_devel/tools/docker-compose/awx-autoreload /awx_devel/awx "$(DEV_RELOAD_COMMAND)"
|
||||
|
||||
daphne:
|
||||
@if [ "$(VENV_BASE)" ]; then \
|
||||
@@ -288,6 +292,7 @@ COLLECTION_TEST_TARGET ?=
|
||||
COLLECTION_PACKAGE ?= awx
|
||||
COLLECTION_NAMESPACE ?= awx
|
||||
COLLECTION_INSTALL = ~/.ansible/collections/ansible_collections/$(COLLECTION_NAMESPACE)/$(COLLECTION_PACKAGE)
|
||||
COLLECTION_TEMPLATE_VERSION ?= false
|
||||
|
||||
test_collection:
|
||||
rm -f $(shell ls -d $(VENV_BASE)/awx/lib/python* | head -n 1)/no-global-site-packages.txt
|
||||
@@ -315,7 +320,7 @@ awx_collection_build: $(shell find awx_collection -type f)
|
||||
-e collection_package=$(COLLECTION_PACKAGE) \
|
||||
-e collection_namespace=$(COLLECTION_NAMESPACE) \
|
||||
-e collection_version=$(COLLECTION_VERSION) \
|
||||
-e '{"awx_template_version":false}'
|
||||
-e '{"awx_template_version": $(COLLECTION_TEMPLATE_VERSION)}'
|
||||
ansible-galaxy collection build awx_collection_build --force --output-path=awx_collection_build
|
||||
|
||||
build_collection: awx_collection_build
|
||||
@@ -469,7 +474,9 @@ docker-compose-sources: .git/hooks/pre-commit
|
||||
-e minikube_container_group=$(MINIKUBE_CONTAINER_GROUP) \
|
||||
-e enable_keycloak=$(KEYCLOAK) \
|
||||
-e enable_ldap=$(LDAP) \
|
||||
-e enable_splunk=$(SPLUNK)
|
||||
-e enable_splunk=$(SPLUNK) \
|
||||
-e enable_prometheus=$(PROMETHEUS) \
|
||||
-e enable_grafana=$(GRAFANA)
|
||||
|
||||
|
||||
docker-compose: awx/projects docker-compose-sources
|
||||
@@ -517,7 +524,7 @@ docker-clean:
|
||||
fi
|
||||
|
||||
docker-clean-volumes: docker-compose-clean docker-compose-container-group-clean
|
||||
docker volume rm tools_awx_db
|
||||
docker volume rm -f tools_awx_db tools_grafana_storage tools_prometheus_storage $(docker volume ls --filter name=tools_redis_socket_ -q)
|
||||
|
||||
docker-refresh: docker-clean docker-compose
|
||||
|
||||
@@ -528,14 +535,6 @@ docker-compose-elk: awx/projects docker-compose-sources
|
||||
docker-compose-cluster-elk: awx/projects docker-compose-sources
|
||||
docker-compose -f tools/docker-compose/_sources/docker-compose.yml -f tools/elastic/docker-compose.logstash-link-cluster.yml -f tools/elastic/docker-compose.elastic-override.yml up --no-recreate
|
||||
|
||||
prometheus:
|
||||
docker volume create prometheus
|
||||
docker run -d --rm --net=_sources_default --link=awx_1:awx1 --volume prometheus-storage:/prometheus --volume `pwd`/tools/prometheus:/etc/prometheus --name prometheus -p 9090:9090 prom/prometheus
|
||||
|
||||
grafana:
|
||||
docker volume create grafana
|
||||
docker run -d --rm --net=_sources_default --volume grafana-storage:/var/lib/grafana --volume `pwd`/tools/grafana:/etc/grafana/provisioning --name grafana -p 3001:3000 grafana/grafana-enterprise
|
||||
|
||||
docker-compose-container-group:
|
||||
MINIKUBE_CONTAINER_GROUP=true make docker-compose
|
||||
|
||||
|
||||
@@ -2236,7 +2236,6 @@ class InventoryUpdateSerializer(UnifiedJobSerializer, InventorySourceOptionsSeri
|
||||
'source_project_update',
|
||||
'custom_virtualenv',
|
||||
'instance_group',
|
||||
'-controller_node',
|
||||
)
|
||||
|
||||
def get_related(self, obj):
|
||||
@@ -2311,7 +2310,6 @@ class InventoryUpdateDetailSerializer(InventoryUpdateSerializer):
|
||||
class InventoryUpdateListSerializer(InventoryUpdateSerializer, UnifiedJobListSerializer):
|
||||
class Meta:
|
||||
model = InventoryUpdate
|
||||
fields = ('*', '-controller_node') # field removal undone by UJ serializer
|
||||
|
||||
|
||||
class InventoryUpdateCancelSerializer(InventoryUpdateSerializer):
|
||||
@@ -4480,7 +4478,10 @@ class NotificationTemplateSerializer(BaseSerializer):
|
||||
body = messages[event].get('body', {})
|
||||
if body:
|
||||
try:
|
||||
potential_body = json.loads(body)
|
||||
rendered_body = (
|
||||
sandbox.ImmutableSandboxedEnvironment(undefined=DescriptiveUndefined).from_string(body).render(JobNotificationMixin.context_stub())
|
||||
)
|
||||
potential_body = json.loads(rendered_body)
|
||||
if not isinstance(potential_body, dict):
|
||||
error_list.append(
|
||||
_("Webhook body for '{}' should be a json dictionary. Found type '{}'.".format(event, type(potential_body).__name__))
|
||||
@@ -4683,8 +4684,14 @@ class SchedulePreviewSerializer(BaseSerializer):
|
||||
class ScheduleSerializer(LaunchConfigurationBaseSerializer, SchedulePreviewSerializer):
|
||||
show_capabilities = ['edit', 'delete']
|
||||
|
||||
timezone = serializers.SerializerMethodField()
|
||||
until = serializers.SerializerMethodField()
|
||||
timezone = serializers.SerializerMethodField(
|
||||
help_text=_(
|
||||
'The timezone this schedule runs in. This field is extracted from the RRULE. If the timezone in the RRULE is a link to another timezone, the link will be reflected in this field.'
|
||||
),
|
||||
)
|
||||
until = serializers.SerializerMethodField(
|
||||
help_text=_('The date this schedule will end. This field is computed from the RRULE. If the schedule does not end an emptry string will be returned'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Schedule
|
||||
|
||||
@@ -578,8 +578,7 @@ class ScheduleZoneInfo(APIView):
|
||||
swagger_topic = 'System Configuration'
|
||||
|
||||
def get(self, request):
|
||||
zones = [{'name': zone} for zone in models.Schedule.get_zoneinfo()]
|
||||
return Response(zones)
|
||||
return Response({'zones': models.Schedule.get_zoneinfo(), 'links': models.Schedule.get_zoneinfo_links()})
|
||||
|
||||
|
||||
class LaunchConfigCredentialsBase(SubListAttachDetachAPIView):
|
||||
@@ -3850,7 +3849,7 @@ class JobJobEventsChildrenSummary(APIView):
|
||||
meta_events = ('debug', 'verbose', 'warning', 'error', 'system_warning', 'deprecated')
|
||||
|
||||
def get(self, request, **kwargs):
|
||||
resp = dict(children_summary={}, meta_event_nested_uuid={}, event_processing_finished=False)
|
||||
resp = dict(children_summary={}, meta_event_nested_uuid={}, event_processing_finished=False, is_tree=True)
|
||||
job = get_object_or_404(models.Job, pk=kwargs['pk'])
|
||||
if not job.event_processing_finished:
|
||||
return Response(resp)
|
||||
@@ -3870,13 +3869,41 @@ class JobJobEventsChildrenSummary(APIView):
|
||||
# key is counter of meta events (i.e. verbose), value is uuid of the assigned parent
|
||||
map_meta_counter_nested_uuid = {}
|
||||
|
||||
# collapsable tree view in the UI only makes sense for tree-like
|
||||
# hierarchy. If ansible is ran with a strategy like free or host_pinned, then
|
||||
# events can be out of sequential order, and no longer follow a tree structure
|
||||
# E1
|
||||
# E2
|
||||
# E3
|
||||
# E4 <- parent is E3
|
||||
# E5 <- parent is E1
|
||||
# in the above, there is no clear way to collapse E1, because E5 comes after
|
||||
# E3, which occurs after E1. Thus the tree view should be disabled.
|
||||
|
||||
# mark the last seen uuid at a given level (0-3)
|
||||
# if a parent uuid is not in this list, then we know the events are not tree-like
|
||||
# and return a response with is_tree: False
|
||||
level_current_uuid = [None, None, None, None]
|
||||
|
||||
prev_non_meta_event = events[0]
|
||||
for i, e in enumerate(events):
|
||||
if not e['event'] in JobJobEventsChildrenSummary.meta_events:
|
||||
prev_non_meta_event = e
|
||||
if not e['uuid']:
|
||||
continue
|
||||
|
||||
if not e['event'] in JobJobEventsChildrenSummary.meta_events:
|
||||
level = models.JobEvent.LEVEL_FOR_EVENT[e['event']]
|
||||
level_current_uuid[level] = e['uuid']
|
||||
# if setting level 1, for example, set levels 2 and 3 back to None
|
||||
for u in range(level + 1, len(level_current_uuid)):
|
||||
level_current_uuid[u] = None
|
||||
|
||||
puuid = e['parent_uuid']
|
||||
if puuid and puuid not in level_current_uuid:
|
||||
# improper tree detected, so bail out early
|
||||
resp['is_tree'] = False
|
||||
return Response(resp)
|
||||
|
||||
# if event is verbose (or debug, etc), we need to "assign" it a
|
||||
# parent. This code looks at the event level of the previous
|
||||
|
||||
@@ -204,7 +204,7 @@ class GitlabWebhookReceiver(WebhookReceiverBase):
|
||||
return h.hexdigest()
|
||||
|
||||
def get_event_status_api(self):
|
||||
if self.get_event_type() != 'Merge Request Hook':
|
||||
if self.get_event_type() not in self.ref_keys.keys():
|
||||
return
|
||||
project = self.request.data.get('project', {})
|
||||
repo_url = project.get('web_url')
|
||||
|
||||
@@ -12,8 +12,6 @@ from django.contrib.sessions.models import Session
|
||||
from django.utils.timezone import now, timedelta
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from psycopg2.errors import UntranslatableCharacter
|
||||
|
||||
from awx.conf.license import get_license
|
||||
from awx.main.utils import get_awx_version, camelcase_to_underscore, datetime_hook
|
||||
from awx.main import models
|
||||
@@ -378,10 +376,7 @@ def _events_table(since, full_path, until, tbl, where_column, project_job_create
|
||||
WHERE ({tbl}.{where_column} > '{since.isoformat()}' AND {tbl}.{where_column} <= '{until.isoformat()}')) TO STDOUT WITH CSV HEADER'''
|
||||
return query
|
||||
|
||||
try:
|
||||
return _copy_table(table='events', query=query(f"{tbl}.event_data::jsonb"), path=full_path)
|
||||
except UntranslatableCharacter:
|
||||
return _copy_table(table='events', query=query(f"replace({tbl}.event_data::text, '\\u0000', '')::jsonb"), path=full_path)
|
||||
return _copy_table(table='events', query=query(fr"replace({tbl}.event_data, '\u', '\u005cu')::jsonb"), path=full_path)
|
||||
|
||||
|
||||
@register('events_table', '1.5', format='csv', description=_('Automation task records'), expensive=four_hour_slicing)
|
||||
|
||||
@@ -8,7 +8,7 @@ from django.apps import apps
|
||||
from awx.main.consumers import emit_channel_notification
|
||||
|
||||
root_key = 'awx_metrics'
|
||||
logger = logging.getLogger('awx.main.wsbroadcast')
|
||||
logger = logging.getLogger('awx.main.analytics')
|
||||
|
||||
|
||||
class BaseM:
|
||||
@@ -16,16 +16,22 @@ class BaseM:
|
||||
self.field = field
|
||||
self.help_text = help_text
|
||||
self.current_value = 0
|
||||
self.metric_has_changed = False
|
||||
|
||||
def clear_value(self, conn):
|
||||
def reset_value(self, conn):
|
||||
conn.hset(root_key, self.field, 0)
|
||||
self.current_value = 0
|
||||
|
||||
def inc(self, value):
|
||||
self.current_value += value
|
||||
self.metric_has_changed = True
|
||||
|
||||
def set(self, value):
|
||||
self.current_value = value
|
||||
self.metric_has_changed = True
|
||||
|
||||
def get(self):
|
||||
return self.current_value
|
||||
|
||||
def decode(self, conn):
|
||||
value = conn.hget(root_key, self.field)
|
||||
@@ -34,7 +40,9 @@ class BaseM:
|
||||
def to_prometheus(self, instance_data):
|
||||
output_text = f"# HELP {self.field} {self.help_text}\n# TYPE {self.field} gauge\n"
|
||||
for instance in instance_data:
|
||||
output_text += f'{self.field}{{node="{instance}"}} {instance_data[instance][self.field]}\n'
|
||||
if self.field in instance_data[instance]:
|
||||
# on upgrade, if there are stale instances, we can end up with issues where new metrics are not present
|
||||
output_text += f'{self.field}{{node="{instance}"}} {instance_data[instance][self.field]}\n'
|
||||
return output_text
|
||||
|
||||
|
||||
@@ -46,8 +54,10 @@ class FloatM(BaseM):
|
||||
return 0.0
|
||||
|
||||
def store_value(self, conn):
|
||||
conn.hincrbyfloat(root_key, self.field, self.current_value)
|
||||
self.current_value = 0
|
||||
if self.metric_has_changed:
|
||||
conn.hincrbyfloat(root_key, self.field, self.current_value)
|
||||
self.current_value = 0
|
||||
self.metric_has_changed = False
|
||||
|
||||
|
||||
class IntM(BaseM):
|
||||
@@ -58,8 +68,10 @@ class IntM(BaseM):
|
||||
return 0
|
||||
|
||||
def store_value(self, conn):
|
||||
conn.hincrby(root_key, self.field, self.current_value)
|
||||
self.current_value = 0
|
||||
if self.metric_has_changed:
|
||||
conn.hincrby(root_key, self.field, self.current_value)
|
||||
self.current_value = 0
|
||||
self.metric_has_changed = False
|
||||
|
||||
|
||||
class SetIntM(BaseM):
|
||||
@@ -70,10 +82,9 @@ class SetIntM(BaseM):
|
||||
return 0
|
||||
|
||||
def store_value(self, conn):
|
||||
# do not set value if it has not changed since last time this was called
|
||||
if self.current_value is not None:
|
||||
if self.metric_has_changed:
|
||||
conn.hset(root_key, self.field, self.current_value)
|
||||
self.current_value = None
|
||||
self.metric_has_changed = False
|
||||
|
||||
|
||||
class SetFloatM(SetIntM):
|
||||
@@ -94,13 +105,13 @@ class HistogramM(BaseM):
|
||||
self.sum = IntM(field + '_sum', '')
|
||||
super(HistogramM, self).__init__(field, help_text)
|
||||
|
||||
def clear_value(self, conn):
|
||||
def reset_value(self, conn):
|
||||
conn.hset(root_key, self.field, 0)
|
||||
self.inf.clear_value(conn)
|
||||
self.sum.clear_value(conn)
|
||||
self.inf.reset_value(conn)
|
||||
self.sum.reset_value(conn)
|
||||
for b in self.buckets_to_keys.values():
|
||||
b.clear_value(conn)
|
||||
super(HistogramM, self).clear_value(conn)
|
||||
b.reset_value(conn)
|
||||
super(HistogramM, self).reset_value(conn)
|
||||
|
||||
def observe(self, value):
|
||||
for b in self.buckets:
|
||||
@@ -136,7 +147,7 @@ class HistogramM(BaseM):
|
||||
|
||||
|
||||
class Metrics:
|
||||
def __init__(self, auto_pipe_execute=True, instance_name=None):
|
||||
def __init__(self, auto_pipe_execute=False, instance_name=None):
|
||||
self.pipe = redis.Redis.from_url(settings.BROKER_URL).pipeline()
|
||||
self.conn = redis.Redis.from_url(settings.BROKER_URL)
|
||||
self.last_pipe_execute = time.time()
|
||||
@@ -152,6 +163,8 @@ class Metrics:
|
||||
Instance = apps.get_model('main', 'Instance')
|
||||
if instance_name:
|
||||
self.instance_name = instance_name
|
||||
elif settings.IS_TESTING():
|
||||
self.instance_name = "awx_testing"
|
||||
else:
|
||||
self.instance_name = Instance.objects.me().hostname
|
||||
|
||||
@@ -161,15 +174,29 @@ class Metrics:
|
||||
IntM('callback_receiver_events_popped_redis', 'Number of events popped from redis'),
|
||||
IntM('callback_receiver_events_in_memory', 'Current number of events in memory (in transfer from redis to db)'),
|
||||
IntM('callback_receiver_batch_events_errors', 'Number of times batch insertion failed'),
|
||||
FloatM('callback_receiver_events_insert_db_seconds', 'Time spent saving events to database'),
|
||||
FloatM('callback_receiver_events_insert_db_seconds', 'Total time spent saving events to database'),
|
||||
IntM('callback_receiver_events_insert_db', 'Number of events batch inserted into database'),
|
||||
IntM('callback_receiver_events_broadcast', 'Number of events broadcast to other control plane nodes'),
|
||||
HistogramM(
|
||||
'callback_receiver_batch_events_insert_db', 'Number of events batch inserted into database', settings.SUBSYSTEM_METRICS_BATCH_INSERT_BUCKETS
|
||||
),
|
||||
SetFloatM('callback_receiver_event_processing_avg_seconds', 'Average processing time per event per callback receiver batch'),
|
||||
FloatM('subsystem_metrics_pipe_execute_seconds', 'Time spent saving metrics to redis'),
|
||||
IntM('subsystem_metrics_pipe_execute_calls', 'Number of calls to pipe_execute'),
|
||||
FloatM('subsystem_metrics_send_metrics_seconds', 'Time spent sending metrics to other nodes'),
|
||||
SetFloatM('task_manager_get_tasks_seconds', 'Time spent in loading all tasks from db'),
|
||||
SetFloatM('task_manager_start_task_seconds', 'Time spent starting task'),
|
||||
SetFloatM('task_manager_process_running_tasks_seconds', 'Time spent processing running tasks'),
|
||||
SetFloatM('task_manager_process_pending_tasks_seconds', 'Time spent processing pending tasks'),
|
||||
SetFloatM('task_manager_generate_dependencies_seconds', 'Time spent generating dependencies for pending tasks'),
|
||||
SetFloatM('task_manager_spawn_workflow_graph_jobs_seconds', 'Time spent spawning workflow jobs'),
|
||||
SetFloatM('task_manager__schedule_seconds', 'Time spent in running the entire _schedule'),
|
||||
IntM('task_manager_schedule_calls', 'Number of calls to task manager schedule'),
|
||||
SetFloatM('task_manager_recorded_timestamp', 'Unix timestamp when metrics were last recorded'),
|
||||
SetIntM('task_manager_tasks_started', 'Number of tasks started'),
|
||||
SetIntM('task_manager_running_processed', 'Number of running tasks processed'),
|
||||
SetIntM('task_manager_pending_processed', 'Number of pending tasks processed'),
|
||||
SetIntM('task_manager_tasks_blocked', 'Number of tasks blocked from running'),
|
||||
]
|
||||
# turn metric list into dictionary with the metric name as a key
|
||||
self.METRICS = {}
|
||||
@@ -179,9 +206,11 @@ class Metrics:
|
||||
# track last time metrics were sent to other nodes
|
||||
self.previous_send_metrics = SetFloatM('send_metrics_time', 'Timestamp of previous send_metrics call')
|
||||
|
||||
def clear_values(self):
|
||||
def reset_values(self):
|
||||
# intended to be called once on app startup to reset all metric
|
||||
# values to 0
|
||||
for m in self.METRICS.values():
|
||||
m.clear_value(self.conn)
|
||||
m.reset_value(self.conn)
|
||||
self.metrics_have_changed = True
|
||||
self.conn.delete(root_key + "_lock")
|
||||
|
||||
@@ -189,19 +218,25 @@ class Metrics:
|
||||
if value != 0:
|
||||
self.METRICS[field].inc(value)
|
||||
self.metrics_have_changed = True
|
||||
if self.auto_pipe_execute is True and self.should_pipe_execute() is True:
|
||||
if self.auto_pipe_execute is True:
|
||||
self.pipe_execute()
|
||||
|
||||
def set(self, field, value):
|
||||
self.METRICS[field].set(value)
|
||||
self.metrics_have_changed = True
|
||||
if self.auto_pipe_execute is True and self.should_pipe_execute() is True:
|
||||
if self.auto_pipe_execute is True:
|
||||
self.pipe_execute()
|
||||
|
||||
def get(self, field):
|
||||
return self.METRICS[field].get()
|
||||
|
||||
def decode(self, field):
|
||||
return self.METRICS[field].decode(self.conn)
|
||||
|
||||
def observe(self, field, value):
|
||||
self.METRICS[field].observe(value)
|
||||
self.metrics_have_changed = True
|
||||
if self.auto_pipe_execute is True and self.should_pipe_execute() is True:
|
||||
if self.auto_pipe_execute is True:
|
||||
self.pipe_execute()
|
||||
|
||||
def serialize_local_metrics(self):
|
||||
@@ -249,8 +284,8 @@ class Metrics:
|
||||
|
||||
def send_metrics(self):
|
||||
# more than one thread could be calling this at the same time, so should
|
||||
# get acquire redis lock before sending metrics
|
||||
lock = self.conn.lock(root_key + '_lock', thread_local=False)
|
||||
# acquire redis lock before sending metrics
|
||||
lock = self.conn.lock(root_key + '_lock')
|
||||
if not lock.acquire(blocking=False):
|
||||
return
|
||||
try:
|
||||
|
||||
@@ -4,6 +4,7 @@ import os
|
||||
import signal
|
||||
import time
|
||||
import traceback
|
||||
import datetime
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.functional import cached_property
|
||||
@@ -151,12 +152,17 @@ class CallbackBrokerWorker(BaseWorker):
|
||||
metrics_singular_events_saved = 0
|
||||
metrics_events_batch_save_errors = 0
|
||||
metrics_events_broadcast = 0
|
||||
metrics_events_missing_created = 0
|
||||
metrics_total_job_event_processing_seconds = datetime.timedelta(seconds=0)
|
||||
for cls, events in self.buff.items():
|
||||
logger.debug(f'{cls.__name__}.objects.bulk_create({len(events)})')
|
||||
for e in events:
|
||||
e.modified = now # this can be set before created because now is set above on line 149
|
||||
if not e.created:
|
||||
e.created = now
|
||||
e.modified = now
|
||||
metrics_events_missing_created += 1
|
||||
else: # only calculate the seconds if the created time already has been set
|
||||
metrics_total_job_event_processing_seconds += e.modified - e.created
|
||||
metrics_duration_to_save = time.perf_counter()
|
||||
try:
|
||||
cls.objects.bulk_create(events)
|
||||
@@ -189,6 +195,11 @@ class CallbackBrokerWorker(BaseWorker):
|
||||
self.subsystem_metrics.observe('callback_receiver_batch_events_insert_db', metrics_bulk_events_saved)
|
||||
self.subsystem_metrics.inc('callback_receiver_events_in_memory', -(metrics_bulk_events_saved + metrics_singular_events_saved))
|
||||
self.subsystem_metrics.inc('callback_receiver_events_broadcast', metrics_events_broadcast)
|
||||
self.subsystem_metrics.set(
|
||||
'callback_receiver_event_processing_avg_seconds',
|
||||
metrics_total_job_event_processing_seconds.total_seconds()
|
||||
/ (metrics_bulk_events_saved + metrics_singular_events_saved - metrics_events_missing_created),
|
||||
)
|
||||
if self.subsystem_metrics.should_pipe_execute() is True:
|
||||
self.subsystem_metrics.pipe_execute()
|
||||
|
||||
|
||||
@@ -32,8 +32,10 @@ class Command(BaseCommand):
|
||||
name='Demo Project',
|
||||
scm_type='git',
|
||||
scm_url='https://github.com/ansible/ansible-tower-samples',
|
||||
scm_update_on_launch=True,
|
||||
scm_update_cache_timeout=0,
|
||||
status='successful',
|
||||
scm_revision='347e44fea036c94d5f60e544de006453ee5c71ad',
|
||||
playbook_files=['hello_world.yml'],
|
||||
)
|
||||
|
||||
p.organization = o
|
||||
|
||||
23
awx/main/migrations/0163_convert_job_tags_to_textfield.py
Normal file
23
awx/main/migrations/0163_convert_job_tags_to_textfield.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.2.13 on 2022-06-02 18:15
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('main', '0162_alter_unifiedjob_dependent_jobs'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='job',
|
||||
name='job_tags',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='jobtemplate',
|
||||
name='job_tags',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
]
|
||||
@@ -130,8 +130,7 @@ class JobOptions(BaseModel):
|
||||
)
|
||||
)
|
||||
)
|
||||
job_tags = models.CharField(
|
||||
max_length=1024,
|
||||
job_tags = models.TextField(
|
||||
blank=True,
|
||||
default='',
|
||||
)
|
||||
|
||||
@@ -354,7 +354,7 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin, CustomVirtualEn
|
||||
# If update_fields has been specified, add our field names to it,
|
||||
# if it hasn't been specified, then we're just doing a normal save.
|
||||
update_fields = kwargs.get('update_fields', [])
|
||||
skip_update = bool(kwargs.pop('skip_update', False))
|
||||
self._skip_update = bool(kwargs.pop('skip_update', False))
|
||||
# Create auto-generated local path if project uses SCM.
|
||||
if self.pk and self.scm_type and not self.local_path.startswith('_'):
|
||||
slug_name = slugify(str(self.name)).replace(u'-', u'_')
|
||||
@@ -372,14 +372,16 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin, CustomVirtualEn
|
||||
from awx.main.signals import disable_activity_stream
|
||||
|
||||
with disable_activity_stream():
|
||||
self.save(update_fields=update_fields)
|
||||
self.save(update_fields=update_fields, skip_update=self._skip_update)
|
||||
# If we just created a new project with SCM, start the initial update.
|
||||
# also update if certain fields have changed
|
||||
relevant_change = any(pre_save_vals.get(fd_name, None) != self._prior_values_store.get(fd_name, None) for fd_name in self.FIELDS_TRIGGER_UPDATE)
|
||||
if (relevant_change or new_instance) and (not skip_update) and self.scm_type:
|
||||
if (relevant_change or new_instance) and (not self._skip_update) and self.scm_type:
|
||||
self.update()
|
||||
|
||||
def _get_current_status(self):
|
||||
if getattr(self, '_skip_update', False):
|
||||
return self.status
|
||||
if self.scm_type:
|
||||
if self.current_job and self.current_job.status:
|
||||
return self.current_job.status
|
||||
|
||||
@@ -85,9 +85,18 @@ class Schedule(PrimordialModel, LaunchTimeConfig):
|
||||
next_run = models.DateTimeField(null=True, default=None, editable=False, help_text=_("The next time that the scheduled action will run."))
|
||||
|
||||
@classmethod
|
||||
def get_zoneinfo(self):
|
||||
def get_zoneinfo(cls):
|
||||
return sorted(get_zonefile_instance().zones)
|
||||
|
||||
@classmethod
|
||||
def get_zoneinfo_links(cls):
|
||||
return_val = {}
|
||||
zone_instance = get_zonefile_instance()
|
||||
for zone_name in zone_instance.zones:
|
||||
if str(zone_name) != str(zone_instance.zones[zone_name]._filename):
|
||||
return_val[zone_name] = zone_instance.zones[zone_name]._filename
|
||||
return return_val
|
||||
|
||||
@property
|
||||
def timezone(self):
|
||||
utc = tzutc()
|
||||
|
||||
@@ -8,7 +8,6 @@ import redis
|
||||
|
||||
# Django
|
||||
from django.conf import settings
|
||||
import awx.main.analytics.subsystem_metrics as s_metrics
|
||||
|
||||
__all__ = ['CallbackQueueDispatcher']
|
||||
|
||||
@@ -28,7 +27,6 @@ class CallbackQueueDispatcher(object):
|
||||
self.queue = getattr(settings, 'CALLBACK_QUEUE', '')
|
||||
self.logger = logging.getLogger('awx.main.queue.CallbackQueueDispatcher')
|
||||
self.connection = redis.Redis.from_url(settings.BROKER_URL)
|
||||
self.subsystem_metrics = s_metrics.Metrics()
|
||||
|
||||
def dispatch(self, obj):
|
||||
self.connection.rpush(self.queue, json.dumps(obj, cls=AnsibleJSONEncoder))
|
||||
|
||||
@@ -6,6 +6,9 @@ from datetime import timedelta
|
||||
import logging
|
||||
import uuid
|
||||
import json
|
||||
import time
|
||||
import sys
|
||||
import signal
|
||||
|
||||
# Django
|
||||
from django.db import transaction, connection
|
||||
@@ -38,12 +41,24 @@ from awx.main.constants import ACTIVE_STATES
|
||||
from awx.main.scheduler.dependency_graph import DependencyGraph
|
||||
from awx.main.scheduler.task_manager_models import TaskManagerInstances
|
||||
from awx.main.scheduler.task_manager_models import TaskManagerInstanceGroups
|
||||
import awx.main.analytics.subsystem_metrics as s_metrics
|
||||
from awx.main.utils import decrypt_field
|
||||
|
||||
|
||||
logger = logging.getLogger('awx.main.scheduler')
|
||||
|
||||
|
||||
def timeit(func):
|
||||
def inner(*args, **kwargs):
|
||||
t_now = time.perf_counter()
|
||||
result = func(*args, **kwargs)
|
||||
dur = time.perf_counter() - t_now
|
||||
args[0].subsystem_metrics.inc("task_manager_" + func.__name__ + "_seconds", dur)
|
||||
return result
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
class TaskManager:
|
||||
def __init__(self):
|
||||
"""
|
||||
@@ -62,6 +77,13 @@ class TaskManager:
|
||||
# will no longer be started and will be started on the next task manager cycle.
|
||||
self.start_task_limit = settings.START_TASK_LIMIT
|
||||
self.time_delta_job_explanation = timedelta(seconds=30)
|
||||
self.subsystem_metrics = s_metrics.Metrics(auto_pipe_execute=False)
|
||||
# initialize each metric to 0 and force metric_has_changed to true. This
|
||||
# ensures each task manager metric will be overridden when pipe_execute
|
||||
# is called later.
|
||||
for m in self.subsystem_metrics.METRICS:
|
||||
if m.startswith("task_manager"):
|
||||
self.subsystem_metrics.set(m, 0)
|
||||
|
||||
def after_lock_init(self, all_sorted_tasks):
|
||||
"""
|
||||
@@ -100,6 +122,7 @@ class TaskManager:
|
||||
|
||||
return None
|
||||
|
||||
@timeit
|
||||
def get_tasks(self, status_list=('pending', 'waiting', 'running')):
|
||||
jobs = [j for j in Job.objects.filter(status__in=status_list).prefetch_related('instance_group')]
|
||||
inventory_updates_qs = (
|
||||
@@ -125,6 +148,7 @@ class TaskManager:
|
||||
inventory_ids.add(task.inventory_id)
|
||||
return [invsrc for invsrc in InventorySource.objects.filter(inventory_id__in=inventory_ids, update_on_launch=True)]
|
||||
|
||||
@timeit
|
||||
def spawn_workflow_graph_jobs(self, workflow_jobs):
|
||||
for workflow_job in workflow_jobs:
|
||||
if workflow_job.cancel_flag:
|
||||
@@ -231,7 +255,9 @@ class TaskManager:
|
||||
schedule_task_manager()
|
||||
return result
|
||||
|
||||
@timeit
|
||||
def start_task(self, task, instance_group, dependent_tasks=None, instance=None):
|
||||
self.subsystem_metrics.inc("task_manager_tasks_started", 1)
|
||||
self.start_task_limit -= 1
|
||||
if self.start_task_limit == 0:
|
||||
# schedule another run immediately after this task manager
|
||||
@@ -291,6 +317,7 @@ class TaskManager:
|
||||
task.websocket_emit_status(task.status) # adds to on_commit
|
||||
connection.on_commit(post_commit)
|
||||
|
||||
@timeit
|
||||
def process_running_tasks(self, running_tasks):
|
||||
for task in running_tasks:
|
||||
self.dependency_graph.add_job(task)
|
||||
@@ -439,6 +466,7 @@ class TaskManager:
|
||||
latest_src_project_update.scm_inventory_updates.add(inventory_task)
|
||||
return created_dependencies
|
||||
|
||||
@timeit
|
||||
def generate_dependencies(self, undeped_tasks):
|
||||
created_dependencies = []
|
||||
for task in undeped_tasks:
|
||||
@@ -453,6 +481,7 @@ class TaskManager:
|
||||
|
||||
return created_dependencies
|
||||
|
||||
@timeit
|
||||
def process_pending_tasks(self, pending_tasks):
|
||||
running_workflow_templates = {wf.unified_job_template_id for wf in self.get_running_workflow_jobs()}
|
||||
tasks_to_update_job_explanation = []
|
||||
@@ -461,6 +490,7 @@ class TaskManager:
|
||||
break
|
||||
blocked_by = self.job_blocked_by(task)
|
||||
if blocked_by:
|
||||
self.subsystem_metrics.inc("task_manager_tasks_blocked", 1)
|
||||
task.log_lifecycle("blocked", blocked_by=blocked_by)
|
||||
job_explanation = gettext_noop(f"waiting for {blocked_by._meta.model_name}-{blocked_by.id} to finish")
|
||||
if task.job_explanation != job_explanation:
|
||||
@@ -602,17 +632,22 @@ class TaskManager:
|
||||
|
||||
def process_tasks(self, all_sorted_tasks):
|
||||
running_tasks = [t for t in all_sorted_tasks if t.status in ['waiting', 'running']]
|
||||
|
||||
self.process_running_tasks(running_tasks)
|
||||
self.subsystem_metrics.inc("task_manager_running_processed", len(running_tasks))
|
||||
|
||||
pending_tasks = [t for t in all_sorted_tasks if t.status == 'pending']
|
||||
|
||||
undeped_tasks = [t for t in pending_tasks if not t.dependencies_processed]
|
||||
dependencies = self.generate_dependencies(undeped_tasks)
|
||||
deps_of_deps = self.generate_dependencies(dependencies)
|
||||
dependencies += deps_of_deps
|
||||
self.process_pending_tasks(dependencies)
|
||||
self.process_pending_tasks(pending_tasks)
|
||||
self.subsystem_metrics.inc("task_manager_pending_processed", len(dependencies))
|
||||
|
||||
self.process_pending_tasks(pending_tasks)
|
||||
self.subsystem_metrics.inc("task_manager_pending_processed", len(pending_tasks))
|
||||
|
||||
@timeit
|
||||
def _schedule(self):
|
||||
finished_wfjs = []
|
||||
all_sorted_tasks = self.get_tasks()
|
||||
@@ -648,6 +683,28 @@ class TaskManager:
|
||||
self.process_tasks(all_sorted_tasks)
|
||||
return finished_wfjs
|
||||
|
||||
def record_aggregate_metrics(self, *args):
|
||||
if not settings.IS_TESTING():
|
||||
# increment task_manager_schedule_calls regardless if the other
|
||||
# metrics are recorded
|
||||
s_metrics.Metrics(auto_pipe_execute=True).inc("task_manager_schedule_calls", 1)
|
||||
# Only record metrics if the last time recording was more
|
||||
# than SUBSYSTEM_METRICS_TASK_MANAGER_RECORD_INTERVAL ago.
|
||||
# Prevents a short-duration task manager that runs directly after a
|
||||
# long task manager to override useful metrics.
|
||||
current_time = time.time()
|
||||
time_last_recorded = current_time - self.subsystem_metrics.decode("task_manager_recorded_timestamp")
|
||||
if time_last_recorded > settings.SUBSYSTEM_METRICS_TASK_MANAGER_RECORD_INTERVAL:
|
||||
logger.debug(f"recording metrics, last recorded {time_last_recorded} seconds ago")
|
||||
self.subsystem_metrics.set("task_manager_recorded_timestamp", current_time)
|
||||
self.subsystem_metrics.pipe_execute()
|
||||
else:
|
||||
logger.debug(f"skipping recording metrics, last recorded {time_last_recorded} seconds ago")
|
||||
|
||||
def record_aggregate_metrics_and_exit(self, *args):
|
||||
self.record_aggregate_metrics()
|
||||
sys.exit(1)
|
||||
|
||||
def schedule(self):
|
||||
# Lock
|
||||
with advisory_lock('task_manager_lock', wait=False) as acquired:
|
||||
@@ -657,5 +714,8 @@ class TaskManager:
|
||||
return
|
||||
logger.debug("Starting Scheduler")
|
||||
with task_manager_bulk_reschedule():
|
||||
# if sigterm due to timeout, still record metrics
|
||||
signal.signal(signal.SIGTERM, self.record_aggregate_metrics_and_exit)
|
||||
self._schedule()
|
||||
self.record_aggregate_metrics()
|
||||
logger.debug("Finishing Scheduler")
|
||||
|
||||
@@ -103,7 +103,8 @@ def dispatch_startup():
|
||||
#
|
||||
apply_cluster_membership_policies()
|
||||
cluster_node_heartbeat()
|
||||
Metrics().clear_values()
|
||||
m = Metrics()
|
||||
m.reset_values()
|
||||
|
||||
# Update Tower's rsyslog.conf file based on loggins settings in the db
|
||||
reconfigure_rsyslog()
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
---
|
||||
- ansible.builtin.import_playbook: foo
|
||||
@@ -0,0 +1,2 @@
|
||||
---
|
||||
- ansible.builtin.include: foo
|
||||
@@ -70,11 +70,11 @@ def test_job_job_events_children_summary(get, organization_factory, job_template
|
||||
job_id=job.pk, uuid='uuid2', parent_uuid='uuid1', event="playbook_on_play_start", counter=2, stdout='a' * 1024, job_created=job.created
|
||||
).save()
|
||||
JobEvent.create_from_data(
|
||||
job_id=job.pk, uuid='uuid3', parent_uuid='uuid2', event="runner_on_start", counter=3, stdout='a' * 1024, job_created=job.created
|
||||
job_id=job.pk, uuid='uuid3', parent_uuid='uuid2', event="playbook_on_task_start", counter=3, stdout='a' * 1024, job_created=job.created
|
||||
).save()
|
||||
JobEvent.create_from_data(job_id=job.pk, uuid='uuid4', parent_uuid='', event='verbose', counter=4, stdout='a' * 1024, job_created=job.created).save()
|
||||
JobEvent.create_from_data(
|
||||
job_id=job.pk, uuid='uuid5', parent_uuid='uuid1', event="playbook_on_task_start", counter=5, stdout='a' * 1024, job_created=job.created
|
||||
job_id=job.pk, uuid='uuid5', parent_uuid='uuid1', event="playbook_on_play_start", counter=5, stdout='a' * 1024, job_created=job.created
|
||||
).save()
|
||||
job.emitted_events = job.get_event_queryset().count()
|
||||
job.status = "successful"
|
||||
@@ -84,3 +84,50 @@ def test_job_job_events_children_summary(get, organization_factory, job_template
|
||||
assert response.data["children_summary"] == {1: {"rowNumber": 0, "numChildren": 4}, 2: {"rowNumber": 1, "numChildren": 2}}
|
||||
assert response.data["meta_event_nested_uuid"] == {4: "uuid2"}
|
||||
assert response.data["event_processing_finished"] == True
|
||||
assert response.data["is_tree"] == True
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_job_job_events_children_summary_is_tree(get, organization_factory, job_template_factory):
|
||||
'''
|
||||
children_summary should return {is_tree: False} if the event structure is not tree-like
|
||||
'''
|
||||
objs = organization_factory("org", superusers=['admin'])
|
||||
jt = job_template_factory("jt", organization=objs.organization, inventory='test_inv', project='test_proj').job_template
|
||||
job = jt.create_unified_job()
|
||||
url = reverse('api:job_job_events_children_summary', kwargs={'pk': job.pk})
|
||||
response = get(url, user=objs.superusers.admin, expect=200)
|
||||
assert response.data["event_processing_finished"] == False
|
||||
'''
|
||||
E1
|
||||
E2
|
||||
E3
|
||||
E4 (verbose)
|
||||
E5
|
||||
E6 <-- parent is E2, but comes after another "branch" E5
|
||||
'''
|
||||
JobEvent.create_from_data(
|
||||
job_id=job.pk, uuid='uuid1', parent_uuid='', event="playbook_on_start", counter=1, stdout='a' * 1024, job_created=job.created
|
||||
).save()
|
||||
JobEvent.create_from_data(
|
||||
job_id=job.pk, uuid='uuid2', parent_uuid='uuid1', event="playbook_on_play_start", counter=2, stdout='a' * 1024, job_created=job.created
|
||||
).save()
|
||||
JobEvent.create_from_data(
|
||||
job_id=job.pk, uuid='uuid3', parent_uuid='uuid2', event="playbook_on_task_start", counter=3, stdout='a' * 1024, job_created=job.created
|
||||
).save()
|
||||
JobEvent.create_from_data(job_id=job.pk, uuid='uuid4', parent_uuid='', event='verbose', counter=4, stdout='a' * 1024, job_created=job.created).save()
|
||||
JobEvent.create_from_data(
|
||||
job_id=job.pk, uuid='uuid5', parent_uuid='uuid1', event="playbook_on_play_start", counter=5, stdout='a' * 1024, job_created=job.created
|
||||
).save()
|
||||
JobEvent.create_from_data(
|
||||
job_id=job.pk, uuid='uuid6', parent_uuid='uuid2', event="playbook_on_task_start", counter=6, stdout='a' * 1024, job_created=job.created
|
||||
).save()
|
||||
job.emitted_events = job.get_event_queryset().count()
|
||||
job.status = "successful"
|
||||
job.save()
|
||||
url = reverse('api:job_job_events_children_summary', kwargs={'pk': job.pk})
|
||||
response = get(url, user=objs.superusers.admin, expect=200)
|
||||
assert response.data["children_summary"] == {}
|
||||
assert response.data["meta_event_nested_uuid"] == {}
|
||||
assert response.data["event_processing_finished"] == True
|
||||
assert response.data["is_tree"] == False
|
||||
|
||||
@@ -220,7 +220,7 @@ class TestControllerNode:
|
||||
assert 'controller_node' not in r.data
|
||||
|
||||
r = get(reverse('api:inventory_update_detail', kwargs={'pk': inventory_update.pk}), admin_user, expect=200)
|
||||
assert 'controller_node' not in r.data
|
||||
assert 'controller_node' in r.data
|
||||
|
||||
r = get(reverse('api:system_job_detail', kwargs={'pk': system_job.pk}), admin_user, expect=200)
|
||||
assert 'controller_node' not in r.data
|
||||
|
||||
@@ -500,7 +500,7 @@ def test_complex_schedule(post, admin_user, rrule, expected_result):
|
||||
def test_zoneinfo(get, admin_user):
|
||||
url = reverse('api:schedule_zoneinfo')
|
||||
r = get(url, admin_user, expect=200)
|
||||
assert {'name': 'America/New_York'} in r.data
|
||||
assert 'America/New_York' in r.data['zones']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
||||
@@ -4,6 +4,7 @@ import json
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import fcntl
|
||||
from unittest import mock
|
||||
@@ -36,12 +37,23 @@ from awx.main.models.credential import HIDDEN_PASSWORD, ManagedCredentialType
|
||||
from awx.main.tasks import jobs, system
|
||||
from awx.main.utils import encrypt_field, encrypt_value
|
||||
from awx.main.utils.safe_yaml import SafeLoader
|
||||
from awx.main.utils.execution_environments import CONTAINER_ROOT, to_host_path
|
||||
from awx.main.utils.execution_environments import CONTAINER_ROOT
|
||||
|
||||
from awx.main.utils.licensing import Licenser
|
||||
from awx.main.constants import JOB_VARIABLE_PREFIXES
|
||||
|
||||
|
||||
def to_host_path(path, private_data_dir):
|
||||
"""Given a path inside of the EE container, this gives the absolute path
|
||||
on the host machine within the private_data_dir
|
||||
"""
|
||||
if not os.path.isabs(private_data_dir):
|
||||
raise RuntimeError('The private_data_dir path must be absolute')
|
||||
if CONTAINER_ROOT != path and Path(CONTAINER_ROOT) not in Path(path).resolve().parents:
|
||||
raise RuntimeError(f'Cannot convert path {path} unless it is a subdir of {CONTAINER_ROOT}')
|
||||
return path.replace(CONTAINER_ROOT, private_data_dir, 1)
|
||||
|
||||
|
||||
class TestJobExecution(object):
|
||||
EXAMPLE_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nxyz==\n-----END PRIVATE KEY-----'
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import shutil
|
||||
import os
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from awx.main.utils.execution_environments import to_container_path, to_host_path
|
||||
from awx.main.utils.execution_environments import to_container_path
|
||||
|
||||
|
||||
private_data_dir = '/tmp/pdd_iso/awx_xxx'
|
||||
@@ -10,26 +14,33 @@ private_data_dir = '/tmp/pdd_iso/awx_xxx'
|
||||
'container_path,host_path',
|
||||
[
|
||||
('/runner', private_data_dir),
|
||||
('/runner/foo', '{0}/foo'.format(private_data_dir)),
|
||||
('/runner/foo/bar', '{0}/foo/bar'.format(private_data_dir)),
|
||||
('/runner{0}'.format(private_data_dir), '{0}{0}'.format(private_data_dir)),
|
||||
('/runner/foo', f'{private_data_dir}/foo'),
|
||||
('/runner', f'{private_data_dir}/foobar/..'), # private_data_dir path needs to be resolved
|
||||
('/runner/bar', f'{private_data_dir}/bar/foo/..'),
|
||||
('/runner/foo/bar', f'{private_data_dir}/foo/bar'),
|
||||
(f'/runner{private_data_dir}', f'{private_data_dir}{private_data_dir}'),
|
||||
],
|
||||
)
|
||||
def test_switch_paths(container_path, host_path):
|
||||
assert to_container_path(host_path, private_data_dir) == container_path
|
||||
assert to_host_path(container_path, private_data_dir) == host_path
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'container_path',
|
||||
[
|
||||
('/foobar'),
|
||||
('/runner/..'),
|
||||
],
|
||||
)
|
||||
def test_invalid_container_path(container_path):
|
||||
with pytest.raises(RuntimeError):
|
||||
to_host_path(container_path, private_data_dir)
|
||||
def test_symlink_isolation_dir(request):
|
||||
rand_str = str(uuid4())[:8]
|
||||
dst_path = f'/tmp/ee_{rand_str}_symlink_dst'
|
||||
src_path = f'/tmp/ee_{rand_str}_symlink_src'
|
||||
|
||||
def remove_folders():
|
||||
os.unlink(dst_path)
|
||||
shutil.rmtree(src_path)
|
||||
|
||||
request.addfinalizer(remove_folders)
|
||||
os.mkdir(src_path)
|
||||
os.symlink(src_path, dst_path)
|
||||
|
||||
pdd = f'{dst_path}/awx_xxx'
|
||||
|
||||
assert to_container_path(f'{pdd}/env/tmp1234', pdd) == '/runner/env/tmp1234'
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -17,7 +17,7 @@ logger = logging.getLogger('awx.main.utils.ansible')
|
||||
__all__ = ['skip_directory', 'could_be_playbook', 'could_be_inventory']
|
||||
|
||||
|
||||
valid_playbook_re = re.compile(r'^\s*?-?\s*?(?:hosts|include|import_playbook):\s*?.*?$')
|
||||
valid_playbook_re = re.compile(r'^\s*?-?\s*?(?:hosts|(ansible\.builtin\.)?include|(ansible\.builtin\.)?import_playbook):\s*?.*?$')
|
||||
valid_inventory_re = re.compile(r'^[a-zA-Z0-9_.=\[\]]')
|
||||
|
||||
|
||||
|
||||
@@ -58,17 +58,9 @@ def to_container_path(path, private_data_dir):
|
||||
"""
|
||||
if not os.path.isabs(private_data_dir):
|
||||
raise RuntimeError('The private_data_dir path must be absolute')
|
||||
if private_data_dir != path and Path(private_data_dir) not in Path(path).resolve().parents:
|
||||
raise RuntimeError(f'Cannot convert path {path} unless it is a subdir of {private_data_dir}')
|
||||
return path.replace(private_data_dir, CONTAINER_ROOT, 1)
|
||||
|
||||
|
||||
def to_host_path(path, private_data_dir):
|
||||
"""Given a path inside of the EE container, this gives the absolute path
|
||||
on the host machine within the private_data_dir
|
||||
"""
|
||||
if not os.path.isabs(private_data_dir):
|
||||
raise RuntimeError('The private_data_dir path must be absolute')
|
||||
if CONTAINER_ROOT != path and Path(CONTAINER_ROOT) not in Path(path).resolve().parents:
|
||||
raise RuntimeError(f'Cannot convert path {path} unless it is a subdir of {CONTAINER_ROOT}')
|
||||
return path.replace(CONTAINER_ROOT, private_data_dir, 1)
|
||||
# due to how tempfile.mkstemp works, we are probably passed a resolved path, but unresolved private_data_dir
|
||||
resolved_path = Path(path).resolve()
|
||||
resolved_pdd = Path(private_data_dir).resolve()
|
||||
if resolved_pdd != resolved_path and resolved_pdd not in resolved_path.parents:
|
||||
raise RuntimeError(f'Cannot convert path {resolved_path} unless it is a subdir of {resolved_pdd}')
|
||||
return str(resolved_path).replace(str(resolved_pdd), CONTAINER_ROOT, 1)
|
||||
|
||||
@@ -241,6 +241,10 @@ SUBSYSTEM_METRICS_INTERVAL_SEND_METRICS = 3
|
||||
# Interval in seconds for saving local metrics to redis
|
||||
SUBSYSTEM_METRICS_INTERVAL_SAVE_TO_REDIS = 2
|
||||
|
||||
# Record task manager metrics at the following interval in seconds
|
||||
# If using Prometheus, it is recommended to be => the Prometheus scrape interval
|
||||
SUBSYSTEM_METRICS_TASK_MANAGER_RECORD_INTERVAL = 15
|
||||
|
||||
# The maximum allowed jobs to start on a given task manager cycle
|
||||
START_TASK_LIMIT = 100
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
},
|
||||
"babelOptions": {
|
||||
"presets": ["@babel/preset-react"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"plugins": ["react-hooks", "jsx-a11y", "i18next", "@babel"],
|
||||
"extends": [
|
||||
@@ -96,9 +96,18 @@
|
||||
"modifier",
|
||||
"data-cy",
|
||||
"fieldName",
|
||||
"splitButtonVariant"
|
||||
"splitButtonVariant",
|
||||
"pageKey"
|
||||
],
|
||||
"ignore": [
|
||||
"Ansible",
|
||||
"Tower",
|
||||
"JSON",
|
||||
"YAML",
|
||||
"lg",
|
||||
"hh:mm AM/PM",
|
||||
"Twilio"
|
||||
],
|
||||
"ignore": ["Ansible", "Tower", "JSON", "YAML", "lg", "hh:mm AM/PM", "Twilio"],
|
||||
"ignoreComponent": [
|
||||
"AboutModal",
|
||||
"code",
|
||||
@@ -139,7 +148,7 @@
|
||||
"object-curly-newline": "off",
|
||||
"no-trailing-spaces": ["error"],
|
||||
"no-unused-expressions": ["error", { "allowShortCircuit": true }],
|
||||
"react/jsx-props-no-spreading":["off"],
|
||||
"react/jsx-props-no-spreading": ["off"],
|
||||
"react/prefer-stateless-function": "off",
|
||||
"react/prop-types": "off",
|
||||
"react/sort-comp": ["error", {}],
|
||||
|
||||
464
awx/ui/package-lock.json
generated
464
awx/ui/package-lock.json
generated
@@ -7,16 +7,17 @@
|
||||
"name": "ui",
|
||||
"dependencies": {
|
||||
"@lingui/react": "3.13.3",
|
||||
"@patternfly/patternfly": "4.194.4",
|
||||
"@patternfly/patternfly": "4.196.7",
|
||||
"@patternfly/react-core": "^4.201.0",
|
||||
"@patternfly/react-icons": "4.49.19",
|
||||
"@patternfly/react-table": "4.83.1",
|
||||
"ace-builds": "^1.5.1",
|
||||
"ace-builds": "^1.6.0",
|
||||
"ansi-to-html": "0.7.2",
|
||||
"axios": "0.22.0",
|
||||
"codemirror": "^5.65.4",
|
||||
"d3": "7.4.4",
|
||||
"dagre": "^0.8.4",
|
||||
"dompurify": "2.3.8",
|
||||
"formik": "2.2.9",
|
||||
"has-ansi": "5.0.1",
|
||||
"html-entities": "2.3.2",
|
||||
@@ -24,13 +25,12 @@
|
||||
"luxon": "^2.4.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "17.0.2",
|
||||
"react-ace": "^9.3.0",
|
||||
"react-ace": "^10.1.0",
|
||||
"react-dom": "17.0.2",
|
||||
"react-error-boundary": "^3.1.4",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-virtualized": "^9.21.1",
|
||||
"rrule": "2.6.4",
|
||||
"sanitize-html": "2.4.0",
|
||||
"rrule": "2.7.0",
|
||||
"styled-components": "5.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -3645,9 +3645,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@patternfly/patternfly": {
|
||||
"version": "4.194.4",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.194.4.tgz",
|
||||
"integrity": "sha512-SJxr502v0xXk1N5OiPLunD9pdKvHp5XXJLXcD5lIPrimjjUcy46m48X8YONjDvnC/Y5xV92UI2KxoCVucE34eA=="
|
||||
"version": "4.196.7",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.196.7.tgz",
|
||||
"integrity": "sha512-hA7Oww411e1p0/IXjC1I+4/1NNis9V+NVBxfUIpRwyuLbCIDHBdtMu2qAPLdKxXjuibV9EE6ZdlT7ra/kcFuJQ=="
|
||||
},
|
||||
"node_modules/@patternfly/react-core": {
|
||||
"version": "4.214.1",
|
||||
@@ -5166,9 +5166,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ace-builds": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.5.1.tgz",
|
||||
"integrity": "sha512-2G313uyM7lfqZgCs6xCW4QPeuX2GZKaCyRqKhTC2mBeZqC7TjkTXguKRyLzsAIMLJfj3koq98RXCBoemoZVAnQ=="
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.6.0.tgz",
|
||||
"integrity": "sha512-qdkx965G/TA12IK7Zk+iCVDtA9wvhxIGivGc2rsID4UYbY2Bpatwep3ZrBZwj1IB2miU6FodDMqM9Kc1lqDlLg=="
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "7.4.1",
|
||||
@@ -5541,9 +5541,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
|
||||
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.14"
|
||||
@@ -8116,6 +8116,7 @@
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.1.tgz",
|
||||
"integrity": "sha512-Pv2ZluG5ife96udGgEDovOOOA5UELkltfJpnIExPrAk1LTvecolUGn6lIaoLh86d83GiB86CjzciMd9BuRB71Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.0.1",
|
||||
"domhandler": "^4.0.0",
|
||||
@@ -8125,7 +8126,8 @@
|
||||
"node_modules/domelementtype": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz",
|
||||
"integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A=="
|
||||
"integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/domexception": {
|
||||
"version": "2.0.1",
|
||||
@@ -8152,6 +8154,7 @@
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.0.tgz",
|
||||
"integrity": "sha512-fC0aXNQXqKSFTr2wDNZDhsEYjCiYsDWl3D01kwt25hm1YIPyDGHvvi3rw+PLqHAl/m71MaiF7d5zvBr0p5UB2g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.2.0"
|
||||
},
|
||||
@@ -8162,10 +8165,16 @@
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.8.tgz",
|
||||
"integrity": "sha512-eVhaWoVibIzqdGYjwsBWodIQIaXFSB+cKDf4cfxLMsK0xiud6SE+/WCVx/Xw/UwQsa4cS3T2eITcdtmTg2UKcw=="
|
||||
},
|
||||
"node_modules/domutils": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
|
||||
"integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"dom-serializer": "^1.0.1",
|
||||
"domelementtype": "^2.2.0",
|
||||
@@ -8219,12 +8228,12 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ejs": {
|
||||
"version": "3.1.6",
|
||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.6.tgz",
|
||||
"integrity": "sha512-9lt9Zse4hPucPkoP7FHDF0LQAlGyF9JVpnClFLFH3aSSbxmyoqINRpp/9wePWJTUl4KOQwRL72Iw3InHPDkoGw==",
|
||||
"version": "3.1.8",
|
||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.8.tgz",
|
||||
"integrity": "sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"jake": "^10.6.1"
|
||||
"jake": "^10.8.5"
|
||||
},
|
||||
"bin": {
|
||||
"ejs": "bin/cli.js"
|
||||
@@ -9778,12 +9787,33 @@
|
||||
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w=="
|
||||
},
|
||||
"node_modules/filelist": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.2.tgz",
|
||||
"integrity": "sha512-z7O0IS8Plc39rTCq6i6iHxk43duYOn8uFJiWSewIq0Bww1RNybVHSCjahmcC87ZqAm4OTvFzlzeGu3XAzG1ctQ==",
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
||||
"integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"minimatch": "^3.0.4"
|
||||
"minimatch": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/filelist/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/filelist/node_modules/minimatch": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz",
|
||||
"integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/filesize": {
|
||||
@@ -10602,6 +10632,7 @@
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
|
||||
"integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.0.1",
|
||||
"domhandler": "^4.0.0",
|
||||
@@ -11227,14 +11258,6 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/is-plain-object": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
|
||||
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-potential-custom-element-name": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
|
||||
@@ -11466,13 +11489,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jake": {
|
||||
"version": "10.8.2",
|
||||
"resolved": "https://registry.npmjs.org/jake/-/jake-10.8.2.tgz",
|
||||
"integrity": "sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A==",
|
||||
"version": "10.8.5",
|
||||
"resolved": "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz",
|
||||
"integrity": "sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"async": "0.9.x",
|
||||
"chalk": "^2.4.2",
|
||||
"async": "^3.2.3",
|
||||
"chalk": "^4.0.2",
|
||||
"filelist": "^1.0.1",
|
||||
"minimatch": "^3.0.4"
|
||||
},
|
||||
@@ -11480,15 +11503,85 @@
|
||||
"jake": "bin/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/jake/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/jake/node_modules/async": {
|
||||
"version": "0.9.2",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz",
|
||||
"integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=",
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz",
|
||||
"integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/jake/node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/jake/node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jake/node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/jake/node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/jake/node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/jest": {
|
||||
"version": "27.4.7",
|
||||
"resolved": "https://registry.npmjs.org/jest/-/jest-27.4.7.tgz",
|
||||
@@ -14998,6 +15091,7 @@
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz",
|
||||
"integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
@@ -15625,6 +15719,7 @@
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz",
|
||||
"integrity": "sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
@@ -15688,9 +15783,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/node-forge": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz",
|
||||
"integrity": "sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
|
||||
"integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 6.13.0"
|
||||
@@ -16200,11 +16295,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-srcset": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
|
||||
"integrity": "sha1-8r0iH2zJcKk42IVWq8WJyqqiveE="
|
||||
},
|
||||
"node_modules/parse5": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
|
||||
@@ -16304,7 +16394,8 @@
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
|
||||
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.2.3",
|
||||
@@ -16438,6 +16529,7 @@
|
||||
"version": "8.4.5",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.5.tgz",
|
||||
"integrity": "sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.1.30",
|
||||
"picocolors": "^1.0.0",
|
||||
@@ -17866,15 +17958,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-ace": {
|
||||
"version": "9.4.0",
|
||||
"resolved": "https://registry.npmjs.org/react-ace/-/react-ace-9.4.0.tgz",
|
||||
"integrity": "sha512-fpY3AGViE1OglXThgn3wZWcPoAxr0bqRYqeG3jY3m1L7OIHo0GfZ3bJI0grhrADDy2i9jQoip9xZfpOFupQCsw==",
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-ace/-/react-ace-10.1.0.tgz",
|
||||
"integrity": "sha512-VkvUjZNhdYTuKOKQpMIZi7uzZZVgzCjM7cLYu6F64V0mejY8a2XTyPUIMszC6A4trbeMIHbK5fYFcT/wkP/8VA==",
|
||||
"dependencies": {
|
||||
"ace-builds": "^1.4.12",
|
||||
"diff-match-patch": "^1.0.4",
|
||||
"ace-builds": "^1.4.14",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"prop-types": "^15.7.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-app-polyfill": {
|
||||
@@ -19223,23 +19319,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rrule": {
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/rrule/-/rrule-2.6.4.tgz",
|
||||
"integrity": "sha512-sLdnh4lmjUqq8liFiOUXD5kWp/FcnbDLPwq5YAc/RrN6120XOPb86Ae5zxF7ttBVq8O3LxjjORMEit1baluahA==",
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/rrule/-/rrule-2.7.0.tgz",
|
||||
"integrity": "sha512-PnSvdJLHrETO4qQxm9nlDvSxNfbPdDFbgdz2BSHXTP+IzHbdwSNvTHOeN0O9khiy91GjzWXyiVJhnPDOQvejNg==",
|
||||
"dependencies": {
|
||||
"tslib": "^1.10.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"luxon": "^1.21.3"
|
||||
}
|
||||
},
|
||||
"node_modules/rrule/node_modules/luxon": {
|
||||
"version": "1.28.0",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.0.tgz",
|
||||
"integrity": "sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ==",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/rst-selector-parser": {
|
||||
@@ -19312,36 +19396,6 @@
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"node_modules/sanitize-html": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.4.0.tgz",
|
||||
"integrity": "sha512-Y1OgkUiTPMqwZNRLPERSEi39iOebn2XJLbeiGOBhaJD/yLqtLGu6GE5w7evx177LeGgSE+4p4e107LMiydOf6A==",
|
||||
"dependencies": {
|
||||
"deepmerge": "^4.2.2",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"htmlparser2": "^6.0.0",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"klona": "^2.0.3",
|
||||
"parse-srcset": "^1.0.2",
|
||||
"postcss": "^8.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/sanitize-html/node_modules/deepmerge": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
|
||||
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sanitize-html/node_modules/escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/sanitize.css": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz",
|
||||
@@ -19683,6 +19737,7 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
|
||||
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -24786,9 +24841,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@patternfly/patternfly": {
|
||||
"version": "4.194.4",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.194.4.tgz",
|
||||
"integrity": "sha512-SJxr502v0xXk1N5OiPLunD9pdKvHp5XXJLXcD5lIPrimjjUcy46m48X8YONjDvnC/Y5xV92UI2KxoCVucE34eA=="
|
||||
"version": "4.196.7",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.196.7.tgz",
|
||||
"integrity": "sha512-hA7Oww411e1p0/IXjC1I+4/1NNis9V+NVBxfUIpRwyuLbCIDHBdtMu2qAPLdKxXjuibV9EE6ZdlT7ra/kcFuJQ=="
|
||||
},
|
||||
"@patternfly/react-core": {
|
||||
"version": "4.214.1",
|
||||
@@ -26036,9 +26091,9 @@
|
||||
}
|
||||
},
|
||||
"ace-builds": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.5.1.tgz",
|
||||
"integrity": "sha512-2G313uyM7lfqZgCs6xCW4QPeuX2GZKaCyRqKhTC2mBeZqC7TjkTXguKRyLzsAIMLJfj3koq98RXCBoemoZVAnQ=="
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.6.0.tgz",
|
||||
"integrity": "sha512-qdkx965G/TA12IK7Zk+iCVDtA9wvhxIGivGc2rsID4UYbY2Bpatwep3ZrBZwj1IB2miU6FodDMqM9Kc1lqDlLg=="
|
||||
},
|
||||
"acorn": {
|
||||
"version": "7.4.1",
|
||||
@@ -26322,9 +26377,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"async": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
|
||||
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lodash": "^4.17.14"
|
||||
@@ -28329,6 +28384,7 @@
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.1.tgz",
|
||||
"integrity": "sha512-Pv2ZluG5ife96udGgEDovOOOA5UELkltfJpnIExPrAk1LTvecolUGn6lIaoLh86d83GiB86CjzciMd9BuRB71Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"domelementtype": "^2.0.1",
|
||||
"domhandler": "^4.0.0",
|
||||
@@ -28338,7 +28394,8 @@
|
||||
"domelementtype": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz",
|
||||
"integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A=="
|
||||
"integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==",
|
||||
"dev": true
|
||||
},
|
||||
"domexception": {
|
||||
"version": "2.0.1",
|
||||
@@ -28361,14 +28418,21 @@
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.0.tgz",
|
||||
"integrity": "sha512-fC0aXNQXqKSFTr2wDNZDhsEYjCiYsDWl3D01kwt25hm1YIPyDGHvvi3rw+PLqHAl/m71MaiF7d5zvBr0p5UB2g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"domelementtype": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"dompurify": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.8.tgz",
|
||||
"integrity": "sha512-eVhaWoVibIzqdGYjwsBWodIQIaXFSB+cKDf4cfxLMsK0xiud6SE+/WCVx/Xw/UwQsa4cS3T2eITcdtmTg2UKcw=="
|
||||
},
|
||||
"domutils": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
|
||||
"integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"dom-serializer": "^1.0.1",
|
||||
"domelementtype": "^2.2.0",
|
||||
@@ -28418,12 +28482,12 @@
|
||||
"dev": true
|
||||
},
|
||||
"ejs": {
|
||||
"version": "3.1.6",
|
||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.6.tgz",
|
||||
"integrity": "sha512-9lt9Zse4hPucPkoP7FHDF0LQAlGyF9JVpnClFLFH3aSSbxmyoqINRpp/9wePWJTUl4KOQwRL72Iw3InHPDkoGw==",
|
||||
"version": "3.1.8",
|
||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.8.tgz",
|
||||
"integrity": "sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"jake": "^10.6.1"
|
||||
"jake": "^10.8.5"
|
||||
}
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
@@ -29631,12 +29695,32 @@
|
||||
}
|
||||
},
|
||||
"filelist": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.2.tgz",
|
||||
"integrity": "sha512-z7O0IS8Plc39rTCq6i6iHxk43duYOn8uFJiWSewIq0Bww1RNybVHSCjahmcC87ZqAm4OTvFzlzeGu3XAzG1ctQ==",
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
||||
"integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"minimatch": "^3.0.4"
|
||||
"minimatch": "^5.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz",
|
||||
"integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"filesize": {
|
||||
@@ -30268,6 +30352,7 @@
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
|
||||
"integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"domelementtype": "^2.0.1",
|
||||
"domhandler": "^4.0.0",
|
||||
@@ -30741,11 +30826,6 @@
|
||||
"integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==",
|
||||
"dev": true
|
||||
},
|
||||
"is-plain-object": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
|
||||
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="
|
||||
},
|
||||
"is-potential-custom-element-name": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
|
||||
@@ -30919,22 +30999,71 @@
|
||||
}
|
||||
},
|
||||
"jake": {
|
||||
"version": "10.8.2",
|
||||
"resolved": "https://registry.npmjs.org/jake/-/jake-10.8.2.tgz",
|
||||
"integrity": "sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A==",
|
||||
"version": "10.8.5",
|
||||
"resolved": "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz",
|
||||
"integrity": "sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"async": "0.9.x",
|
||||
"chalk": "^2.4.2",
|
||||
"async": "^3.2.3",
|
||||
"chalk": "^4.0.2",
|
||||
"filelist": "^1.0.1",
|
||||
"minimatch": "^3.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"async": {
|
||||
"version": "0.9.2",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz",
|
||||
"integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=",
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz",
|
||||
"integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==",
|
||||
"dev": true
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -33602,7 +33731,8 @@
|
||||
"klona": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz",
|
||||
"integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ=="
|
||||
"integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==",
|
||||
"dev": true
|
||||
},
|
||||
"language-subtag-registry": {
|
||||
"version": "0.3.21",
|
||||
@@ -34106,7 +34236,8 @@
|
||||
"nanoid": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz",
|
||||
"integrity": "sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA=="
|
||||
"integrity": "sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==",
|
||||
"dev": true
|
||||
},
|
||||
"natural-compare": {
|
||||
"version": "1.4.0",
|
||||
@@ -34157,9 +34288,9 @@
|
||||
}
|
||||
},
|
||||
"node-forge": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz",
|
||||
"integrity": "sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
|
||||
"integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
|
||||
"dev": true
|
||||
},
|
||||
"node-gettext": {
|
||||
@@ -34550,11 +34681,6 @@
|
||||
"lines-and-columns": "^1.1.6"
|
||||
}
|
||||
},
|
||||
"parse-srcset": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
|
||||
"integrity": "sha1-8r0iH2zJcKk42IVWq8WJyqqiveE="
|
||||
},
|
||||
"parse5": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
|
||||
@@ -34641,7 +34767,8 @@
|
||||
"picocolors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
|
||||
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
|
||||
"dev": true
|
||||
},
|
||||
"picomatch": {
|
||||
"version": "2.2.3",
|
||||
@@ -34752,6 +34879,7 @@
|
||||
"version": "8.4.5",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.5.tgz",
|
||||
"integrity": "sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"nanoid": "^3.1.30",
|
||||
"picocolors": "^1.0.0",
|
||||
@@ -35705,12 +35833,12 @@
|
||||
}
|
||||
},
|
||||
"react-ace": {
|
||||
"version": "9.4.0",
|
||||
"resolved": "https://registry.npmjs.org/react-ace/-/react-ace-9.4.0.tgz",
|
||||
"integrity": "sha512-fpY3AGViE1OglXThgn3wZWcPoAxr0bqRYqeG3jY3m1L7OIHo0GfZ3bJI0grhrADDy2i9jQoip9xZfpOFupQCsw==",
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-ace/-/react-ace-10.1.0.tgz",
|
||||
"integrity": "sha512-VkvUjZNhdYTuKOKQpMIZi7uzZZVgzCjM7cLYu6F64V0mejY8a2XTyPUIMszC6A4trbeMIHbK5fYFcT/wkP/8VA==",
|
||||
"requires": {
|
||||
"ace-builds": "^1.4.12",
|
||||
"diff-match-patch": "^1.0.4",
|
||||
"ace-builds": "^1.4.14",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"prop-types": "^15.7.2"
|
||||
@@ -36664,20 +36792,11 @@
|
||||
}
|
||||
},
|
||||
"rrule": {
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/rrule/-/rrule-2.6.4.tgz",
|
||||
"integrity": "sha512-sLdnh4lmjUqq8liFiOUXD5kWp/FcnbDLPwq5YAc/RrN6120XOPb86Ae5zxF7ttBVq8O3LxjjORMEit1baluahA==",
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/rrule/-/rrule-2.7.0.tgz",
|
||||
"integrity": "sha512-PnSvdJLHrETO4qQxm9nlDvSxNfbPdDFbgdz2BSHXTP+IzHbdwSNvTHOeN0O9khiy91GjzWXyiVJhnPDOQvejNg==",
|
||||
"requires": {
|
||||
"luxon": "^1.21.3",
|
||||
"tslib": "^1.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"luxon": {
|
||||
"version": "1.28.0",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.0.tgz",
|
||||
"integrity": "sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ==",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"rst-selector-parser": {
|
||||
@@ -36730,32 +36849,6 @@
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"sanitize-html": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.4.0.tgz",
|
||||
"integrity": "sha512-Y1OgkUiTPMqwZNRLPERSEi39iOebn2XJLbeiGOBhaJD/yLqtLGu6GE5w7evx177LeGgSE+4p4e107LMiydOf6A==",
|
||||
"requires": {
|
||||
"deepmerge": "^4.2.2",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"htmlparser2": "^6.0.0",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"klona": "^2.0.3",
|
||||
"parse-srcset": "^1.0.2",
|
||||
"postcss": "^8.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"deepmerge": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
|
||||
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg=="
|
||||
},
|
||||
"escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"sanitize.css": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz",
|
||||
@@ -37038,7 +37131,8 @@
|
||||
"source-map-js": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
|
||||
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw=="
|
||||
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
|
||||
"dev": true
|
||||
},
|
||||
"source-map-loader": {
|
||||
"version": "3.0.1",
|
||||
|
||||
@@ -7,16 +7,17 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@lingui/react": "3.13.3",
|
||||
"@patternfly/patternfly": "4.194.4",
|
||||
"@patternfly/patternfly": "4.196.7",
|
||||
"@patternfly/react-core": "^4.201.0",
|
||||
"@patternfly/react-icons": "4.49.19",
|
||||
"@patternfly/react-table": "4.83.1",
|
||||
"ace-builds": "^1.5.1",
|
||||
"ace-builds": "^1.6.0",
|
||||
"ansi-to-html": "0.7.2",
|
||||
"axios": "0.22.0",
|
||||
"codemirror": "^5.65.4",
|
||||
"d3": "7.4.4",
|
||||
"dagre": "^0.8.4",
|
||||
"dompurify": "2.3.8",
|
||||
"formik": "2.2.9",
|
||||
"has-ansi": "5.0.1",
|
||||
"html-entities": "2.3.2",
|
||||
@@ -24,13 +25,12 @@
|
||||
"luxon": "^2.4.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "17.0.2",
|
||||
"react-ace": "^9.3.0",
|
||||
"react-ace": "^10.1.0",
|
||||
"react-dom": "17.0.2",
|
||||
"react-error-boundary": "^3.1.4",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-virtualized": "^9.21.1",
|
||||
"rrule": "2.6.4",
|
||||
"sanitize-html": "2.4.0",
|
||||
"rrule": "2.7.0",
|
||||
"styled-components": "5.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</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 https://d3js.org; img-src 'self' *.pendo.io data:; worker-src 'self';"
|
||||
content="default-src 'self'; connect-src 'self' ws: wss:; style-src 'self' 'unsafe-inline'; script-src 'self' 'nonce-{{ csp_nonce }}' *.pendo.io https://d3js.org; img-src 'self' *.pendo.io data:; worker-src 'self' blob: ;"
|
||||
/>
|
||||
<link rel="shortcut icon" href="{% static 'media/favicon.ico' %}" />
|
||||
<% } else { %>
|
||||
|
||||
@@ -101,9 +101,9 @@ const AuthorizedRoutes = ({ routeConfig }) => {
|
||||
export function ProtectedRoute({ children, ...rest }) {
|
||||
const {
|
||||
authRedirectTo,
|
||||
setAuthRedirectTo,
|
||||
loginRedirectOverride,
|
||||
isUserBeingLoggedOut,
|
||||
loginRedirectOverride,
|
||||
setAuthRedirectTo,
|
||||
} = useSession();
|
||||
const location = useLocation();
|
||||
|
||||
|
||||
@@ -77,6 +77,10 @@ class Organizations extends InstanceGroupsMixin(NotificationsMixin(Base)) {
|
||||
disassociate: true,
|
||||
});
|
||||
}
|
||||
|
||||
readAdmins(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/admins/`, { params });
|
||||
}
|
||||
}
|
||||
|
||||
export default Organizations;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useEffect, useRef, useCallback } from 'react';
|
||||
import { oneOf, bool, number, string, func, oneOfType } from 'prop-types';
|
||||
import { config } from 'ace-builds';
|
||||
|
||||
import ReactAce from 'react-ace';
|
||||
import 'ace-builds/src-noconflict/mode-json';
|
||||
@@ -13,8 +12,6 @@ import { t } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
import debounce from 'util/debounce';
|
||||
|
||||
config.set('loadWorkerFromBlob', false);
|
||||
|
||||
const LINE_HEIGHT = 24;
|
||||
const PADDING = 12;
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ function VariablesDetail({
|
||||
{error && (
|
||||
<div
|
||||
css="color: var(--pf-global--danger-color--100);
|
||||
font-size: var(--pf-global--FontSize--sm"
|
||||
font-size: var(--pf-global--FontSize--sm)"
|
||||
>
|
||||
{t`Error:`} {error.message}
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,7 @@ function HealthCheckButton({ isDisabled, onClick, selectedItems }) {
|
||||
onClick={onClick}
|
||||
ouiaId="health-check"
|
||||
>
|
||||
{t`Health Check`}
|
||||
{t`Run health check`}
|
||||
</DropdownItem>
|
||||
</Tooltip>
|
||||
);
|
||||
@@ -42,7 +42,7 @@ function HealthCheckButton({ isDisabled, onClick, selectedItems }) {
|
||||
variant="secondary"
|
||||
ouiaId="health-check"
|
||||
onClick={onClick}
|
||||
>{t`Health Check`}</Button>
|
||||
>{t`Run health check`}</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -8,12 +8,13 @@ import AlertModal from '../AlertModal';
|
||||
import ErrorDetail from '../ErrorDetail';
|
||||
|
||||
function JobCancelButton({
|
||||
job = {},
|
||||
errorTitle,
|
||||
title,
|
||||
showIconButton,
|
||||
errorMessage,
|
||||
buttonText,
|
||||
style = {},
|
||||
job = {},
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { error: cancelError, request: cancelJob } = useRequest(
|
||||
@@ -38,6 +39,7 @@ function JobCancelButton({
|
||||
ouiaId="cancel-job-button"
|
||||
onClick={() => setIsOpen(true)}
|
||||
variant="plain"
|
||||
style={style}
|
||||
>
|
||||
<MinusCircleIcon />
|
||||
</Button>
|
||||
@@ -48,6 +50,7 @@ function JobCancelButton({
|
||||
variant="secondary"
|
||||
ouiaId="cancel-job-button"
|
||||
onClick={() => setIsOpen(true)}
|
||||
style={style}
|
||||
>
|
||||
{buttonText || t`Cancel Job`}
|
||||
</Button>
|
||||
|
||||
@@ -122,7 +122,7 @@ function MultipleChoiceField({ question }) {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
selections={field.value}
|
||||
variant={SelectVariant.single}
|
||||
variant={SelectVariant.typeahead}
|
||||
id={id}
|
||||
ouiaId={`single-survey-question-${question.variable}`}
|
||||
isOpen={isOpen}
|
||||
|
||||
@@ -83,6 +83,7 @@ function ApplicationLookup({ onChange, value, label, fieldName, validate }) {
|
||||
header={t`Application`}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onUpdate={fetchApplications}
|
||||
onDebounce={checkApplicationName}
|
||||
fieldName={fieldName}
|
||||
validate={validate}
|
||||
|
||||
@@ -168,6 +168,7 @@ function CredentialLookup({
|
||||
value={value}
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
onUpdate={fetchCredentials}
|
||||
onDebounce={checkCredentialName}
|
||||
fieldName={fieldName}
|
||||
validate={validate}
|
||||
|
||||
@@ -156,6 +156,7 @@ function ExecutionEnvironmentLookup({
|
||||
value={value}
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
onUpdate={fetchExecutionEnvironments}
|
||||
onDebounce={checkExecutionEnvironmentName}
|
||||
fieldName={fieldName}
|
||||
validate={validate}
|
||||
|
||||
@@ -271,6 +271,7 @@ function HostFilterLookup({
|
||||
pathname: `${location.pathname}`,
|
||||
search: queryString,
|
||||
});
|
||||
fetchHosts(organizationId);
|
||||
toggleModal();
|
||||
};
|
||||
|
||||
@@ -419,6 +420,7 @@ function HostFilterLookup({
|
||||
headerRow={
|
||||
<HeaderRow qsConfig={QS_CONFIG} isSelectable={false}>
|
||||
<HeaderCell sortKey="name">{t`Name`}</HeaderCell>
|
||||
<HeaderCell sortKey="description">{t`Description`}</HeaderCell>
|
||||
<HeaderCell>{t`Inventory`}</HeaderCell>
|
||||
</HeaderRow>
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ function HostListItem({ item }) {
|
||||
return (
|
||||
<Tr ouiaId={`host-list-item-${item.id}`}>
|
||||
<Td dataLabel={t`Name`}>{item.name}</Td>
|
||||
<Td dataLabel={t`Description`}>{item.description}</Td>
|
||||
<Td dataLabel={t`Inventory`}>{item.summary_fields.inventory.name}</Td>
|
||||
</Tr>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ describe('HostListItem', () => {
|
||||
id: 1,
|
||||
type: 'inventory',
|
||||
name: 'Foo',
|
||||
description: 'Buzz',
|
||||
summary_fields: {
|
||||
inventory: {
|
||||
name: 'Bar',
|
||||
@@ -24,6 +25,7 @@ describe('HostListItem', () => {
|
||||
);
|
||||
expect(wrapper.find('HostListItem').length).toBe(1);
|
||||
expect(wrapper.find('Td').at(0).text()).toBe('Foo');
|
||||
expect(wrapper.find('Td').at(1).text()).toBe('Bar');
|
||||
expect(wrapper.find('Td').at(1).text()).toBe('Buzz');
|
||||
expect(wrapper.find('Td').at(2).text()).toBe('Bar');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -75,6 +75,7 @@ function InstanceGroupsLookup({
|
||||
header={t`Instance Groups`}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onUpdate={fetchInstanceGroups}
|
||||
fieldName={fieldName}
|
||||
validate={validate}
|
||||
qsConfig={QS_CONFIG}
|
||||
|
||||
@@ -138,6 +138,7 @@ function InventoryLookup({
|
||||
header={t`Inventory`}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onUpdate={fetchInventories}
|
||||
onBlur={onBlur}
|
||||
required={required}
|
||||
onDebounce={checkInventoryName}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
oneOfType,
|
||||
shape,
|
||||
node,
|
||||
object,
|
||||
} from 'prop-types';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { useField } from 'formik';
|
||||
@@ -51,6 +52,7 @@ function Lookup(props) {
|
||||
fieldName,
|
||||
validate,
|
||||
modalDescription,
|
||||
onUpdate,
|
||||
} = props;
|
||||
const [typedText, setTypedText] = useState('');
|
||||
const debounceRequest = useDebounce(onDebounce, 1000);
|
||||
@@ -119,6 +121,11 @@ function Lookup(props) {
|
||||
dispatch({ type: 'CLOSE_MODAL' });
|
||||
};
|
||||
|
||||
const onClick = () => {
|
||||
onUpdate();
|
||||
dispatch({ type: 'TOGGLE_MODAL' });
|
||||
};
|
||||
|
||||
const { isModalOpen, selectedItems } = state;
|
||||
const canDelete =
|
||||
(!required || (multiple && value.length > 1)) && !isDisabled;
|
||||
@@ -136,7 +143,7 @@ function Lookup(props) {
|
||||
aria-label={t`Search`}
|
||||
id={`${id}-open`}
|
||||
ouiaId={`${id}-open`}
|
||||
onClick={() => dispatch({ type: 'TOGGLE_MODAL' })}
|
||||
onClick={onClick}
|
||||
variant={ButtonVariant.control}
|
||||
isDisabled={isLoading || isDisabled}
|
||||
>
|
||||
@@ -222,7 +229,8 @@ Lookup.propTypes = {
|
||||
header: string,
|
||||
modalDescription: oneOfType([string, node]),
|
||||
onChange: func.isRequired,
|
||||
value: oneOfType([Item, arrayOf(Item)]),
|
||||
onUpdate: func,
|
||||
value: oneOfType([Item, arrayOf(Item), object]),
|
||||
multiple: bool,
|
||||
required: bool,
|
||||
onBlur: func,
|
||||
@@ -254,6 +262,7 @@ Lookup.defaultProps = {
|
||||
),
|
||||
validate: () => undefined,
|
||||
onDebounce: () => undefined,
|
||||
onUpdate: () => {},
|
||||
isDisabled: false,
|
||||
};
|
||||
|
||||
|
||||
@@ -141,6 +141,7 @@ function MultiCredentialsLookup({
|
||||
validate={validate}
|
||||
multiple
|
||||
onChange={onChange}
|
||||
onUpdate={fetchCredentials}
|
||||
qsConfig={QS_CONFIG}
|
||||
isLoading={isTypesLoading || isCredentialsLoading}
|
||||
renderItemChip={renderChip}
|
||||
|
||||
@@ -203,6 +203,7 @@ describe('<Formik><MultiCredentialsLookup /></Formik>', () => {
|
||||
await act(async () => {
|
||||
searchButton.invoke('onClick')();
|
||||
});
|
||||
expect(CredentialsAPI.read).toHaveBeenCalledTimes(2);
|
||||
const select = await waitForElement(wrapper, 'AnsibleSelect');
|
||||
CredentialsAPI.read.mockResolvedValueOnce({
|
||||
data: {
|
||||
@@ -212,12 +213,10 @@ describe('<Formik><MultiCredentialsLookup /></Formik>', () => {
|
||||
count: 1,
|
||||
},
|
||||
});
|
||||
expect(CredentialsAPI.read).toHaveBeenCalledTimes(1);
|
||||
await act(async () => {
|
||||
select.invoke('onChange')({}, 500);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(CredentialsAPI.read).toHaveBeenCalledTimes(2);
|
||||
expect(wrapper.find('OptionsList').prop('options')).toEqual([
|
||||
{
|
||||
id: 1,
|
||||
|
||||
@@ -109,6 +109,7 @@ function OrganizationLookup({
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
onDebounce={checkOrganizationName}
|
||||
onUpdate={fetchOrganizations}
|
||||
fieldName={fieldName}
|
||||
validate={validate}
|
||||
qsConfig={QS_CONFIG}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { node, string, func, bool } from 'prop-types';
|
||||
import { node, string, func, bool, object, oneOfType } from 'prop-types';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { t } from '@lingui/macro';
|
||||
import { FormGroup } from '@patternfly/react-core';
|
||||
@@ -111,6 +111,7 @@ function ProjectLookup({
|
||||
value={value}
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
onUpdate={fetchProjects}
|
||||
onDebounce={checkProjectName}
|
||||
fieldName={fieldName}
|
||||
validate={validate}
|
||||
@@ -184,7 +185,7 @@ ProjectLookup.propTypes = {
|
||||
onChange: func.isRequired,
|
||||
required: bool,
|
||||
tooltip: string,
|
||||
value: Project,
|
||||
value: oneOfType([Project, object]),
|
||||
isOverrideDisabled: bool,
|
||||
validate: func,
|
||||
fieldName: string,
|
||||
|
||||
@@ -59,6 +59,7 @@ function TagMultiSelect({ onChange, value }) {
|
||||
typeAheadAriaLabel={t`Select tags`}
|
||||
noResultsFoundText={t`No results found`}
|
||||
ouiaId="tag-multiselect"
|
||||
createText={t`Create`}
|
||||
>
|
||||
{renderOptions(options)}
|
||||
</Select>
|
||||
|
||||
33
awx/ui/src/components/PersistentFilters/PersistentFilters.js
Normal file
33
awx/ui/src/components/PersistentFilters/PersistentFilters.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation, useHistory } from 'react-router';
|
||||
import { PERSISTENT_FILTER_KEY } from '../../constants';
|
||||
|
||||
export default function PersistentFilters({ pageKey, children }) {
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
|
||||
useEffect(() => {
|
||||
if (!location.search.includes('restoreFilters=true')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filterString = sessionStorage.getItem(PERSISTENT_FILTER_KEY);
|
||||
const filter = filterString ? JSON.parse(filterString) : { qs: '' };
|
||||
|
||||
if (filter.pageKey === pageKey) {
|
||||
history.replace(`${location.pathname}${filter.qs}`);
|
||||
} else {
|
||||
history.replace(location.pathname);
|
||||
}
|
||||
}, [history, location, pageKey]);
|
||||
|
||||
useEffect(() => {
|
||||
const filter = {
|
||||
pageKey,
|
||||
qs: location.search,
|
||||
};
|
||||
sessionStorage.setItem(PERSISTENT_FILTER_KEY, JSON.stringify(filter));
|
||||
}, [location.search, pageKey]);
|
||||
|
||||
return children;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import React from 'react';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { Router, Route } from 'react-router-dom';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import PersistentFilters from './PersistentFilters';
|
||||
|
||||
const KEY = 'awx-persistent-filter';
|
||||
|
||||
describe('PersistentFilters', () => {
|
||||
test('should initialize filter in sessionStorage', () => {
|
||||
expect(sessionStorage.getItem(KEY)).toEqual(null);
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ['/templates'],
|
||||
});
|
||||
render(
|
||||
<Router history={history}>
|
||||
<PersistentFilters pageKey="templates">test</PersistentFilters>
|
||||
</Router>
|
||||
);
|
||||
|
||||
expect(JSON.parse(sessionStorage.getItem(KEY))).toEqual({
|
||||
pageKey: 'templates',
|
||||
qs: '',
|
||||
});
|
||||
});
|
||||
|
||||
test('should restore filters from sessionStorage', () => {
|
||||
expect(
|
||||
sessionStorage.setItem(
|
||||
KEY,
|
||||
JSON.stringify({
|
||||
pageKey: 'templates',
|
||||
qs: '?page=2&name=foo',
|
||||
})
|
||||
)
|
||||
);
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ['/templates?restoreFilters=true'],
|
||||
});
|
||||
render(
|
||||
<Router history={history}>
|
||||
<PersistentFilters pageKey="templates">test</PersistentFilters>
|
||||
</Router>
|
||||
);
|
||||
|
||||
expect(history.location.search).toEqual('?page=2&name=foo');
|
||||
});
|
||||
|
||||
test('should not restore filters without restoreFilters query param', () => {
|
||||
expect(
|
||||
sessionStorage.setItem(
|
||||
KEY,
|
||||
JSON.stringify({
|
||||
pageKey: 'templates',
|
||||
qs: '?page=2&name=foo',
|
||||
})
|
||||
)
|
||||
);
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ['/templates'],
|
||||
});
|
||||
render(
|
||||
<Router history={history}>
|
||||
<PersistentFilters pageKey="templates">test</PersistentFilters>
|
||||
</Router>
|
||||
);
|
||||
|
||||
expect(history.location.search).toEqual('');
|
||||
});
|
||||
|
||||
test("should not restore filters if page key doesn't match", () => {
|
||||
expect(
|
||||
sessionStorage.setItem(
|
||||
KEY,
|
||||
JSON.stringify({
|
||||
pageKey: 'projects',
|
||||
qs: '?page=2&name=foo',
|
||||
})
|
||||
)
|
||||
);
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ['/templates?restoreFilters=true'],
|
||||
});
|
||||
render(
|
||||
<Router history={history}>
|
||||
<PersistentFilters pageKey="templates">test</PersistentFilters>
|
||||
</Router>
|
||||
);
|
||||
|
||||
expect(history.location.search).toEqual('');
|
||||
});
|
||||
|
||||
test('should update stored filters when qs changes', async () => {
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ['/templates'],
|
||||
});
|
||||
render(
|
||||
<Router history={history}>
|
||||
<PersistentFilters pageKey="templates">test</PersistentFilters>
|
||||
</Router>
|
||||
);
|
||||
|
||||
history.push('/templates?page=3');
|
||||
await waitFor(() => true);
|
||||
|
||||
expect(JSON.parse(sessionStorage.getItem(KEY))).toEqual({
|
||||
pageKey: 'templates',
|
||||
qs: '?page=3',
|
||||
});
|
||||
});
|
||||
});
|
||||
1
awx/ui/src/components/PersistentFilters/index.js
Normal file
1
awx/ui/src/components/PersistentFilters/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './PersistentFilters';
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useParams, useLocation } from 'react-router-dom';
|
||||
|
||||
import { t, Plural } from '@lingui/macro';
|
||||
import { Card } from '@patternfly/react-core';
|
||||
@@ -14,7 +14,12 @@ import PaginatedTable, {
|
||||
ToolbarDeleteButton,
|
||||
getSearchableKeys,
|
||||
} from 'components/PaginatedTable';
|
||||
import { getQSConfig, parseQueryString, mergeParams } from 'util/qs';
|
||||
import {
|
||||
getQSConfig,
|
||||
parseQueryString,
|
||||
mergeParams,
|
||||
encodeQueryString,
|
||||
} from 'util/qs';
|
||||
import useWsTemplates from 'hooks/useWsTemplates';
|
||||
import useSelected from 'hooks/useSelected';
|
||||
import useExpanded from 'hooks/useExpanded';
|
||||
@@ -29,7 +34,8 @@ const QS_CONFIG = getQSConfig('template', {
|
||||
order_by: 'name',
|
||||
});
|
||||
|
||||
function RelatedTemplateList({ searchParams }) {
|
||||
function RelatedTemplateList({ searchParams, projectName = null }) {
|
||||
const { id: projectId } = useParams();
|
||||
const location = useLocation();
|
||||
const { addToast, Toast, toastProps } = useToast();
|
||||
|
||||
@@ -122,9 +128,18 @@ function RelatedTemplateList({ searchParams }) {
|
||||
const canAddJT =
|
||||
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
||||
|
||||
const addButton = (
|
||||
<ToolbarAddButton key="add" linkTo="/templates/job_template/add/" />
|
||||
);
|
||||
let linkTo = '';
|
||||
|
||||
if (projectName) {
|
||||
const qs = encodeQueryString({
|
||||
project_id: projectId,
|
||||
project_name: projectName,
|
||||
});
|
||||
linkTo = `/templates/job_template/add/?${qs}`;
|
||||
} else {
|
||||
linkTo = '/templates/job_template/add';
|
||||
}
|
||||
const addButton = <ToolbarAddButton key="add" linkTo={linkTo} />;
|
||||
|
||||
const deleteDetailsRequests = relatedResourceDeleteRequests.template(
|
||||
selected[0]
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { t } from '@lingui/macro';
|
||||
import { RolesAPI, TeamsAPI, UsersAPI } from 'api';
|
||||
import { RolesAPI, TeamsAPI, UsersAPI, OrganizationsAPI } from 'api';
|
||||
import { getQSConfig, parseQueryString } from 'util/qs';
|
||||
import useRequest, { useDeleteItems } from 'hooks/useRequest';
|
||||
import { useUserProfile, useConfig } from 'contexts/Config';
|
||||
import AddResourceRole from '../AddRole/AddResourceRole';
|
||||
import AlertModal from '../AlertModal';
|
||||
import DataListToolbar from '../DataListToolbar';
|
||||
@@ -24,6 +25,8 @@ const QS_CONFIG = getQSConfig('access', {
|
||||
});
|
||||
|
||||
function ResourceAccessList({ apiModel, resource }) {
|
||||
const { isSuperUser, isOrgAdmin } = useUserProfile();
|
||||
const { me } = useConfig();
|
||||
const [submitError, setSubmitError] = useState(null);
|
||||
const [deletionRecord, setDeletionRecord] = useState(null);
|
||||
const [deletionRole, setDeletionRole] = useState(null);
|
||||
@@ -31,6 +34,49 @@ function ResourceAccessList({ apiModel, resource }) {
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const location = useLocation();
|
||||
|
||||
const {
|
||||
isLoading: isFetchingOrgAdmins,
|
||||
error: errorFetchingOrgAdmins,
|
||||
request: fetchOrgAdmins,
|
||||
result: { isCredentialOrgAdmin },
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
if (
|
||||
isSuperUser ||
|
||||
resource.type !== 'credential' ||
|
||||
!isOrgAdmin ||
|
||||
!resource?.organization
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const {
|
||||
data: { count },
|
||||
} = await OrganizationsAPI.readAdmins(resource.organization, {
|
||||
id: me.id,
|
||||
});
|
||||
return { isCredentialOrgAdmin: !!count };
|
||||
}, [me.id, isOrgAdmin, isSuperUser, resource.type, resource.organization]),
|
||||
{
|
||||
isCredentialOrgAdmin: false,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchOrgAdmins();
|
||||
}, [fetchOrgAdmins]);
|
||||
|
||||
let canAddAdditionalControls = false;
|
||||
if (isSuperUser) {
|
||||
canAddAdditionalControls = true;
|
||||
}
|
||||
if (resource.type === 'credential' && isOrgAdmin && isCredentialOrgAdmin) {
|
||||
canAddAdditionalControls = true;
|
||||
}
|
||||
if (resource.type !== 'credential') {
|
||||
canAddAdditionalControls =
|
||||
resource?.summary_fields?.user_capabilities?.edit;
|
||||
}
|
||||
|
||||
const {
|
||||
result: {
|
||||
accessRecords,
|
||||
@@ -149,8 +195,8 @@ function ResourceAccessList({ apiModel, resource }) {
|
||||
return (
|
||||
<>
|
||||
<PaginatedTable
|
||||
error={contentError}
|
||||
hasContentLoading={isLoading || isDeleteLoading}
|
||||
error={contentError || errorFetchingOrgAdmins}
|
||||
hasContentLoading={isLoading || isDeleteLoading || isFetchingOrgAdmins}
|
||||
items={accessRecords}
|
||||
itemCount={itemCount}
|
||||
pluralizedItemName={t`Roles`}
|
||||
@@ -163,7 +209,7 @@ function ResourceAccessList({ apiModel, resource }) {
|
||||
{...props}
|
||||
qsConfig={QS_CONFIG}
|
||||
additionalControls={
|
||||
resource?.summary_fields?.user_capabilities?.edit
|
||||
canAddAdditionalControls
|
||||
? [
|
||||
<ToolbarAddButton
|
||||
ouiaId="access-add-button"
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { OrganizationsAPI, TeamsAPI, UsersAPI, RolesAPI } from 'api';
|
||||
import {
|
||||
CredentialsAPI,
|
||||
OrganizationsAPI,
|
||||
RolesAPI,
|
||||
TeamsAPI,
|
||||
UsersAPI,
|
||||
} from 'api';
|
||||
import { useUserProfile } from 'contexts/Config';
|
||||
import * as ConfigContext from 'contexts/Config';
|
||||
import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
@@ -91,11 +99,227 @@ describe('<ResourceAccessList />', () => {
|
||||
],
|
||||
};
|
||||
|
||||
const credentialAccessList = {
|
||||
count: 2,
|
||||
results: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'user',
|
||||
url: '/api/v2/users/1/',
|
||||
summary_fields: {
|
||||
direct_access: [
|
||||
{
|
||||
role: {
|
||||
id: 20,
|
||||
name: 'Admin',
|
||||
description: 'Can manage all aspects of the credential',
|
||||
resource_name: 'Demo Credential',
|
||||
resource_type: 'credential',
|
||||
related: { credential: '/api/v2/credentials/1/' },
|
||||
user_capabilities: { unattach: false },
|
||||
},
|
||||
descendant_roles: ['admin_role', 'read_role', 'use_role'],
|
||||
},
|
||||
],
|
||||
indirect_access: [
|
||||
{
|
||||
role: {
|
||||
id: 1,
|
||||
name: 'System Administrator',
|
||||
description: 'Can manage all aspects of the system',
|
||||
user_capabilities: { unattach: false },
|
||||
},
|
||||
descendant_roles: ['admin_role', 'read_role', 'use_role'],
|
||||
},
|
||||
],
|
||||
},
|
||||
created: '2022-06-08T18:31:35.834036Z',
|
||||
modified: '2022-06-09T16:47:54.712473Z',
|
||||
username: 'admin',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: 'admin@localhost',
|
||||
is_superuser: true,
|
||||
is_system_auditor: false,
|
||||
ldap_dn: '',
|
||||
last_login: '2022-06-09T16:47:54.712473Z',
|
||||
external_account: null,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'user',
|
||||
url: '/api/v2/users/2/',
|
||||
related: {
|
||||
teams: '/api/v2/users/2/teams/',
|
||||
organizations: '/api/v2/users/2/organizations/',
|
||||
admin_of_organizations: '/api/v2/users/2/admin_of_organizations/',
|
||||
projects: '/api/v2/users/2/projects/',
|
||||
credentials: '/api/v2/users/2/credentials/',
|
||||
roles: '/api/v2/users/2/roles/',
|
||||
activity_stream: '/api/v2/users/2/activity_stream/',
|
||||
access_list: '/api/v2/users/2/access_list/',
|
||||
tokens: '/api/v2/users/2/tokens/',
|
||||
authorized_tokens: '/api/v2/users/2/authorized_tokens/',
|
||||
personal_tokens: '/api/v2/users/2/personal_tokens/',
|
||||
},
|
||||
summary_fields: {
|
||||
direct_access: [
|
||||
{
|
||||
role: {
|
||||
id: 22,
|
||||
name: 'Read',
|
||||
description: 'May view settings for the credential',
|
||||
resource_name: 'Demo Credential',
|
||||
resource_type: 'credential',
|
||||
related: { credential: '/api/v2/credentials/1/' },
|
||||
user_capabilities: { unattach: false },
|
||||
},
|
||||
descendant_roles: ['read_role'],
|
||||
},
|
||||
],
|
||||
indirect_access: [],
|
||||
},
|
||||
created: '2022-06-09T13:45:56.049783Z',
|
||||
modified: '2022-06-09T16:48:46.169760Z',
|
||||
username: 'second',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: '',
|
||||
is_superuser: false,
|
||||
is_system_auditor: false,
|
||||
ldap_dn: '',
|
||||
last_login: '2022-06-09T16:48:46.169760Z',
|
||||
external_account: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const credential = {
|
||||
id: 1,
|
||||
type: 'credential',
|
||||
url: '/api/v2/credentials/1/',
|
||||
related: {
|
||||
named_url: '/api/v2/credentials/Demo Credential++Machine+ssh++Default/',
|
||||
created_by: '/api/v2/users/1/',
|
||||
modified_by: '/api/v2/users/1/',
|
||||
organization: '/api/v2/organizations/1/',
|
||||
activity_stream: '/api/v2/credentials/1/activity_stream/',
|
||||
access_list: '/api/v2/credentials/1/access_list/',
|
||||
object_roles: '/api/v2/credentials/1/object_roles/',
|
||||
owner_users: '/api/v2/credentials/1/owner_users/',
|
||||
owner_teams: '/api/v2/credentials/1/owner_teams/',
|
||||
copy: '/api/v2/credentials/1/copy/',
|
||||
input_sources: '/api/v2/credentials/1/input_sources/',
|
||||
credential_type: '/api/v2/credential_types/1/',
|
||||
},
|
||||
summary_fields: {
|
||||
organization: {
|
||||
id: 1,
|
||||
name: 'Default',
|
||||
description: '',
|
||||
},
|
||||
credential_type: {
|
||||
id: 1,
|
||||
name: 'Machine',
|
||||
description: '',
|
||||
},
|
||||
created_by: {
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
},
|
||||
modified_by: {
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
},
|
||||
object_roles: {
|
||||
admin_role: {
|
||||
description: 'Can manage all aspects of the credential',
|
||||
name: 'Admin',
|
||||
id: 20,
|
||||
},
|
||||
use_role: {
|
||||
description: 'Can use the credential in a job template',
|
||||
name: 'Use',
|
||||
id: 21,
|
||||
},
|
||||
read_role: {
|
||||
description: 'May view settings for the credential',
|
||||
name: 'Read',
|
||||
id: 22,
|
||||
},
|
||||
},
|
||||
user_capabilities: {
|
||||
edit: true,
|
||||
delete: true,
|
||||
copy: false,
|
||||
use: true,
|
||||
},
|
||||
owners: [
|
||||
{
|
||||
id: 3,
|
||||
type: 'user',
|
||||
name: 'third',
|
||||
description: ' ',
|
||||
url: '/api/v2/users/3/',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
type: 'user',
|
||||
name: 'admin',
|
||||
description: ' ',
|
||||
url: '/api/v2/users/1/',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
type: 'organization',
|
||||
name: 'Default',
|
||||
description: '',
|
||||
url: '/api/v2/organizations/1/',
|
||||
},
|
||||
],
|
||||
},
|
||||
created: '2022-06-08T18:31:43.491973Z',
|
||||
modified: '2022-06-09T19:40:49.460771Z',
|
||||
name: 'Demo Credential',
|
||||
description: '',
|
||||
organization: 1,
|
||||
credential_type: 1,
|
||||
managed: false,
|
||||
inputs: {
|
||||
username: 'admin',
|
||||
become_method: '',
|
||||
become_username: '',
|
||||
},
|
||||
kind: 'ssh',
|
||||
cloud: false,
|
||||
kubernetes: false,
|
||||
};
|
||||
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ['/organizations/1/access'],
|
||||
});
|
||||
|
||||
const credentialHistory = createMemoryHistory({
|
||||
initialEntries: ['/credentials/1/access'],
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.spyOn(ConfigContext, 'useConfig').mockImplementation(() => ({
|
||||
me: { id: 2 },
|
||||
}));
|
||||
useUserProfile.mockImplementation(() => {
|
||||
return {
|
||||
isSuperUser: true,
|
||||
isSystemAuditor: false,
|
||||
isOrgAdmin: false,
|
||||
isNotificationAdmin: false,
|
||||
isExecEnvAdmin: false,
|
||||
};
|
||||
});
|
||||
OrganizationsAPI.readAccessList.mockResolvedValue({ data });
|
||||
OrganizationsAPI.readAccessOptions.mockResolvedValue({
|
||||
data: {
|
||||
@@ -106,6 +330,7 @@ describe('<ResourceAccessList />', () => {
|
||||
related_search_fields: [],
|
||||
},
|
||||
});
|
||||
OrganizationsAPI.readAdmins.mockResolvedValue({ data: { count: 1 } });
|
||||
TeamsAPI.disassociateRole.mockResolvedValue({});
|
||||
UsersAPI.disassociateRole.mockResolvedValue({});
|
||||
RolesAPI.read.mockResolvedValue({
|
||||
@@ -116,6 +341,16 @@ describe('<ResourceAccessList />', () => {
|
||||
],
|
||||
},
|
||||
});
|
||||
CredentialsAPI.readAccessList.mockResolvedValue({ credentialAccessList });
|
||||
CredentialsAPI.readAccessOptions.mockResolvedValue({
|
||||
data: {
|
||||
actions: {
|
||||
GET: {},
|
||||
POST: {},
|
||||
},
|
||||
related_search_fields: [],
|
||||
},
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
@@ -213,4 +448,90 @@ describe('<ResourceAccessList />', () => {
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should show add button for system admin', async () => {
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ResourceAccessList resource={credential} apiModel={CredentialsAPI} />,
|
||||
{ context: { router: { credentialHistory } } }
|
||||
);
|
||||
});
|
||||
|
||||
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('ToolbarAddButton').length).toEqual(1);
|
||||
});
|
||||
|
||||
test('should not show add button for non system admin & non org admin', async () => {
|
||||
useUserProfile.mockImplementation(() => {
|
||||
return {
|
||||
isSuperUser: false,
|
||||
isSystemAuditor: false,
|
||||
isOrgAdmin: false,
|
||||
isNotificationAdmin: false,
|
||||
isExecEnvAdmin: false,
|
||||
};
|
||||
});
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ResourceAccessList resource={credential} apiModel={CredentialsAPI} />,
|
||||
{ context: { router: { credentialHistory } } }
|
||||
);
|
||||
});
|
||||
|
||||
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('ToolbarAddButton').length).toEqual(0);
|
||||
});
|
||||
|
||||
test('should show add button for non system admin, org admin, credential admin for credentials associated with org', async () => {
|
||||
useUserProfile.mockImplementation(() => {
|
||||
return {
|
||||
isSuperUser: false,
|
||||
isSystemAuditor: false,
|
||||
isOrgAdmin: true,
|
||||
isNotificationAdmin: false,
|
||||
isExecEnvAdmin: false,
|
||||
};
|
||||
});
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ResourceAccessList resource={credential} apiModel={CredentialsAPI} />,
|
||||
{ context: { router: { credentialHistory } } }
|
||||
);
|
||||
});
|
||||
|
||||
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('ToolbarAddButton').length).toEqual(1);
|
||||
});
|
||||
|
||||
test('should not show add button for non system admin, org admin, credential admin for credentials non associated with org', async () => {
|
||||
useUserProfile.mockImplementation(() => {
|
||||
return {
|
||||
isSuperUser: false,
|
||||
isSystemAuditor: false,
|
||||
isOrgAdmin: true,
|
||||
isNotificationAdmin: false,
|
||||
isExecEnvAdmin: false,
|
||||
};
|
||||
});
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ResourceAccessList
|
||||
resource={{ ...credential, organization: null }}
|
||||
apiModel={CredentialsAPI}
|
||||
/>,
|
||||
{ context: { router: { credentialHistory } } }
|
||||
);
|
||||
});
|
||||
|
||||
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('ToolbarAddButton').length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,7 +24,11 @@ function RoutedTabs({ tabsArray }) {
|
||||
const handleTabSelect = (event, eventKey) => {
|
||||
const match = tabsArray.find((tab) => tab.id === eventKey);
|
||||
if (match) {
|
||||
history.push(match.link);
|
||||
event.preventDefault();
|
||||
const link = match.isBackButton
|
||||
? `${match.link}?restoreFilters=true`
|
||||
: match.link;
|
||||
history.push(link);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -39,7 +43,7 @@ function RoutedTabs({ tabsArray }) {
|
||||
aria-label={typeof tab.name === 'string' ? tab.name : null}
|
||||
eventKey={tab.id}
|
||||
key={tab.id}
|
||||
link={tab.link}
|
||||
href={`#${tab.link}`}
|
||||
title={<TabTitleText>{tab.name}</TabTitleText>}
|
||||
aria-controls=""
|
||||
ouiaId={`${tab.name}-tab`}
|
||||
|
||||
@@ -37,7 +37,12 @@ describe('<RoutedTabs />', () => {
|
||||
});
|
||||
|
||||
test('should update history when new tab selected', async () => {
|
||||
wrapper.find('Tabs').invoke('onSelect')({}, 2);
|
||||
wrapper.find('Tabs').invoke('onSelect')(
|
||||
{
|
||||
preventDefault: () => {},
|
||||
},
|
||||
2
|
||||
);
|
||||
wrapper.update();
|
||||
|
||||
expect(history.location.pathname).toEqual('/organizations/19/access');
|
||||
|
||||
@@ -119,9 +119,10 @@ describe('<Schedule />', () => {
|
||||
});
|
||||
|
||||
test('expect all tabs to exist, including Back to Schedules', async () => {
|
||||
expect(
|
||||
wrapper.find('button[link="/templates/job_template/1/schedules"]').length
|
||||
).toBe(1);
|
||||
expect(wrapper.find('button[aria-label="Details"]').length).toBe(1);
|
||||
const routedTabs = wrapper.find('RoutedTabs');
|
||||
const tabs = routedTabs.prop('tabsArray');
|
||||
|
||||
expect(tabs[0].link).toEqual('/templates/job_template/1/schedules');
|
||||
expect(tabs[1].name).toEqual('Details');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -166,7 +166,8 @@ function ScheduleList({
|
||||
headerRow={
|
||||
<HeaderRow qsConfig={QS_CONFIG}>
|
||||
<HeaderCell sortKey="name">{t`Name`}</HeaderCell>
|
||||
<HeaderCell sortKey="unified_job_template__polymorphic_ctype__model">{t`Type`}</HeaderCell>
|
||||
<HeaderCell sortKey="unified_job_template">{t`Related resource`}</HeaderCell>
|
||||
<HeaderCell sortKey="unified_job_template__polymorphic_ctype__model">{t`Resource type`}</HeaderCell>
|
||||
<HeaderCell sortKey="next_run">{t`Next Run`}</HeaderCell>
|
||||
<HeaderCell>{t`Actions`}</HeaderCell>
|
||||
</HeaderRow>
|
||||
|
||||
@@ -41,22 +41,28 @@ function ScheduleListItem({
|
||||
};
|
||||
|
||||
let scheduleBaseUrl;
|
||||
let relatedResourceUrl;
|
||||
|
||||
switch (schedule.summary_fields.unified_job_template.unified_job_type) {
|
||||
case 'inventory_update':
|
||||
scheduleBaseUrl = `/inventories/inventory/${schedule.summary_fields.inventory.id}/sources/${schedule.summary_fields.unified_job_template.id}/schedules/${schedule.id}`;
|
||||
relatedResourceUrl = `/inventories/inventory/${schedule.summary_fields.inventory.id}/sources/${schedule.summary_fields.unified_job_template.id}/details`;
|
||||
break;
|
||||
case 'job':
|
||||
scheduleBaseUrl = `/templates/job_template/${schedule.summary_fields.unified_job_template.id}/schedules/${schedule.id}`;
|
||||
relatedResourceUrl = `/templates/job_template/${schedule.summary_fields.unified_job_template.id}/details`;
|
||||
break;
|
||||
case 'project_update':
|
||||
scheduleBaseUrl = `/projects/${schedule.summary_fields.unified_job_template.id}/schedules/${schedule.id}`;
|
||||
relatedResourceUrl = `/projects/${schedule.summary_fields.unified_job_template.id}/details`;
|
||||
break;
|
||||
case 'system_job':
|
||||
scheduleBaseUrl = `/management_jobs/${schedule.summary_fields.unified_job_template.id}/schedules/${schedule.id}`;
|
||||
relatedResourceUrl = `/management_jobs`;
|
||||
break;
|
||||
case 'workflow_job':
|
||||
scheduleBaseUrl = `/templates/workflow_job_template/${schedule.summary_fields.unified_job_template.id}/schedules/${schedule.id}`;
|
||||
relatedResourceUrl = `/templates/workflow_job_template/${schedule.summary_fields.unified_job_template.id}/details`;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -94,7 +100,15 @@ function ScheduleListItem({
|
||||
</span>
|
||||
)}
|
||||
</TdBreakWord>
|
||||
<Td dataLabel={t`Type`}>
|
||||
<TdBreakWord
|
||||
id={`related-resource-${schedule.id}`}
|
||||
dataLabel={t`Related resource`}
|
||||
>
|
||||
<Link to={`${relatedResourceUrl}`}>
|
||||
<b>{schedule.summary_fields.unified_job_template.name}</b>
|
||||
</Link>
|
||||
</TdBreakWord>
|
||||
<Td dataLabel={t`Resource type`}>
|
||||
{
|
||||
jobTypeLabels[
|
||||
schedule.summary_fields.unified_job_template.unified_job_type
|
||||
|
||||
@@ -65,14 +65,30 @@ describe('ScheduleListItem', () => {
|
||||
});
|
||||
|
||||
test('Name correctly shown with correct link', () => {
|
||||
expect(wrapper.find('Td').at(1).prop('dataLabel')).toBe('Name');
|
||||
expect(wrapper.find('Td').at(1).text()).toBe('Mock Schedule');
|
||||
expect(wrapper.find('Td').at(1).find('Link').props().to).toBe(
|
||||
'/templates/job_template/12/schedules/6/details'
|
||||
);
|
||||
});
|
||||
|
||||
test('Type correctly shown', () => {
|
||||
expect(wrapper.find('Td').at(2).text()).toBe('Playbook Run');
|
||||
test('Related resource correctly shown', () => {
|
||||
expect(wrapper.find('Td').at(2).prop('dataLabel')).toBe(
|
||||
'Related resource'
|
||||
);
|
||||
expect(wrapper.find('Td').at(2).text()).toBe('Mock JT');
|
||||
});
|
||||
|
||||
test('Resource type correctly shown', () => {
|
||||
expect(wrapper.find('Td').at(3).prop('dataLabel')).toBe('Resource type');
|
||||
expect(wrapper.find('Td').at(3).text()).toBe('Playbook Run');
|
||||
});
|
||||
|
||||
test('Next run correctly shown', () => {
|
||||
expect(wrapper.find('Td').at(4).prop('dataLabel')).toBe('Next Run');
|
||||
expect(wrapper.find('Td').at(4).text()).toBe(
|
||||
'Next Run2/20/2020, 12:00:00 AM'
|
||||
);
|
||||
});
|
||||
|
||||
test('Edit button shown with correct link', () => {
|
||||
@@ -120,16 +136,31 @@ describe('ScheduleListItem', () => {
|
||||
});
|
||||
|
||||
test('Name correctly shown with correct link', () => {
|
||||
expect(wrapper.find('Td').at(1).prop('dataLabel')).toBe('Name');
|
||||
expect(wrapper.find('Td').at(1).text()).toBe('Mock Schedule');
|
||||
expect(wrapper.find('Td').at(1).find('Link').props().to).toBe(
|
||||
'/templates/job_template/12/schedules/6/details'
|
||||
);
|
||||
});
|
||||
|
||||
test('Type correctly shown', () => {
|
||||
expect(wrapper.find('Td').at(2).text()).toBe('Playbook Run');
|
||||
test('Related resource correctly shown', () => {
|
||||
expect(wrapper.find('Td').at(2).prop('dataLabel')).toBe(
|
||||
'Related resource'
|
||||
);
|
||||
expect(wrapper.find('Td').at(2).text()).toBe('Mock JT');
|
||||
});
|
||||
|
||||
test('Resource type correctly shown', () => {
|
||||
expect(wrapper.find('Td').at(3).prop('dataLabel')).toBe('Resource type');
|
||||
expect(wrapper.find('Td').at(3).text()).toBe('Playbook Run');
|
||||
});
|
||||
|
||||
test('Next run correctly shown', () => {
|
||||
expect(wrapper.find('Td').at(4).prop('dataLabel')).toBe('Next Run');
|
||||
expect(wrapper.find('Td').at(4).text()).toBe(
|
||||
'Next Run2/20/2020, 12:00:00 AM'
|
||||
);
|
||||
});
|
||||
test('Edit button hidden', () => {
|
||||
expect(wrapper.find('PencilAltIcon').length).toBe(0);
|
||||
});
|
||||
|
||||
@@ -89,7 +89,7 @@ const generateRunOnTheDay = (days = []) => {
|
||||
return null;
|
||||
};
|
||||
|
||||
function ScheduleFormFields({ hasDaysToKeepField, zoneOptions }) {
|
||||
function ScheduleFormFields({ hasDaysToKeepField, zoneOptions, zoneLinks }) {
|
||||
const [timezone, timezoneMeta] = useField({
|
||||
name: 'timezone',
|
||||
validate: required(t`Select a value for this field`),
|
||||
@@ -100,6 +100,24 @@ function ScheduleFormFields({ hasDaysToKeepField, zoneOptions }) {
|
||||
});
|
||||
const [{ name: dateFieldName }] = useField('startDate');
|
||||
const [{ name: timeFieldName }] = useField('startTime');
|
||||
const [timezoneMessage, setTimezoneMessage] = useState('');
|
||||
const warnLinkedTZ = (event, selectedValue) => {
|
||||
if (zoneLinks[selectedValue]) {
|
||||
setTimezoneMessage(
|
||||
`Warning: ${selectedValue} is a link to ${zoneLinks[selectedValue]} and will be saved as that.`
|
||||
);
|
||||
} else {
|
||||
setTimezoneMessage('');
|
||||
}
|
||||
timezone.onChange(event, selectedValue);
|
||||
};
|
||||
|
||||
let timezoneValidatedStatus = 'default';
|
||||
if (timezoneMeta.touched && timezoneMeta.error) {
|
||||
timezoneValidatedStatus = 'error';
|
||||
} else if (timezoneMessage) {
|
||||
timezoneValidatedStatus = 'warning';
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<FormField
|
||||
@@ -124,17 +142,17 @@ function ScheduleFormFields({ hasDaysToKeepField, zoneOptions }) {
|
||||
<FormGroup
|
||||
name="timezone"
|
||||
fieldId="schedule-timezone"
|
||||
helperTextInvalid={timezoneMeta.error}
|
||||
helperTextInvalid={timezoneMeta.error || timezoneMessage}
|
||||
isRequired
|
||||
validated={
|
||||
!timezoneMeta.touched || !timezoneMeta.error ? 'default' : 'error'
|
||||
}
|
||||
validated={timezoneValidatedStatus}
|
||||
label={t`Local time zone`}
|
||||
helperText={timezoneMessage}
|
||||
>
|
||||
<AnsibleSelect
|
||||
id="schedule-timezone"
|
||||
data={zoneOptions}
|
||||
{...timezone}
|
||||
onChange={warnLinkedTZ}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
@@ -212,7 +230,7 @@ function ScheduleForm({
|
||||
request: loadScheduleData,
|
||||
error: contentError,
|
||||
isLoading: contentLoading,
|
||||
result: { zoneOptions, credentials },
|
||||
result: { zoneOptions, zoneLinks, credentials },
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const { data } = await SchedulesAPI.readZoneInfo();
|
||||
@@ -225,19 +243,21 @@ function ScheduleForm({
|
||||
creds = results;
|
||||
}
|
||||
|
||||
const zones = data.map((zone) => ({
|
||||
value: zone.name,
|
||||
key: zone.name,
|
||||
label: zone.name,
|
||||
const zones = (data.zones || []).map((zone) => ({
|
||||
value: zone,
|
||||
key: zone,
|
||||
label: zone,
|
||||
}));
|
||||
|
||||
return {
|
||||
zoneOptions: zones,
|
||||
zoneLinks: data.links,
|
||||
credentials: creds || [],
|
||||
};
|
||||
}, [schedule]),
|
||||
{
|
||||
zonesOptions: [],
|
||||
zoneLinks: {},
|
||||
credentials: [],
|
||||
isLoading: true,
|
||||
}
|
||||
@@ -630,6 +650,7 @@ function ScheduleForm({
|
||||
<ScheduleFormFields
|
||||
hasDaysToKeepField={hasDaysToKeepField}
|
||||
zoneOptions={zoneOptions}
|
||||
zoneLinks={zoneLinks}
|
||||
/>
|
||||
{isWizardOpen && (
|
||||
<SchedulePromptableFields
|
||||
|
||||
@@ -241,6 +241,7 @@ function TemplateList({ defaultParams }) {
|
||||
<HeaderRow qsConfig={qsConfig} isExpandable>
|
||||
<HeaderCell sortKey="name">{t`Name`}</HeaderCell>
|
||||
<HeaderCell sortKey="type">{t`Type`}</HeaderCell>
|
||||
<HeaderCell sortKey="organization">{t`Organization`}</HeaderCell>
|
||||
<HeaderCell sortKey="last_job_run">{t`Last Ran`}</HeaderCell>
|
||||
<HeaderCell>{t`Actions`}</HeaderCell>
|
||||
</HeaderRow>
|
||||
|
||||
@@ -182,6 +182,15 @@ function TemplateListItem({
|
||||
)}
|
||||
</TdBreakWord>
|
||||
<Td dataLabel={t`Type`}>{toTitleCase(template.type)}</Td>
|
||||
<Td dataLabel={t`Organization`}>
|
||||
{summaryFields.organization ? (
|
||||
<Link
|
||||
to={`/organizations/${summaryFields.organization.id}/details`}
|
||||
>
|
||||
{summaryFields.organization.name}
|
||||
</Link>
|
||||
) : null}
|
||||
</Td>
|
||||
<Td dataLabel={t`Last Ran`}>{lastRun}</Td>
|
||||
<ActionsTd dataLabel={t`Actions`}>
|
||||
<ActionItem
|
||||
@@ -270,19 +279,6 @@ function TemplateListItem({
|
||||
dataCy={`template-${template.id}-activity`}
|
||||
/>
|
||||
) : null}
|
||||
{summaryFields.organization && (
|
||||
<Detail
|
||||
label={t`Organization`}
|
||||
value={
|
||||
<Link
|
||||
to={`/organizations/${summaryFields.organization.id}/details`}
|
||||
>
|
||||
{summaryFields.organization.name}
|
||||
</Link>
|
||||
}
|
||||
dataCy={`template-${template.id}-organization`}
|
||||
/>
|
||||
)}
|
||||
{summaryFields.inventory ? (
|
||||
<Detail
|
||||
label={t`Inventory`}
|
||||
|
||||
@@ -10,6 +10,50 @@ import TemplateListItem from './TemplateListItem';
|
||||
jest.mock('../../api');
|
||||
|
||||
describe('<TemplateListItem />', () => {
|
||||
test('should display expected data', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<table>
|
||||
<tbody>
|
||||
<TemplateListItem
|
||||
isSelected={false}
|
||||
template={{
|
||||
id: 1,
|
||||
name: 'Template 1',
|
||||
url: '/templates/job_template/1',
|
||||
type: 'job_template',
|
||||
summary_fields: {
|
||||
organization: {
|
||||
id: 1,
|
||||
name: 'Foo',
|
||||
},
|
||||
user_capabilities: {
|
||||
start: true,
|
||||
},
|
||||
recent_jobs: [
|
||||
{
|
||||
id: 123,
|
||||
name: 'Template 1',
|
||||
status: 'failed',
|
||||
finished: '2020-02-26T22:38:41.037991Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
expect(wrapper.find('Td[dataLabel="Name"]').text()).toBe('Template 1');
|
||||
expect(wrapper.find('Td[dataLabel="Type"]').text()).toBe('Job Template');
|
||||
expect(wrapper.find('Td[dataLabel="Organization"]').text()).toBe('Foo');
|
||||
expect(
|
||||
wrapper.find('Td[dataLabel="Organization"]').find('Link').prop('to')
|
||||
).toBe('/organizations/1/details');
|
||||
expect(wrapper.find('Td[dataLabel="Last Ran"]').text()).toBe(
|
||||
'2/26/2020, 10:38:41 PM'
|
||||
);
|
||||
});
|
||||
|
||||
test('launch button shown to users with start capabilities', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<table>
|
||||
@@ -401,7 +445,6 @@ describe('<TemplateListItem />', () => {
|
||||
}
|
||||
|
||||
assertDetail('Description', 'mock description');
|
||||
assertDetail('Organization', "Mike's Org");
|
||||
assertDetail('Inventory', "Mike's Inventory");
|
||||
assertDetail('Project', "Mike's Project");
|
||||
assertDetail('Execution Environment', 'Mock EE 1.2.3');
|
||||
@@ -420,9 +463,6 @@ describe('<TemplateListItem />', () => {
|
||||
.find('Detail[label="Labels"]')
|
||||
.containsAllMatchingElements([<span>L_91o2</span>])
|
||||
).toEqual(true);
|
||||
expect(wrapper.find('Detail[label="Organization"] dd a').prop('href')).toBe(
|
||||
'/organizations/1/details'
|
||||
);
|
||||
expect(wrapper.find(`Detail[label="Activity"] Sparkline`)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,3 +10,5 @@ export const JOB_TYPE_URL_SEGMENTS = {
|
||||
|
||||
export const SESSION_TIMEOUT_KEY = 'awx-session-timeout';
|
||||
export const SESSION_REDIRECT_URL = 'awx-redirect-url';
|
||||
export const PERSISTENT_FILTER_KEY = 'awx-persistent-filter';
|
||||
export const SESSION_USER_ID = 'awx-session-user-id';
|
||||
|
||||
@@ -11,7 +11,7 @@ import { DateTime } from 'luxon';
|
||||
import { RootAPI, MeAPI } from 'api';
|
||||
import { isAuthenticated } from 'util/auth';
|
||||
import useRequest from 'hooks/useRequest';
|
||||
import { SESSION_TIMEOUT_KEY } from '../constants';
|
||||
import { SESSION_TIMEOUT_KEY, SESSION_USER_ID } from '../constants';
|
||||
|
||||
// The maximum supported timeout for setTimeout(), in milliseconds,
|
||||
// is the highest number you can represent as a signed 32bit
|
||||
@@ -101,7 +101,9 @@ function SessionProvider({ children }) {
|
||||
setIsUserBeingLoggedOut(true);
|
||||
if (!isSessionExpired.current) {
|
||||
setAuthRedirectTo('/logout');
|
||||
window.localStorage.setItem(SESSION_USER_ID, null);
|
||||
}
|
||||
sessionStorage.clear();
|
||||
await RootAPI.logout();
|
||||
setSessionTimeout(0);
|
||||
setSessionCountdown(0);
|
||||
@@ -166,21 +168,21 @@ function SessionProvider({ children }) {
|
||||
|
||||
const sessionValue = useMemo(
|
||||
() => ({
|
||||
isUserBeingLoggedOut,
|
||||
loginRedirectOverride,
|
||||
authRedirectTo,
|
||||
handleSessionContinue,
|
||||
isSessionExpired,
|
||||
isUserBeingLoggedOut,
|
||||
loginRedirectOverride,
|
||||
logout,
|
||||
sessionCountdown,
|
||||
setAuthRedirectTo,
|
||||
}),
|
||||
[
|
||||
isUserBeingLoggedOut,
|
||||
loginRedirectOverride,
|
||||
authRedirectTo,
|
||||
handleSessionContinue,
|
||||
isSessionExpired,
|
||||
isUserBeingLoggedOut,
|
||||
loginRedirectOverride,
|
||||
logout,
|
||||
sessionCountdown,
|
||||
setAuthRedirectTo,
|
||||
|
||||
@@ -74,6 +74,7 @@ function Application({ setBreadcrumb }) {
|
||||
),
|
||||
link: '/applications',
|
||||
id: 0,
|
||||
isBackButton: true,
|
||||
},
|
||||
{ name: t`Details`, link: `/applications/${id}/details`, id: 1 },
|
||||
{ name: t`Tokens`, link: `/applications/${id}/tokens`, id: 2 },
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '@patternfly/react-core';
|
||||
import ScreenHeader from 'components/ScreenHeader';
|
||||
import { Detail, DetailList } from 'components/DetailList';
|
||||
import PersistentFilters from 'components/PersistentFilters';
|
||||
import ApplicationsList from './ApplicationsList';
|
||||
import ApplicationAdd from './ApplicationAdd';
|
||||
import Application from './Application';
|
||||
@@ -56,7 +57,9 @@ function Applications() {
|
||||
<Application setBreadcrumb={buildBreadcrumbConfig} />
|
||||
</Route>
|
||||
<Route path="/applications">
|
||||
<ApplicationsList />
|
||||
<PersistentFilters pageKey="applications">
|
||||
<ApplicationsList />
|
||||
</PersistentFilters>
|
||||
</Route>
|
||||
</Switch>
|
||||
{applicationModalSource && (
|
||||
|
||||
@@ -67,6 +67,7 @@ function Credential({ setBreadcrumb }) {
|
||||
),
|
||||
link: `/credentials`,
|
||||
id: 99,
|
||||
isBackButton: true,
|
||||
},
|
||||
{ name: t`Details`, link: `/credentials/${id}/details`, id: 0 },
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Route, Switch } from 'react-router-dom';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Config } from 'contexts/Config';
|
||||
import ScreenHeader from 'components/ScreenHeader';
|
||||
import PersistentFilters from 'components/PersistentFilters';
|
||||
import Credential from './Credential';
|
||||
import CredentialAdd from './CredentialAdd';
|
||||
import { CredentialList } from './CredentialList';
|
||||
@@ -44,7 +45,9 @@ function Credentials() {
|
||||
<Credential setBreadcrumb={buildBreadcrumbConfig} />
|
||||
</Route>
|
||||
<Route path="/credentials">
|
||||
<CredentialList />
|
||||
<PersistentFilters pageKey="credentials">
|
||||
<CredentialList />
|
||||
</PersistentFilters>
|
||||
</Route>
|
||||
</Switch>
|
||||
</>
|
||||
|
||||
@@ -63,6 +63,7 @@ function BecomeMethodField({ fieldOptions, isRequired }) {
|
||||
setOptions([...options, { value: option }]);
|
||||
}}
|
||||
noResultsFoundText={t`No results found`}
|
||||
createText={t`Create`}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<SelectOption key={option.value} value={option.value} />
|
||||
|
||||
@@ -57,6 +57,7 @@ function CredentialType({ setBreadcrumb }) {
|
||||
),
|
||||
link: '/credential_types',
|
||||
id: 99,
|
||||
isBackButton: true,
|
||||
},
|
||||
{
|
||||
name: t`Details`,
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useCallback } from 'react';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
|
||||
import PersistentFilters from 'components/PersistentFilters';
|
||||
import ScreenHeader from 'components/ScreenHeader';
|
||||
import CredentialTypeAdd from './CredentialTypeAdd';
|
||||
import CredentialTypeList from './CredentialTypeList';
|
||||
@@ -40,7 +40,9 @@ function CredentialTypes() {
|
||||
<CredentialType setBreadcrumb={buildBreadcrumbConfig} />
|
||||
</Route>
|
||||
<Route path="/credential_types">
|
||||
<CredentialTypeList />
|
||||
<PersistentFilters pageKey="credentialTypes">
|
||||
<CredentialTypeList />
|
||||
</PersistentFilters>
|
||||
</Route>
|
||||
</Switch>
|
||||
</>
|
||||
|
||||
@@ -59,6 +59,7 @@ function ExecutionEnvironment({ setBreadcrumb }) {
|
||||
),
|
||||
link: '/execution_environments',
|
||||
id: 99,
|
||||
isBackButton: true,
|
||||
},
|
||||
{
|
||||
name: t`Details`,
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useCallback } from 'react';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
|
||||
import PersistentFilters from 'components/PersistentFilters';
|
||||
import ScreenHeader from 'components/ScreenHeader/ScreenHeader';
|
||||
import ExecutionEnvironment from './ExecutionEnvironment';
|
||||
import ExecutionEnvironmentAdd from './ExecutionEnvironmentAdd';
|
||||
@@ -40,7 +40,9 @@ function ExecutionEnvironments() {
|
||||
<ExecutionEnvironment setBreadcrumb={buildBreadcrumbConfig} />
|
||||
</Route>
|
||||
<Route path="/execution_environments">
|
||||
<ExecutionEnvironmentList />
|
||||
<PersistentFilters pageKey="executionEnvironments">
|
||||
<ExecutionEnvironmentList />
|
||||
</PersistentFilters>
|
||||
</Route>
|
||||
</Switch>
|
||||
</>
|
||||
|
||||
@@ -52,6 +52,7 @@ function Host({ setBreadcrumb }) {
|
||||
),
|
||||
link: `/hosts`,
|
||||
id: 99,
|
||||
isBackButton: true,
|
||||
},
|
||||
{
|
||||
name: t`Details`,
|
||||
|
||||
@@ -167,6 +167,7 @@ function HostList() {
|
||||
headerRow={
|
||||
<HeaderRow qsConfig={QS_CONFIG} isExpandable>
|
||||
<HeaderCell sortKey="name">{t`Name`}</HeaderCell>
|
||||
<HeaderCell sortKey="description">{t`Description`}</HeaderCell>
|
||||
<HeaderCell>{t`Inventory`}</HeaderCell>
|
||||
<HeaderCell>{t`Actions`}</HeaderCell>
|
||||
</HeaderRow>
|
||||
|
||||
@@ -52,6 +52,12 @@ function HostListItem({
|
||||
<b>{host.name}</b>
|
||||
</Link>
|
||||
</TdBreakWord>
|
||||
<TdBreakWord
|
||||
id={`host-description-${host.id}}`}
|
||||
dataLabel={t`Description`}
|
||||
>
|
||||
{host.description}
|
||||
</TdBreakWord>
|
||||
<TdBreakWord dataLabel={t`Inventory`}>
|
||||
{host.summary_fields.inventory && (
|
||||
<Link
|
||||
|
||||
@@ -7,6 +7,7 @@ const mockHost = {
|
||||
id: 1,
|
||||
name: 'Host 1',
|
||||
url: '/api/v2/hosts/1',
|
||||
description: 'Buzz',
|
||||
inventory: 1,
|
||||
summary_fields: {
|
||||
inventory: {
|
||||
@@ -38,6 +39,14 @@ describe('<HostsListItem />', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('should display expected details', () => {
|
||||
expect(wrapper.find('HostListItem').length).toBe(1);
|
||||
expect(wrapper.find('Td[dataLabel="Name"]').find('Link').prop('to')).toBe(
|
||||
'/host/1'
|
||||
);
|
||||
expect(wrapper.find('Td[dataLabel="Description"]').text()).toBe('Buzz');
|
||||
});
|
||||
|
||||
test('edit button shown to users with edit capabilities', () => {
|
||||
expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
import { Config } from 'contexts/Config';
|
||||
import ScreenHeader from 'components/ScreenHeader/ScreenHeader';
|
||||
|
||||
import PersistentFilters from 'components/PersistentFilters';
|
||||
import HostList from './HostList';
|
||||
import HostAdd from './HostAdd';
|
||||
import Host from './Host';
|
||||
@@ -47,7 +46,9 @@ function Hosts() {
|
||||
</Config>
|
||||
</Route>
|
||||
<Route path="/hosts">
|
||||
<HostList />
|
||||
<PersistentFilters pageKey="hosts">
|
||||
<HostList />
|
||||
</PersistentFilters>
|
||||
</Route>
|
||||
</Switch>
|
||||
</>
|
||||
|
||||
@@ -116,7 +116,11 @@ function InstanceDetails({ setBreadcrumb, instanceGroup }) {
|
||||
fetchDetails();
|
||||
}, [fetchDetails]);
|
||||
|
||||
const { error: healthCheckError, request: fetchHealthCheck } = useRequest(
|
||||
const {
|
||||
error: healthCheckError,
|
||||
isLoading: isRunningHealthCheck,
|
||||
request: fetchHealthCheck,
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const { data } = await InstancesAPI.healthCheck(instanceId);
|
||||
setHealthCheck(data);
|
||||
@@ -265,12 +269,14 @@ function InstanceDetails({ setBreadcrumb, instanceGroup }) {
|
||||
<CardActionsRow>
|
||||
<Tooltip content={t`Run a health check on the instance`}>
|
||||
<Button
|
||||
isDisabled={!me.is_superuser}
|
||||
isDisabled={!me.is_superuser || isRunningHealthCheck}
|
||||
variant="primary"
|
||||
ouiaId="health-check-button"
|
||||
onClick={fetchHealthCheck}
|
||||
isLoading={isRunningHealthCheck}
|
||||
spinnerAriaLabel={t`Running health check`}
|
||||
>
|
||||
{t`Health Check`}
|
||||
{t`Run health check`}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{me.is_superuser && instance.node_type !== 'control' && (
|
||||
|
||||
@@ -63,6 +63,7 @@ function InstanceGroup({ setBreadcrumb }) {
|
||||
),
|
||||
link: '/instance_groups',
|
||||
id: 99,
|
||||
isBackButton: true,
|
||||
},
|
||||
{
|
||||
name: t`Details`,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user