add server route
This commit is contained in:
@ -4,6 +4,7 @@ import { LoginForm } from './routes/Login/LoginForm';
|
||||
import { Authenticated } from './routes/Login/Authenticated';
|
||||
import { Dashboard } from './routes/Authenticated/Dashboard/Dashboard';
|
||||
import { Apps } from './routes/Authenticated/Apps/Apps';
|
||||
import { Servers } from './routes/Authenticated/Servers/Servers';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
@ -23,7 +24,6 @@ function App() {
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Placeholder routes for navigation */}
|
||||
<Route
|
||||
path="/apps"
|
||||
element={
|
||||
@ -37,7 +37,7 @@ function App() {
|
||||
path="/servers"
|
||||
element={
|
||||
<Authenticated>
|
||||
<div style={{ padding: '2rem' }}>Servers page - Coming soon</div>
|
||||
<Servers />
|
||||
</Authenticated>
|
||||
}
|
||||
/>
|
||||
|
||||
@ -62,5 +62,5 @@
|
||||
font-size: $font-size-sm;
|
||||
font-weight: $font-weight-semibold;
|
||||
background-color: rgba($color, 0.1);
|
||||
color: darken($color, 20%);
|
||||
// color: darken($color, 20%);
|
||||
}
|
||||
@ -208,7 +208,7 @@
|
||||
display: inline-block;
|
||||
padding: $spacing-xs $spacing-sm;
|
||||
background-color: rgba($info, 0.1);
|
||||
color: darken($info, 20%);
|
||||
// color: darken($info, 20%);
|
||||
border-radius: $radius-sm;
|
||||
font-size: $font-size-sm;
|
||||
font-weight: $font-weight-medium;
|
||||
@ -265,7 +265,7 @@
|
||||
|
||||
&.status-deployed {
|
||||
background-color: rgba($success, 0.1);
|
||||
color: darken($success, 20%);
|
||||
// color: darken($success, 20%);
|
||||
}
|
||||
|
||||
&.status-stopped {
|
||||
@ -275,7 +275,7 @@
|
||||
|
||||
&.status-error {
|
||||
background-color: rgba($error, 0.1);
|
||||
color: darken($error, 10%);
|
||||
// color: darken($error, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
// routes/Apps/Apps.tsx
|
||||
import React, { useEffect, useState, useMemo } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Header } from '../../../components/Header/Header';
|
||||
import { apiService } from '../../../services/api';
|
||||
import type { AbraApp, AppWithServer, ServerAppsResponse } from '../../../types';
|
||||
|
||||
345
src/routes/Authenticated/Servers/Servers.scss
Normal file
345
src/routes/Authenticated/Servers/Servers.scss
Normal file
@ -0,0 +1,345 @@
|
||||
@use '../../../assets/scss/variables' as *;
|
||||
@use '../../../assets/scss/mixins' as *;
|
||||
|
||||
.servers-page {
|
||||
min-height: 100vh;
|
||||
background-color: $bg-secondary;
|
||||
}
|
||||
|
||||
.servers-content {
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
padding: $spacing-2xl $spacing-xl;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: $spacing-xl $spacing-md;
|
||||
}
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: $spacing-2xl;
|
||||
|
||||
h1 {
|
||||
font-size: $font-size-3xl;
|
||||
margin: 0 0 $spacing-sm;
|
||||
color: $text-primary;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: $font-size-lg;
|
||||
color: $text-secondary;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: $spacing-lg;
|
||||
margin-bottom: $spacing-2xl;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
@include card;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-lg;
|
||||
padding: $spacing-xl;
|
||||
transition: transform $transition-base, box-shadow $transition-base;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: $shadow-lg;
|
||||
}
|
||||
|
||||
&.upgrade {
|
||||
border-left: 4px solid $warning;
|
||||
}
|
||||
|
||||
&.chaos {
|
||||
border-left: 4px solid $info;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
.stat-number {
|
||||
font-size: $font-size-3xl;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $text-primary;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: $font-size-sm;
|
||||
color: $text-secondary;
|
||||
margin: $spacing-xs 0 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filters {
|
||||
@include card;
|
||||
display: flex;
|
||||
gap: $spacing-md;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: $spacing-xl;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
min-width: 250px;
|
||||
padding: $spacing-sm $spacing-md;
|
||||
border: 2px solid $border-color;
|
||||
border-radius: $radius-md;
|
||||
font-size: $font-size-base;
|
||||
transition: border-color $transition-base;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
padding: $spacing-sm $spacing-md;
|
||||
border: 2px solid $border-color;
|
||||
border-radius: $radius-md;
|
||||
font-size: $font-size-base;
|
||||
background-color: white;
|
||||
cursor: pointer;
|
||||
transition: border-color $transition-base;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: $primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.servers-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
gap: $spacing-xl;
|
||||
margin-bottom: $spacing-xl;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.server-card {
|
||||
@include card;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: transform $transition-base, box-shadow $transition-base;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: $shadow-xl;
|
||||
}
|
||||
|
||||
.server-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: $spacing-lg;
|
||||
padding-bottom: $spacing-md;
|
||||
border-bottom: 2px solid $bg-secondary;
|
||||
|
||||
.server-title {
|
||||
h3 {
|
||||
margin: 0 0 $spacing-xs;
|
||||
font-size: $font-size-xl;
|
||||
color: $text-primary;
|
||||
font-weight: $font-weight-bold;
|
||||
}
|
||||
|
||||
.server-host {
|
||||
font-size: $font-size-sm;
|
||||
color: $text-muted;
|
||||
font-family: monospace;
|
||||
}
|
||||
}
|
||||
|
||||
.server-status {
|
||||
.status-indicator {
|
||||
font-size: $font-size-xl;
|
||||
|
||||
&.connected {
|
||||
color: $success;
|
||||
}
|
||||
|
||||
&.disconnected {
|
||||
color: $error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.server-stats {
|
||||
flex: 1;
|
||||
margin-bottom: $spacing-lg;
|
||||
|
||||
.stat-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: $spacing-sm 0;
|
||||
border-bottom: 1px solid $bg-secondary;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.highlight {
|
||||
background-color: rgba($warning, 0.05);
|
||||
padding: $spacing-sm $spacing-md;
|
||||
margin: $spacing-sm (-$spacing-xl);
|
||||
padding-left: calc($spacing-xl + $spacing-md);
|
||||
padding-right: calc($spacing-xl + $spacing-md);
|
||||
border-bottom: none;
|
||||
|
||||
.stat-label {
|
||||
// color: darken($warning, 10%);
|
||||
font-weight: $font-weight-semibold;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
// color: darken($warning, 10%);
|
||||
font-weight: $font-weight-bold;
|
||||
}
|
||||
}
|
||||
|
||||
&.chaos-row {
|
||||
background-color: rgba($info, 0.05);
|
||||
padding: $spacing-sm $spacing-md;
|
||||
margin: $spacing-sm (-$spacing-xl);
|
||||
padding-left: calc($spacing-xl + $spacing-md);
|
||||
padding-right: calc($spacing-xl + $spacing-md);
|
||||
|
||||
.stat-label {
|
||||
// color: darken($info, 10%);
|
||||
font-weight: $font-weight-semibold;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
// color: darken($info, 10%);
|
||||
font-weight: $font-weight-bold;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: $font-size-sm;
|
||||
color: $text-secondary;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-xs;
|
||||
|
||||
.upgrade-icon,
|
||||
.chaos-icon {
|
||||
font-size: $font-size-base;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: $font-size-lg;
|
||||
color: $text-primary;
|
||||
font-weight: $font-weight-semibold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.server-actions {
|
||||
display: flex;
|
||||
gap: $spacing-sm;
|
||||
margin-bottom: $spacing-md;
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
padding: $spacing-sm $spacing-md;
|
||||
border: 2px solid $border-color;
|
||||
color: $text-primary;
|
||||
border-radius: $radius-md;
|
||||
font-size: $font-size-sm;
|
||||
font-weight: $font-weight-medium;
|
||||
cursor: pointer;
|
||||
transition: all $transition-base;
|
||||
|
||||
&:hover {
|
||||
background-color: $primary-dark;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&.primary {
|
||||
color: white;
|
||||
border-color: $primary;
|
||||
|
||||
&:hover {
|
||||
background-color: $primary-dark;
|
||||
border-color: $primary-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.server-alert {
|
||||
background-color: rgba($warning, 0.1);
|
||||
border-left: 4px solid $warning;
|
||||
padding: $spacing-sm $spacing-md;
|
||||
margin: 0 (-$spacing-xl) (-$spacing-xl);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-sm;
|
||||
|
||||
.alert-icon {
|
||||
font-size: $font-size-lg;
|
||||
}
|
||||
|
||||
.alert-text {
|
||||
font-size: $font-size-sm;
|
||||
// color: darken($warning, 20%);
|
||||
font-weight: $font-weight-medium;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-results {
|
||||
@include card;
|
||||
text-align: center;
|
||||
padding: $spacing-3xl;
|
||||
color: $text-secondary;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.results-count {
|
||||
text-align: center;
|
||||
color: $text-secondary;
|
||||
font-size: $font-size-sm;
|
||||
padding: $spacing-md;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.error {
|
||||
text-align: center;
|
||||
padding: $spacing-3xl;
|
||||
font-size: $font-size-lg;
|
||||
}
|
||||
|
||||
.loading {
|
||||
color: $text-secondary;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: $error;
|
||||
}
|
||||
269
src/routes/Authenticated/Servers/Servers.tsx
Normal file
269
src/routes/Authenticated/Servers/Servers.tsx
Normal file
@ -0,0 +1,269 @@
|
||||
// routes/Servers/Servers.tsx
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Header } from '../../../components/Header/Header';
|
||||
import { apiService } from '../../../services/api';
|
||||
import type { AbraServer, ServerAppsResponse } from '../../../types';
|
||||
import './Servers.scss';
|
||||
|
||||
interface ServerWithStats extends AbraServer {
|
||||
appCount: number;
|
||||
versionCount: number;
|
||||
latestCount: number;
|
||||
upgradeCount: number;
|
||||
chaosCount: number;
|
||||
}
|
||||
|
||||
export const Servers: React.FC = () => {
|
||||
const [servers, setServers] = useState<ServerWithStats[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [sortBy, setSortBy] = useState<'name' | 'apps' | 'upgrades'>('name');
|
||||
|
||||
const isMockMode = import.meta.env.VITE_MOCK_AUTH === 'true';
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
if (isMockMode) {
|
||||
const { mockApiService } = await import('../../../services/mockApi');
|
||||
const [serversData, appsData] = await Promise.all([
|
||||
mockApiService.getServers(),
|
||||
mockApiService.getAppsGrouped(),
|
||||
]);
|
||||
|
||||
// Enrich servers with stats from apps data
|
||||
const enrichedServers = serversData.map(server => {
|
||||
const serverStats = appsData[server.name];
|
||||
const chaosCount = serverStats?.apps.filter(app => app.chaos === 'true').length || 0;
|
||||
|
||||
return {
|
||||
...server,
|
||||
appCount: serverStats?.appCount || 0,
|
||||
versionCount: serverStats?.versionCount || 0,
|
||||
latestCount: serverStats?.latestCount || 0,
|
||||
upgradeCount: serverStats?.upgradeCount || 0,
|
||||
chaosCount,
|
||||
};
|
||||
});
|
||||
|
||||
setServers(enrichedServers);
|
||||
} else {
|
||||
const [serversData, appsData] = await Promise.all([
|
||||
apiService.getServers(),
|
||||
apiService.getAppsGrouped(),
|
||||
]);
|
||||
|
||||
// Enrich servers with stats from apps data
|
||||
const enrichedServers = serversData.map(server => {
|
||||
const serverStats = appsData[server.name];
|
||||
const chaosCount = serverStats?.apps.filter(app => app.chaos === 'true').length || 0;
|
||||
|
||||
return {
|
||||
...server,
|
||||
appCount: serverStats?.appCount || 0,
|
||||
versionCount: serverStats?.versionCount || 0,
|
||||
latestCount: serverStats?.latestCount || 0,
|
||||
upgradeCount: serverStats?.upgradeCount || 0,
|
||||
chaosCount,
|
||||
};
|
||||
});
|
||||
|
||||
setServers(enrichedServers);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load servers');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [isMockMode]);
|
||||
|
||||
// Calculate overall stats
|
||||
const stats = useMemo(() => {
|
||||
const totalServers = servers.length;
|
||||
const totalApps = servers.reduce((sum, s) => sum + s.appCount, 0);
|
||||
const totalUpgrades = servers.reduce((sum, s) => sum + s.upgradeCount, 0);
|
||||
const totalChaos = servers.reduce((sum, s) => sum + s.chaosCount, 0);
|
||||
|
||||
return { totalServers, totalApps, totalUpgrades, totalChaos };
|
||||
}, [servers]);
|
||||
|
||||
// Filter and sort servers
|
||||
const filteredServers = useMemo(() => {
|
||||
const filtered = servers.filter(server =>
|
||||
server.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
server.host.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
// Sort
|
||||
filtered.sort((a, b) => {
|
||||
switch (sortBy) {
|
||||
case 'apps':
|
||||
return b.appCount - a.appCount;
|
||||
case 'upgrades':
|
||||
return b.upgradeCount - a.upgradeCount;
|
||||
case 'name':
|
||||
default:
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
});
|
||||
|
||||
return filtered;
|
||||
}, [servers, searchTerm, sortBy]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="servers-page">
|
||||
<Header />
|
||||
<main className="servers-content">
|
||||
<div className="loading">Loading servers...</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="servers-page">
|
||||
<Header />
|
||||
<main className="servers-content">
|
||||
<div className="error">Error: {error}</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="servers-page">
|
||||
<Header />
|
||||
<main className="servers-content">
|
||||
<div className="page-header">
|
||||
<h1>Servers</h1>
|
||||
<p className="subtitle">Managing {stats.totalServers} servers with {stats.totalApps} applications</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Overview */}
|
||||
<div className="stats-grid">
|
||||
<div className="stat-card">
|
||||
<div className="stat-icon"></div>
|
||||
<div className="stat-info">
|
||||
<p className="stat-number">{stats.totalServers}</p>
|
||||
<p className="stat-label">Total Servers</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-icon"></div>
|
||||
<div className="stat-info">
|
||||
<p className="stat-number">{stats.totalApps}</p>
|
||||
<p className="stat-label">Total Apps</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-card upgrade">
|
||||
<div className="stat-icon"></div>
|
||||
<div className="stat-info">
|
||||
<p className="stat-number">{stats.totalUpgrades}</p>
|
||||
<p className="stat-label">Apps Need Upgrade</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-card chaos">
|
||||
<div className="stat-icon"></div>
|
||||
<div className="stat-info">
|
||||
<p className="stat-number">{stats.totalChaos}</p>
|
||||
<p className="stat-label">Chaos Apps</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="filters">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search servers by name or host..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="search-input"
|
||||
/>
|
||||
|
||||
<select value={sortBy} onChange={(e) => setSortBy(e.target.value as any)}>
|
||||
<option value="name">Sort by Name</option>
|
||||
<option value="apps">Sort by App Count</option>
|
||||
<option value="upgrades">Sort by Upgrades</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Server Cards */}
|
||||
<div className="servers-grid">
|
||||
{filteredServers.length === 0 ? (
|
||||
<div className="no-results">No servers found matching your search</div>
|
||||
) : (
|
||||
filteredServers.map((server) => (
|
||||
<div key={server.name} className="server-card">
|
||||
<div className="server-header">
|
||||
<div className="server-title">
|
||||
<h3>{server.name}</h3>
|
||||
<span className="server-host">{server.host}</span>
|
||||
</div>
|
||||
<div className="server-status">
|
||||
<span className="status-indicator connected" title="Connected">●</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="server-stats">
|
||||
<div className="stat-row">
|
||||
<span className="stat-label">Applications</span>
|
||||
<span className="stat-value">{server.appCount}</span>
|
||||
</div>
|
||||
<div className="stat-row">
|
||||
<span className="stat-label">Versioned</span>
|
||||
<span className="stat-value">{server.versionCount}</span>
|
||||
</div>
|
||||
<div className="stat-row">
|
||||
<span className="stat-label">Latest</span>
|
||||
<span className="stat-value">{server.latestCount}</span>
|
||||
</div>
|
||||
{server.upgradeCount > 0 && (
|
||||
<div className="stat-row highlight">
|
||||
<span className="stat-label">
|
||||
<span className="upgrade-icon"></span> Need Upgrade
|
||||
</span>
|
||||
<span className="stat-value">{server.upgradeCount}</span>
|
||||
</div>
|
||||
)}
|
||||
{server.chaosCount > 0 && (
|
||||
<div className="stat-row chaos-row">
|
||||
<span className="stat-label">
|
||||
<span className="chaos-icon">☠️</span> Chaos Mode
|
||||
</span>
|
||||
<span className="stat-value">{server.chaosCount}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="server-actions">
|
||||
<button className="action-btn primary">View Apps</button>
|
||||
<button className="action-btn">Manage</button>
|
||||
</div>
|
||||
|
||||
{server.upgradeCount > 0 && (
|
||||
<div className="server-alert">
|
||||
<span className="alert-icon"></span>
|
||||
<span className="alert-text">
|
||||
{server.upgradeCount} app{server.upgradeCount > 1 ? 's' : ''} can be upgraded
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="results-count">
|
||||
Showing {filteredServers.length} of {servers.length} servers
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user