Add table developer page, with api docs

This commit is contained in:
2023-03-28 09:38:41 -05:00
parent 49f49e4695
commit af6c000e14
7 changed files with 4073 additions and 566 deletions

4238
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -31,6 +31,7 @@
"formik": "2.2.9",
"html-react-parser": "1.4.8",
"http-proxy-middleware": "2.0.6",
"rapidoc": "9.3.4",
"react": "17.0.2",
"react-ace": "10.1.0",
"react-chartjs-2": "3.0.4",

View File

@ -44,6 +44,7 @@ import NoApps from "qqq/pages/apps/NoApps";
import ProcessRun from "qqq/pages/processes/ProcessRun";
import ReportRun from "qqq/pages/processes/ReportRun";
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 RecordQuery from "qqq/pages/records/query/RecordQuery";
import RecordDeveloperView from "qqq/pages/records/view/RecordDeveloperView";
@ -279,6 +280,13 @@ export default function App()
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 //
// 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")
if(runRecordScriptProcess)
const runRecordScriptProcess = metaData.processes.get("runRecordScript");
if (runRecordScriptProcess)
{
const process = runRecordScriptProcess;
routeList.push({
@ -403,7 +411,7 @@ export default function App()
}
if (metaData.branding.accentColor)
{
setAccentColor(metaData.branding.accentColor)
setAccentColor(metaData.branding.accentColor);
}
}

View 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;

View 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;

View File

@ -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 onClick={() => navigate("dev")}><ListItemIcon><Icon>code</Icon></ListItemIcon>Developer Mode</MenuItem>);
if(tableProcesses && tableProcesses.length)
{
pushDividerIfNeeded(menuItems);

View File

@ -52,4 +52,5 @@ module.exports = function (app)
app.use("/processes", getRequestHandler());
app.use("/reports", getRequestHandler());
app.use("/images", getRequestHandler());
app.use("/api", getRequestHandler());
};