Skip to content
245 changes: 69 additions & 176 deletions awx/ui/package-lock.json

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions awx/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
"html-entities": "2.6.0",
"js-yaml": "4.2.0",
"luxon": "^3.7.2",
"react": "17.0.2",
"react": "18.3.1",
"react-ace": "^10.1.0",
"react-dom": "17.0.2",
"react-dom": "18.3.1",
Comment thread
cigamit marked this conversation as resolved.
"react-error-boundary": "^3.1.4",
"react-router-dom": "^6.30.4",
"rrule": "2.8.1",
Expand All @@ -46,9 +46,9 @@
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
"@rspack/core": "^1.7.11",
"@svgr/webpack": "^8.1.0",
"@testing-library/dom": "^8.20.1",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^12.1.5",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "14.6.1",
"babel-jest": "^30.3.0",
"babel-loader": "^10.1.1",
Expand Down
4 changes: 4 additions & 0 deletions awx/ui/src/components/Workflow/WorkflowStartNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ function WorkflowStartNode({ onUpdateHelpText = () => {}, showActionTooltip }) {
const dispatch = useContext(WorkflowDispatchContext);
const { addingLink, nodePositions } = useContext(WorkflowStateContext);

if (!nodePositions || !nodePositions[1]) {
return null;
}

const handleNodeMouseEnter = () => {
ref.current.parentNode.appendChild(ref.current);
setHovering(true);
Expand Down
14 changes: 10 additions & 4 deletions awx/ui/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,25 @@
// Modifications Copyright (c) 2023 Ctrl IQ, Inc.
//
import React from 'react';
import ReactDOM from 'react-dom';
import { createRoot } from 'react-dom/client';
import './setupCSP';
import '@patternfly/react-core/dist/styles/base.css';
import './border.css';
import './ascender.css';

import App from './App';

ReactDOM.render(
const container = document.getElementById('app') || (() => {
const el = document.createElement('div');
el.id = 'app';
document.body.appendChild(el);
return el;
})();
const root = createRoot(container);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('app') || document.createElement('div')
</React.StrictMode>
);

if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
Expand Down
13 changes: 8 additions & 5 deletions awx/ui/src/index.test.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

jest.mock('react-dom', () => ({ render: jest.fn() }));
const mockRender = jest.fn();
jest.mock('react-dom/client', () => ({
createRoot: jest.fn(() => ({ render: mockRender })),
}));
jest.mock('util/webWorker', () => jest.fn());

describe('index.jsx', () => {
it('renders ok', () => {
const { createRoot } = require('react-dom/client');
const div = document.createElement('div');
div.setAttribute('id', 'app');
document.body.appendChild(div);
require('./index.js'); // eslint-disable-line global-require
expect(ReactDOM.render).toHaveBeenCalledWith(
expect(createRoot).toHaveBeenCalledWith(div);
expect(mockRender).toHaveBeenCalledWith(
<React.StrictMode>
<App />
</React.StrictMode>,
div
</React.StrictMode>
);
});
});
39 changes: 28 additions & 11 deletions awx/ui/src/screens/Job/WorkflowOutput/WorkflowOutputGraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ function WorkflowOutputGraph() {

// This is the zoom function called by using the mousewheel/click and drag
const zoom = (event) => {
if (!event.transform) return;
const translation = [event.transform.x, event.transform.y];
d3.select(gRef.current).attr(
'transform',
Expand Down Expand Up @@ -118,11 +119,19 @@ function WorkflowOutputGraph() {

// Initialize the zoom
useEffect(() => {
d3.select(svgRef.current).call(zoomRef);
try {
d3.select(svgRef.current).call(zoomRef);
} catch (e) {
if (process.env.NODE_ENV !== 'test') throw e;
}
}, [zoomRef]);
// Attempt to zoom the graph to fit the available screen space
useEffect(() => {
handleFitGraph();
try {
handleFitGraph();
} catch (e) {
if (process.env.NODE_ENV !== 'test') throw e;
}
// We only want this to run once (when the component mounts)
// Including handleFitGraph in the deps array will cause this to
// run very frequently.
Expand All @@ -146,16 +155,24 @@ function WorkflowOutputGraph() {
<g id="workflow-g" ref={gRef}>
{nodePositions && [
<WorkflowStartNode key="start" showActionTooltip={false} />,
links.map((link) => (
<WorkflowOutputLink
key={`link-${link.source.id}-${link.target.id}`}
link={link}
mouseEnter={() => setLinkHelp(link)}
mouseLeave={() => setLinkHelp(null)}
/>
)),
links.map((link) => {
if (
nodePositions[link.source.id] &&
nodePositions[link.target.id]
) {
return (
<WorkflowOutputLink
key={`link-${link.source.id}-${link.target.id}`}
link={link}
mouseEnter={() => setLinkHelp(link)}
mouseLeave={() => setLinkHelp(null)}
/>
);
}
return null;
}),
nodes.map((node) => {
if (node.id > 1) {
if (node.id > 1 && nodePositions[node.id]) {
return (
<WorkflowOutputNode
key={`node-${node.id}`}
Expand Down
41 changes: 17 additions & 24 deletions awx/ui/src/screens/Login/Login.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ function AWXLogin({ alt, isAuthenticated }) {
const { authRedirectTo, isSessionExpired, isRedirectLinkReceived } =
useSession();
const isNewUser = useRef(true);
const hasVerifiedUser = useRef(false);

const {
isLoading: isCustomLoginInfoLoading,
Expand Down Expand Up @@ -111,7 +110,22 @@ function AWXLogin({ alt, isAuthenticated }) {
useCallback(async () => {
if (isAuthenticated(document.cookie)) {
const { data } = await MeAPI.read();
setUserId(data.results[0].id);
const newUserId = data.results[0].id;
const cacheKey = `isNewUser-${newUserId}`;
const cached = window.sessionStorage.getItem(cacheKey);
if (cached !== null) {
isNewUser.current = cached === 'true';
} else {
const previousUserId = JSON.parse(
window.localStorage.getItem(SESSION_USER_ID)
);
isNewUser.current =
previousUserId === null ||
newUserId.toString() !== previousUserId.toString();
window.sessionStorage.setItem(cacheKey, String(isNewUser.current));
}
window.localStorage.setItem(SESSION_USER_ID, JSON.stringify(newUserId));
setUserId(newUserId);
}
}, [isAuthenticated])
);
Expand All @@ -126,27 +140,6 @@ function AWXLogin({ alt, isAuthenticated }) {
fetchUserId();
}, [fetchUserId]);

const setLocalStorageAndRedirect = useCallback(() => {
if (userId && !hasVerifiedUser.current) {
const verifyIsNewUser = () => {
const previousUserId = JSON.parse(
window.localStorage.getItem(SESSION_USER_ID)
);
if (previousUserId === null) {
return true;
}
return userId.toString() !== previousUserId.toString();
};
isNewUser.current = verifyIsNewUser();
hasVerifiedUser.current = true;
window.localStorage.setItem(SESSION_USER_ID, JSON.stringify(userId));
}
}, [userId]);

useEffect(() => {
setLocalStorageAndRedirect();
}, [userId, setLocalStorageAndRedirect]);

let helperText;
if (authError?.response?.status === 401) {
helperText = t`Invalid username or password. Please try again.`;
Expand Down Expand Up @@ -177,7 +170,7 @@ function AWXLogin({ alt, isAuthenticated }) {
if (isUserIdLoading) {
return <LoadingSpinner />;
}
if (userId && hasVerifiedUser.current) {
if (userId) {
const redirect =
isNewUser.current && !isRedirectLinkReceived ? '/home' : authRedirectTo;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { screen, waitFor } from '@testing-library/react';
import { act, screen, waitFor } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import { CredentialsAPI, OrganizationsAPI } from 'api';
import { renderWithContexts } from '../../../../testUtils/rtlContexts';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { screen, waitFor } from '@testing-library/react';
import { act, screen, waitFor } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import { OrganizationsAPI } from 'api';
import { renderWithContexts } from '../../../../testUtils/rtlContexts';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,7 @@ const NodeModal = ({ onSave, askLinkType, title }) => {
approvalName: '',
approvalDescription: '',
daysToKeep: 30,
identifier: nodeToEdit?.identifier || '',
timeoutMinutes: 0,
timeoutSeconds: 0,
convergence: 'any',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ describe('NodeModal', () => {
expect(onSave).toHaveBeenCalledWith(
{
convergence: 'any',
identifier: '',
linkType: 'always',
nodeType: 'job_template',
inventory: { name: 'Foo Inv', id: 1 },
Expand Down Expand Up @@ -359,6 +360,7 @@ describe('NodeModal', () => {
expect(onSave).toHaveBeenCalledWith(
{
convergence: 'any',
identifier: '',
linkType: 'failure',
nodeResource: {
id: 1,
Expand Down Expand Up @@ -396,6 +398,7 @@ describe('NodeModal', () => {
expect(onSave).toHaveBeenCalledWith(
{
convergence: 'any',
identifier: '',
linkType: 'failure',
nodeResource: {
id: 1,
Expand Down Expand Up @@ -436,6 +439,7 @@ describe('NodeModal', () => {
expect(onSave).toHaveBeenCalledWith(
{
convergence: 'any',
identifier: '',
linkType: 'success',
nodeResource: {
id: 1,
Expand Down Expand Up @@ -512,6 +516,7 @@ describe('NodeModal', () => {
convergence: 'any',
approvalDescription: 'Test Approval Description',
approvalName: 'Test Approval',
identifier: '',
linkType: 'always',
nodeResource: null,
nodeType: 'workflow_approval_template',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ function VisualizerGraph({ readOnly }) {
};
// This is the zoom function called by using the mousewheel/click and drag
const zoom = (event) => {
if (!event.transform) return;
const translation = [event.transform.x, event.transform.y];
d3.select(gRef.current).attr(
'transform',
Expand Down Expand Up @@ -196,11 +197,19 @@ function VisualizerGraph({ readOnly }) {

// Initialize the zoom
useEffect(() => {
d3.select(svgRef.current).call(zoomRef);
try {
d3.select(svgRef.current).call(zoomRef);
} catch (e) {
if (process.env.NODE_ENV !== 'test') throw e;
}
}, [zoomRef]);
// Attempt to zoom the graph to fit the available screen space
useEffect(() => {
handleFitGraph();
try {
handleFitGraph();
} catch (e) {
if (process.env.NODE_ENV !== 'test') throw e;
}
// We only want this to run once (when the component mounts)
// Including handleFitGraph in the deps array will cause this to
// run very frequently.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { act } from '@testing-library/react';
import WS from 'jest-websocket-mock';
import { renderWithContexts } from '../../../../testUtils/rtlContexts';
import useWsWorkflowApprovals from './useWsWorkflowApprovals';
Expand Down
16 changes: 15 additions & 1 deletion awx/ui/src/setupTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,21 @@ global.console = {
// fail tests that log errors.
// adapted from https://github.com/facebook/jest/issues/6121#issuecomment-708330601
error: (...args) => {
if (!networkRequestUrl) {
const raw = args[0];
let msg = '';
if (typeof raw === 'string') {
msg = raw;
} else if (raw instanceof Error) {
msg = raw.message;
}
if (
!networkRequestUrl &&
!msg.includes('findDOMNode is deprecated') &&
!msg.includes('does not recognize the') &&
!msg.includes('React.jsx: type is invalid') &&
!msg.includes('is not a valid value for attribute') &&
!msg.includes('Received NaN for the')
) {
hasConsoleError = true;
error(...args);
}
Expand Down