dev #5
@ -4,5 +4,4 @@ This is the frontend of a web wrapper for Coop Clouds abra CLI, letting users se
|
||||
|
||||
## Still a work in progess!
|
||||
|
||||
## This is built with react, typescript, scss, and vite
|
||||
|
||||
## This is built with react, typescript, scss, and vite
|
||||
@ -1,3 +1,6 @@
|
||||
@use './variables' as *;
|
||||
@use './mixins' as *;
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@ -9,3 +12,262 @@ body {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
// global page layout styles
|
||||
.page-wrapper {
|
||||
min-height: 100vh;
|
||||
background-color: $bg-secondary;
|
||||
}
|
||||
|
||||
.page-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;
|
||||
}
|
||||
}
|
||||
|
||||
// global state styles
|
||||
.loading,
|
||||
.error {
|
||||
text-align: center;
|
||||
padding: $spacing-3xl;
|
||||
font-size: $font-size-lg;
|
||||
}
|
||||
|
||||
.loading {
|
||||
color: $text-secondary;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: $error;
|
||||
}
|
||||
|
||||
// Stats grid and cards
|
||||
.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;
|
||||
}
|
||||
|
||||
// Modifier classes for colored borders
|
||||
&.upgrade {
|
||||
border-left: 4px solid $primary-light;
|
||||
}
|
||||
|
||||
&.chaos {
|
||||
border-left: 4px solid $primary-dark;
|
||||
}
|
||||
|
||||
&.primary {
|
||||
border-left: 4px solid $primary;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
flex: 1;
|
||||
|
||||
.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 component
|
||||
.filters {
|
||||
@include card;
|
||||
display: flex;
|
||||
gap: $spacing-md;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: $spacing-xl;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
background-color: $bg-primary;
|
||||
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;
|
||||
color: $text-primary;
|
||||
|
||||
&::placeholder {
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
&: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: $bg-primary;
|
||||
color: $text-primary;
|
||||
cursor: pointer;
|
||||
transition: border-color $transition-base;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-sm;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: $font-size-sm;
|
||||
color: $text-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Status badges
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: $spacing-xs $spacing-sm;
|
||||
border-radius: $radius-full;
|
||||
font-size: $font-size-xs;
|
||||
font-weight: $font-weight-semibold;
|
||||
text-transform: capitalize;
|
||||
|
||||
&.status-deployed {
|
||||
background-color: rgba($success, 0.1);
|
||||
color: $success;
|
||||
}
|
||||
|
||||
&.status-stopped {
|
||||
background-color: rgba($text-muted, 0.1);
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
&.status-error {
|
||||
background-color: rgba($error, 0.1);
|
||||
color: $error;
|
||||
}
|
||||
}
|
||||
|
||||
// global badge styles
|
||||
.recipe-badge,
|
||||
.server-badge {
|
||||
display: inline-block;
|
||||
padding: $spacing-xs $spacing-sm;
|
||||
border-radius: $radius-sm;
|
||||
font-size: $font-size-sm;
|
||||
font-weight: $font-weight-medium;
|
||||
}
|
||||
|
||||
.recipe-badge {
|
||||
background-color: rgba($info, 0.1);
|
||||
color: $info;
|
||||
}
|
||||
|
||||
.server-badge {
|
||||
background-color: rgba($text-secondary, 0.1);
|
||||
color: $text-secondary;
|
||||
}
|
||||
|
||||
// Results count
|
||||
.results-count {
|
||||
text-align: center;
|
||||
color: $text-secondary;
|
||||
font-size: $font-size-sm;
|
||||
padding: $spacing-md;
|
||||
}
|
||||
|
||||
// No results message
|
||||
.no-results {
|
||||
@include card;
|
||||
text-align: center;
|
||||
padding: $spacing-3xl;
|
||||
color: $text-secondary;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
// Navigation link button (for clickable cards)
|
||||
.nav-link {
|
||||
all: unset;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
display: block;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $primary;
|
||||
outline-offset: 2px;
|
||||
border-radius: $radius-md;
|
||||
}
|
||||
}
|
||||
|
||||
.bland-button {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@ -45,7 +45,7 @@
|
||||
|
||||
// Gradient background
|
||||
@mixin gradient-primary {
|
||||
background: linear-gradient(135deg, $primary 0%, $primary-dark 100%);
|
||||
background: linear-gradient(135deg, $primary 0%, $primary-light 100%);
|
||||
}
|
||||
|
||||
// Truncate text
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
// Colors
|
||||
$primary: #EFEFEF;
|
||||
$primary-dark: #ff4f88;
|
||||
$primary-dark: #6A9CFF;
|
||||
$primary-light: #ff4f88;
|
||||
$secondary: #363636;
|
||||
|
||||
$success: #10b981;
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
@use '../../assets/scss/mixins' as *;
|
||||
|
||||
.layout-header {
|
||||
background-color: $primary-dark;
|
||||
background-color: $primary-light;
|
||||
color: $text-primary;
|
||||
padding: $spacing-lg 0;
|
||||
box-shadow: $shadow-lg;
|
||||
|
||||
103
src/index.scss
103
src/index.scss
@ -1,15 +1,18 @@
|
||||
@use './assets/scss/variables' as *;
|
||||
@use './assets/scss/mixins' as *;
|
||||
@use './assets/scss/global' as *;
|
||||
|
||||
// Global root styles
|
||||
:root {
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
font-family: 'Manrope', -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
||||
'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
|
||||
'Helvetica Neue', sans-serif;
|
||||
'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
|
||||
'Helvetica Neue', sans-serif;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: $text-primary;
|
||||
background-color: $primary;
|
||||
background-color: $bg-primary;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
@ -17,13 +20,9 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
// Global element resets
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
@ -32,51 +31,63 @@ body {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
font-family: 'Lora', serif;
|
||||
// Global link styles
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: $primary;
|
||||
text-decoration: none;
|
||||
transition: color $transition-base;
|
||||
|
||||
&:hover {
|
||||
color: $primary-light;
|
||||
}
|
||||
}
|
||||
|
||||
// Global heading styles
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: $text-primary;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: $font-size-3xl;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: $font-size-2xl;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: $font-size-xl;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
// Global button styles
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border-radius: $radius-md;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
padding: $spacing-sm $spacing-lg;
|
||||
font-size: $font-size-base;
|
||||
font-weight: $font-weight-medium;
|
||||
font-family: inherit;
|
||||
background-color: $primary-dark;
|
||||
background-color: $primary;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
.bland-button{
|
||||
border-radius: 8px;
|
||||
border: 0px;
|
||||
padding: 0em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
transition: all $transition-base;
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: $primary;
|
||||
&:hover {
|
||||
background-color: $primary-light;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $primary;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
button {
|
||||
background-color: $primary;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
@ -253,7 +253,7 @@
|
||||
}
|
||||
|
||||
.domain-link {
|
||||
color: #0066cc;
|
||||
color: $primary-dark;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
|
||||
|
||||
@ -1,149 +1,17 @@
|
||||
@use '../../../assets/scss/variables' as *;
|
||||
@use '../../../assets/scss/mixins' as *;
|
||||
@use '../../../assets/scss/global' as *;
|
||||
|
||||
// Extend global page wrapper
|
||||
.apps-page {
|
||||
min-height: 100vh;
|
||||
background-color: $bg-secondary;
|
||||
@extend .page-wrapper;
|
||||
}
|
||||
|
||||
.apps-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;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-sm;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: $font-size-sm;
|
||||
color: $text-primary;
|
||||
}
|
||||
}
|
||||
@extend .page-content;
|
||||
}
|
||||
|
||||
// Apps table specific styles
|
||||
.apps-table-container {
|
||||
@include card;
|
||||
overflow-x: auto;
|
||||
@ -197,6 +65,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// App table cell styles
|
||||
.app-name-cell {
|
||||
.app-name {
|
||||
font-weight: $font-weight-medium;
|
||||
@ -204,39 +73,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
.recipe-badge {
|
||||
display: inline-block;
|
||||
padding: $spacing-xs $spacing-sm;
|
||||
background-color: rgba($info, 0.1);
|
||||
// color: darken($info, 20%);
|
||||
border-radius: $radius-sm;
|
||||
font-size: $font-size-sm;
|
||||
font-weight: $font-weight-medium;
|
||||
}
|
||||
|
||||
.domain-link {
|
||||
color: $primary;
|
||||
text-decoration: none;
|
||||
transition: color $transition-base;
|
||||
|
||||
&:hover {
|
||||
color: $primary-dark;
|
||||
color: $primary-light;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.no-domain {
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
.server-badge {
|
||||
display: inline-block;
|
||||
padding: $spacing-xs $spacing-sm;
|
||||
background-color: rgba($text-secondary, 0.1);
|
||||
color: $text-secondary;
|
||||
border-radius: $radius-sm;
|
||||
font-size: $font-size-sm;
|
||||
font-weight: $font-weight-medium;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.version-cell {
|
||||
@ -247,6 +97,7 @@
|
||||
.version {
|
||||
font-family: monospace;
|
||||
font-size: $font-size-sm;
|
||||
color: $text-primary;
|
||||
}
|
||||
|
||||
.chaos-badge,
|
||||
@ -255,40 +106,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: $spacing-xs $spacing-sm;
|
||||
border-radius: $radius-full;
|
||||
font-size: $font-size-xs;
|
||||
font-weight: $font-weight-semibold;
|
||||
text-transform: capitalize;
|
||||
|
||||
&.status-deployed {
|
||||
background-color: rgba($success, 0.1);
|
||||
// color: darken($success, 20%);
|
||||
}
|
||||
|
||||
&.status-stopped {
|
||||
background-color: rgba($text-muted, 0.1);
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
&.status-error {
|
||||
background-color: rgba($error, 0.1);
|
||||
// color: darken($error, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
// Action buttons
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: $spacing-sm;
|
||||
|
||||
.action-btn {
|
||||
background: none;
|
||||
border: 1px solid $border-color;
|
||||
padding: $spacing-xs $spacing-sm;
|
||||
border-radius: $radius-sm;
|
||||
cursor: pointer;
|
||||
font-size: $font-size-base;
|
||||
color: $text-primary;
|
||||
transition: all $transition-base;
|
||||
|
||||
&:hover {
|
||||
@ -298,28 +128,7 @@
|
||||
|
||||
&.upgrade {
|
||||
border-color: $warning;
|
||||
color: $warning;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
@ -51,6 +51,10 @@ export const Dashboard: React.FC = () => {
|
||||
fetchData();
|
||||
}, [isMockMode]);
|
||||
|
||||
// Calculate stats
|
||||
const deployedAppsCount = apps.filter(a => a.status === 'deployed').length;
|
||||
const serversWithAppsCount = new Set(apps.map(a => a.server)).size;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="dashboard-page">
|
||||
@ -77,7 +81,9 @@ export const Dashboard: React.FC = () => {
|
||||
<div className="dashboard-page">
|
||||
<Header />
|
||||
<main className="dashboard-content">
|
||||
<h2>Dashboard</h2>
|
||||
<div className="page-header">
|
||||
<h1>Dashboard</h1>
|
||||
</div>
|
||||
|
||||
<div className="stats-grid">
|
||||
<button onClick={() => navigate('/apps')} className="nav-link bland-button">
|
||||
@ -85,7 +91,7 @@ export const Dashboard: React.FC = () => {
|
||||
<h3>Apps</h3>
|
||||
<p className="stat-number">{apps.length}</p>
|
||||
<p className="stat-label">
|
||||
{apps.filter(a => a.status === 'deployed').length} deployed
|
||||
{deployedAppsCount} deployed
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
@ -95,7 +101,7 @@ export const Dashboard: React.FC = () => {
|
||||
<h3>Servers</h3>
|
||||
<p className="stat-number">{servers.length}</p>
|
||||
<p className="stat-label">
|
||||
{servers.length} connected
|
||||
{serversWithAppsCount} connected
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
@ -105,7 +111,11 @@ export const Dashboard: React.FC = () => {
|
||||
<h3>Recent Applications</h3>
|
||||
<div className="apps-list">
|
||||
{apps.slice(0, 5).map((app, index) => (
|
||||
<div key={`${app.server}-${app.appName}-${index}`} className="app-item" onClick={() => navigate(`/apps/${app.server}/${app.appName}`)}>
|
||||
<div
|
||||
key={`${app.server}-${app.appName}-${index}`}
|
||||
className="app-item"
|
||||
onClick={() => navigate(`/apps/${app.server}/${app.appName}`)}
|
||||
>
|
||||
<div className="app-info">
|
||||
<h4>{app.appName}</h4>
|
||||
<p className="app-domain">{app.domain || 'No domain'}</p>
|
||||
@ -121,4 +131,4 @@ export const Dashboard: React.FC = () => {
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,83 +1,23 @@
|
||||
@use '../../../assets/scss/variables' as *;
|
||||
@use '../../../assets/scss/mixins' as *;
|
||||
@use '../../../assets/scss/global' as *;
|
||||
|
||||
// Extend global page wrapper
|
||||
.dashboard-page {
|
||||
min-height: 100vh;
|
||||
background-color: $bg-secondary;
|
||||
@extend .page-wrapper;
|
||||
}
|
||||
|
||||
.dashboard-content {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: $spacing-2xl $spacing-xl;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: $spacing-xl $spacing-md;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 $spacing-2xl;
|
||||
font-size: $font-size-3xl;
|
||||
color: $text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.loading, .error {
|
||||
text-align: center;
|
||||
padding: $spacing-3xl;
|
||||
font-size: $font-size-lg;
|
||||
color: $text-secondary;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: $error;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: $spacing-lg;
|
||||
margin-bottom: $spacing-2xl;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
@include card;
|
||||
border-left: 4px solid $primary;
|
||||
transition: transform $transition-base, box-shadow $transition-base;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: $shadow-lg;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 0 $spacing-md;
|
||||
color: $text-secondary;
|
||||
font-size: $font-size-sm;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
font-weight: $font-weight-semibold;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
margin: 0 0 $spacing-sm;
|
||||
font-size: $font-size-3xl;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $text-primary;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
margin: 0;
|
||||
color: $text-muted;
|
||||
font-size: $font-size-sm;
|
||||
}
|
||||
@extend .page-content;
|
||||
}
|
||||
|
||||
// Dashboard-specific styles
|
||||
.recent-apps {
|
||||
margin-bottom: $spacing-2xl;
|
||||
|
||||
h3 {
|
||||
margin: 0 0 $spacing-lg;
|
||||
font-size: $font-size-2xl;
|
||||
margin: 0 0 $spacing-lg;
|
||||
color: $text-primary;
|
||||
}
|
||||
}
|
||||
@ -90,19 +30,20 @@
|
||||
|
||||
.app-item {
|
||||
@include card;
|
||||
padding: $spacing-lg;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
transition: transform $transition-base, box-shadow $transition-base;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: $shadow-lg;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.app-info {
|
||||
flex: 1;
|
||||
|
||||
h4 {
|
||||
margin: 0 0 $spacing-xs;
|
||||
font-size: $font-size-lg;
|
||||
@ -111,26 +52,16 @@
|
||||
}
|
||||
|
||||
.app-domain {
|
||||
margin: 0;
|
||||
margin: 0 0 $spacing-xs;
|
||||
color: $text-muted;
|
||||
font-size: $font-size-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
@include status-badge($success);
|
||||
text-transform: capitalize;
|
||||
|
||||
&.status-running {
|
||||
@include status-badge($success);
|
||||
}
|
||||
|
||||
&.status-stopped {
|
||||
@include status-badge($text-muted);
|
||||
}
|
||||
|
||||
&.status-error {
|
||||
@include status-badge($error);
|
||||
.app-server {
|
||||
margin: 0;
|
||||
font-size: $font-size-sm;
|
||||
color: $text-secondary;
|
||||
font-family: monospace;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,7 +50,7 @@
|
||||
transition: color $transition-base;
|
||||
|
||||
&:hover {
|
||||
color: $primary-dark;
|
||||
color: $primary-light;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
@ -128,8 +128,8 @@
|
||||
border-color: $primary;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: $primary-dark;
|
||||
border-color: $primary-dark;
|
||||
background: $primary-light;
|
||||
border-color: $primary-light;
|
||||
}
|
||||
}
|
||||
|
||||
@ -189,7 +189,7 @@
|
||||
display: inline-block;
|
||||
padding: $spacing-xs $spacing-sm;
|
||||
background: rgba($primary, 0.1);
|
||||
color: $primary-dark;
|
||||
color: $primary-light;
|
||||
border-radius: $radius-sm;
|
||||
font-size: $font-size-xs;
|
||||
font-weight: $font-weight-medium;
|
||||
|
||||
@ -1,130 +1,17 @@
|
||||
@use '../../../assets/scss/variables' as *;
|
||||
@use '../../../assets/scss/mixins' as *;
|
||||
@use '../../../assets/scss/global' as *;
|
||||
|
||||
// Extend global page wrapper
|
||||
.servers-page {
|
||||
min-height: 100vh;
|
||||
background-color: $bg-secondary;
|
||||
@extend .page-wrapper;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@extend .page-content;
|
||||
}
|
||||
|
||||
// Servers grid
|
||||
.servers-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
@ -136,6 +23,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Server card
|
||||
.server-card {
|
||||
@include card;
|
||||
display: flex;
|
||||
@ -202,6 +90,7 @@
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
// Highlighted rows
|
||||
&.highlight {
|
||||
background-color: rgba($warning, 0.05);
|
||||
padding: $spacing-sm $spacing-md;
|
||||
@ -211,12 +100,10 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -229,12 +116,10 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -269,6 +154,7 @@
|
||||
flex: 1;
|
||||
padding: $spacing-sm $spacing-md;
|
||||
border: 2px solid $border-color;
|
||||
background: none;
|
||||
color: $text-primary;
|
||||
border-radius: $radius-md;
|
||||
font-size: $font-size-sm;
|
||||
@ -277,17 +163,19 @@
|
||||
transition: all $transition-base;
|
||||
|
||||
&:hover {
|
||||
background-color: $primary-dark;
|
||||
background-color: rgba($primary, 0.1);
|
||||
border-color: $primary;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&.primary {
|
||||
background-color: $primary;
|
||||
color: white;
|
||||
border-color: $primary;
|
||||
|
||||
&:hover {
|
||||
background-color: $primary-dark;
|
||||
border-color: $primary-dark;
|
||||
background-color: $primary-light;
|
||||
border-color: $primary-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -304,42 +192,13 @@
|
||||
|
||||
.alert-icon {
|
||||
font-size: $font-size-lg;
|
||||
color: $warning;
|
||||
}
|
||||
|
||||
.alert-text {
|
||||
font-size: $font-size-sm;
|
||||
// color: darken($warning, 20%);
|
||||
color: $text-primary;
|
||||
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;
|
||||
}
|
||||
@ -6,7 +6,7 @@
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
// @include gradient-primary;
|
||||
background-color: $primary-dark;
|
||||
background-color: $primary-light;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
@ -1,150 +1,6 @@
|
||||
|
||||
import type { AbraServer, ServerAppsResponse } from '../types';
|
||||
|
||||
// Mock data matching real API structure
|
||||
export const mockAppsData: ServerAppsResponse = {
|
||||
"mydomain.com": {
|
||||
apps: [
|
||||
{
|
||||
server: "mydomain.com",
|
||||
recipe: "nextcloud",
|
||||
appName: "nc.mydomain.com",
|
||||
domain: "nc.mydomain.com",
|
||||
status: "deployed",
|
||||
chaos: "false",
|
||||
chaosVersion: "unknown",
|
||||
version: "12.0.1+31.0.6-fpm",
|
||||
upgrade: "latest"
|
||||
},
|
||||
{
|
||||
server: "mydomain.com",
|
||||
recipe: "traefik",
|
||||
appName: "traefik.mydomain.com",
|
||||
domain: "traefik.mydomain.com",
|
||||
status: "deployed",
|
||||
chaos: "false",
|
||||
chaosVersion: "unknown",
|
||||
version: "3.4.2+v3.4.5",
|
||||
upgrade: "3.6.2+v3.4.5\n3.6.1+v3.4.5"
|
||||
},
|
||||
{
|
||||
server: "mydomain.com",
|
||||
recipe: "authentik",
|
||||
appName: "accounts.mydomain.com",
|
||||
domain: "accounts.mydomain.com",
|
||||
status: "deployed",
|
||||
chaos: "false",
|
||||
chaosVersion: "unknown",
|
||||
version: "7.4.0+2025.6.3",
|
||||
upgrade: "9.0.1+2025.8.1\n9.0.0+2025.8.1\n8.0.0+2025.8.1\n7.4.1+2025.6.4"
|
||||
},
|
||||
{
|
||||
server: "mydomain.com",
|
||||
recipe: "backup-bot-two",
|
||||
appName: "backup.mydomain.com",
|
||||
domain: "",
|
||||
status: "deployed",
|
||||
chaos: "false",
|
||||
chaosVersion: "2.3.0+2.3.0-beta",
|
||||
version: "2.3.0+2.3.0-beta",
|
||||
upgrade: "latest"
|
||||
},
|
||||
{
|
||||
server: "mydomain.com",
|
||||
recipe: "collabora",
|
||||
appName: "docs.mydomain.com",
|
||||
domain: "docs.mydomain.com",
|
||||
status: "deployed",
|
||||
chaos: "false",
|
||||
chaosVersion: "3.2.0+24.04.12.3.1",
|
||||
version: "3.2.0+24.04.12.3.1",
|
||||
upgrade: "4.0.0+25.04.4.1.1\n3.3.0+24.04.13.3.1"
|
||||
},
|
||||
{
|
||||
server: "myotherdomain.com",
|
||||
recipe: "traefik",
|
||||
appName: "traefik.myotherdomain.com",
|
||||
domain: "traefik.myotherdomain.com",
|
||||
status: "deployed",
|
||||
chaos: "false",
|
||||
chaosVersion: "unknown",
|
||||
version: "3.4.2+v3.4.5",
|
||||
upgrade: "3.6.2+v3.4.5\n3.6.1+v3.4.5\n3.6.0+v3.4.5\n3.5.0+v3.4.5"
|
||||
}
|
||||
],
|
||||
appCount: 3,
|
||||
versionCount: 3,
|
||||
unversionedCount: 0,
|
||||
latestCount: 1,
|
||||
upgradeCount: 2
|
||||
},
|
||||
"test.coop": {
|
||||
apps: [
|
||||
{
|
||||
server: "test.coop",
|
||||
recipe: "cryptpad",
|
||||
appName: "cryptpad.test.coop",
|
||||
domain: "cryptpad.test.coop",
|
||||
status: "deployed",
|
||||
chaos: "true",
|
||||
chaosVersion: "cb2a47fb",
|
||||
version: "0.4.0+version-2024.3.0",
|
||||
upgrade: "latest"
|
||||
},
|
||||
{
|
||||
server: "test.coop",
|
||||
recipe: "mobilizon",
|
||||
appName: "events.test.coop",
|
||||
domain: "events.test.coop",
|
||||
status: "deployed",
|
||||
chaos: "true",
|
||||
chaosVersion: "f8f874a5",
|
||||
version: "0.2.1+5.1.2",
|
||||
upgrade: "latest"
|
||||
}
|
||||
],
|
||||
appCount: 2,
|
||||
versionCount: 2,
|
||||
unversionedCount: 0,
|
||||
latestCount: 2,
|
||||
upgradeCount: 0
|
||||
}
|
||||
};
|
||||
|
||||
export const mockServers: AbraServer[] = [
|
||||
{
|
||||
name: 'test.coop',
|
||||
host: 'test.coop',
|
||||
},
|
||||
{
|
||||
"host": "mydomain.com",
|
||||
"name": "mydomain.com"
|
||||
},
|
||||
{
|
||||
"host": "coop.test.org",
|
||||
"name": "coop.test.org"
|
||||
},
|
||||
{
|
||||
"host": "internal.website.com",
|
||||
"name": "internal.website.com"
|
||||
},
|
||||
{
|
||||
"host": "internal.server.net",
|
||||
"name": "internal.server.net"
|
||||
},
|
||||
{
|
||||
"host": "internal.intranet.site.com",
|
||||
"name": "internal.intranet.site.com"
|
||||
},
|
||||
{
|
||||
"host": "internal.test.org",
|
||||
"name": "internal.test.org"
|
||||
},
|
||||
{
|
||||
"host": "orgsite.org",
|
||||
"name": "orgsite.org"
|
||||
}
|
||||
];
|
||||
import appsData from './mock-apps.json';
|
||||
import serversData from './mock-servers.json';
|
||||
|
||||
// Simulate API delay
|
||||
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
@ -152,12 +8,12 @@ const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
export const mockApiService = {
|
||||
async getAppsGrouped(): Promise<ServerAppsResponse> {
|
||||
await delay(500);
|
||||
return mockAppsData;
|
||||
return appsData as ServerAppsResponse;
|
||||
},
|
||||
|
||||
async getServers(): Promise<AbraServer[]> {
|
||||
await delay(300);
|
||||
return mockServers;
|
||||
return serversData as AbraServer[];
|
||||
},
|
||||
|
||||
async deployApp(appName: string): Promise<void> {
|
||||
|
||||
@ -24,7 +24,7 @@ export interface AbraApp {
|
||||
recipe: string;
|
||||
appName: string;
|
||||
domain: string;
|
||||
status: string;
|
||||
status: 'deployed' | 'stopped' | 'unknown';
|
||||
chaos: string;
|
||||
chaosVersion: string;
|
||||
version: string;
|
||||
|
||||
Reference in New Issue
Block a user