feat: add internal API test harness
This commit introduces a new internal API test harness built with React and Vite. This harness provides a user interface for testing various API endpoints, including authentication, core integrations, and entity-related APIs. The harness includes the following features: - Firebase authentication integration for secure API testing. - A modular design with separate components for different API categories. - Form-based input for API parameters, allowing users to easily configure requests. - JSON-based response display for clear and readable API results. - Error handling and display for debugging purposes. - A navigation system for easy access to different API endpoints. - Environment-specific configuration for testing in different environments. This harness will enable developers to quickly and efficiently test API endpoints, ensuring the quality and reliability of the KROW backend services. The following files were added: - Makefile: Added targets for installing, developing, building, and deploying the API test harness. - firebase.json: Added hosting configurations for the API test harness in development and staging environments. - firebase/internal-launchpad/index.html: Updated with accordion styles and navigation for diagrams and documents. - internal-api-harness/.env.example: Example environment variables for the API test harness. - internal-api-harness/.gitignore: Git ignore file for the API test harness. - internal-api-harness/README.md: README file for the API test harness. - internal-api-harness/components.json: Configuration file for shadcn-ui components. - internal-api-harness/eslint.config.js: ESLint configuration file. - internal-api-harness/index.html: Main HTML file for the API test harness. - internal-api-harness/jsconfig.json: JSConfig file for the API test harness. - internal-api-harness/package.json: Package file for the API test harness. - internal-api-harness/postcss.config.js: PostCSS configuration file. - internal-api-harness/public/logo.svg: Krow logo. - internal-api-harness/public/vite.svg: Vite logo. - internal-api-harness/src/App.css: CSS file for the App component. - internal-api-harness/src/App.jsx: Main App component. - internal-api-harness/src/api/client.js: API client for making requests to the backend. - internal-api-harness/src/api/krowSDK.js: SDK for interacting with Krow APIs. - internal-api-harness/src/assets/react.svg: React logo. - internal-api-harness/src/components/ApiResponse.jsx: Component for displaying API responses. - internal-api-harness/src/components/Layout.jsx: Layout component for the API test harness. - internal-api-harness/src/components/ServiceTester.jsx: Component for testing individual services. - internal-api-harness/src/components/ui/button.jsx: Button component. - internal-api-harness/src/components/ui/card.jsx: Card component. - internal-api-harness/src/components/ui/collapsible.jsx: Collapsible component. - internal-api-harness/src/components/ui/input.jsx: Input component. - internal-api-harness/src/components/ui/label.jsx: Label component. - internal-api-harness/src/components/ui/select.jsx: Select component. - internal-api-harness/src/components/ui/textarea.jsx: Textarea component. - internal-api-harness/src/firebase.js: Firebase configuration file. - internal-api-harness/src/index.css: Main CSS file. - internal-api-harness/src/lib/utils.js: Utility functions. - internal-api-harness/src/main.jsx: Main entry point for the React application. - internal-api-harness/src/pages/ApiPlaceholder.jsx: Placeholder component for unimplemented APIs. - internal-api-harness/src/pages/EntityTester.jsx: Component for testing entity APIs. - internal-api-harness/src/pages/GenerateImage.jsx: Component for testing the Generate Image API. - internal-api-harness/src/pages/Home.jsx: Home page component. - internal-api-harness/src/pages/Login.jsx: Login page component. - internal-api-harness/src/pages/auth/GetMe.jsx: Component for testing the Get Me API. - internal-api-harness/src/pages/core/CreateSignedUrl.jsx: Component for testing the Create Signed URL API. - internal-api-harness/src/pages/core/InvokeLLM.jsx: Component for testing the Invoke LLM API. - internal-api-harness/src/pages/core/SendEmail.jsx: Component for testing the Send Email API. - internal-api-harness/src/pages/core/UploadFile.jsx: Component for testing the Upload File API. - internal-api-harness/src/pages/core/UploadPrivateFile.jsx: Component for testing the Upload Private File API. - internal-api-harness/tailwind.config.js: Tailwind CSS configuration file. - internal-api-harness/vite.config.js: Vite configuration file.
This commit is contained in:
17
Makefile
17
Makefile
@@ -145,6 +145,23 @@ admin-build:
|
||||
@node scripts/patch-admin-layout-for-env-label.js
|
||||
@cd admin-web && VITE_APP_ENV=$(ENV) npm run build
|
||||
|
||||
# --- API Test Harness ---
|
||||
harness-install:
|
||||
@echo "--> Installing API Test Harness dependencies..."
|
||||
@cd internal-api-harness && npm install
|
||||
|
||||
harness-dev:
|
||||
@echo "--> Starting API Test Harness development server on http://localhost:5175 ..."
|
||||
@cd internal-api-harness && npm run dev -- --port 5175
|
||||
|
||||
harness-build:
|
||||
@echo "--> Building API Test Harness for production..."
|
||||
@cd internal-api-harness && npm run build -- --mode $(ENV)
|
||||
|
||||
harness-deploy: harness-build
|
||||
@echo "--> Deploying API Test Harness to [$(ENV)] environment..."
|
||||
@firebase deploy --only hosting:api-harness-$(ENV) --project=$(FIREBASE_ALIAS)
|
||||
|
||||
deploy-admin: admin-build
|
||||
@echo "--> Building and deploying Admin Console to Cloud Run [$(ENV)]..."
|
||||
@echo " - Step 1: Building container image..."
|
||||
|
||||
@@ -44,6 +44,38 @@
|
||||
"destination": "/index.html"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "api-harness-dev",
|
||||
"public": "internal-api-harness/dist",
|
||||
"site": "krow-api-harness-dev",
|
||||
"ignore": [
|
||||
"firebase.json",
|
||||
"**/.*",
|
||||
"**/node_modules/**"
|
||||
],
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "**",
|
||||
"destination": "/index.html"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "api-harness-staging",
|
||||
"public": "internal-api-harness/dist",
|
||||
"site": "krow-api-harness-staging",
|
||||
"ignore": [
|
||||
"firebase.json",
|
||||
"**/.*",
|
||||
"**/node_modules/**"
|
||||
],
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "**",
|
||||
"destination": "/index.html"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"emulators": {
|
||||
|
||||
@@ -94,6 +94,35 @@
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* Accordion styles */
|
||||
.accordion-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
color: #4b5563; /* text-gray-600 */
|
||||
text-align: left;
|
||||
background-color: #f9fafb; /* bg-gray-50 */
|
||||
border-radius: 0.5rem; /* rounded-lg */
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
.accordion-button:hover {
|
||||
background-color: #f3f4f6; /* bg-gray-100 */
|
||||
}
|
||||
.accordion-button .chevron {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.accordion-button[aria-expanded="true"] .chevron {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
.accordion-panel {
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease-out;
|
||||
max-height: 0;
|
||||
}
|
||||
|
||||
/* Modal styles */
|
||||
.modal-overlay {
|
||||
backdrop-filter: blur(4px);
|
||||
@@ -566,150 +595,96 @@
|
||||
});
|
||||
|
||||
// Build hierarchical structure from paths
|
||||
function buildDiagramHierarchy(diagrams) {
|
||||
const hierarchy = {};
|
||||
function buildHierarchy(items, pathPrefix) {
|
||||
const hierarchy = { _root: { _items: [], _children: {} } };
|
||||
|
||||
diagrams.forEach(diagram => {
|
||||
const parts = diagram.path.split('/');
|
||||
const relevantParts = parts.slice(2, -1); // Remove 'assets/diagrams/' and filename
|
||||
items.forEach(item => {
|
||||
let relativePath = item.path;
|
||||
if (relativePath.startsWith('./')) {
|
||||
relativePath = relativePath.substring(2);
|
||||
}
|
||||
if (relativePath.startsWith(pathPrefix)) {
|
||||
relativePath = relativePath.substring(pathPrefix.length);
|
||||
}
|
||||
|
||||
let current = hierarchy;
|
||||
const parts = relativePath.split('/');
|
||||
const relevantParts = parts.slice(0, -1); // remove filename
|
||||
|
||||
let current = hierarchy._root;
|
||||
relevantParts.forEach(part => {
|
||||
if (!current[part]) {
|
||||
current[part] = { _items: [], _children: {} };
|
||||
if (!current._children[part]) {
|
||||
current._children[part] = { _items: [], _children: {} };
|
||||
}
|
||||
current = current[part]._children;
|
||||
current = current._children[part];
|
||||
});
|
||||
|
||||
// Add the item to appropriate level
|
||||
if (relevantParts.length > 0) {
|
||||
let parent = hierarchy[relevantParts[0]];
|
||||
for (let i = 1; i < relevantParts.length; i++) {
|
||||
parent = parent._children[relevantParts[i]];
|
||||
}
|
||||
parent._items.push(diagram);
|
||||
} else {
|
||||
// Root level diagrams
|
||||
if (!hierarchy._root) {
|
||||
hierarchy._root = { _items: [], _children: {} };
|
||||
}
|
||||
hierarchy._root._items.push(diagram);
|
||||
}
|
||||
current._items.push(item);
|
||||
});
|
||||
|
||||
return hierarchy;
|
||||
}
|
||||
|
||||
// Build hierarchical structure from paths (for documents)
|
||||
function buildDocumentHierarchy(documents) {
|
||||
const hierarchy = {};
|
||||
// Generic function to create accordion navigation
|
||||
function createAccordionNavigation(hierarchy, parentElement, createLinkFunction, sectionTitle) {
|
||||
const createAccordion = (title, items, children) => {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'mb-1';
|
||||
|
||||
documents.forEach(doc => {
|
||||
const parts = doc.path.split('/');
|
||||
const relevantParts = parts.slice(2, -1); // Remove 'assets/documents/' and filename
|
||||
const button = document.createElement('button');
|
||||
button.className = 'accordion-button';
|
||||
button.setAttribute('aria-expanded', 'false');
|
||||
button.innerHTML = `
|
||||
<span class="font-medium">${title.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}</span>
|
||||
<svg class="chevron w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
|
||||
`;
|
||||
|
||||
let current = hierarchy;
|
||||
relevantParts.forEach(part => {
|
||||
if (!current[part]) {
|
||||
current[part] = { _items: [], _children: {} };
|
||||
const panel = document.createElement('div');
|
||||
panel.className = 'accordion-panel pl-4 pt-1';
|
||||
|
||||
if (items) {
|
||||
items.forEach(item => createLinkFunction(item, panel, 1));
|
||||
}
|
||||
current = current[part]._children;
|
||||
|
||||
if (children) {
|
||||
Object.keys(children).forEach(childKey => {
|
||||
const childSection = children[childKey];
|
||||
const childHeading = document.createElement('div');
|
||||
childHeading.className = 'px-4 pt-2 pb-1 text-xs font-semibold text-gray-400 uppercase tracking-wider';
|
||||
childHeading.textContent = childKey.replace(/-/g, ' ');
|
||||
panel.appendChild(childHeading);
|
||||
childSection._items.forEach(item => createLinkFunction(item, panel, 2));
|
||||
});
|
||||
|
||||
// Add the item to appropriate level
|
||||
if (relevantParts.length > 0) {
|
||||
let parent = hierarchy[relevantParts[0]];
|
||||
for (let i = 1; i < relevantParts.length; i++) {
|
||||
parent = parent._children[relevantParts[i]];
|
||||
}
|
||||
parent._items.push(doc);
|
||||
|
||||
button.addEventListener('click', () => {
|
||||
const isExpanded = button.getAttribute('aria-expanded') === 'true';
|
||||
button.setAttribute('aria-expanded', !isExpanded);
|
||||
if (!isExpanded) {
|
||||
panel.style.maxHeight = panel.scrollHeight + 'px';
|
||||
} else {
|
||||
// Root level documents
|
||||
if (!hierarchy._root) {
|
||||
hierarchy._root = { _items: [], _children: {} };
|
||||
}
|
||||
hierarchy._root._items.push(doc);
|
||||
panel.style.maxHeight = '0px';
|
||||
}
|
||||
});
|
||||
|
||||
return hierarchy;
|
||||
}
|
||||
container.appendChild(button);
|
||||
container.appendChild(panel);
|
||||
return container;
|
||||
};
|
||||
|
||||
// Create navigation from hierarchy
|
||||
function createNavigation(hierarchy, parentElement, level = 0) {
|
||||
// First, show root level items if any
|
||||
if (hierarchy._root && hierarchy._root._items.length > 0) {
|
||||
const mainHeading = document.createElement('div');
|
||||
mainHeading.className = 'px-4 text-xs font-semibold text-gray-500 uppercase tracking-wider mt-6 mb-3';
|
||||
mainHeading.textContent = 'Diagrams';
|
||||
parentElement.appendChild(mainHeading);
|
||||
|
||||
hierarchy._root._items.forEach(diagram => {
|
||||
createDiagramLink(diagram, parentElement, 0);
|
||||
});
|
||||
}
|
||||
|
||||
// Then process nested categories
|
||||
Object.keys(hierarchy).forEach(key => {
|
||||
if (key === '_items' || key === '_children' || key === '_root') return;
|
||||
|
||||
const section = hierarchy[key];
|
||||
const heading = document.createElement('div');
|
||||
heading.className = 'px-4 text-xs font-semibold text-gray-500 uppercase tracking-wider ' +
|
||||
(level === 0 ? 'mt-6 mb-3' : 'mt-4 mb-2 pl-8');
|
||||
heading.textContent = key.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
||||
heading.className = 'px-4 text-xs font-semibold text-gray-500 uppercase tracking-wider mt-6 mb-3';
|
||||
heading.textContent = sectionTitle;
|
||||
parentElement.appendChild(heading);
|
||||
|
||||
// Add items in this section
|
||||
if (section._items && section._items.length > 0) {
|
||||
section._items.forEach(diagram => {
|
||||
createDiagramLink(diagram, parentElement, level);
|
||||
});
|
||||
}
|
||||
|
||||
// Recursively add children
|
||||
if (section._children && Object.keys(section._children).length > 0) {
|
||||
createNavigation(section._children, parentElement, level + 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Create document navigation from hierarchy
|
||||
function createDocumentNavigation(hierarchy, parentElement, level = 0) {
|
||||
// First, show root level items if any
|
||||
// Process root items first
|
||||
if (hierarchy._root && hierarchy._root._items.length > 0) {
|
||||
const mainHeading = document.createElement('div');
|
||||
mainHeading.className = 'px-4 text-xs font-semibold text-gray-500 uppercase tracking-wider mt-6 mb-3';
|
||||
mainHeading.textContent = 'Documentation';
|
||||
parentElement.appendChild(mainHeading);
|
||||
|
||||
hierarchy._root._items.forEach(doc => {
|
||||
createDocumentLink(doc, parentElement, 0);
|
||||
});
|
||||
hierarchy._root._items.forEach(item => createLinkFunction(item, parentElement, 0));
|
||||
}
|
||||
|
||||
// Then process nested categories
|
||||
Object.keys(hierarchy).forEach(key => {
|
||||
if (key === '_items' || key === '_children' || key === '_root') return;
|
||||
|
||||
const section = hierarchy[key];
|
||||
const heading = document.createElement('div');
|
||||
heading.className = 'px-4 text-xs font-semibold text-gray-500 uppercase tracking-wider ' +
|
||||
(level === 0 ? 'mt-6 mb-3' : 'mt-4 mb-2 pl-8');
|
||||
heading.textContent = key.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
||||
parentElement.appendChild(heading);
|
||||
|
||||
// Add items in this section
|
||||
if (section._items && section._items.length > 0) {
|
||||
section._items.forEach(doc => {
|
||||
createDocumentLink(doc, parentElement, level);
|
||||
});
|
||||
}
|
||||
|
||||
// Recursively add children
|
||||
if (section._children && Object.keys(section._children).length > 0) {
|
||||
createDocumentNavigation(section._children, parentElement, level + 1);
|
||||
}
|
||||
// Process categories as accordions
|
||||
Object.keys(hierarchy._root._children).forEach(key => {
|
||||
if (key.startsWith('_')) return;
|
||||
const section = hierarchy._root._children[key];
|
||||
const accordion = createAccordion(key, section._items, section._children);
|
||||
parentElement.appendChild(accordion);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -717,8 +692,8 @@
|
||||
function createDocumentLink(doc, parentElement, level) {
|
||||
const link = document.createElement('a');
|
||||
link.href = '#';
|
||||
link.className = 'nav-item flex items-center space-x-3 px-4 py-3 rounded-xl text-gray-700 hover:bg-gray-50 mb-1' +
|
||||
(level > 0 ? ' pl-8' : '');
|
||||
link.className = 'nav-item flex items-center space-x-3 px-4 py-2.5 rounded-lg text-gray-600 hover:bg-gray-100 text-sm mb-1' +
|
||||
(level > 0 ? ' ' : '');
|
||||
link.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
showView('document', link, doc.path, doc.title);
|
||||
@@ -728,7 +703,7 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>`;
|
||||
|
||||
link.innerHTML = `${iconSvg}<span class="text-sm">${doc.title}</span>`;
|
||||
link.innerHTML = `${iconSvg}<span class="truncate">${doc.title}</span>`;
|
||||
parentElement.appendChild(link);
|
||||
}
|
||||
|
||||
@@ -736,26 +711,18 @@
|
||||
function createDiagramLink(diagram, parentElement, level) {
|
||||
const link = document.createElement('a');
|
||||
link.href = '#';
|
||||
link.className = 'nav-item flex items-center space-x-3 px-4 py-3 rounded-xl text-gray-700 hover:bg-gray-50 mb-1' +
|
||||
(level > 0 ? ' pl-8' : '');
|
||||
link.className = 'nav-item flex items-center space-x-3 px-4 py-2.5 rounded-lg text-gray-600 hover:bg-gray-100 text-sm mb-1' +
|
||||
(level > 0 ? ' ' : '');
|
||||
link.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
showView('diagram', link, diagram.path, diagram.title, diagram.type);
|
||||
};
|
||||
|
||||
// Get icon based on type or custom icon
|
||||
let iconSvg = '';
|
||||
if (diagram.type === 'svg') {
|
||||
iconSvg = `<svg class="w-5 h-5 text-gray-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||
const iconSvg = `<svg class="w-5 h-5 text-gray-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4l2 2h4a2 2 0 012 2v12a2 2 0 01-2 2h-4l-2-2H7z"></path>
|
||||
</svg>`;
|
||||
} else {
|
||||
iconSvg = `<svg class="w-5 h-5 text-gray-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
link.innerHTML = iconSvg + '<span class="text-sm">' + diagram.title + '</span>';
|
||||
link.innerHTML = `${iconSvg}<span class="truncate">${diagram.title}</span>`;
|
||||
parentElement.appendChild(link);
|
||||
}
|
||||
|
||||
@@ -770,12 +737,11 @@
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
console.log('Loaded config:', text);
|
||||
allDiagrams = JSON.parse(text);
|
||||
|
||||
if (allDiagrams && allDiagrams.length > 0) {
|
||||
const hierarchy = buildDiagramHierarchy(allDiagrams);
|
||||
createNavigation(hierarchy, dynamicSection);
|
||||
const hierarchy = buildHierarchy(allDiagrams, 'assets/diagrams/');
|
||||
createAccordionNavigation(hierarchy, dynamicSection, createDiagramLink, 'Diagrams');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading diagrams configuration:', error);
|
||||
@@ -801,12 +767,11 @@
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
console.log('Loaded documents config:', text);
|
||||
allDocuments = JSON.parse(text);
|
||||
|
||||
if (allDocuments && allDocuments.length > 0) {
|
||||
const hierarchy = buildDocumentHierarchy(allDocuments);
|
||||
createDocumentNavigation(hierarchy, documentationSection);
|
||||
const hierarchy = buildHierarchy(allDocuments, 'assets/documents/');
|
||||
createAccordionNavigation(hierarchy, documentationSection, createDocumentLink, 'Documentation');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading documents configuration:', error);
|
||||
|
||||
8
internal-api-harness/.env.example
Normal file
8
internal-api-harness/.env.example
Normal file
@@ -0,0 +1,8 @@
|
||||
VITE_HARNESS_FIREBASE_API_KEY="your-api-key"
|
||||
VITE_HARNESS_FIREBASE_AUTH_DOMAIN="your-auth-domain"
|
||||
VITE_HARNESS_FIREBASE_PROJECT_ID="your-project-id"
|
||||
VITE_HARNESS_FIREBASE_STORAGE_BUCKET="your-storage-bucket"
|
||||
VITE_HARNESS_FIREBASE_MESSAGING_SENDER_ID="your-messaging-sender-id"
|
||||
VITE_HARNESS_FIREBASE_APP_ID="your-app-id"
|
||||
VITE_HARNESS_ENVIRONMENT="dev"
|
||||
VITE_API_BASE_URL="http://localhost:8080"
|
||||
24
internal-api-harness/.gitignore
vendored
Normal file
24
internal-api-harness/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
16
internal-api-harness/README.md
Normal file
16
internal-api-harness/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# React + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
||||
17
internal-api-harness/components.json
Normal file
17
internal-api-harness/components.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": false,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
||||
29
internal-api-harness/eslint.config.js
Normal file
29
internal-api-harness/eslint.config.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
reactHooks.configs.flat.recommended,
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||
},
|
||||
},
|
||||
])
|
||||
13
internal-api-harness/index.html
Normal file
13
internal-api-harness/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>internal-api-harness</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
10
internal-api-harness/jsconfig.json
Normal file
10
internal-api-harness/jsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
6611
internal-api-harness/package-lock.json
generated
Normal file
6611
internal-api-harness/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
45
internal-api-harness/package.json
Normal file
45
internal-api-harness/package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "internal-api-harness",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"axios": "^1.13.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"firebase": "^12.6.0",
|
||||
"lucide-react": "^0.553.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-firebase-hooks": "^5.1.1",
|
||||
"react-router-dom": "^7.9.6",
|
||||
"shadcn-ui": "^0.9.5",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"@vitejs/plugin-react": "^5.1.0",
|
||||
"autoprefixer": "^10.4.22",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"globals": "^16.5.0",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^3.4.18",
|
||||
"vite": "^7.2.2"
|
||||
}
|
||||
}
|
||||
6
internal-api-harness/postcss.config.js
Normal file
6
internal-api-harness/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
18
internal-api-harness/public/logo.svg
Normal file
18
internal-api-harness/public/logo.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="200 180 280 140" style="enable-background:new 0 0 500 500;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#24303B;}
|
||||
.st1{fill:#002FE3;}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st1" d="M459.81,202.55c-5.03,0.59-9.08,4.49-10.36,9.38l-15.99,59.71l-16.24-56.3
|
||||
c-1.68-5.92-6.22-10.86-12.19-12.34c-1.58-0.39-3.11-0.54-4.64-0.49h-0.15c-1.53-0.05-3.11,0.1-4.64,0.49
|
||||
c-5.97,1.48-10.51,6.42-12.24,12.34l-3.6,12.53l-11.35,39.38l-7.9-27.54c-10.76-37.5-48.56-62.23-88.38-55.32
|
||||
c-33.26,5.82-57.05,35.68-56.99,69.48v0.79c0,4.34,0.39,8.73,1.13,13.18c0.18,1.02,0.37,2.03,0.6,3.03
|
||||
c1.84,8.31,10.93,12.73,18.49,8.8v0c5.36-2.79,7.84-8.89,6.42-14.77c-0.85-3.54-1.28-7.23-1.23-11.03
|
||||
c0-25.02,20.48-45.5,45.55-45.2c7.6,0.1,15.59,2.07,23.59,6.37c13.52,7.3,23.15,20.18,27.34,34.94l13.32,46.34
|
||||
c1.73,5.97,6.22,11,12.24,12.58c9.62,2.62,19-3.06,21.51-12.04l16.09-56.7l0.2-0.1l16.09,56.85c1.63,5.68,5.87,10.41,11.55,11.99
|
||||
c9.13,2.57,18.11-2.66,20.67-11.2l24.13-79.6C475.35,209.85,468.64,201.56,459.81,202.55z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
1
internal-api-harness/public/vite.svg
Normal file
1
internal-api-harness/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
42
internal-api-harness/src/App.css
Normal file
42
internal-api-harness/src/App.css
Normal file
@@ -0,0 +1,42 @@
|
||||
#root {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
}
|
||||
|
||||
@keyframes logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
a:nth-of-type(2) .logo {
|
||||
animation: logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
45
internal-api-harness/src/App.jsx
Normal file
45
internal-api-harness/src/App.jsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
||||
import Layout from "./components/Layout";
|
||||
import Login from "./pages/Login";
|
||||
import Home from "./pages/Home";
|
||||
import GenerateImage from "./pages/GenerateImage";
|
||||
import ApiPlaceholder from "./pages/ApiPlaceholder";
|
||||
import GetMe from "./pages/auth/GetMe";
|
||||
import SendEmail from "./pages/core/SendEmail";
|
||||
import EntityTester from "./pages/EntityTester";
|
||||
|
||||
import InvokeLLM from "./pages/core/InvokeLLM";
|
||||
import UploadFile from "./pages/core/UploadFile";
|
||||
import UploadPrivateFile from "./pages/core/UploadPrivateFile";
|
||||
import CreateSignedUrl from "./pages/core/CreateSignedUrl";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/" element={<Layout />}>
|
||||
<Route index element={<Home />} />
|
||||
|
||||
{/* Auth APIs */}
|
||||
<Route path="/auth/me" element={<GetMe />} />
|
||||
<Route path="/auth/update-me" element={<ApiPlaceholder title="Update Me" />} />
|
||||
|
||||
{/* Core APIs */}
|
||||
<Route path="/core/send-email" element={<SendEmail />} />
|
||||
<Route path="/core/invoke-llm" element={<InvokeLLM />} />
|
||||
<Route path="/core/upload-file" element={<UploadFile />} />
|
||||
<Route path="/core/upload-private-file" element={<UploadPrivateFile />} />
|
||||
<Route path="/core/create-signed-url" element={<CreateSignedUrl />} />
|
||||
<Route path="/core/extract-data" element={<ApiPlaceholder title="Extract Data from File" />} />
|
||||
<Route path="/core/generate-image" element={<GenerateImage />} />
|
||||
|
||||
{/* Entity APIs */}
|
||||
<Route path="/entities" element={<EntityTester />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
22
internal-api-harness/src/api/client.js
Normal file
22
internal-api-harness/src/api/client.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import axios from "axios";
|
||||
import { auth } from "../firebase";
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL, // You will need to add this to your .env file
|
||||
});
|
||||
|
||||
apiClient.interceptors.request.use(
|
||||
async (config) => {
|
||||
const user = auth.currentUser;
|
||||
if (user) {
|
||||
const token = await user.getIdToken();
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default apiClient;
|
||||
147
internal-api-harness/src/api/krowSDK.js
Normal file
147
internal-api-harness/src/api/krowSDK.js
Normal file
@@ -0,0 +1,147 @@
|
||||
import apiClient from './client';
|
||||
import { auth } from '../firebase';
|
||||
import { signOut } from 'firebase/auth';
|
||||
|
||||
// --- Auth Module ---
|
||||
const authModule = {
|
||||
/**
|
||||
* Fetches the currently authenticated user's profile from the backend.
|
||||
* @returns {Promise<object>} The user profile.
|
||||
*/
|
||||
me: async () => {
|
||||
const { data } = await apiClient.get('/auth/me');
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Logs the user out.
|
||||
* @param {string} [redirectUrl] - Optional URL to redirect to after logout.
|
||||
*/
|
||||
logout: async (redirectUrl) => {
|
||||
await signOut(auth);
|
||||
if (redirectUrl) {
|
||||
window.location.href = redirectUrl;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if a user is currently authenticated.
|
||||
* @returns {boolean} True if a user is authenticated.
|
||||
*/
|
||||
isAuthenticated: () => {
|
||||
return !!auth.currentUser;
|
||||
},
|
||||
};
|
||||
|
||||
// --- Core Integrations Module ---
|
||||
const coreIntegrationsModule = {
|
||||
/**
|
||||
* Sends an email.
|
||||
* @param {object} params - { to, subject, body }
|
||||
* @returns {Promise<object>} API response.
|
||||
*/
|
||||
SendEmail: async (params) => {
|
||||
const { data } = await apiClient.post('/sendEmail', params);
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Invokes a large language model.
|
||||
* @param {object} params - { prompt, response_json_schema, file_urls }
|
||||
* @returns {Promise<object>} API response.
|
||||
*/
|
||||
InvokeLLM: async (params) => {
|
||||
const { data } = await apiClient.post('/invokeLLM', params);
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Uploads a public file.
|
||||
* @param {File} file - The file to upload.
|
||||
* @returns {Promise<object>} API response with file_url.
|
||||
*/
|
||||
UploadFile: async ({ file }) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const { data } = await apiClient.post('/uploadFile', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Uploads a private file.
|
||||
* @param {File} file - The file to upload.
|
||||
* @returns {Promise<object>} API response with file_uri.
|
||||
*/
|
||||
UploadPrivateFile: async ({ file }) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const { data } = await apiClient.post('/uploadPrivateFile', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a temporary signed URL for a private file.
|
||||
* @param {object} params - { file_uri, expires_in }
|
||||
* @returns {Promise<object>} API response with signed_url.
|
||||
*/
|
||||
CreateFileSignedUrl: async (params) => {
|
||||
const { data } = await apiClient.post('/createSignedUrl', params);
|
||||
return data;
|
||||
},
|
||||
};
|
||||
|
||||
// --- Entities Module ---
|
||||
// Based on docs/07-reference-base44-api-export.md
|
||||
const entityNames = [
|
||||
"User", "Event", "Staff", "Vendor", "VendorRate", "Invoice", "Business",
|
||||
"Certification", "Team", "Conversation", "Message", "ActivityLog",
|
||||
"Enterprise", "Sector", "Partner", "Order", "Shift"
|
||||
];
|
||||
|
||||
const entitiesModule = {};
|
||||
|
||||
entityNames.forEach(entityName => {
|
||||
// This factory creates a standard set of CRUD-like methods for each entity.
|
||||
// It assumes a consistent RESTful endpoint structure: /entities/{EntityName}/{method}
|
||||
entitiesModule[entityName] = {
|
||||
get: async (params) => {
|
||||
const { data } = await apiClient.get(`/entities/${entityName}/get`, { params });
|
||||
return data;
|
||||
},
|
||||
create: async (params) => {
|
||||
const { data } = await apiClient.post(`/entities/${entityName}/create`, params);
|
||||
return data;
|
||||
},
|
||||
update: async (params) => {
|
||||
const { data } = await apiClient.post(`/entities/${entityName}/update`, params);
|
||||
return data;
|
||||
},
|
||||
delete: async (params) => {
|
||||
const { data } = await apiClient.post(`/entities/${entityName}/delete`, params);
|
||||
return data;
|
||||
},
|
||||
filter: async (params) => {
|
||||
const { data } = await apiClient.post(`/entities/${entityName}/filter`, params);
|
||||
return data;
|
||||
},
|
||||
list: async () => {
|
||||
// Assuming a 'filter' with no params can act as 'list'
|
||||
const { data } = await apiClient.post(`/entities/${entityName}/filter`, {});
|
||||
return data;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
// --- Main SDK Export ---
|
||||
export const krowSDK = {
|
||||
auth: authModule,
|
||||
integrations: {
|
||||
Core: coreIntegrationsModule,
|
||||
},
|
||||
entities: entitiesModule,
|
||||
};
|
||||
1
internal-api-harness/src/assets/react.svg
Normal file
1
internal-api-harness/src/assets/react.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
38
internal-api-harness/src/components/ApiResponse.jsx
Normal file
38
internal-api-harness/src/components/ApiResponse.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
const ApiResponse = ({ response, error, loading }) => {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<h3 className="text-lg font-semibold text-slate-800 mb-2">Response</h3>
|
||||
<div className="bg-slate-100 border border-slate-200 rounded-lg p-4">
|
||||
<p className="text-slate-500">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<h3 className="text-lg font-semibold text-red-800 mb-2">Error</h3>
|
||||
<pre className="bg-red-50 border border-red-200 text-red-700 rounded-lg p-4 text-sm whitespace-pre-wrap">
|
||||
{JSON.stringify(error, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (response) {
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<h3 className="text-lg font-semibold text-slate-800 mb-2">Response</h3>
|
||||
<pre className="bg-slate-100 border border-slate-200 rounded-lg p-4 text-sm whitespace-pre-wrap">
|
||||
{JSON.stringify(response, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default ApiResponse;
|
||||
125
internal-api-harness/src/components/Layout.jsx
Normal file
125
internal-api-harness/src/components/Layout.jsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import { Link, Navigate, Outlet, useLocation } from "react-router-dom";
|
||||
import { useAuthState } from "react-firebase-hooks/auth";
|
||||
import { auth } from "../firebase";
|
||||
import { krowSDK } from "@/api/krowSDK";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
const navLinks = {
|
||||
auth: [
|
||||
{ path: "/auth/me", title: "Get Me" },
|
||||
{ path: "/auth/update-me", title: "Update Me" },
|
||||
],
|
||||
core: [
|
||||
{ path: "/core/send-email", title: "Send Email" },
|
||||
{ path: "/core/invoke-llm", title: "Invoke LLM" },
|
||||
{ path: "/core/upload-file", title: "Upload File" },
|
||||
{ path: "/core/upload-private-file", title: "Upload Private File" },
|
||||
{ path: "/core/create-signed-url", title: "Create Signed URL" },
|
||||
{ path: "/core/extract-data", title: "Extract Data from File" },
|
||||
{ path: "/core/generate-image", title: "Generate Image" },
|
||||
],
|
||||
entities: [
|
||||
{ path: "/entities", title: "Entity API Tester" }
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
const NavSection = ({ title, links }) => {
|
||||
const location = useLocation();
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
|
||||
const navLinkClasses = (path) =>
|
||||
`flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors ${
|
||||
location.pathname === path
|
||||
? "bg-slate-200 text-slate-900"
|
||||
: "text-slate-600 hover:bg-slate-100"
|
||||
}`;
|
||||
|
||||
return (
|
||||
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
||||
<CollapsibleTrigger className="w-full">
|
||||
<div className="flex items-center justify-between px-3 py-2">
|
||||
<h2 className="text-xs font-semibold text-slate-400 uppercase tracking-wider">
|
||||
{title}
|
||||
</h2>
|
||||
<ChevronDown className={`w-4 h-4 text-slate-400 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="space-y-1">
|
||||
{links.map((api) => (
|
||||
<Link key={api.path} to={api.path} className={navLinkClasses(api.path)}>
|
||||
{api.title}
|
||||
</Link>
|
||||
))}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const Layout = () => {
|
||||
const [user, loading] = useAuthState(auth);
|
||||
const location = useLocation();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<div>Loading...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return <Navigate to="/login" />;
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
krowSDK.auth.logout();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-slate-50">
|
||||
{/* Sidebar */}
|
||||
<aside className="w-72 bg-white border-r border-slate-200 flex flex-col">
|
||||
<div className="p-6 border-b border-slate-200">
|
||||
<div className="flex items-center space-x-3">
|
||||
<img src="/logo.svg" alt="Krow Logo" className="h-12 w-12" />
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-slate-900">KROW</h1>
|
||||
<p className="text-xs text-slate-500">API Test Harness ({import.meta.env.VITE_HARNESS_ENVIRONMENT})</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 overflow-y-auto p-4 space-y-2">
|
||||
<NavSection title="Auth" links={navLinks.auth} />
|
||||
<NavSection title="Core Integrations" links={navLinks.core} />
|
||||
<NavSection title="Entities" links={navLinks.entities} />
|
||||
</nav>
|
||||
|
||||
<div className="p-4 border-t border-slate-200">
|
||||
<div className="text-sm text-slate-600 truncate mb-2" title={user.email}>{user.email}</div>
|
||||
<Button onClick={handleLogout} className="w-full">
|
||||
Logout
|
||||
</Button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 overflow-y-auto">
|
||||
<div className="p-8">
|
||||
<Outlet />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
120
internal-api-harness/src/components/ServiceTester.jsx
Normal file
120
internal-api-harness/src/components/ServiceTester.jsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import { useState } from "react";
|
||||
import apiClient from "../api/client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
const ServiceTester = ({ serviceName, serviceDescription, endpoint, fields }) => {
|
||||
const [formData, setFormData] = useState({});
|
||||
const [response, setResponse] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value, files } = e.target;
|
||||
if (files) {
|
||||
setFormData({ ...formData, [name]: files[0] });
|
||||
} else {
|
||||
setFormData({ ...formData, [name]: value });
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setResponse(null);
|
||||
|
||||
const data = new FormData();
|
||||
for (const key in formData) {
|
||||
data.append(key, formData[key]);
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await apiClient.post(endpoint, data);
|
||||
setResponse(res.data);
|
||||
} catch (err) {
|
||||
setError(err.response?.data || err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-slate-900">{serviceName}</h1>
|
||||
<p className="text-slate-600 mt-1">{serviceDescription}</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Parameters</CardTitle>
|
||||
<CardDescription>
|
||||
Fill in the required parameters to execute the service.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{fields.map((field) => (
|
||||
<div key={field.name} className="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Label htmlFor={field.name}>{field.label}</Label>
|
||||
{field.type === "textarea" ? (
|
||||
<Textarea
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
onChange={handleChange}
|
||||
placeholder={field.placeholder}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
type={field.type}
|
||||
onChange={handleChange}
|
||||
placeholder={field.placeholder}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? "Executing..." : "Execute"}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</form>
|
||||
|
||||
{response && (
|
||||
<div className="mt-8">
|
||||
<h2 className="text-xl font-bold text-slate-900 mb-2">Response</h2>
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<pre className="text-sm overflow-x-auto bg-slate-900 text-white p-4 rounded-md">
|
||||
{JSON.stringify(response, null, 2)}
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="mt-8">
|
||||
<h2 className="text-xl font-bold text-red-600 mb-2">Error</h2>
|
||||
<Card className="border-red-500">
|
||||
<CardContent className="p-4">
|
||||
<pre className="text-sm overflow-x-auto bg-red-50 text-red-800 p-4 rounded-md">
|
||||
{JSON.stringify(error, null, 2)}
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServiceTester;
|
||||
48
internal-api-harness/src/components/ui/button.jsx
Normal file
48
internal-api-harness/src/components/ui/button.jsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props} />
|
||||
);
|
||||
})
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
50
internal-api-harness/src/components/ui/card.jsx
Normal file
50
internal-api-harness/src/components/ui/card.jsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("rounded-xl border bg-card text-card-foreground shadow", className)}
|
||||
{...props} />
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props} />
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("font-semibold leading-none tracking-tight", className)}
|
||||
{...props} />
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props} />
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props} />
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
11
internal-api-harness/src/components/ui/collapsible.jsx
Normal file
11
internal-api-harness/src/components/ui/collapsible.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
"use client"
|
||||
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
||||
|
||||
const Collapsible = CollapsiblePrimitive.Root
|
||||
|
||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
|
||||
|
||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||
19
internal-api-harness/src/components/ui/input.jsx
Normal file
19
internal-api-harness/src/components/ui/input.jsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Input = React.forwardRef(({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props} />
|
||||
);
|
||||
})
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
||||
16
internal-api-harness/src/components/ui/label.jsx
Normal file
16
internal-api-harness/src/components/ui/label.jsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
|
||||
const Label = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
||||
105
internal-api-harness/src/components/ui/select.jsx
Normal file
105
internal-api-harness/src/components/ui/select.jsx
Normal file
@@ -0,0 +1,105 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Select = SelectPrimitive.Root
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group
|
||||
|
||||
const SelectValue = SelectPrimitive.Value
|
||||
|
||||
const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
))
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||
|
||||
const SelectContent = React.forwardRef(({ className, children, position = "popper", ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
))
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||
|
||||
const SelectLabel = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||
|
||||
const SelectItem = React.forwardRef(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
))
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||
|
||||
const SelectSeparator = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
}
|
||||
18
internal-api-harness/src/components/ui/textarea.jsx
Normal file
18
internal-api-harness/src/components/ui/textarea.jsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Textarea = React.forwardRef(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props} />
|
||||
);
|
||||
})
|
||||
Textarea.displayName = "Textarea"
|
||||
|
||||
export { Textarea }
|
||||
17
internal-api-harness/src/firebase.js
Normal file
17
internal-api-harness/src/firebase.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// Import the functions you need from the SDKs you need
|
||||
import { initializeApp } from "firebase/app";
|
||||
import { getAuth } from "firebase/auth";
|
||||
|
||||
// Your web app's Firebase configuration
|
||||
const firebaseConfig = {
|
||||
apiKey: import.meta.env.VITE_HARNESS_FIREBASE_API_KEY,
|
||||
authDomain: import.meta.env.VITE_HARNESS_FIREBASE_AUTH_DOMAIN,
|
||||
projectId: import.meta.env.VITE_HARNESS_FIREBASE_PROJECT_ID,
|
||||
storageBucket: import.meta.env.VITE_HARNESS_FIREBASE_STORAGE_BUCKET,
|
||||
messagingSenderId: import.meta.env.VITE_HARNESS_FIREBASE_MESSAGING_SENDER_ID,
|
||||
appId: import.meta.env.VITE_HARNESS_FIREBASE_APP_ID
|
||||
};
|
||||
|
||||
// Initialize Firebase
|
||||
const app = initializeApp(firebaseConfig);
|
||||
export const auth = getAuth(app);
|
||||
76
internal-api-harness/src/index.css
Normal file
76
internal-api-harness/src/index.css
Normal file
@@ -0,0 +1,76 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
6
internal-api-harness/src/lib/utils.js
Normal file
6
internal-api-harness/src/lib/utils.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
10
internal-api-harness/src/main.jsx
Normal file
10
internal-api-harness/src/main.jsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.jsx'
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
13
internal-api-harness/src/pages/ApiPlaceholder.jsx
Normal file
13
internal-api-harness/src/pages/ApiPlaceholder.jsx
Normal file
@@ -0,0 +1,13 @@
|
||||
const ApiPlaceholder = ({ title }) => {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-slate-900 mb-4">{title}</h1>
|
||||
<div className="bg-slate-100 border border-slate-200 rounded-lg p-8 text-center">
|
||||
<p className="text-slate-500">This page is a placeholder for the "{title}" API test harness.</p>
|
||||
<p className="text-slate-500 mt-2">Implementation is pending.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiPlaceholder;
|
||||
190
internal-api-harness/src/pages/EntityTester.jsx
Normal file
190
internal-api-harness/src/pages/EntityTester.jsx
Normal file
@@ -0,0 +1,190 @@
|
||||
import { useState, useMemo } from "react";
|
||||
import { krowSDK } from "@/api/krowSDK";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import ApiResponse from "@/components/ApiResponse";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
const entityNames = Object.keys(krowSDK.entities).sort();
|
||||
|
||||
const getPrettifiedJSON = (entity, method) => {
|
||||
// Basic placeholder payloads. A more advanced SDK could provide detailed examples.
|
||||
const payloads = {
|
||||
get: { id: "some-id" },
|
||||
create: { data: { property: "value" } },
|
||||
update: { id: "some-id", data: { property: "new-value" } },
|
||||
delete: { id: "some-id" },
|
||||
filter: { where: { property: { _eq: "value" } } },
|
||||
list: {}
|
||||
};
|
||||
return JSON.stringify(payloads[method] || {}, null, 2);
|
||||
};
|
||||
|
||||
const EntityTester = () => {
|
||||
const [selectedEntity, setSelectedEntity] = useState(null);
|
||||
const [selectedMethod, setSelectedMethod] = useState(null);
|
||||
const [response, setResponse] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [jsonInput, setJsonInput] = useState("");
|
||||
const [jsonError, setJsonError] = useState(null);
|
||||
|
||||
const availableMethods = useMemo(() => {
|
||||
if (!selectedEntity) return [];
|
||||
return Object.keys(krowSDK.entities[selectedEntity]);
|
||||
}, [selectedEntity]);
|
||||
|
||||
const handleEntityChange = (entity) => {
|
||||
setSelectedEntity(entity);
|
||||
setSelectedMethod(null);
|
||||
setJsonInput("");
|
||||
setJsonError(null);
|
||||
setResponse(null);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const handleMethodSelect = (method) => {
|
||||
setSelectedMethod(method);
|
||||
setJsonInput(getPrettifiedJSON(selectedEntity, method));
|
||||
setJsonError(null);
|
||||
setResponse(null);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const handleJsonInputChange = (e) => {
|
||||
setJsonInput(e.target.value);
|
||||
try {
|
||||
JSON.parse(e.target.value);
|
||||
setJsonError(null);
|
||||
} catch (err) {
|
||||
setJsonError("Invalid JSON format");
|
||||
}
|
||||
};
|
||||
|
||||
const executeApi = async () => {
|
||||
if (!selectedEntity || !selectedMethod || jsonError) return;
|
||||
|
||||
setLoading(true);
|
||||
setResponse(null);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const params = JSON.parse(jsonInput);
|
||||
const sdkMethod = krowSDK.entities[selectedEntity][selectedMethod];
|
||||
const res = await sdkMethod(params);
|
||||
setResponse(res);
|
||||
} catch (err) {
|
||||
setError(err.response?.data || err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderMethodForm = () => {
|
||||
if (!selectedMethod) {
|
||||
return (
|
||||
<div className="mt-4 p-4 text-center text-slate-500 bg-slate-50 rounded-lg">
|
||||
<p>Select a method to begin.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4 mt-6">
|
||||
<h3 className="text-lg font-semibold text-slate-800">
|
||||
Parameters for <code className="bg-slate-100 p-1 rounded text-sm">/{selectedEntity}/{selectedMethod}</code>
|
||||
</h3>
|
||||
|
||||
{/*
|
||||
This is a textarea for JSON input. A more advanced implementation could
|
||||
dynamically generate a form based on the expected parameters of each
|
||||
SDK method, but that requires metadata about each method's signature
|
||||
which is not currently available in the mock client.
|
||||
*/}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="params">JSON Payload</Label>
|
||||
<Textarea
|
||||
id="params"
|
||||
name="params"
|
||||
value={jsonInput}
|
||||
onChange={handleJsonInputChange}
|
||||
rows={8}
|
||||
className={`font-mono text-sm ${jsonError ? 'border-red-500 focus-visible:ring-red-500' : ''}`}
|
||||
/>
|
||||
{jsonError && <p className="text-xs text-red-600">{jsonError}</p>}
|
||||
</div>
|
||||
|
||||
<Button onClick={executeApi} disabled={loading || !!jsonError}>
|
||||
{loading ? "Executing..." : "Execute"}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<header className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-slate-900">Entity API Tester</h1>
|
||||
<p className="text-slate-600 mt-1">
|
||||
Select an entity and method, provide the required parameters in JSON format, and execute the API call.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<Card className="shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle>API Configuration</CardTitle>
|
||||
<CardDescription>Choose a Base44 entity and method to interact with.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 items-start">
|
||||
<div className="col-span-1">
|
||||
<Label>Entity</Label>
|
||||
<Select onValueChange={handleEntityChange} value={selectedEntity || ""}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select an entity" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{entityNames.map(entity => (
|
||||
<SelectItem key={entity} value={entity}>{entity}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{selectedEntity && (
|
||||
<div className="col-span-2">
|
||||
<Label>Method</Label>
|
||||
<div className="flex flex-wrap items-center gap-2 pt-2">
|
||||
{availableMethods.map(method => (
|
||||
<Button
|
||||
key={method}
|
||||
variant={selectedMethod === method ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => handleMethodSelect(method)}
|
||||
>
|
||||
{method}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{renderMethodForm()}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<ApiResponse response={response} error={error} loading={loading} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EntityTester;
|
||||
28
internal-api-harness/src/pages/GenerateImage.jsx
Normal file
28
internal-api-harness/src/pages/GenerateImage.jsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import ServiceTester from "@/components/ServiceTester";
|
||||
|
||||
const GenerateImage = () => {
|
||||
const fields = [
|
||||
{
|
||||
name: "prompt",
|
||||
label: "Prompt",
|
||||
type: "textarea",
|
||||
placeholder: "Enter a prompt for the image",
|
||||
},
|
||||
{
|
||||
name: "file",
|
||||
label: "File",
|
||||
type: "file",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ServiceTester
|
||||
serviceName="Generate Image"
|
||||
serviceDescription="Test the Generate Image service"
|
||||
endpoint="/generate-image"
|
||||
fields={fields}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default GenerateImage;
|
||||
31
internal-api-harness/src/pages/Home.jsx
Normal file
31
internal-api-harness/src/pages/Home.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
||||
const Home = () => {
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-slate-900">Welcome to KROW API Test Harness</h1>
|
||||
<p className="text-slate-600 mt-1">
|
||||
Your dedicated tool for rapid and authenticated testing of KROW backend services.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Get Started</CardTitle>
|
||||
<CardDescription>
|
||||
Use the sidebar navigation to select an API category and then choose a specific endpoint to test.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-slate-700">
|
||||
This tool automatically handles Firebase authentication, injecting the necessary ID tokens into your API requests.
|
||||
Simply log in, select an API, provide the required parameters, and execute to see the raw JSON response.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
37
internal-api-harness/src/pages/Login.jsx
Normal file
37
internal-api-harness/src/pages/Login.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { GoogleAuthProvider, signInWithPopup, setPersistence, browserLocalPersistence } from "firebase/auth";
|
||||
import { useAuthState } from "react-firebase-hooks/auth";
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { auth } from "../firebase";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
const Login = () => {
|
||||
const [user, loading] = useAuthState(auth);
|
||||
|
||||
const handleGoogleLogin = async () => {
|
||||
const provider = new GoogleAuthProvider();
|
||||
try {
|
||||
await setPersistence(auth, browserLocalPersistence);
|
||||
await signInWithPopup(auth, provider);
|
||||
} catch (error) {
|
||||
console.error("Error signing in with Google", error);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
// If user is logged in, redirect to the home page
|
||||
if (user) {
|
||||
return <Navigate to="/" />;
|
||||
}
|
||||
|
||||
// If no user, show the login button
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<Button onClick={handleGoogleLogin}>Sign in with Google</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
49
internal-api-harness/src/pages/auth/GetMe.jsx
Normal file
49
internal-api-harness/src/pages/auth/GetMe.jsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import ApiResponse from "@/components/ApiResponse";
|
||||
import { krowSDK } from "@/api/krowSDK";
|
||||
|
||||
const GetMe = () => {
|
||||
const [response, setResponse] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleGetMe = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setResponse(null);
|
||||
try {
|
||||
const res = await krowSDK.auth.me();
|
||||
setResponse(res);
|
||||
} catch (err) {
|
||||
setError(err.response?.data || err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-slate-900 mb-4">Get Me</h1>
|
||||
<p className="text-slate-600 mb-6">Fetches the currently authenticated user's profile.</p>
|
||||
|
||||
<Card className="max-w-2xl">
|
||||
<CardHeader>
|
||||
<CardTitle>Test `/auth/me`</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button onClick={handleGetMe} disabled={loading}>
|
||||
{loading ? "Loading..." : "Execute"}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<ApiResponse response={response} error={error} loading={loading} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// We need to re-import Card components because they are not globally available.
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||
|
||||
export default GetMe;
|
||||
74
internal-api-harness/src/pages/core/CreateSignedUrl.jsx
Normal file
74
internal-api-harness/src/pages/core/CreateSignedUrl.jsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import ApiResponse from "@/components/ApiResponse";
|
||||
import { krowSDK } from "@/api/krowSDK";
|
||||
import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "@/components/ui/card";
|
||||
|
||||
const CreateSignedUrl = () => {
|
||||
const [response, setResponse] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
file_uri: "gs://your-bucket/private-file.pdf",
|
||||
expires_in: 3600,
|
||||
});
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { id, value } = e.target;
|
||||
setFormData(prev => ({ ...prev, [id]: value }));
|
||||
};
|
||||
|
||||
const handleCreateUrl = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setResponse(null);
|
||||
try {
|
||||
const params = {
|
||||
...formData,
|
||||
expires_in: parseInt(formData.expires_in, 10),
|
||||
};
|
||||
const res = await krowSDK.integrations.Core.CreateFileSignedUrl(params);
|
||||
setResponse(res);
|
||||
} catch (err) {
|
||||
setError(err.response?.data || err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-slate-900 mb-4">Create Signed URL</h1>
|
||||
<p className="text-slate-600 mb-6">Tests the `integrations.Core.CreateFileSignedUrl` endpoint.</p>
|
||||
|
||||
<Card className="max-w-2xl">
|
||||
<CardHeader>
|
||||
<CardTitle>Test `/createSignedUrl`</CardTitle>
|
||||
<CardDescription>Creates a temporary access URL for a private file stored in GCS.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleCreateUrl} className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="file_uri">File URI</Label>
|
||||
<Input id="file_uri" value={formData.file_uri} onChange={handleChange} />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="expires_in">Expires In (seconds)</Label>
|
||||
<Input id="expires_in" type="number" value={formData.expires_in} onChange={handleChange} />
|
||||
</div>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? "Creating..." : "Create Signed URL"}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<ApiResponse response={response} error={error} loading={loading} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateSignedUrl;
|
||||
81
internal-api-harness/src/pages/core/InvokeLLM.jsx
Normal file
81
internal-api-harness/src/pages/core/InvokeLLM.jsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import ApiResponse from "@/components/ApiResponse";
|
||||
import { krowSDK } from "@/api/krowSDK";
|
||||
import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
const InvokeLLM = () => {
|
||||
const [response, setResponse] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
prompt: "Extract the total amount from the attached invoice.",
|
||||
response_json_schema: JSON.stringify({ type: "object", properties: { total_amount: { type: "number" } } }, null, 2),
|
||||
file_urls: "https://example.com/invoice.pdf",
|
||||
});
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { id, value } = e.target;
|
||||
setFormData(prev => ({ ...prev, [id]: value }));
|
||||
};
|
||||
|
||||
const handleInvokeLLM = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setResponse(null);
|
||||
try {
|
||||
const params = {
|
||||
...formData,
|
||||
response_json_schema: JSON.parse(formData.response_json_schema),
|
||||
file_urls: formData.file_urls.split(',').map(url => url.trim()),
|
||||
};
|
||||
const res = await krowSDK.integrations.Core.InvokeLLM(params);
|
||||
setResponse(res);
|
||||
} catch (err) {
|
||||
setError(err.response?.data || err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-slate-900 mb-4">Invoke LLM</h1>
|
||||
<p className="text-slate-600 mb-6">Tests the `integrations.Core.InvokeLLM` endpoint.</p>
|
||||
|
||||
<Card className="max-w-2xl">
|
||||
<CardHeader>
|
||||
<CardTitle>Test `/invokeLLM`</CardTitle>
|
||||
<CardDescription>Calls a large language model (e.g., Vertex AI).</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleInvokeLLM} className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="prompt">Prompt</Label>
|
||||
<Textarea id="prompt" value={formData.prompt} onChange={handleChange} rows={4} />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="response_json_schema">Response JSON Schema</Label>
|
||||
<Textarea id="response_json_schema" value={formData.response_json_schema} onChange={handleChange} rows={6} className="font-mono" />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="file_urls">File URLs (comma-separated)</Label>
|
||||
<Input id="file_urls" value={formData.file_urls} onChange={handleChange} />
|
||||
</div>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? "Invoking..." : "Invoke LLM"}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<ApiResponse response={response} error={error} loading={loading} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InvokeLLM;
|
||||
75
internal-api-harness/src/pages/core/SendEmail.jsx
Normal file
75
internal-api-harness/src/pages/core/SendEmail.jsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import ApiResponse from "@/components/ApiResponse";
|
||||
import { krowSDK } from "@/api/krowSDK";
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||
|
||||
const SendEmail = () => {
|
||||
const [response, setResponse] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
to: "test@example.com",
|
||||
subject: "Test Email from Harness",
|
||||
body: "This is a test email.",
|
||||
});
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { id, value } = e.target;
|
||||
setFormData(prev => ({ ...prev, [id]: value }));
|
||||
};
|
||||
|
||||
const handleSendEmail = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setResponse(null);
|
||||
try {
|
||||
const res = await krowSDK.integrations.Core.SendEmail(formData);
|
||||
setResponse(res);
|
||||
} catch (err) {
|
||||
setError(err.response?.data || err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-slate-900 mb-4">Send Email</h1>
|
||||
<p className="text-slate-600 mb-6">Tests the `integrations.Core.SendEmail` endpoint.</p>
|
||||
|
||||
<Card className="max-w-2xl">
|
||||
<CardHeader>
|
||||
<CardTitle>Test `/sendEmail`</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSendEmail} className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="to">To</Label>
|
||||
<Input id="to" type="email" value={formData.to} onChange={handleChange} />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="subject">Subject</Label>
|
||||
<Input id="subject" value={formData.subject} onChange={handleChange} />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="body">Body</Label>
|
||||
<Textarea id="body" value={formData.body} onChange={handleChange} rows={5} />
|
||||
</div>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? "Sending..." : "Send Email"}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<ApiResponse response={response} error={error} loading={loading} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SendEmail;
|
||||
67
internal-api-harness/src/pages/core/UploadFile.jsx
Normal file
67
internal-api-harness/src/pages/core/UploadFile.jsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import ApiResponse from "@/components/ApiResponse";
|
||||
import { krowSDK } from "@/api/krowSDK";
|
||||
import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "@/components/ui/card";
|
||||
|
||||
const UploadFile = () => {
|
||||
const [response, setResponse] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [file, setFile] = useState(null);
|
||||
|
||||
const handleFileChange = (e) => {
|
||||
setFile(e.target.files[0]);
|
||||
};
|
||||
|
||||
const handleUpload = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!file) {
|
||||
setError({ message: "Please select a file to upload." });
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setResponse(null);
|
||||
|
||||
try {
|
||||
const res = await krowSDK.integrations.Core.UploadFile({ file });
|
||||
setResponse(res);
|
||||
} catch (err) {
|
||||
setError(err.response?.data || err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-slate-900 mb-4">Upload Public File</h1>
|
||||
<p className="text-slate-600 mb-6">Tests the `integrations.Core.UploadFile` endpoint for public files.</p>
|
||||
|
||||
<Card className="max-w-2xl">
|
||||
<CardHeader>
|
||||
<CardTitle>Test `/uploadFile`</CardTitle>
|
||||
<CardDescription>Handles multipart/form-data upload to GCS and returns a public URL.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleUpload} className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="file">File</Label>
|
||||
<Input id="file" type="file" onChange={handleFileChange} />
|
||||
</div>
|
||||
<Button type="submit" disabled={loading || !file}>
|
||||
{loading ? "Uploading..." : "Upload File"}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<ApiResponse response={response} error={error} loading={loading} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UploadFile;
|
||||
67
internal-api-harness/src/pages/core/UploadPrivateFile.jsx
Normal file
67
internal-api-harness/src/pages/core/UploadPrivateFile.jsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import ApiResponse from "@/components/ApiResponse";
|
||||
import { krowSDK } from "@/api/krowSDK";
|
||||
import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "@/components/ui/card";
|
||||
|
||||
const UploadPrivateFile = () => {
|
||||
const [response, setResponse] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [file, setFile] = useState(null);
|
||||
|
||||
const handleFileChange = (e) => {
|
||||
setFile(e.target.files[0]);
|
||||
};
|
||||
|
||||
const handleUpload = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!file) {
|
||||
setError({ message: "Please select a file to upload." });
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setResponse(null);
|
||||
|
||||
try {
|
||||
const res = await krowSDK.integrations.Core.UploadPrivateFile({ file });
|
||||
setResponse(res);
|
||||
} catch (err) {
|
||||
setError(err.response?.data || err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-slate-900 mb-4">Upload Private File</h1>
|
||||
<p className="text-slate-600 mb-6">Tests the `integrations.Core.UploadPrivateFile` endpoint.</p>
|
||||
|
||||
<Card className="max-w-2xl">
|
||||
<CardHeader>
|
||||
<CardTitle>Test `/uploadPrivateFile`</CardTitle>
|
||||
<CardDescription>Handles multipart/form-data upload to GCS and returns a secure gs:// URI.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleUpload} className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="file">File</Label>
|
||||
<Input id="file" type="file" onChange={handleFileChange} />
|
||||
</div>
|
||||
<Button type="submit" disabled={loading || !file}>
|
||||
{loading ? "Uploading..." : "Upload File"}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<ApiResponse response={response} error={error} loading={loading} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UploadPrivateFile;
|
||||
96
internal-api-harness/tailwind.config.js
Normal file
96
internal-api-harness/tailwind.config.js
Normal file
@@ -0,0 +1,96 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: ["class"],
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)'
|
||||
},
|
||||
colors: {
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))'
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))'
|
||||
},
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))'
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))'
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))'
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))'
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))'
|
||||
},
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
chart: {
|
||||
'1': 'hsl(var(--chart-1))',
|
||||
'2': 'hsl(var(--chart-2))',
|
||||
'3': 'hsl(var(--chart-3))',
|
||||
'4': 'hsl(var(--chart-4))',
|
||||
'5': 'hsl(var(--chart-5))'
|
||||
},
|
||||
sidebar: {
|
||||
DEFAULT: 'hsl(var(--sidebar-background))',
|
||||
foreground: 'hsl(var(--sidebar-foreground))',
|
||||
primary: 'hsl(var(--sidebar-primary))',
|
||||
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
|
||||
accent: 'hsl(var(--sidebar-accent))',
|
||||
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
|
||||
border: 'hsl(var(--sidebar-border))',
|
||||
ring: 'hsl(var(--sidebar-ring))'
|
||||
}
|
||||
},
|
||||
keyframes: {
|
||||
'accordion-down': {
|
||||
from: {
|
||||
height: '0'
|
||||
},
|
||||
to: {
|
||||
height: 'var(--radix-accordion-content-height)'
|
||||
}
|
||||
},
|
||||
'accordion-up': {
|
||||
from: {
|
||||
height: 'var(--radix-accordion-content-height)'
|
||||
},
|
||||
to: {
|
||||
height: '0'
|
||||
}
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||
'accordion-up': 'accordion-up 0.2s ease-out'
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
}
|
||||
17
internal-api-harness/vite.config.js
Normal file
17
internal-api-harness/vite.config.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import path from "path"
|
||||
import react from "@vitejs/plugin-react"
|
||||
import { defineConfig } from "vite"
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
headers: {
|
||||
"Cross-Origin-Opener-Policy": "same-origin-allow-popups",
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user