diff --git a/src/Frontend/.eslintrc.cjs b/src/Frontend/.eslintrc.cjs
new file mode 100644
index 0000000..39aef40
--- /dev/null
+++ b/src/Frontend/.eslintrc.cjs
@@ -0,0 +1,21 @@
+module.exports = {
+ root: true,
+ env: { browser: true, es2020: true },
+ extends: [
+ 'eslint:recommended',
+ '@typescript-eslint/recommended',
+ 'eslint:recommended',
+ '@typescript-eslint/recommended',
+ ],
+ ignorePatterns: ['dist', '.eslintrc.cjs'],
+ parser: '@typescript-eslint/parser',
+ plugins: ['react-refresh'],
+ rules: {
+ 'react-refresh/only-export-components': [
+ 'warn',
+ { allowConstantExport: true },
+ ],
+ '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
+ '@typescript-eslint/no-explicit-any': 'off',
+ },
+}
\ No newline at end of file
diff --git a/src/Frontend/Dockerfile b/src/Frontend/Dockerfile
new file mode 100644
index 0000000..57be778
--- /dev/null
+++ b/src/Frontend/Dockerfile
@@ -0,0 +1,31 @@
+# Build stage
+FROM node:18-alpine as builder
+
+WORKDIR /app
+
+# Copy package files
+COPY package*.json ./
+
+# Install dependencies
+RUN npm ci --only=production
+
+# Copy source code
+COPY . .
+
+# Build the app
+RUN npm run build
+
+# Production stage
+FROM nginx:alpine
+
+# Copy built assets from builder stage
+COPY --from=builder /app/dist /usr/share/nginx/html
+
+# Copy nginx configuration
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+
+# Expose port 80
+EXPOSE 80
+
+# Start nginx
+CMD ["nginx", "-g", "daemon off;"]
\ No newline at end of file
diff --git a/src/Frontend/README.md b/src/Frontend/README.md
new file mode 100644
index 0000000..d97c271
--- /dev/null
+++ b/src/Frontend/README.md
@@ -0,0 +1,356 @@
+# Release Management Platform - Frontend
+
+Modern React TypeScript frontend for the Release Management Platform, built with Material-UI and real-time SignalR integration.
+
+## Features
+
+- **Dashboard**: Overview of packages, projects, and active publications
+- **Package Management**: Create, edit, and manage software packages
+- **Real-time Publishing**: Live progress tracking with WebSocket updates
+- **Project Monitoring**: Build status and history from CruiseControl.NET
+- **Responsive Design**: Works on desktop and mobile devices
+
+## Technology Stack
+
+- **React 18+** with TypeScript
+- **Material-UI** for component library and theming
+- **Vite** for fast development and building
+- **React Query** for server state management
+- **SignalR** for real-time updates
+- **React Hook Form** with Yup validation
+- **React Router** for navigation
+
+## Project Structure
+
+```
+src/
+├── components/ # Reusable UI components
+│ ├── Layout/ # App layout and navigation
+│ ├── Packages/ # Package management components
+│ ├── Publishing/ # Publishing dashboard components
+│ └── common/ # Shared components
+├── hooks/ # Custom React hooks
+├── services/ # API clients and services
+├── types/ # TypeScript type definitions
+├── contexts/ # React contexts and providers
+├── pages/ # Page components
+└── utils/ # Utility functions
+```
+
+## Getting Started
+
+### Development
+
+1. Install dependencies:
+```bash
+npm install
+```
+
+2. Start the development server:
+```bash
+npm run dev
+```
+
+The application will be available at http://localhost:3000
+
+### Building for Production
+
+```bash
+npm run build
+```
+
+### Docker Deployment
+
+Build the Docker image:
+```bash
+docker build -t release-management-frontend .
+```
+
+Run the container:
+```bash
+docker run -p 3000:80 release-management-frontend
+```
+
+## Configuration
+
+The frontend is configured to proxy API requests to the backend services:
+
+- `/api/*` → API Gateway on port 5000
+- `/hubs/*` → SignalR hubs on port 5000
+
+For production deployment, configure the nginx.conf file or environment variables as needed.
+
+## Key Components
+
+### Package Management
+- **PackageList**: Displays all packages with filtering and search
+- **PackageForm**: Create and edit package configurations
+- Supports tabbed interface for basic info, configuration, storage, and help center settings
+
+### Publishing Dashboard
+- **PublishingDashboard**: Real-time view of active publications
+- **ProgressBar**: Visual progress indicators for publishing steps
+- Live updates via SignalR for step completion and progress
+
+### Real-time Updates
+- SignalR integration for live publishing progress
+- Build status updates from CruiseControl.NET
+- Automatic UI refresh on publication completion
+
+## API Integration
+
+The frontend integrates with the following backend services:
+
+- **Package Service**: CRUD operations for packages
+- **Project Service**: CruiseControl.NET integration
+- **Publication Service**: Publishing workflow management
+- **SignalR Hub**: Real-time notifications
+
+## Development Guidelines
+
+- Use TypeScript for type safety
+- Follow Material-UI design patterns
+- Implement proper error handling and loading states
+- Use React Query for server state management
+- Write reusable components with proper prop interfaces
+- Follow responsive design principles
+
+## Environment Variables
+
+Create a `.env.local` file for local development:
+
+```env
+VITE_API_BASE_URL=http://localhost:5000
+VITE_SIGNALR_HUB_URL=http://localhost:5000/hubs
+```
+
+## Browser Support
+
+- Chrome (latest)
+- Firefox (latest)
+- Safari (latest)
+- Edge (latest)
+
+## Contributing
+
+1. Follow the existing code style and patterns
+2. Write TypeScript interfaces for all data structures
+3. Include proper error handling and loading states
+4. Test responsive behavior on different screen sizes
+5. Document complex components and hooks
+
+---
+
+## 📝 Developer Notes for Future Development
+
+### Architecture Decisions & Patterns
+
+#### **State Management Strategy**
+- **React Query**: Used for all server state (packages, projects, publications)
+- **Local State**: useState/useReducer for UI-only state
+- **Context API**: Only for truly global state (currently just QueryProvider)
+- **No Redux**: Kept simple with React Query + local state
+
+#### **Component Architecture**
+```
+Pages -> Layout -> Feature Components -> Common Components
+```
+- **Pages**: Route-level components, minimal logic
+- **Feature Components**: Domain-specific (Packages, Publishing)
+- **Common Components**: Reusable UI elements
+- **Layout**: Navigation and app shell
+
+#### **API Integration Patterns**
+- **Services Layer**: Clean separation of API logic
+- **Custom Hooks**: Encapsulate data fetching and mutations
+- **Error Boundaries**: Not implemented yet - consider adding
+- **Optimistic Updates**: Implemented in mutation hooks
+
+### 🔧 Technical Implementation Notes
+
+#### **SignalR Integration**
+```typescript
+// SignalR connection lifecycle managed in useSignalR hook
+// Automatic reconnection enabled
+// Group-based subscriptions for publication updates
+```
+
+**Important**: SignalR connection state should be displayed in UI (connection indicator in AppLayout)
+
+#### **Form Handling**
+- **React Hook Form + Yup**: Chosen for performance and TypeScript support
+- **Tabbed Forms**: PackageForm uses Material-UI Tabs
+- **Dynamic Fields**: Build selection depends on project selection
+
+#### **Real-time Updates Flow**
+```
+1. User triggers publish -> API call
+2. SignalR sends progress updates -> useSignalR hook
+3. Hook invalidates React Query cache -> UI updates
+4. Publication completes -> Final UI refresh
+```
+
+### 🚨 Known Issues & TODOs
+
+#### **Missing Components**
+- [ ] **Projects Page**: Currently just placeholder
+- [ ] **History Page**: Publication history view needed
+- [ ] **Settings Page**: User preferences, API configuration
+- [ ] **User Management**: Authentication UI
+- [ ] **Error Boundaries**: Global error handling
+
+#### **UX Improvements Needed**
+- [ ] **Loading Skeletons**: Replace spinners with skeleton screens
+- [ ] **Toast Notifications**: Success/error feedback
+- [ ] **Keyboard Navigation**: Accessibility improvements
+- [ ] **Mobile Optimization**: Touch gestures, better responsive design
+- [ ] **Dark Mode**: Theme switcher
+
+#### **Performance Optimizations**
+- [ ] **Virtualization**: For large lists (packages, publications)
+- [ ] **Code Splitting**: Route-based lazy loading
+- [ ] **Bundle Analysis**: webpack-bundle-analyzer equivalent for Vite
+- [ ] **Image Optimization**: If images are added later
+
+### 🔐 Security Considerations
+
+#### **Current Implementation**
+- JWT tokens stored in localStorage (not ideal for production)
+- CORS handled by backend
+- CSP headers in nginx.conf
+- Input validation with Yup schemas
+
+#### **Production Security TODOs**
+- [ ] **HttpOnly Cookies**: Move JWT to secure cookies
+- [ ] **CSRF Protection**: If switching to cookies
+- [ ] **Content Security Policy**: Strengthen CSP rules
+- [ ] **Input Sanitization**: XSS prevention for rich text fields
+
+### 🎨 UI/UX Design System
+
+#### **Material-UI Customizations**
+```typescript
+// Theme customization in App.tsx
+// Custom shadows for cards/papers
+// Color palette matches backend architecture diagram
+```
+
+#### **Component Patterns**
+- **StatusChip**: Consistent status display with icons/colors
+- **ProgressBar**: Reusable for publishing workflows
+- **LoadingSpinner**: Consistent loading states
+- **ErrorDisplay**: Standard error handling UI
+
+#### **Responsive Breakpoints**
+```typescript
+// Material-UI defaults:
+xs: 0px, sm: 600px, md: 900px, lg: 1200px, xl: 1536px
+// Drawer collapses below lg (1200px)
+```
+
+### 🧪 Testing Strategy (Not Implemented)
+
+#### **Recommended Testing Approach**
+```bash
+# Unit Tests: React Testing Library + Jest
+npm install --save-dev @testing-library/react @testing-library/jest-dom
+
+# E2E Tests: Playwright or Cypress
+npm install --save-dev playwright
+
+# Component Testing: Storybook
+npm install --save-dev @storybook/react
+```
+
+#### **Test Priorities**
+1. **Critical User Flows**: Package creation, publishing workflow
+2. **Real-time Features**: SignalR connection handling
+3. **Form Validation**: Package form edge cases
+4. **API Error Handling**: Network failure scenarios
+
+### 🚀 Deployment Notes
+
+#### **Environment Configuration**
+```bash
+# Development
+VITE_API_BASE_URL=http://localhost:5000
+
+# Production
+VITE_API_BASE_URL=https://api.yourdomain.com
+```
+
+#### **Docker Multi-stage Build**
+- Stage 1: Node.js build environment
+- Stage 2: Nginx serving static files
+- nginx.conf proxies API requests to backend
+
+#### **Monitoring & Observability**
+- [ ] **Error Tracking**: Sentry or similar
+- [ ] **Analytics**: User behavior tracking
+- [ ] **Performance**: Core Web Vitals monitoring
+- [ ] **Logging**: Frontend error logging
+
+### 🔄 Integration with Backend Services
+
+#### **Expected API Endpoints**
+```typescript
+// Based on implementation.md architecture:
+GET /api/packages # List packages
+POST /api/packages # Create package
+GET /api/packages/{id} # Get package details
+PUT /api/packages/{id} # Update package
+DELETE /api/packages/{id} # Delete package
+POST /api/packages/{id}/publish # Start publishing
+
+GET /api/projects # List projects
+GET /api/projects/{id}/builds # Get project builds
+GET /api/builds/{id}/commits # Get build commits
+
+GET /api/publications # List publications
+GET /api/publications/active # Active publications
+POST /api/publications/{id}/cancel # Cancel publication
+```
+
+#### **SignalR Hub Events**
+```typescript
+// Expected events from backend SignalR hub:
+'PublishingProgress' // Step progress updates
+'BuildStatusUpdate' # Project build status changes
+'PublicationCompleted' // Publication finished successfully
+'PublicationFailed' // Publication failed with error
+```
+
+### 💡 Future Enhancement Ideas
+
+#### **Advanced Features**
+- **Bulk Operations**: Select multiple packages for batch publishing
+- **Publishing Templates**: Reusable configuration templates
+- **Workflow Visualization**: Visual pipeline representation
+- **Audit Trail**: Detailed action history with user attribution
+- **Notifications**: Email/Slack integration for publish completion
+- **Rollback**: Revert to previous package versions
+
+#### **Developer Experience**
+- **Hot Reload**: Already implemented with Vite
+- **TypeScript Strict Mode**: Enable stricter type checking
+- **ESLint Rules**: Add more React-specific linting rules
+- **Prettier**: Code formatting consistency
+- **Husky**: Pre-commit hooks for quality checks
+
+### 🤝 Team Collaboration Notes
+
+#### **Code Review Checklist**
+- [ ] TypeScript interfaces defined for new data structures
+- [ ] Loading and error states handled
+- [ ] Mobile responsiveness tested
+- [ ] SignalR subscriptions properly cleaned up
+- [ ] React Query cache invalidation logic correct
+- [ ] Form validation covers edge cases
+
+#### **Naming Conventions**
+- **Components**: PascalCase (PackageList.tsx)
+- **Hooks**: camelCase with 'use' prefix (usePackages.ts)
+- **Types**: PascalCase interfaces/enums (Package, PackageStatus)
+- **Files**: PascalCase for components, camelCase for utilities
+
+This frontend implementation provides a solid foundation for the Release Management Platform. The architecture is scalable and the patterns established should guide future development efforts.
\ No newline at end of file
diff --git a/src/Frontend/index.html b/src/Frontend/index.html
new file mode 100644
index 0000000..eafe862
--- /dev/null
+++ b/src/Frontend/index.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ Release Management Platform
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Frontend/nginx.conf b/src/Frontend/nginx.conf
new file mode 100644
index 0000000..827a483
--- /dev/null
+++ b/src/Frontend/nginx.conf
@@ -0,0 +1,50 @@
+server {
+ listen 80;
+ server_name localhost;
+ root /usr/share/nginx/html;
+ index index.html index.htm;
+
+ # Serve static assets with caching
+ location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
+ expires 1y;
+ add_header Cache-Control "public, immutable";
+ }
+
+ # API proxy
+ location /api/ {
+ proxy_pass http://api-gateway:80/api/;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection 'upgrade';
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_cache_bypass $http_upgrade;
+ }
+
+ # SignalR hub proxy
+ location /hubs/ {
+ proxy_pass http://api-gateway:80/hubs/;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_cache_bypass $http_upgrade;
+ }
+
+ # Handle client-side routing
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+
+ # Security headers
+ add_header X-Frame-Options "SAMEORIGIN" always;
+ add_header X-XSS-Protection "1; mode=block" always;
+ add_header X-Content-Type-Options "nosniff" always;
+ add_header Referrer-Policy "no-referrer-when-downgrade" always;
+ add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
+}
\ No newline at end of file
diff --git a/src/Frontend/package.json b/src/Frontend/package.json
new file mode 100644
index 0000000..d9ba37e
--- /dev/null
+++ b/src/Frontend/package.json
@@ -0,0 +1,43 @@
+{
+ "name": "release-management-frontend",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "@mui/material": "^5.15.1",
+ "@mui/icons-material": "^5.15.1",
+ "@mui/x-data-grid": "^6.18.2",
+ "@mui/x-date-pickers": "^6.18.2",
+ "@emotion/react": "^11.11.1",
+ "@emotion/styled": "^11.11.0",
+ "@microsoft/signalr": "^8.0.0",
+ "@tanstack/react-query": "^5.8.4",
+ "@tanstack/react-query-devtools": "^5.8.4",
+ "axios": "^1.6.2",
+ "react-router-dom": "^6.20.1",
+ "date-fns": "^2.30.0",
+ "react-hook-form": "^7.48.2",
+ "@hookform/resolvers": "^3.3.2",
+ "yup": "^1.3.3"
+ },
+ "devDependencies": {
+ "@types/react": "^18.2.37",
+ "@types/react-dom": "^18.2.15",
+ "@typescript-eslint/eslint-plugin": "^6.10.0",
+ "@typescript-eslint/parser": "^6.10.0",
+ "@vitejs/plugin-react": "^4.1.1",
+ "eslint": "^8.53.0",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "eslint-plugin-react-refresh": "^0.4.4",
+ "typescript": "^5.2.2",
+ "vite": "^5.0.0"
+ }
+}
\ No newline at end of file
diff --git a/src/Frontend/src/App.tsx b/src/Frontend/src/App.tsx
new file mode 100644
index 0000000..c52fd9c
--- /dev/null
+++ b/src/Frontend/src/App.tsx
@@ -0,0 +1,75 @@
+import React from 'react';
+import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
+import { ThemeProvider, createTheme, CssBaseline } from '@mui/material';
+import { LocalizationProvider } from '@mui/x-date-pickers';
+import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
+import { QueryProvider } from './contexts/QueryProvider';
+import { AppLayout } from './components/Layout/AppLayout';
+import { DashboardPage } from './pages/DashboardPage';
+import { PackagesPage } from './pages/PackagesPage';
+import { PublishingPage } from './pages/PublishingPage';
+
+// Create Material-UI theme
+const theme = createTheme({
+ palette: {
+ mode: 'light',
+ primary: {
+ main: '#1976d2',
+ },
+ secondary: {
+ main: '#dc004e',
+ },
+ },
+ typography: {
+ fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
+ h4: {
+ fontWeight: 600,
+ },
+ h6: {
+ fontWeight: 600,
+ },
+ },
+ components: {
+ MuiCard: {
+ styleOverrides: {
+ root: {
+ boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
+ },
+ },
+ },
+ MuiPaper: {
+ styleOverrides: {
+ root: {
+ boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
+ },
+ },
+ },
+ },
+});
+
+const App: React.FC = () => {
+ return (
+
+
+
+
+
+
+
+ } />
+ } />
+ } />
+ Projects page - Coming soon} />
+ History page - Coming soon} />
+ Settings page - Coming soon} />
+ } />
+
+
+
+
+
+
+ );
+};
+
+export default App;
\ No newline at end of file
diff --git a/src/Frontend/src/components/Layout/AppLayout.tsx b/src/Frontend/src/components/Layout/AppLayout.tsx
new file mode 100644
index 0000000..925d78f
--- /dev/null
+++ b/src/Frontend/src/components/Layout/AppLayout.tsx
@@ -0,0 +1,205 @@
+import React, { useState } from 'react';
+import {
+ AppBar,
+ Toolbar,
+ Typography,
+ Drawer,
+ List,
+ ListItem,
+ ListItemIcon,
+ ListItemText,
+ ListItemButton,
+ Box,
+ IconButton,
+ Badge,
+ Tooltip,
+ Divider,
+} from '@mui/material';
+import {
+ Menu as MenuIcon,
+ Dashboard as DashboardIcon,
+ Inventory as PackagesIcon,
+ Build as BuildIcon,
+ Publish as PublishIcon,
+ History as HistoryIcon,
+ Settings as SettingsIcon,
+ Notifications as NotificationsIcon,
+ AccountCircle as AccountIcon,
+} from '@mui/icons-material';
+import { useNavigate, useLocation } from 'react-router-dom';
+import { useSignalR } from '../../hooks/useSignalR';
+import { useActivePublications } from '../../hooks/usePublications';
+
+const DRAWER_WIDTH = 240;
+
+interface AppLayoutProps {
+ children: React.ReactNode;
+}
+
+export const AppLayout: React.FC = ({ children }) => {
+ const [drawerOpen, setDrawerOpen] = useState(false);
+ const navigate = useNavigate();
+ const location = useLocation();
+ const { isConnected } = useSignalR();
+ const { data: activePublications } = useActivePublications();
+
+ const menuItems = [
+ { text: 'Dashboard', icon: , path: '/' },
+ { text: 'Packages', icon: , path: '/packages' },
+ { text: 'Projects', icon: , path: '/projects' },
+ { text: 'Publishing', icon: , path: '/publishing' },
+ { text: 'History', icon: , path: '/history' },
+ ];
+
+ const activePublicationCount = activePublications?.data?.length || 0;
+
+ const handleDrawerToggle = () => {
+ setDrawerOpen(!drawerOpen);
+ };
+
+ const handleNavigation = (path: string) => {
+ navigate(path);
+ if (window.innerWidth < 900) {
+ setDrawerOpen(false);
+ }
+ };
+
+ const drawer = (
+
+
+
+ Release Management
+
+
+
+
+ {menuItems.map((item) => (
+
+ handleNavigation(item.path)}
+ >
+
+ {item.text === 'Publishing' ? (
+
+ {item.icon}
+
+ ) : (
+ item.icon
+ )}
+
+
+
+
+ ))}
+
+
+
+
+ handleNavigation('/settings')}>
+
+
+
+
+
+
+
+
+ );
+
+ return (
+
+ theme.zIndex.drawer + 1,
+ bgcolor: 'primary.main',
+ }}
+ >
+
+
+
+
+
+
+ Release Management Platform
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {drawer}
+
+
+
+ {drawer}
+
+
+
+
+ {children}
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/Frontend/src/components/Packages/PackageForm.tsx b/src/Frontend/src/components/Packages/PackageForm.tsx
new file mode 100644
index 0000000..002d915
--- /dev/null
+++ b/src/Frontend/src/components/Packages/PackageForm.tsx
@@ -0,0 +1,411 @@
+import React, { useEffect } from 'react';
+import {
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogActions,
+ Button,
+ TextField,
+ FormControl,
+ InputLabel,
+ Select,
+ MenuItem,
+ Box,
+ Tabs,
+ Tab,
+ Typography,
+ Switch,
+ FormControlLabel,
+ Grid,
+ Autocomplete,
+} from '@mui/material';
+import { useForm, Controller } from 'react-hook-form';
+import { yupResolver } from '@hookform/resolvers/yup';
+import * as yup from 'yup';
+import {
+ Package,
+ CreatePackageRequest,
+ Build
+} from '../../types';
+import { useProjects, useProjectBuilds } from '../../hooks/useProjects';
+import { useCreatePackage, useUpdatePackage } from '../../hooks/usePackages';
+import { LoadingSpinner } from '../common/LoadingSpinner';
+
+interface PackageFormProps {
+ open: boolean;
+ onClose: () => void;
+ package?: Package;
+}
+
+interface TabPanelProps {
+ children?: React.ReactNode;
+ index: number;
+ value: number;
+}
+
+const TabPanel: React.FC = ({ children, value, index, ...other }) => (
+
+ {value === index && {children}}
+
+);
+
+const schema = yup.object({
+ title: yup.string().required('Title is required'),
+ version: yup.string().required('Version is required'),
+ description: yup.string().required('Description is required'),
+ projectId: yup.number().required('Project is required'),
+ sourceBuildId: yup.number().required('Source build is required'),
+ configuration: yup.object({
+ buildFolder: yup.string().required('Build folder is required'),
+ zipContents: yup.boolean().required(),
+ deleteOldPublishedBuilds: yup.boolean().required(),
+ releaseNoteTemplate: yup.string().required('Release note template is required'),
+ storageSettings: yup.object().required(),
+ helpCenterSettings: yup.object().required(),
+ }),
+});
+
+export const PackageForm: React.FC = ({ open, onClose, package: pkg }) => {
+ const [currentTab, setCurrentTab] = React.useState(0);
+ const [selectedProjectId, setSelectedProjectId] = React.useState(null);
+
+ const { data: projectsData } = useProjects();
+ const { data: buildsData } = useProjectBuilds(
+ selectedProjectId || 0,
+ 1,
+ 50,
+ { status: 'Success' }
+ );
+
+ const createMutation = useCreatePackage();
+ const updateMutation = useUpdatePackage();
+
+ const {
+ control,
+ handleSubmit,
+ reset,
+ watch,
+ formState: { errors, isValid },
+ } = useForm({
+ resolver: yupResolver(schema),
+ defaultValues: {
+ title: '',
+ version: '',
+ description: '',
+ projectId: 0,
+ sourceBuildId: 0,
+ configuration: {
+ buildFolder: '',
+ zipContents: true,
+ deleteOldPublishedBuilds: true,
+ releaseNoteTemplate: '',
+ storageSettings: {},
+ helpCenterSettings: {},
+ },
+ },
+ });
+
+ const watchedProjectId = watch('projectId');
+
+ useEffect(() => {
+ if (watchedProjectId) {
+ setSelectedProjectId(watchedProjectId);
+ }
+ }, [watchedProjectId]);
+
+ useEffect(() => {
+ if (pkg) {
+ reset({
+ title: pkg.title,
+ version: pkg.version,
+ description: pkg.description,
+ projectId: pkg.projectId,
+ sourceBuildId: pkg.sourceBuildId,
+ configuration: pkg.configuration || {
+ buildFolder: '',
+ zipContents: true,
+ deleteOldPublishedBuilds: true,
+ releaseNoteTemplate: '',
+ storageSettings: {},
+ helpCenterSettings: {},
+ },
+ });
+ setSelectedProjectId(pkg.projectId);
+ } else {
+ reset({
+ title: '',
+ version: '',
+ description: '',
+ projectId: 0,
+ sourceBuildId: 0,
+ configuration: {
+ buildFolder: '',
+ zipContents: true,
+ deleteOldPublishedBuilds: true,
+ releaseNoteTemplate: '',
+ storageSettings: {},
+ helpCenterSettings: {},
+ },
+ });
+ setSelectedProjectId(null);
+ }
+ }, [pkg, reset]);
+
+ const onSubmit = async (data: CreatePackageRequest) => {
+ try {
+ if (pkg) {
+ await updateMutation.mutateAsync({ id: pkg.id, request: data });
+ } else {
+ await createMutation.mutateAsync(data);
+ }
+ onClose();
+ } catch (error) {
+ console.error('Failed to save package:', error);
+ }
+ };
+
+ const handleClose = () => {
+ setCurrentTab(0);
+ onClose();
+ };
+
+ const projects = projectsData?.data || [];
+ const builds = buildsData?.data || [];
+
+ return (
+
+ );
+};
\ No newline at end of file
diff --git a/src/Frontend/src/components/Packages/PackageList.tsx b/src/Frontend/src/components/Packages/PackageList.tsx
new file mode 100644
index 0000000..29083b2
--- /dev/null
+++ b/src/Frontend/src/components/Packages/PackageList.tsx
@@ -0,0 +1,270 @@
+import React, { useState } from 'react';
+import {
+ Box,
+ Card,
+ CardContent,
+ Typography,
+ Button,
+ TextField,
+ Grid,
+ FormControl,
+ InputLabel,
+ Select,
+ MenuItem,
+ IconButton,
+ Tooltip,
+ Chip,
+ Paper,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ TablePagination,
+} from '@mui/material';
+import {
+ Add as AddIcon,
+ Edit as EditIcon,
+ Delete as DeleteIcon,
+ Publish as PublishIcon,
+ Search as SearchIcon,
+ FilterList as FilterIcon,
+} from '@mui/icons-material';
+import { usePackages, useDeletePackage } from '../../hooks/usePackages';
+import { useProjects } from '../../hooks/useProjects';
+import { Package, PackageStatus, PackageFilter } from '../../types';
+import { StatusChip } from '../common/StatusChip';
+import { LoadingSpinner } from '../common/LoadingSpinner';
+import { ErrorDisplay } from '../common/ErrorDisplay';
+import { format } from 'date-fns';
+
+interface PackageListProps {
+ onCreatePackage: () => void;
+ onEditPackage: (packageItem: Package) => void;
+ onPublishPackage: (packageItem: Package) => void;
+}
+
+export const PackageList: React.FC = ({
+ onCreatePackage,
+ onEditPackage,
+ onPublishPackage,
+}) => {
+ const [page, setPage] = useState(0);
+ const [pageSize, setPageSize] = useState(10);
+ const [filter, setFilter] = useState({});
+ const [showFilters, setShowFilters] = useState(false);
+
+ const { data: packagesData, isLoading, error, refetch } = usePackages(
+ page + 1,
+ pageSize,
+ filter
+ );
+ const { data: projectsData } = useProjects();
+ const deletePackageMutation = useDeletePackage();
+
+ const handleFilterChange = (field: keyof PackageFilter, value: any) => {
+ setFilter(prev => ({ ...prev, [field]: value }));
+ setPage(0);
+ };
+
+ const handleDelete = async (id: number) => {
+ if (window.confirm('Are you sure you want to delete this package?')) {
+ await deletePackageMutation.mutateAsync(id);
+ }
+ };
+
+ const canPublish = (pkg: Package): boolean => {
+ return pkg.status === PackageStatus.Ready || pkg.status === PackageStatus.Failed;
+ };
+
+ const canEdit = (pkg: Package): boolean => {
+ return pkg.status !== PackageStatus.Publishing;
+ };
+
+ if (isLoading) return ;
+ if (error) return ;
+
+ const packages = packagesData?.data || [];
+ const totalCount = packagesData?.totalCount || 0;
+
+ return (
+
+ {/* Header */}
+
+
+ Packages
+
+ }
+ onClick={onCreatePackage}
+ >
+ Create Package
+
+
+
+ {/* Filters */}
+
+
+
+ handleFilterChange('searchTerm', e.target.value)}
+ InputProps={{
+ startAdornment: ,
+ }}
+ />
+ }
+ onClick={() => setShowFilters(!showFilters)}
+ >
+ {showFilters ? 'Hide Filters' : 'Show Filters'}
+
+
+
+ {showFilters && (
+
+
+
+ Project
+
+
+
+
+
+ Status
+
+
+
+
+ )}
+
+
+
+ {/* Packages Table */}
+
+
+
+
+ Title
+ Version
+ Project
+ Status
+ Published Date
+ Actions
+
+
+
+ {packages.map((pkg) => (
+
+
+
+ {pkg.title}
+
+ {pkg.description}
+
+
+
+
+
+
+ {pkg.project?.name || 'Unknown'}
+
+
+
+
+ {pkg.publishDate ? format(new Date(pkg.publishDate), 'MMM dd, yyyy HH:mm') : '-'}
+
+
+
+ {canEdit(pkg) && (
+
+ onEditPackage(pkg)}
+ >
+
+
+
+ )}
+ {canPublish(pkg) && (
+
+ onPublishPackage(pkg)}
+ >
+
+
+
+ )}
+ {canEdit(pkg) && (
+
+ handleDelete(pkg.id)}
+ disabled={deletePackageMutation.isPending}
+ >
+
+
+
+ )}
+
+
+
+ ))}
+ {packages.length === 0 && (
+
+
+
+ No packages found. Create your first package to get started.
+
+
+
+ )}
+
+
+ setPage(newPage)}
+ rowsPerPage={pageSize}
+ onRowsPerPageChange={(e) => {
+ setPageSize(parseInt(e.target.value, 10));
+ setPage(0);
+ }}
+ rowsPerPageOptions={[5, 10, 25, 50]}
+ />
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/Frontend/src/components/Publishing/PublishingDashboard.tsx b/src/Frontend/src/components/Publishing/PublishingDashboard.tsx
new file mode 100644
index 0000000..5b64a3d
--- /dev/null
+++ b/src/Frontend/src/components/Publishing/PublishingDashboard.tsx
@@ -0,0 +1,362 @@
+import React, { useEffect, useState } from 'react';
+import {
+ Box,
+ Typography,
+ Grid,
+ Card,
+ CardContent,
+ List,
+ ListItem,
+ Button,
+ Chip,
+ Alert,
+ IconButton,
+ Tooltip,
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogActions,
+} from '@mui/material';
+import {
+ Cancel as CancelIcon,
+ Refresh as RefreshIcon,
+ PlayArrow as PlayIcon,
+ Stop as StopIcon,
+} from '@mui/icons-material';
+import {
+ useActivePublications,
+ useCancelPublication,
+ useRetryPublication
+} from '../../hooks/usePublications';
+import { useSignalR } from '../../hooks/useSignalR';
+import {
+ Publication,
+ PublishingStep,
+ PublishingProgressUpdate,
+ PublicationStatus,
+ StepStatus
+} from '../../types';
+import { ProgressBar } from '../common/ProgressBar';
+import { StatusChip } from '../common/StatusChip';
+import { LoadingSpinner } from '../common/LoadingSpinner';
+import { ErrorDisplay } from '../common/ErrorDisplay';
+import { format } from 'date-fns';
+
+export const PublishingDashboard: React.FC = () => {
+ const [selectedPublication, setSelectedPublication] = useState(null);
+ const [progressUpdates, setProgressUpdates] = useState