mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-19 05:40:44 +00:00
Add table developer page, with api docs
This commit is contained in:
4238
package-lock.json
generated
4238
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -31,6 +31,7 @@
|
|||||||
"formik": "2.2.9",
|
"formik": "2.2.9",
|
||||||
"html-react-parser": "1.4.8",
|
"html-react-parser": "1.4.8",
|
||||||
"http-proxy-middleware": "2.0.6",
|
"http-proxy-middleware": "2.0.6",
|
||||||
|
"rapidoc": "9.3.4",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-ace": "10.1.0",
|
"react-ace": "10.1.0",
|
||||||
"react-chartjs-2": "3.0.4",
|
"react-chartjs-2": "3.0.4",
|
||||||
|
14
src/App.tsx
14
src/App.tsx
@ -44,6 +44,7 @@ import NoApps from "qqq/pages/apps/NoApps";
|
|||||||
import ProcessRun from "qqq/pages/processes/ProcessRun";
|
import ProcessRun from "qqq/pages/processes/ProcessRun";
|
||||||
import ReportRun from "qqq/pages/processes/ReportRun";
|
import ReportRun from "qqq/pages/processes/ReportRun";
|
||||||
import EntityCreate from "qqq/pages/records/create/RecordCreate";
|
import EntityCreate from "qqq/pages/records/create/RecordCreate";
|
||||||
|
import TableDeveloperView from "qqq/pages/records/developer/TableDeveloperView";
|
||||||
import EntityEdit from "qqq/pages/records/edit/RecordEdit";
|
import EntityEdit from "qqq/pages/records/edit/RecordEdit";
|
||||||
import RecordQuery from "qqq/pages/records/query/RecordQuery";
|
import RecordQuery from "qqq/pages/records/query/RecordQuery";
|
||||||
import RecordDeveloperView from "qqq/pages/records/view/RecordDeveloperView";
|
import RecordDeveloperView from "qqq/pages/records/view/RecordDeveloperView";
|
||||||
@ -279,6 +280,13 @@ export default function App()
|
|||||||
component: <EntityCreate table={table} />,
|
component: <EntityCreate table={table} />,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
routeList.push({
|
||||||
|
name: `${app.label}`,
|
||||||
|
key: `${app.name}.dev`,
|
||||||
|
route: `${path}/dev`,
|
||||||
|
component: <TableDeveloperView table={table} />,
|
||||||
|
});
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// this is the path to open a modal-form when viewing a record, to create a different (child) record //
|
// this is the path to open a modal-form when viewing a record, to create a different (child) record //
|
||||||
// it can also be done with a hash like: #/createChild=:childTableName //
|
// it can also be done with a hash like: #/createChild=:childTableName //
|
||||||
@ -332,8 +340,8 @@ export default function App()
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const runRecordScriptProcess = metaData.processes.get("runRecordScript")
|
const runRecordScriptProcess = metaData.processes.get("runRecordScript");
|
||||||
if(runRecordScriptProcess)
|
if (runRecordScriptProcess)
|
||||||
{
|
{
|
||||||
const process = runRecordScriptProcess;
|
const process = runRecordScriptProcess;
|
||||||
routeList.push({
|
routeList.push({
|
||||||
@ -403,7 +411,7 @@ export default function App()
|
|||||||
}
|
}
|
||||||
if (metaData.branding.accentColor)
|
if (metaData.branding.accentColor)
|
||||||
{
|
{
|
||||||
setAccentColor(metaData.branding.accentColor)
|
setAccentColor(metaData.branding.accentColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
216
src/qqq/pages/records/developer/RapiDocReact.tsx
Normal file
216
src/qqq/pages/records/developer/RapiDocReact.tsx
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2023. 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import React from "react";
|
||||||
|
import "rapidoc";
|
||||||
|
|
||||||
|
interface RapiDocProps
|
||||||
|
extends React.DetailedHTMLProps<
|
||||||
|
React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
>
|
||||||
|
{
|
||||||
|
// General
|
||||||
|
"spec-url": string;
|
||||||
|
"update-route"?: boolean;
|
||||||
|
"route-prefix"?: string;
|
||||||
|
"sort-tags"?: boolean;
|
||||||
|
"sort-endpoints-by"?: "path" | "method" | "summary" | "none";
|
||||||
|
"heading-text"?: string;
|
||||||
|
"goto-path"?: string;
|
||||||
|
"fill-request-fields-with-example"?: boolean;
|
||||||
|
"persist-auth"?: boolean;
|
||||||
|
// UI Colors and Fonts
|
||||||
|
theme?: "light" | "dark";
|
||||||
|
"bg-color"?: string;
|
||||||
|
"text-color"?: string;
|
||||||
|
"header-color"?: string;
|
||||||
|
"primary-color"?: string;
|
||||||
|
"load-fonts"?: boolean;
|
||||||
|
"regular-fonts"?: string;
|
||||||
|
"mono-fonts"?: string;
|
||||||
|
"font-size"?: "default" | "large" | "largest";
|
||||||
|
// Navigation
|
||||||
|
"use-path-in-nav-bar"?: boolean;
|
||||||
|
"nav-bg-color"?: string;
|
||||||
|
"nav-text-color"?: string;
|
||||||
|
"nav-hover-bg-color"?: string;
|
||||||
|
"nav-hover-text-color"?: string;
|
||||||
|
"nav-accent-color"?: string;
|
||||||
|
"nav-item-spacing"?: "default" | "compact" | "relaxed";
|
||||||
|
// UI Layout & Placement
|
||||||
|
layout?: "row" | "column";
|
||||||
|
"render-style"?: "read" | "view" | "focused";
|
||||||
|
"on-nav-tag-click"?: "expand-collapse" | "show-description";
|
||||||
|
"schema-style"?: "tree" | "table";
|
||||||
|
"schema-expand-level"?: number;
|
||||||
|
"schema-description-expanded"?: boolean;
|
||||||
|
"schema-hide-read-only"?: "always" | "never" | string;
|
||||||
|
"default-schema-tab"?: "model" | "example";
|
||||||
|
"response-area-height"?: string;
|
||||||
|
// Hide/Show Sections
|
||||||
|
"show-info"?: boolean;
|
||||||
|
"info-description-headings-in-navbar"?: boolean;
|
||||||
|
"show-components"?: boolean;
|
||||||
|
"show-header"?: boolean;
|
||||||
|
"allow-authentication"?: boolean;
|
||||||
|
"allow-spec-url-load"?: boolean;
|
||||||
|
"allow-spec-file-load"?: boolean;
|
||||||
|
"allow-spec-file-download"?: boolean;
|
||||||
|
"allow-search"?: boolean;
|
||||||
|
"allow-advanced-search"?: boolean;
|
||||||
|
"allow-try"?: boolean;
|
||||||
|
"allow-server-selection"?: boolean;
|
||||||
|
"allow-schema-description-expand-toggle"?: boolean;
|
||||||
|
// API Server & calls
|
||||||
|
"server-url"?: string;
|
||||||
|
"default-api-server"?: string;
|
||||||
|
"api-key-name"?: string;
|
||||||
|
"api-key-location"?: "header" | "query";
|
||||||
|
"api-key-value"?: string;
|
||||||
|
"fetch-credentials"?: "omit" | "same-origin" | "include";
|
||||||
|
// Events
|
||||||
|
beforeRender?: (spec: any) => void;
|
||||||
|
specLoaded?: (spec: any) => void;
|
||||||
|
beforeTry?: (request: any) => any;
|
||||||
|
afterTry?: (data: any) => any;
|
||||||
|
apiServerChange?: (server: any) => any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global
|
||||||
|
{
|
||||||
|
interface HTMLElementTagNameMap
|
||||||
|
{
|
||||||
|
"rapi-doc": HTMLDivElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-namespace */
|
||||||
|
namespace JSX
|
||||||
|
{
|
||||||
|
interface IntrinsicElements
|
||||||
|
{
|
||||||
|
"rapi-doc": RapiDocProps;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RapiDocReact = React.forwardRef<HTMLDivElement, RapiDocProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
beforeRender,
|
||||||
|
specLoaded,
|
||||||
|
beforeTry,
|
||||||
|
afterTry,
|
||||||
|
apiServerChange,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: RapiDocProps,
|
||||||
|
ref
|
||||||
|
) =>
|
||||||
|
{
|
||||||
|
const localRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
React.useEffect(() =>
|
||||||
|
{
|
||||||
|
const rapiDocRef =
|
||||||
|
typeof ref === "object" && ref?.current
|
||||||
|
? ref?.current
|
||||||
|
: localRef.current;
|
||||||
|
|
||||||
|
const handleBeforeRender = (spec: any) =>
|
||||||
|
{
|
||||||
|
beforeRender && beforeRender(spec);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSpecLoaded = (spec: any) =>
|
||||||
|
{
|
||||||
|
specLoaded && specLoaded(spec);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBeforeTry = (request: any) =>
|
||||||
|
{
|
||||||
|
beforeTry && beforeTry(request);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAfterTry = (data: any) =>
|
||||||
|
{
|
||||||
|
afterTry && afterTry(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleApiServerChange = (server: any) =>
|
||||||
|
{
|
||||||
|
apiServerChange && apiServerChange(server);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("rapiDocRef", rapiDocRef);
|
||||||
|
if (rapiDocRef)
|
||||||
|
{
|
||||||
|
beforeRender &&
|
||||||
|
rapiDocRef.addEventListener("before-render", handleBeforeRender);
|
||||||
|
specLoaded &&
|
||||||
|
rapiDocRef.addEventListener("spec-loaded", handleSpecLoaded);
|
||||||
|
beforeTry && rapiDocRef.addEventListener("before-try", handleBeforeTry);
|
||||||
|
afterTry && rapiDocRef.addEventListener("after-try", handleAfterTry);
|
||||||
|
apiServerChange &&
|
||||||
|
rapiDocRef.addEventListener(
|
||||||
|
"api-server-change",
|
||||||
|
handleApiServerChange
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return () =>
|
||||||
|
{
|
||||||
|
if (rapiDocRef)
|
||||||
|
{
|
||||||
|
beforeRender &&
|
||||||
|
rapiDocRef.removeEventListener("before-render", handleBeforeRender);
|
||||||
|
specLoaded &&
|
||||||
|
rapiDocRef.removeEventListener("spec-loaded", handleSpecLoaded);
|
||||||
|
beforeTry &&
|
||||||
|
rapiDocRef.removeEventListener("before-try", handleBeforeTry);
|
||||||
|
afterTry &&
|
||||||
|
rapiDocRef.removeEventListener("after-try", handleAfterTry);
|
||||||
|
apiServerChange &&
|
||||||
|
rapiDocRef.removeEventListener(
|
||||||
|
"api-server-change",
|
||||||
|
handleApiServerChange
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
ref,
|
||||||
|
localRef,
|
||||||
|
specLoaded,
|
||||||
|
beforeRender,
|
||||||
|
beforeTry,
|
||||||
|
afterTry,
|
||||||
|
apiServerChange,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<rapi-doc {...props} ref={ref || localRef}>
|
||||||
|
{children}
|
||||||
|
</rapi-doc>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default RapiDocReact;
|
167
src/qqq/pages/records/developer/TableDeveloperView.tsx
Normal file
167
src/qqq/pages/records/developer/TableDeveloperView.tsx
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2023. 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {useAuth0} from "@auth0/auth0-react";
|
||||||
|
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
||||||
|
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||||
|
import {Select, SelectChangeEvent, Typography} from "@mui/material";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Card from "@mui/material/Card";
|
||||||
|
import Grid from "@mui/material/Grid";
|
||||||
|
import React, {useContext, useState} from "react";
|
||||||
|
import {useParams} from "react-router-dom";
|
||||||
|
import QContext from "QContext";
|
||||||
|
import BaseLayout from "qqq/layouts/BaseLayout";
|
||||||
|
import {RapiDocReact} from "qqq/pages/records/developer/RapiDocReact";
|
||||||
|
import Client from "qqq/utils/qqq/Client";
|
||||||
|
|
||||||
|
const qController = Client.getInstance();
|
||||||
|
|
||||||
|
interface Props
|
||||||
|
{
|
||||||
|
table?: QTableMetaData;
|
||||||
|
}
|
||||||
|
|
||||||
|
TableDeveloperView.defaultProps =
|
||||||
|
{
|
||||||
|
table: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
function TableDeveloperView({table}: Props): JSX.Element
|
||||||
|
{
|
||||||
|
const {id} = useParams();
|
||||||
|
|
||||||
|
const {getAccessTokenSilently} = useAuth0();
|
||||||
|
const [accessToken, setAccessToken] = useState(null as string);
|
||||||
|
|
||||||
|
const tableName = table.name;
|
||||||
|
const [asyncLoadInited, setAsyncLoadInited] = useState(false);
|
||||||
|
const [tableMetaData, setTableMetaData] = useState(null);
|
||||||
|
const [metaData, setMetaData] = useState(null as QInstance);
|
||||||
|
const [supportedVersions, setSupportedVersions] = useState([] as string[]);
|
||||||
|
const [currentVersion, setCurrentVersion] = useState(null as string);
|
||||||
|
const [selectedVersion, setSelectedVersion] = useState(null as string);
|
||||||
|
|
||||||
|
const {setPageHeader} = useContext(QContext);
|
||||||
|
|
||||||
|
(async () =>
|
||||||
|
{
|
||||||
|
const accessToken = await getAccessTokenSilently();
|
||||||
|
setAccessToken(accessToken);
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (!asyncLoadInited)
|
||||||
|
{
|
||||||
|
setAsyncLoadInited(true);
|
||||||
|
|
||||||
|
(async () =>
|
||||||
|
{
|
||||||
|
const versionsResponse = await fetch("/api/versions.json");
|
||||||
|
const versionsJson = await versionsResponse.json();
|
||||||
|
console.log(versionsJson);
|
||||||
|
|
||||||
|
setSupportedVersions(versionsJson.supportedVersions);
|
||||||
|
if (versionsJson.currentVersion)
|
||||||
|
{
|
||||||
|
setCurrentVersion(versionsJson.currentVersion);
|
||||||
|
setSelectedVersion(versionsJson.currentVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////
|
||||||
|
// load the full table meta-data (the one we took in is a partial) //
|
||||||
|
/////////////////////////////////////////////////////////////////////
|
||||||
|
const tableMetaData = await qController.loadTableMetaData(tableName);
|
||||||
|
setTableMetaData(tableMetaData);
|
||||||
|
|
||||||
|
//////////////////////////////
|
||||||
|
// load top-level meta-data //
|
||||||
|
//////////////////////////////
|
||||||
|
const metaData = await qController.loadMetaData();
|
||||||
|
setMetaData(metaData);
|
||||||
|
|
||||||
|
setPageHeader(tableMetaData.label + " Developer Mode");
|
||||||
|
|
||||||
|
// forceUpdate();
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
const beforeTry = (e: any) =>
|
||||||
|
{
|
||||||
|
e.detail.request.headers.append("Authorization", "Bearer " + accessToken);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectVersion = (event: SelectChangeEvent) =>
|
||||||
|
{
|
||||||
|
setSelectedVersion(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseLayout>
|
||||||
|
<Box>
|
||||||
|
<Grid container>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Box mb={3}>
|
||||||
|
{
|
||||||
|
accessToken && metaData && selectedVersion &&
|
||||||
|
<Card sx={{pb: 1}}>
|
||||||
|
<Box display="flex" alignItems="center">
|
||||||
|
<Typography variant="h6" p={2} pl={3} pb={1}>API Docs & Playground</Typography>
|
||||||
|
<Box display="inline-block" pl={2}>
|
||||||
|
<Typography fontSize="0.875rem" display="inline-block" pr={0.5} position="relative" top="2px">Version:</Typography>
|
||||||
|
<Select
|
||||||
|
native
|
||||||
|
value={selectedVersion}
|
||||||
|
onChange={selectVersion}
|
||||||
|
size="small"
|
||||||
|
inputProps={{
|
||||||
|
id: "select-native",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{supportedVersions.map((v) => (<option key={v} value={v}>{v}</option>))}
|
||||||
|
</Select>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<RapiDocReact
|
||||||
|
spec-url={`/api/${selectedVersion}/${tableName}/openapi.json`}
|
||||||
|
regular-font="Roboto,Helvetica,Arial,sans-serif"
|
||||||
|
mono-font="Monaco, Menlo, Consolas, source-code-pro, monospace"
|
||||||
|
primary-color={metaData.branding.accentColor || "blue"}
|
||||||
|
font-size="large"
|
||||||
|
render-style="view"
|
||||||
|
show-header={false}
|
||||||
|
allow-authentication={false}
|
||||||
|
allow-server-selection={false}
|
||||||
|
allow-spec-file-download={true}
|
||||||
|
beforeTry={beforeTry}
|
||||||
|
css-file={"/api/rapi-doc.css"}
|
||||||
|
css-classes={"qqq-rapi-doc"}
|
||||||
|
></RapiDocReact>
|
||||||
|
</Card>
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
</BaseLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TableDeveloperView;
|
@ -1171,6 +1171,8 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
menuItems.push(<MenuItem key={process.name} onClick={() => processClicked(process)}><ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon>{process.label}</MenuItem>);
|
menuItems.push(<MenuItem key={process.name} onClick={() => processClicked(process)}><ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon>{process.label}</MenuItem>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
menuItems.push(<MenuItem onClick={() => navigate("dev")}><ListItemIcon><Icon>code</Icon></ListItemIcon>Developer Mode</MenuItem>);
|
||||||
|
|
||||||
if(tableProcesses && tableProcesses.length)
|
if(tableProcesses && tableProcesses.length)
|
||||||
{
|
{
|
||||||
pushDividerIfNeeded(menuItems);
|
pushDividerIfNeeded(menuItems);
|
||||||
|
@ -52,4 +52,5 @@ module.exports = function (app)
|
|||||||
app.use("/processes", getRequestHandler());
|
app.use("/processes", getRequestHandler());
|
||||||
app.use("/reports", getRequestHandler());
|
app.use("/reports", getRequestHandler());
|
||||||
app.use("/images", getRequestHandler());
|
app.use("/images", getRequestHandler());
|
||||||
|
app.use("/api", getRequestHandler());
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user