mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-22 07:08:44 +00:00
Compare commits
4 Commits
snapshot-f
...
snapshot-f
Author | SHA1 | Date | |
---|---|---|---|
7ea50dd7bb | |||
b6b7d8d8b3 | |||
7bf515554d | |||
7fa42a6eb5 |
@ -2,7 +2,7 @@ version: 2.1
|
|||||||
|
|
||||||
orbs:
|
orbs:
|
||||||
node: circleci/node@5.1.0
|
node: circleci/node@5.1.0
|
||||||
browser-tools: circleci/browser-tools@1.4.5
|
browser-tools: circleci/browser-tools@1.4.3
|
||||||
|
|
||||||
executors:
|
executors:
|
||||||
java17:
|
java17:
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
"@auth0/auth0-react": "1.10.2",
|
"@auth0/auth0-react": "1.10.2",
|
||||||
"@emotion/react": "11.7.1",
|
"@emotion/react": "11.7.1",
|
||||||
"@emotion/styled": "11.6.0",
|
"@emotion/styled": "11.6.0",
|
||||||
"@kingsrook/qqq-frontend-core": "1.0.80",
|
"@kingsrook/qqq-frontend-core": "1.0.81",
|
||||||
"@mui/icons-material": "5.4.1",
|
"@mui/icons-material": "5.4.1",
|
||||||
"@mui/material": "5.11.1",
|
"@mui/material": "5.11.1",
|
||||||
"@mui/styles": "5.11.1",
|
"@mui/styles": "5.11.1",
|
||||||
|
14
pom.xml
14
pom.xml
@ -161,20 +161,6 @@
|
|||||||
<skipUpdateVersion>true</skipUpdateVersion>
|
<skipUpdateVersion>true</skipUpdateVersion>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
||||||
<!-- Publish this project's test code as a jar -->
|
|
||||||
<plugin>
|
|
||||||
<artifactId>maven-jar-plugin</artifactId>
|
|
||||||
<executions>
|
|
||||||
<execution>
|
|
||||||
<phase>package</phase>
|
|
||||||
<goals>
|
|
||||||
<goal>test-jar</goal>
|
|
||||||
</goals>
|
|
||||||
</execution>
|
|
||||||
</executions>
|
|
||||||
</plugin>
|
|
||||||
|
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
|
104
src/App.tsx
104
src/App.tsx
@ -33,7 +33,7 @@ import CssBaseline from "@mui/material/CssBaseline";
|
|||||||
import Icon from "@mui/material/Icon";
|
import Icon from "@mui/material/Icon";
|
||||||
import {ThemeProvider} from "@mui/material/styles";
|
import {ThemeProvider} from "@mui/material/styles";
|
||||||
import {LicenseInfo} from "@mui/x-license-pro";
|
import {LicenseInfo} from "@mui/x-license-pro";
|
||||||
import React, {JSXElementConstructor, Key, ReactElement, useContext, useEffect, useState,} from "react";
|
import React, {JSXElementConstructor, Key, ReactElement, useEffect, useState,} from "react";
|
||||||
import {useCookies} from "react-cookie";
|
import {useCookies} from "react-cookie";
|
||||||
import {Navigate, Route, Routes, useLocation,} from "react-router-dom";
|
import {Navigate, Route, Routes, useLocation,} from "react-router-dom";
|
||||||
import {Md5} from "ts-md5/dist/md5";
|
import {Md5} from "ts-md5/dist/md5";
|
||||||
@ -57,11 +57,11 @@ import ProcessUtils from "qqq/utils/qqq/ProcessUtils";
|
|||||||
|
|
||||||
|
|
||||||
const qController = Client.getInstance();
|
const qController = Client.getInstance();
|
||||||
export const SESSION_ID_COOKIE_NAME = "sessionId";
|
export const SESSION_UUID_COOKIE_NAME = "sessionUUID";
|
||||||
|
|
||||||
export default function App()
|
export default function App()
|
||||||
{
|
{
|
||||||
const [, setCookie, removeCookie] = useCookies([SESSION_ID_COOKIE_NAME]);
|
const [, setCookie, removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
|
||||||
const {user, getAccessTokenSilently, logout} = useAuth0();
|
const {user, getAccessTokenSilently, logout} = useAuth0();
|
||||||
const [loadingToken, setLoadingToken] = useState(false);
|
const [loadingToken, setLoadingToken] = useState(false);
|
||||||
const [isFullyAuthenticated, setIsFullyAuthenticated] = useState(false);
|
const [isFullyAuthenticated, setIsFullyAuthenticated] = useState(false);
|
||||||
@ -69,8 +69,67 @@ export default function App()
|
|||||||
const [branding, setBranding] = useState({} as QBrandingMetaData);
|
const [branding, setBranding] = useState({} as QBrandingMetaData);
|
||||||
const [metaData, setMetaData] = useState({} as QInstance);
|
const [metaData, setMetaData] = useState({} as QInstance);
|
||||||
const [needLicenseKey, setNeedLicenseKey] = useState(true);
|
const [needLicenseKey, setNeedLicenseKey] = useState(true);
|
||||||
|
const [loggedInUser, setLoggedInUser] = useState({} as { name?: string, email?: string });
|
||||||
const [defaultRoute, setDefaultRoute] = useState("/no-apps");
|
const [defaultRoute, setDefaultRoute] = useState("/no-apps");
|
||||||
|
|
||||||
|
const decodeJWT = (jwt: string): any =>
|
||||||
|
{
|
||||||
|
const base64Url = jwt.split(".")[1];
|
||||||
|
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
||||||
|
const jsonPayload = decodeURIComponent(window.atob(base64).split("").map(function (c)
|
||||||
|
{
|
||||||
|
return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
|
||||||
|
}).join(""));
|
||||||
|
|
||||||
|
return JSON.parse(jsonPayload);
|
||||||
|
};
|
||||||
|
|
||||||
|
const shouldStoreNewToken = (newToken: string, oldToken: string): boolean =>
|
||||||
|
{
|
||||||
|
if (!oldToken)
|
||||||
|
{
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const oldJSON = decodeJWT(oldToken);
|
||||||
|
const newJSON = decodeJWT(newToken);
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if the old (local storage) token is expired, then we need to store the new one //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
const oldExp = oldJSON["exp"];
|
||||||
|
if(oldExp * 1000 < (new Date().getTime()))
|
||||||
|
{
|
||||||
|
console.log("Access token in local storage was expired.");
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// remove the exp & iat values from what we compare - as they are always different from auth0 //
|
||||||
|
// note, this is only deleting them from what we compare, not from what we'd store. //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
delete newJSON["exp"]
|
||||||
|
delete newJSON["iat"]
|
||||||
|
delete oldJSON["exp"]
|
||||||
|
delete oldJSON["iat"]
|
||||||
|
|
||||||
|
const different = JSON.stringify(newJSON) !== JSON.stringify(oldJSON);
|
||||||
|
if(different)
|
||||||
|
{
|
||||||
|
console.log("Latest access token from auth0 has changed vs localStorage.");
|
||||||
|
}
|
||||||
|
return (different);
|
||||||
|
}
|
||||||
|
catch(e)
|
||||||
|
{
|
||||||
|
console.log("Caught in shouldStoreNewToken: " + e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (true);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
if (loadingToken)
|
if (loadingToken)
|
||||||
@ -92,20 +151,38 @@ export default function App()
|
|||||||
{
|
{
|
||||||
console.log("Loading token from auth0...");
|
console.log("Loading token from auth0...");
|
||||||
const accessToken = await getAccessTokenSilently();
|
const accessToken = await getAccessTokenSilently();
|
||||||
qController.setAuthorizationHeaderValue("Bearer " + accessToken);
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////
|
const lsAccessToken = localStorage.getItem("accessToken");
|
||||||
// we've stopped using session id cook with auth0, so make sure it is not set. //
|
if (shouldStoreNewToken(accessToken, lsAccessToken))
|
||||||
/////////////////////////////////////////////////////////////////////////////////
|
{
|
||||||
removeCookie(SESSION_ID_COOKIE_NAME);
|
console.log("Sending accessToken to backend, requesting a sessionUUID...");
|
||||||
|
const newSessionUuid = await qController.manageSession(accessToken, null);
|
||||||
|
setCookie(SESSION_UUID_COOKIE_NAME, newSessionUuid, {path: "/"});
|
||||||
|
localStorage.setItem("accessToken", accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// todo#authHeader - this is our quick rollback plan - if we feel the need to stop using the cookie approach. //
|
||||||
|
// we turn off the shouldStoreNewToken block above, and turn on these 2 lines. //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||||
|
localStorage.removeItem("accessToken");
|
||||||
|
*/
|
||||||
|
|
||||||
setIsFullyAuthenticated(true);
|
setIsFullyAuthenticated(true);
|
||||||
|
qController.setGotAuthentication();
|
||||||
|
qController.setAuthorizationHeaderValue("Bearer " + accessToken);
|
||||||
|
|
||||||
|
setLoggedInUser(user);
|
||||||
console.log("Token load complete.");
|
console.log("Token load complete.");
|
||||||
}
|
}
|
||||||
catch (e)
|
catch (e)
|
||||||
{
|
{
|
||||||
console.log(`Error loading token: ${JSON.stringify(e)}`);
|
console.log(`Error loading token: ${JSON.stringify(e)}`);
|
||||||
qController.clearAuthenticationMetaDataLocalStorage();
|
qController.clearAuthenticationMetaDataLocalStorage();
|
||||||
|
localStorage.removeItem("accessToken")
|
||||||
|
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||||
logout();
|
logout();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -116,9 +193,9 @@ export default function App()
|
|||||||
// use a random token if anonymous or mock //
|
// use a random token if anonymous or mock //
|
||||||
/////////////////////////////////////////////
|
/////////////////////////////////////////////
|
||||||
console.log("Generating random token...");
|
console.log("Generating random token...");
|
||||||
qController.setAuthorizationHeaderValue(null);
|
qController.setAuthorizationHeaderValue(Md5.hashStr(`${new Date()}`));
|
||||||
setIsFullyAuthenticated(true);
|
setIsFullyAuthenticated(true);
|
||||||
setCookie(SESSION_ID_COOKIE_NAME, Md5.hashStr(`${new Date()}`), {path: "/"});
|
setCookie(SESSION_UUID_COOKIE_NAME, Md5.hashStr(`${new Date()}`), {path: "/"});
|
||||||
console.log("Token generation complete.");
|
console.log("Token generation complete.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -429,11 +506,11 @@ export default function App()
|
|||||||
|
|
||||||
let profileRoutes = {};
|
let profileRoutes = {};
|
||||||
const gravatarBase = "https://www.gravatar.com/avatar/";
|
const gravatarBase = "https://www.gravatar.com/avatar/";
|
||||||
const hash = Md5.hashStr(user?.email || "user");
|
const hash = Md5.hashStr(loggedInUser?.email || "user");
|
||||||
const profilePicture = `${gravatarBase}${hash}`;
|
const profilePicture = `${gravatarBase}${hash}`;
|
||||||
profileRoutes = {
|
profileRoutes = {
|
||||||
type: "collapse",
|
type: "collapse",
|
||||||
name: user?.name,
|
name: loggedInUser?.name ?? "Anonymous",
|
||||||
key: "username",
|
key: "username",
|
||||||
noCollapse: true,
|
noCollapse: true,
|
||||||
icon: <Avatar src={profilePicture} alt="{user?.name}" />,
|
icon: <Avatar src={profilePicture} alt="{user?.name}" />,
|
||||||
@ -495,7 +572,10 @@ export default function App()
|
|||||||
{
|
{
|
||||||
if ((e as QException).status === "401")
|
if ((e as QException).status === "401")
|
||||||
{
|
{
|
||||||
|
console.log("Exception is a QException with status = 401. Clearing some of localStorage & cookies");
|
||||||
qController.clearAuthenticationMetaDataLocalStorage();
|
qController.clearAuthenticationMetaDataLocalStorage();
|
||||||
|
localStorage.removeItem("accessToken")
|
||||||
|
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||||
|
|
||||||
//////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////
|
||||||
// todo - this is auth0 logout... make more generic //
|
// todo - this is auth0 logout... make more generic //
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
import {useAuth0} from "@auth0/auth0-react";
|
import {useAuth0} from "@auth0/auth0-react";
|
||||||
import React, {useEffect} from "react";
|
import React, {useEffect} from "react";
|
||||||
import {useCookies} from "react-cookie";
|
import {useCookies} from "react-cookie";
|
||||||
import {SESSION_ID_COOKIE_NAME} from "App";
|
import {SESSION_UUID_COOKIE_NAME} from "App";
|
||||||
|
|
||||||
interface Props
|
interface Props
|
||||||
{
|
{
|
||||||
@ -33,13 +33,13 @@ interface Props
|
|||||||
function HandleAuthorizationError({errorMessage}: Props)
|
function HandleAuthorizationError({errorMessage}: Props)
|
||||||
{
|
{
|
||||||
|
|
||||||
const [, , removeCookie] = useCookies([SESSION_ID_COOKIE_NAME]);
|
const [, , removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
|
||||||
const {logout} = useAuth0();
|
const {logout} = useAuth0();
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
logout();
|
logout();
|
||||||
removeCookie(SESSION_ID_COOKIE_NAME, {path: "/"});
|
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -221,12 +221,19 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
|
|
||||||
const download = (url: string, fileName: string) =>
|
const download = (url: string, fileName: string) =>
|
||||||
{
|
{
|
||||||
const qController = Client.getInstance();
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// todo - this could be simplified. //
|
||||||
|
// it was originally built like this when we had to submit full access token to backend... //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
let xhr = new XMLHttpRequest();
|
let xhr = new XMLHttpRequest();
|
||||||
xhr.open("POST", url);
|
xhr.open("POST", url);
|
||||||
xhr.responseType = "blob";
|
xhr.responseType = "blob";
|
||||||
let formData = new FormData();
|
let formData = new FormData();
|
||||||
|
|
||||||
|
////////////////////////////////////
|
||||||
|
// todo#authHeader - delete this. //
|
||||||
|
////////////////////////////////////
|
||||||
|
const qController = Client.getInstance();
|
||||||
formData.append("Authorization", qController.getAuthorizationHeaderValue());
|
formData.append("Authorization", qController.getAuthorizationHeaderValue());
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -1139,6 +1139,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
<body>
|
<body>
|
||||||
Generating file <u>${filename}</u>${totalRecords ? " with " + totalRecords.toLocaleString() + " record" + (totalRecords == 1 ? "" : "s") : ""}...
|
Generating file <u>${filename}</u>${totalRecords ? " with " + totalRecords.toLocaleString() + " record" + (totalRecords == 1 ? "" : "s") : ""}...
|
||||||
<form id="exportForm" method="post" action="${url}" >
|
<form id="exportForm" method="post" action="${url}" >
|
||||||
|
<!-- todo#authHeader - remove this. -->
|
||||||
<input type="hidden" name="Authorization" value="${qController.getAuthorizationHeaderValue()}">
|
<input type="hidden" name="Authorization" value="${qController.getAuthorizationHeaderValue()}">
|
||||||
<input type="hidden" name="fields" value="${visibleFields.join(",")}">
|
<input type="hidden" name="fields" value="${visibleFields.join(",")}">
|
||||||
<input type="hidden" name="filter" id="filter">
|
<input type="hidden" name="filter" id="filter">
|
||||||
|
@ -193,7 +193,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
document.removeEventListener("keydown", down)
|
document.removeEventListener("keydown", down)
|
||||||
}
|
}
|
||||||
}, [dotMenuOpen, keyboardHelpOpen, showEditChildForm, showAudit, metaData, location])
|
}, [dotMenuOpen, keyboardHelpOpen, showEditChildForm, showAudit, metaData])
|
||||||
|
|
||||||
const gotoCreate = () =>
|
const gotoCreate = () =>
|
||||||
{
|
{
|
||||||
|
@ -95,6 +95,11 @@ export default class HtmlUtils
|
|||||||
form.setAttribute("target", "downloadIframe");
|
form.setAttribute("target", "downloadIframe");
|
||||||
iframe.appendChild(form);
|
iframe.appendChild(form);
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// todo#authHeader - remove after comfortable with sessionUUID //
|
||||||
|
// todo - this could be simplified (i think?) //
|
||||||
|
// it was originally built like this when we had to submit full access token to backend... //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
const authorizationInput = document.createElement("input");
|
const authorizationInput = document.createElement("input");
|
||||||
authorizationInput.setAttribute("type", "hidden");
|
authorizationInput.setAttribute("type", "hidden");
|
||||||
authorizationInput.setAttribute("id", "authorizationInput");
|
authorizationInput.setAttribute("id", "authorizationInput");
|
||||||
@ -118,6 +123,11 @@ export default class HtmlUtils
|
|||||||
{
|
{
|
||||||
if(url.startsWith("data:"))
|
if(url.startsWith("data:"))
|
||||||
{
|
{
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// todo#authHeader - remove the Authorization input after comfortable with sessionUUID //
|
||||||
|
// todo - this could be simplified (i think?) //
|
||||||
|
// it was originally built like this when we had to submit full access token to backend... //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
const openInWindow = window.open("", "_blank");
|
const openInWindow = window.open("", "_blank");
|
||||||
openInWindow.document.write(`<html lang="en">
|
openInWindow.document.write(`<html lang="en">
|
||||||
<body style="margin: 0">
|
<body style="margin: 0">
|
||||||
|
@ -49,6 +49,7 @@ module.exports = function (app)
|
|||||||
app.use("/data/*", getRequestHandler());
|
app.use("/data/*", getRequestHandler());
|
||||||
app.use("/widget/*", getRequestHandler());
|
app.use("/widget/*", getRequestHandler());
|
||||||
app.use("/serverInfo", getRequestHandler());
|
app.use("/serverInfo", getRequestHandler());
|
||||||
|
app.use("/manageSession", getRequestHandler());
|
||||||
app.use("/processes", getRequestHandler());
|
app.use("/processes", getRequestHandler());
|
||||||
app.use("/reports", getRequestHandler());
|
app.use("/reports", getRequestHandler());
|
||||||
app.use("/images", getRequestHandler());
|
app.use("/images", getRequestHandler());
|
||||||
|
@ -18,7 +18,7 @@ import org.openqa.selenium.chrome.ChromeOptions;
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public class QBaseSeleniumTest
|
public class QBaseSeleniumTest
|
||||||
{
|
{
|
||||||
protected static ChromeOptions chromeOptions;
|
private static ChromeOptions chromeOptions;
|
||||||
|
|
||||||
protected WebDriver driver;
|
protected WebDriver driver;
|
||||||
protected QSeleniumJavalin qSeleniumJavalin;
|
protected QSeleniumJavalin qSeleniumJavalin;
|
||||||
@ -52,30 +52,16 @@ public class QBaseSeleniumTest
|
|||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
public void beforeEach()
|
void beforeEach()
|
||||||
{
|
{
|
||||||
driver = new ChromeDriver(chromeOptions);
|
driver = new ChromeDriver(chromeOptions);
|
||||||
driver.manage().window().setSize(new Dimension(1700, 1300));
|
driver.manage().window().setSize(new Dimension(1700, 1300));
|
||||||
qSeleniumLib = new QSeleniumLib(driver);
|
qSeleniumLib = new QSeleniumLib(driver);
|
||||||
|
|
||||||
if(useInternalJavalin())
|
|
||||||
{
|
|
||||||
qSeleniumJavalin = new QSeleniumJavalin();
|
qSeleniumJavalin = new QSeleniumJavalin();
|
||||||
addJavalinRoutes(qSeleniumJavalin);
|
addJavalinRoutes(qSeleniumJavalin);
|
||||||
qSeleniumJavalin.start();
|
qSeleniumJavalin.start();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
** control if the test needs to start its own javalin server, or if we're running
|
|
||||||
** in an environment where an external web server is being used.
|
|
||||||
*******************************************************************************/
|
|
||||||
protected boolean useInternalJavalin()
|
|
||||||
{
|
|
||||||
return (true);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -89,8 +75,6 @@ public class QBaseSeleniumTest
|
|||||||
.withRouteToFile("/metaData/authentication", "metaData/authentication.json")
|
.withRouteToFile("/metaData/authentication", "metaData/authentication.json")
|
||||||
.withRouteToFile("/metaData/table/person", "metaData/table/person.json")
|
.withRouteToFile("/metaData/table/person", "metaData/table/person.json")
|
||||||
.withRouteToFile("/metaData/table/city", "metaData/table/person.json")
|
.withRouteToFile("/metaData/table/city", "metaData/table/person.json")
|
||||||
.withRouteToFile("/metaData/table/script", "metaData/table/script.json")
|
|
||||||
.withRouteToFile("/metaData/table/scriptRevision", "metaData/table/scriptRevision.json")
|
|
||||||
.withRouteToFile("/processes/querySavedFilter/init", "processes/querySavedFilter/init.json");
|
.withRouteToFile("/processes/querySavedFilter/init", "processes/querySavedFilter/init.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@ import java.io.File;
|
|||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
|
||||||
import org.apache.commons.io.FileUtils;
|
import org.apache.commons.io.FileUtils;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
@ -97,17 +96,6 @@ public class QSeleniumLib
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
** Getter for BASE_URL
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
public String getBaseUrl()
|
|
||||||
{
|
|
||||||
return BASE_URL;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@ -277,31 +265,6 @@ public class QSeleniumLib
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
public void waitForNumberOfWindowsToBe(int number)
|
|
||||||
{
|
|
||||||
LOG.debug("Waiting for number of windows (tabs) to be [" + number + "]");
|
|
||||||
long start = System.currentTimeMillis();
|
|
||||||
|
|
||||||
do
|
|
||||||
{
|
|
||||||
if(driver.getWindowHandles().size() == number)
|
|
||||||
{
|
|
||||||
LOG.debug("Number of windows (tabs) is [" + number + "]");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
sleepABit();
|
|
||||||
}
|
|
||||||
while(start + (1000 * WAIT_SECONDS) > System.currentTimeMillis());
|
|
||||||
|
|
||||||
fail("Failed waiting for number of windows (tabs) to be [" + number + "] after [" + WAIT_SECONDS + "] seconds.");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@ -330,53 +293,6 @@ public class QSeleniumLib
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
public void switchToSecondaryTab()
|
|
||||||
{
|
|
||||||
String originalWindow = driver.getWindowHandle();
|
|
||||||
|
|
||||||
waitForNumberOfWindowsToBe(2);
|
|
||||||
|
|
||||||
Set<String> windowHandles = driver.getWindowHandles();
|
|
||||||
for(String windowHandle : windowHandles)
|
|
||||||
{
|
|
||||||
if(!windowHandle.equals(originalWindow))
|
|
||||||
{
|
|
||||||
driver.switchTo().window(windowHandle);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fail("Failed to find a window handle not equal to the original window handle. Original=[" + originalWindow + "]. All=[" + windowHandles + "]");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
public void closeSecondaryTab()
|
|
||||||
{
|
|
||||||
String originalWindow = driver.getWindowHandle();
|
|
||||||
driver.close();
|
|
||||||
|
|
||||||
Set<String> windowHandles = driver.getWindowHandles();
|
|
||||||
for(String windowHandle : windowHandles)
|
|
||||||
{
|
|
||||||
if(!windowHandle.equals(originalWindow))
|
|
||||||
{
|
|
||||||
driver.switchTo().window(windowHandle);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fail("Failed to find a window handle not equal to the original window handle. Original=[" + originalWindow + "]. All=[" + windowHandles + "]");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface Code<T>
|
public interface Code<T>
|
||||||
{
|
{
|
||||||
|
@ -1,63 +0,0 @@
|
|||||||
/*
|
|
||||||
* QQQ - Low-code Application Framework for Engineers.
|
|
||||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
|
||||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
|
||||||
* contact@kingsrook.com
|
|
||||||
* https://github.com/Kingsrook/
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as
|
|
||||||
* published by the Free Software Foundation, either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.kingsrook.qqq.materialdashboard.tests;
|
|
||||||
|
|
||||||
|
|
||||||
import com.kingsrook.qqq.materialdashboard.lib.QBaseSeleniumTest;
|
|
||||||
import com.kingsrook.qqq.materialdashboard.lib.javalin.QSeleniumJavalin;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
** Test for Associated Record Scripts functionality.
|
|
||||||
*******************************************************************************/
|
|
||||||
public class ClickLinkOnRecordThenEditShortcutTest extends QBaseSeleniumTest
|
|
||||||
{
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
@Override
|
|
||||||
protected void addJavalinRoutes(QSeleniumJavalin qSeleniumJavalin)
|
|
||||||
{
|
|
||||||
super.addJavalinRoutes(qSeleniumJavalin);
|
|
||||||
qSeleniumJavalin.withRouteToFile("/data/script/1", "data/script/1.json");
|
|
||||||
qSeleniumJavalin.withRouteToFile("/data/scriptRevision/100", "data/scriptRevision/100.json");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
@Test
|
|
||||||
void testClickLinkOnRecordThenEditShortcutTest()
|
|
||||||
{
|
|
||||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/developer/script/1", "Hello, Script");
|
|
||||||
qSeleniumLib.waitForSelectorContaining("A", "100").click();
|
|
||||||
|
|
||||||
qSeleniumLib.waitForSelectorContaining("BUTTON", "actions").sendKeys("e");
|
|
||||||
assertTrue(qSeleniumLib.driver.getCurrentUrl().endsWith("/scriptRevision/100/edit"));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -64,6 +64,8 @@ public class ScriptTableTest extends QBaseSeleniumTest
|
|||||||
qSeleniumLib.waitForSelectorContaining("DIV.ace_line", "var hello;");
|
qSeleniumLib.waitForSelectorContaining("DIV.ace_line", "var hello;");
|
||||||
qSeleniumLib.waitForSelectorContaining("DIV", "2nd commit");
|
qSeleniumLib.waitForSelectorContaining("DIV", "2nd commit");
|
||||||
qSeleniumLib.waitForSelectorContaining("DIV", "Initial checkin");
|
qSeleniumLib.waitForSelectorContaining("DIV", "Initial checkin");
|
||||||
|
|
||||||
|
qSeleniumLib.waitForever();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,14 @@
|
|||||||
{
|
{
|
||||||
"tableName": "scriptRevision",
|
"tableName": "scriptRevision",
|
||||||
"recordLabel": "Hello, Script Revision",
|
|
||||||
"values": {
|
"values": {
|
||||||
"id": "100",
|
"id": 100,
|
||||||
"name": "Hello, Script Revision",
|
|
||||||
"sequenceNo": "22",
|
|
||||||
"commitMessage": "Initial checkin",
|
"commitMessage": "Initial checkin",
|
||||||
"author": "Jon Programmer",
|
"author": "Jon Programmer",
|
||||||
"createDate": "2023-02-18T00:47:51Z",
|
"createDate": "2023-02-18T00:47:51Z",
|
||||||
"modifyDate": "2023-02-18T00:47:51Z"
|
"modifyDate": "2023-02-18T00:47:51Z"
|
||||||
},
|
},
|
||||||
"displayValues": {
|
"displayValues": {
|
||||||
"id": "1",
|
|
||||||
"name": "Hello, Script Revision",
|
|
||||||
"scriptId": "1",
|
|
||||||
"sequenceNo": "22",
|
|
||||||
"createDate": "2023-02-18T00:47:51Z",
|
|
||||||
"modifyDate": "2023-02-18T00:47:51Z"
|
|
||||||
},
|
},
|
||||||
"associatedRecords": {
|
"associatedRecords": {
|
||||||
"files": [
|
"files": [
|
||||||
|
@ -131,8 +131,7 @@
|
|||||||
"capabilities": [
|
"capabilities": [
|
||||||
"TABLE_COUNT",
|
"TABLE_COUNT",
|
||||||
"TABLE_GET",
|
"TABLE_GET",
|
||||||
"TABLE_QUERY",
|
"TABLE_QUERY"
|
||||||
"TABLE_UPDATE"
|
|
||||||
],
|
],
|
||||||
"readPermission": true,
|
"readPermission": true,
|
||||||
"insertPermission": true,
|
"insertPermission": true,
|
||||||
|
@ -1,152 +0,0 @@
|
|||||||
{
|
|
||||||
"table": {
|
|
||||||
"name": "scriptRevision",
|
|
||||||
"label": "Script Revision",
|
|
||||||
"isHidden": false,
|
|
||||||
"primaryKeyField": "id",
|
|
||||||
"iconName": "history_edu",
|
|
||||||
"fields": {
|
|
||||||
"scriptId": {
|
|
||||||
"name": "scriptId",
|
|
||||||
"label": "Script",
|
|
||||||
"type": "INTEGER",
|
|
||||||
"isRequired": false,
|
|
||||||
"isEditable": true,
|
|
||||||
"isHeavy": false,
|
|
||||||
"possibleValueSourceName": "script",
|
|
||||||
"displayFormat": "%s",
|
|
||||||
"adornments": [
|
|
||||||
{
|
|
||||||
"type": "SIZE",
|
|
||||||
"values": {
|
|
||||||
"width": "large"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "LINK",
|
|
||||||
"values": {
|
|
||||||
"toRecordFromTable": "script"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"apiName": {
|
|
||||||
"name": "apiName",
|
|
||||||
"label": "API Name",
|
|
||||||
"type": "STRING",
|
|
||||||
"isRequired": false,
|
|
||||||
"isEditable": true,
|
|
||||||
"isHeavy": false,
|
|
||||||
"possibleValueSourceName": "apiName",
|
|
||||||
"displayFormat": "%s"
|
|
||||||
},
|
|
||||||
"sequenceNo": {
|
|
||||||
"name": "sequenceNo",
|
|
||||||
"label": "Sequence No",
|
|
||||||
"type": "INTEGER",
|
|
||||||
"isRequired": false,
|
|
||||||
"isEditable": true,
|
|
||||||
"isHeavy": false,
|
|
||||||
"displayFormat": "%s"
|
|
||||||
},
|
|
||||||
"apiVersion": {
|
|
||||||
"name": "apiVersion",
|
|
||||||
"label": "API Version",
|
|
||||||
"type": "STRING",
|
|
||||||
"isRequired": false,
|
|
||||||
"isEditable": true,
|
|
||||||
"isHeavy": false,
|
|
||||||
"possibleValueSourceName": "apiVersion",
|
|
||||||
"displayFormat": "%s"
|
|
||||||
},
|
|
||||||
"commitMessage": {
|
|
||||||
"name": "commitMessage",
|
|
||||||
"label": "Commit Message",
|
|
||||||
"type": "STRING",
|
|
||||||
"isRequired": false,
|
|
||||||
"isEditable": true,
|
|
||||||
"isHeavy": false,
|
|
||||||
"displayFormat": "%s"
|
|
||||||
},
|
|
||||||
"modifyDate": {
|
|
||||||
"name": "modifyDate",
|
|
||||||
"label": "Modify Date",
|
|
||||||
"type": "DATE_TIME",
|
|
||||||
"isRequired": false,
|
|
||||||
"isEditable": false,
|
|
||||||
"isHeavy": false,
|
|
||||||
"displayFormat": "%s"
|
|
||||||
},
|
|
||||||
"author": {
|
|
||||||
"name": "author",
|
|
||||||
"label": "Author",
|
|
||||||
"type": "STRING",
|
|
||||||
"isRequired": false,
|
|
||||||
"isEditable": true,
|
|
||||||
"isHeavy": false,
|
|
||||||
"displayFormat": "%s"
|
|
||||||
},
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"label": "Id",
|
|
||||||
"type": "INTEGER",
|
|
||||||
"isRequired": false,
|
|
||||||
"isEditable": false,
|
|
||||||
"isHeavy": false,
|
|
||||||
"displayFormat": "%s"
|
|
||||||
},
|
|
||||||
"createDate": {
|
|
||||||
"name": "createDate",
|
|
||||||
"label": "Create Date",
|
|
||||||
"type": "DATE_TIME",
|
|
||||||
"isRequired": false,
|
|
||||||
"isEditable": false,
|
|
||||||
"isHeavy": false,
|
|
||||||
"displayFormat": "%s"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sections": [
|
|
||||||
{
|
|
||||||
"name": "identity",
|
|
||||||
"label": "Identity",
|
|
||||||
"tier": "T1",
|
|
||||||
"fieldNames": [
|
|
||||||
"id",
|
|
||||||
"scriptId",
|
|
||||||
"sequenceNo"
|
|
||||||
],
|
|
||||||
"icon": {
|
|
||||||
"name": "badge"
|
|
||||||
},
|
|
||||||
"isHidden": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "dates",
|
|
||||||
"label": "Dates",
|
|
||||||
"tier": "T3",
|
|
||||||
"fieldNames": [
|
|
||||||
"createDate",
|
|
||||||
"modifyDate"
|
|
||||||
],
|
|
||||||
"icon": {
|
|
||||||
"name": "calendar_month"
|
|
||||||
},
|
|
||||||
"isHidden": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"exposedJoins": [],
|
|
||||||
"capabilities": [
|
|
||||||
"TABLE_COUNT",
|
|
||||||
"TABLE_GET",
|
|
||||||
"TABLE_QUERY",
|
|
||||||
"TABLE_INSERT",
|
|
||||||
"TABLE_UPDATE",
|
|
||||||
"QUERY_STATS"
|
|
||||||
],
|
|
||||||
"readPermission": true,
|
|
||||||
"insertPermission": true,
|
|
||||||
"editPermission": true,
|
|
||||||
"deletePermission": true,
|
|
||||||
"usesVariants": false
|
|
||||||
}
|
|
||||||
}
|
|
Reference in New Issue
Block a user