Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .github/workflows/deploy-demo.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Deploy Demo to GitHub Pages

on:
push:
branches: [main]
paths:
- 'client/**'
- '.github/workflows/deploy-demo.yml'
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
cancel-in-progress: true

jobs:
build:
name: 🎭 Build Demo
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
cache-dependency-path: client/package-lock.json

- name: Install dependencies
working-directory: client
run: npm ci --legacy-peer-deps

- name: Build demo
working-directory: client
env:
REACT_APP_DEMO_MODE: 'true'
PUBLIC_URL: '/RunWatch'
run: npm run build

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: client/build

deploy:
name: 🚀 Deploy to Pages
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
7 changes: 6 additions & 1 deletion client/src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { BrowserRouter, HashRouter, Routes, Route } from 'react-router-dom';
import { ThemeProvider, CssBaseline } from '@mui/material';
import { theme } from './common/theme/theme';
import Layout from './common/components/Layout';
Expand All @@ -10,14 +10,19 @@ import WorkflowHistory from './features/workflows/WorkflowHistory';
import RepositoryView from './features/repository/RepositoryView';
import Settings from './features/settings/Settings';
import { AdminTokenProvider } from './common/context/AdminTokenContext';
import DemoBanner from './common/components/DemoBanner';
import './App.css';

const isDemoMode = process.env.REACT_APP_DEMO_MODE === 'true';
const Router = isDemoMode ? HashRouter : BrowserRouter;

function App() {
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<AdminTokenProvider>
<Router>
{isDemoMode && <DemoBanner />}
<Layout>
<Routes>
<Route path="/" element={<Dashboard />} />
Expand Down
264 changes: 9 additions & 255 deletions client/src/api/apiService.js
Original file line number Diff line number Diff line change
@@ -1,258 +1,12 @@
import axios from 'axios';
import io from 'socket.io-client';
// API Service proxy — switches between real and mock based on REACT_APP_DEMO_MODE
import realApiService from './realApiService';
import mockApiService from './mockApiService';

// Use the environment variables, falling back to development defaults if not set
const API_URL = process.env.REACT_APP_API_URL || 'http://localhost/api';
const WS_URL = process.env.REACT_APP_WEBSOCKET_URL || 'ws://localhost';
const isDemoMode = process.env.REACT_APP_DEMO_MODE === 'true';

console.log('Using API URL:', API_URL);
console.log('Using WebSocket URL:', WS_URL);
if (isDemoMode) {
console.log('🎭 RunWatch Demo Mode — using mock data');
}

// Axios instance with 429 retry logic (exponential backoff)
const api = axios.create();
api.interceptors.response.use(undefined, async (error) => {
const config = error.config;
if (error.response?.status === 429 && (!config._retryCount || config._retryCount < 3)) {
config._retryCount = (config._retryCount || 0) + 1;
const retryAfter = error.response.headers['retry-after'];
const delay = retryAfter ? Number(retryAfter) * 1000 : Math.min(1000 * 2 ** config._retryCount, 15000);
await new Promise(resolve => setTimeout(resolve, delay));
return api(config);
}
return Promise.reject(error);
});

// Create socket connection with environment-aware configuration
export const socket = io(WS_URL, {
transports: ['websocket'],
path: '/socket.io'
});

// API Services
const apiService = {
// Get all workflow runs
getWorkflowRuns: async (page = 1, pageSize = 30, search = '', status = 'all') => {
try {
const response = await api.get(`${API_URL}/workflow-runs`, {
params: { page, pageSize, search, status }
});
return response.data.data || { data: [], pagination: { total: 0, page: 1, pageSize: 30, totalPages: 1 } };
} catch (error) {
console.error('Error fetching workflow runs:', error);
throw error;
}
},

// Get workflow runs for a specific repository
getRepoWorkflowRuns: async (repoName, workflowName = null, page = 1, pageSize = 30) => {
try {
const params = { page, pageSize };
if (workflowName) {
params.workflowName = workflowName;
}
const response = await api.get(`${API_URL}/workflow-runs/repo/${repoName}`, { params });
return response.data.data || { data: [], pagination: { total: 0, page: 1, pageSize: 0, totalPages: 1 } };
} catch (error) {
console.error(`Error fetching workflow runs for repo ${repoName}:`, error);
throw error;
}
},

// Get workflow run by ID
getWorkflowRunById: async (id) => {
try {
const response = await api.get(`${API_URL}/workflow-runs/${id}`);
return response.data.data;
} catch (error) {
console.error(`Error fetching workflow run ${id}:`, error);
throw error;
}
},

// Sync workflow run
syncWorkflowRun: async (id) => {
try {
const response = await api.post(`${API_URL}/workflow-runs/${id}/sync`);
return response.data.data;
} catch (error) {
console.error(`Error syncing workflow run ${id}:`, error);
throw error;
}
},

deleteWorkflowRun: async (id) => {
try {
const response = await api.delete(`${API_URL}/workflow-runs/${id}`);
return response.data.data;
} catch (error) {
console.error(`Error deleting workflow run ${id}:`, error);
throw error;
}
},

// Sync all workflow runs for a repository
syncWorkflowRuns: async (repoName) => {
try {
const response = await api.post(`${API_URL}/workflow-runs/repo/${repoName}/sync`);
return response.data.data;
} catch (error) {
console.error(`Error syncing workflow runs for repo ${repoName}:`, error);
throw error;
}
},

// Get workflow statistics
getWorkflowStats: async () => {
try {
const response = await api.get(`${API_URL}/stats`);
return response.data.data || {};
} catch (error) {
console.error('Error fetching workflow stats:', error);
throw error;
}
},

// Get available organizations
getOrganizations: async () => {
try {
const response = await api.get(`${API_URL}/organizations`);
return response.data;
} catch (error) {
console.error('Error fetching organizations:', error);
throw error;
}
},

// Sync GitHub data using installation ID
syncGitHubData: async (installationId, options = { maxWorkflowRuns: 100 }) => {
try {
const response = await fetch(`${API_URL}/sync/${encodeURIComponent(String(installationId))}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(options)
});

if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Failed to sync GitHub data');
}

return await response.json();
} catch (error) {
console.error('Error syncing GitHub data:', error);
throw error;
}
},

// Get database status
getDatabaseStatus: async () => {
try {
const response = await api.get(`${API_URL}/db/status`);
return response.data.data || {};
} catch (error) {
console.error('Error fetching database status:', error);
throw error;
}
},

// Get sync history
getSyncHistory: async () => {
try {
const response = await api.get(`${API_URL}/sync/history`);
return response.data;
} catch (error) {
console.error('Error fetching sync history:', error);
throw error;
}
},

// Get active sync status
getActiveSync: async () => {
try {
const response = await api.get(`${API_URL}/sync/active`);
return response.data;
} catch (error) {
console.error('Error fetching active sync:', error);
throw error;
}
},

// Get active metrics
getActiveMetrics: async () => {
try {
const response = await api.get(`${API_URL}/workflow-runs/metrics`);
return response.data.data;
} catch (error) {
console.error('Error fetching active metrics:', error);
throw error;
}
},

// Get queued workflows
getQueuedWorkflows: async () => {
try {
const response = await api.get(`${API_URL}/workflow-runs/queued`);
return response.data.data;
} catch (error) {
console.error('Error fetching queued workflows:', error);
throw error;
}
},

// Create database backup (requires admin token)
async createDatabaseBackup(adminToken) {
const headers = {};
if (adminToken) {
headers['Authorization'] = `Bearer ${adminToken}`;
}
const response = await api.get(`${API_URL}/database/backup`, { headers });
return response.data;
},

// Restore database backup (requires admin token)
async restoreDatabaseBackup(backupData, adminToken) {
const headers = {};
if (adminToken) {
headers['Authorization'] = `Bearer ${adminToken}`;
}
const response = await api.post(`${API_URL}/database/restore`, backupData, { headers });
return response.data;
},

// Get workflow metrics
getWorkflowMetrics: async () => {
try {
const response = await api.get(`${API_URL}/workflow-runs/metrics`);
return response.data.data;
} catch (error) {
console.error('Error fetching workflow metrics:', error);
throw error;
}
},

// Get job metrics
getJobMetrics: async () => {
try {
const response = await api.get(`${API_URL}/workflow-runs/jobs/metrics`);
return response.data.data;
} catch (error) {
console.error('Error fetching job metrics:', error);
throw error;
}
},

// Get the status of the database
getDatabaseStatus: async () => {
try {
const response = await api.get(`${API_URL}/db/status`);
return response.data.data || {};
} catch (error) {
console.error('Error fetching database status:', error);
throw error;
}
}
};

export default apiService;
const apiService = isDemoMode ? mockApiService : realApiService;
export default apiService;
Loading
Loading