ProcessCard.svelte raw

   1  <script>
   2      import { createEventDispatcher } from 'svelte';
   3  
   4      export let process;
   5      export let isLoading = false;
   6  
   7      const dispatch = createEventDispatcher();
   8  
   9      // Categories that are mutually exclusive (only one can be enabled)
  10      const exclusiveCategories = ['database', 'acl'];
  11  
  12      // Check if this process can be toggled (relay is always on)
  13      $: canToggle = process.category !== 'relay';
  14  
  15      // Check if this is in an exclusive category
  16      $: isExclusive = exclusiveCategories.includes(process.category);
  17  
  18      function getStatusColor(status) {
  19          switch (status) {
  20              case 'running': return 'var(--success)';
  21              case 'stopped': return 'var(--muted-color)';
  22              case 'disabled': return 'var(--muted-color)';
  23              case 'crashed': return 'var(--error)';
  24              default: return 'var(--muted-color)';
  25          }
  26      }
  27  
  28      function getStatusIcon(status) {
  29          switch (status) {
  30              case 'running': return '●';
  31              case 'stopped': return '○';
  32              case 'disabled': return '◌';
  33              case 'crashed': return '✗';
  34              default: return '?';
  35          }
  36      }
  37  
  38      function getCategoryLabel(category) {
  39          switch (category) {
  40              case 'database': return 'Database';
  41              case 'acl': return 'Access Control';
  42              case 'sync': return 'Sync Service';
  43              case 'certs': return 'Certificates';
  44              case 'relay': return 'Relay';
  45              default: return category;
  46          }
  47      }
  48  
  49      function handleStart() {
  50          dispatch('start', { service: process.name });
  51      }
  52  
  53      function handleStop() {
  54          dispatch('stop', { service: process.name });
  55      }
  56  
  57      function handleRestart() {
  58          dispatch('restart', { service: process.name });
  59      }
  60  
  61      function handleToggleEnabled(event) {
  62          const newEnabled = event.target.checked;
  63          dispatch('toggle-enabled', {
  64              service: process.name,
  65              enabled: newEnabled,
  66              category: process.category,
  67              isExclusive: isExclusive
  68          });
  69      }
  70  
  71      $: canStart = process.enabled && process.status !== 'running';
  72      $: canStop = process.status === 'running';
  73      $: canRestart = process.status === 'running';
  74  </script>
  75  
  76  <div class="process-card" class:disabled={!process.enabled}>
  77      <div class="process-header">
  78          <span class="status-indicator" style="color: {getStatusColor(process.status)}">
  79              {getStatusIcon(process.status)}
  80          </span>
  81          <div class="name-section">
  82              <span class="process-name">{process.name}</span>
  83              <span class="category-badge" class:exclusive={isExclusive}>{getCategoryLabel(process.category)}</span>
  84          </div>
  85          {#if canToggle}
  86              <label class="enable-toggle" title={process.enabled ? 'Disable' : 'Enable'}>
  87                  <input
  88                      type="checkbox"
  89                      checked={process.enabled}
  90                      on:change={handleToggleEnabled}
  91                      disabled={isLoading || process.status === 'running'}
  92                  />
  93                  <span class="toggle-slider"></span>
  94              </label>
  95          {:else}
  96              <span class="badge required-badge">always on</span>
  97          {/if}
  98      </div>
  99  
 100      <p class="description">{process.description}</p>
 101  
 102      <div class="process-details">
 103          <div class="detail-row">
 104              <span class="label">Status:</span>
 105              <span class="value" style="color: {getStatusColor(process.status)}">
 106                  {process.status}
 107              </span>
 108          </div>
 109  
 110          {#if process.pid > 0}
 111              <div class="detail-row">
 112                  <span class="label">PID:</span>
 113                  <span class="value">{process.pid}</span>
 114              </div>
 115          {/if}
 116  
 117          {#if process.restarts > 0}
 118              <div class="detail-row">
 119                  <span class="label">Restarts:</span>
 120                  <span class="value warning">{process.restarts}</span>
 121              </div>
 122          {/if}
 123      </div>
 124  
 125      <div class="process-actions">
 126          {#if canStart}
 127              <button class="action-btn start-btn" on:click={handleStart} disabled={isLoading} title="Start service">
 128   129              </button>
 130          {/if}
 131          {#if canStop}
 132              <button class="action-btn stop-btn" on:click={handleStop} disabled={isLoading} title="Stop service">
 133   134              </button>
 135          {/if}
 136          {#if canRestart}
 137              <button class="action-btn restart-btn" on:click={handleRestart} disabled={isLoading} title="Restart service">
 138   139              </button>
 140          {/if}
 141          {#if !process.enabled && !canStart && !canStop}
 142              <span class="hint">Enable to start</span>
 143          {/if}
 144      </div>
 145  </div>
 146  
 147  <style>
 148      .process-card {
 149          background: var(--card-bg);
 150          border: 1px solid var(--border-color);
 151          border-radius: 8px;
 152          padding: 16px;
 153          display: flex;
 154          flex-direction: column;
 155          gap: 10px;
 156      }
 157  
 158      .process-card.disabled {
 159          opacity: 0.6;
 160      }
 161  
 162      .process-header {
 163          display: flex;
 164          align-items: center;
 165          gap: 8px;
 166      }
 167  
 168      .status-indicator {
 169          font-size: 1.2rem;
 170          flex-shrink: 0;
 171      }
 172  
 173      .name-section {
 174          flex: 1;
 175          display: flex;
 176          flex-direction: column;
 177          gap: 2px;
 178      }
 179  
 180      .process-name {
 181          font-weight: 600;
 182          font-size: 0.95rem;
 183          color: var(--text-color);
 184      }
 185  
 186      .category-badge {
 187          font-size: 0.65rem;
 188          padding: 1px 4px;
 189          border-radius: 3px;
 190          text-transform: uppercase;
 191          background: var(--border-color);
 192          color: var(--muted-color);
 193          width: fit-content;
 194      }
 195  
 196      .category-badge.exclusive {
 197          background: var(--warning, #ff9800);
 198          color: white;
 199          opacity: 0.8;
 200      }
 201  
 202      .description {
 203          font-size: 0.8rem;
 204          color: var(--muted-color);
 205          margin: 0;
 206          line-height: 1.3;
 207      }
 208  
 209      .badge {
 210          font-size: 0.65rem;
 211          padding: 2px 6px;
 212          border-radius: 4px;
 213          text-transform: uppercase;
 214          flex-shrink: 0;
 215      }
 216  
 217      .required-badge {
 218          background: var(--text-color);
 219          color: var(--card-bg);
 220          opacity: 0.4;
 221      }
 222  
 223      .enable-toggle {
 224          position: relative;
 225          display: inline-block;
 226          width: 36px;
 227          height: 20px;
 228          cursor: pointer;
 229          flex-shrink: 0;
 230      }
 231  
 232      .enable-toggle input {
 233          opacity: 0;
 234          width: 0;
 235          height: 0;
 236      }
 237  
 238      .toggle-slider {
 239          position: absolute;
 240          top: 0;
 241          left: 0;
 242          right: 0;
 243          bottom: 0;
 244          background-color: var(--muted-color);
 245          border-radius: 20px;
 246          transition: 0.2s;
 247      }
 248  
 249      .toggle-slider:before {
 250          position: absolute;
 251          content: "";
 252          height: 14px;
 253          width: 14px;
 254          left: 3px;
 255          bottom: 3px;
 256          background-color: white;
 257          border-radius: 50%;
 258          transition: 0.2s;
 259      }
 260  
 261      .enable-toggle input:checked + .toggle-slider {
 262          background-color: var(--success, #4caf50);
 263      }
 264  
 265      .enable-toggle input:checked + .toggle-slider:before {
 266          transform: translateX(16px);
 267      }
 268  
 269      .enable-toggle input:disabled + .toggle-slider {
 270          opacity: 0.5;
 271          cursor: not-allowed;
 272      }
 273  
 274      .process-details {
 275          display: flex;
 276          flex-direction: column;
 277          gap: 4px;
 278      }
 279  
 280      .detail-row {
 281          display: flex;
 282          justify-content: space-between;
 283          font-size: 0.8rem;
 284      }
 285  
 286      .label {
 287          color: var(--muted-color);
 288      }
 289  
 290      .value {
 291          color: var(--text-color);
 292          font-family: monospace;
 293      }
 294  
 295      .value.warning {
 296          color: var(--warning);
 297      }
 298  
 299      .process-actions {
 300          display: flex;
 301          gap: 8px;
 302          align-items: center;
 303          margin-top: 4px;
 304          padding-top: 10px;
 305          border-top: 1px solid var(--border-color);
 306      }
 307  
 308      .action-btn {
 309          width: 32px;
 310          height: 32px;
 311          border-radius: 4px;
 312          border: none;
 313          cursor: pointer;
 314          font-size: 1rem;
 315          display: flex;
 316          align-items: center;
 317          justify-content: center;
 318          transition: opacity 0.2s, transform 0.1s;
 319      }
 320  
 321      .action-btn:hover:not(:disabled) {
 322          transform: scale(1.05);
 323      }
 324  
 325      .action-btn:disabled {
 326          opacity: 0.5;
 327          cursor: not-allowed;
 328      }
 329  
 330      .start-btn {
 331          background: var(--success, #4caf50);
 332          color: white;
 333      }
 334  
 335      .stop-btn {
 336          background: var(--error, #f44336);
 337          color: white;
 338      }
 339  
 340      .restart-btn {
 341          background: var(--warning, #ff9800);
 342          color: white;
 343      }
 344  
 345      .hint {
 346          font-size: 0.75rem;
 347          color: var(--muted-color);
 348          font-style: italic;
 349      }
 350  </style>
 351