Use slider for storage limit
This commit is contained in:
@@ -225,6 +225,11 @@
|
||||
let configPolicy = $state<"manual" | "auto-evict">("manual");
|
||||
let configSaving = $state(false);
|
||||
let configApplyAll = $state(false);
|
||||
let configDiskTotalGb = $derived(
|
||||
storageConfigNode
|
||||
? Math.round((storageConfigNode.diskTotal ?? 0) / 1024 ** 3)
|
||||
: 0,
|
||||
);
|
||||
|
||||
function openStorageConfig(col: NodeColumn) {
|
||||
storageConfigNode = col;
|
||||
@@ -1074,31 +1079,52 @@
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={configNoLimit}
|
||||
onchange={() => {
|
||||
if (!configNoLimit && configMaxGb == null) {
|
||||
configMaxGb = configDiskTotalGb || 50;
|
||||
}
|
||||
}}
|
||||
class="accent-exo-yellow w-4 h-4"
|
||||
/>
|
||||
<span class="text-xs font-mono text-white/80">Unlimited storage</span>
|
||||
</label>
|
||||
|
||||
<!-- Max storage input -->
|
||||
<div class="space-y-1">
|
||||
<label
|
||||
class="text-[11px] font-mono text-white/50 uppercase tracking-wider"
|
||||
for="storage-max-gb"
|
||||
>
|
||||
Max storage (GB)
|
||||
</label>
|
||||
<!-- Max storage slider -->
|
||||
<div class="space-y-1.5">
|
||||
<div class="flex items-baseline justify-between">
|
||||
<label
|
||||
class="text-[11px] font-mono text-white/50 uppercase tracking-wider"
|
||||
for="storage-max-gb"
|
||||
>
|
||||
Max storage
|
||||
</label>
|
||||
<span
|
||||
class="text-xs font-mono tabular-nums transition-opacity {configNoLimit
|
||||
? 'opacity-30'
|
||||
: 'text-white'}"
|
||||
>
|
||||
{configMaxGb ?? 0} GB
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
id="storage-max-gb"
|
||||
type="number"
|
||||
type="range"
|
||||
min="1"
|
||||
max={Math.max(configDiskTotalGb, configMaxGb ?? 1)}
|
||||
step="1"
|
||||
min="0"
|
||||
bind:value={configMaxGb}
|
||||
disabled={configNoLimit}
|
||||
class="w-full bg-exo-black/60 border border-exo-medium-gray/30 rounded px-3 py-1.5 text-xs font-mono text-white
|
||||
focus:outline-none focus:border-exo-yellow/40
|
||||
class="slider w-full h-1.5 rounded-full appearance-none cursor-pointer
|
||||
disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
placeholder="e.g. 50"
|
||||
/>
|
||||
<div
|
||||
class="flex justify-between text-[10px] font-mono text-white/30 transition-opacity {configNoLimit
|
||||
? 'opacity-30'
|
||||
: ''}"
|
||||
>
|
||||
<span>1 GB</span>
|
||||
<span>{Math.max(configDiskTotalGb, configMaxGb ?? 1)} GB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Policy selector -->
|
||||
@@ -1167,4 +1193,30 @@
|
||||
table {
|
||||
min-width: max-content;
|
||||
}
|
||||
|
||||
.slider {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background: #f5c518;
|
||||
cursor: pointer;
|
||||
}
|
||||
.slider::-moz-range-thumb {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: #f5c518;
|
||||
cursor: pointer;
|
||||
}
|
||||
.slider:disabled::-webkit-slider-thumb {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.slider:disabled::-moz-range-thumb {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -32,10 +32,11 @@ from exo.shared.types.memory import Memory
|
||||
from exo.shared.types.profiling import MemoryUsage, NodeNetworkInfo
|
||||
from exo.shared.types.tasks import Task, TaskId, TaskStatus
|
||||
from exo.shared.types.worker.downloads import (
|
||||
ModelModelDownloadFailed,
|
||||
ModelDownloadFailed,
|
||||
ModelDownloading,
|
||||
ModelNotDownloading,
|
||||
ModelReady,
|
||||
ModelRejected,
|
||||
ModelStatus,
|
||||
)
|
||||
from exo.shared.types.worker.instances import (
|
||||
@@ -87,6 +88,8 @@ def _get_node_download_fraction(
|
||||
return progress.downloaded.in_bytes / total if total > 0 else 0.0
|
||||
case ModelDownloadFailed():
|
||||
return 0.0
|
||||
case ModelRejected():
|
||||
return 0.0
|
||||
return 0.0
|
||||
|
||||
|
||||
|
||||
@@ -26,10 +26,10 @@ from exo.shared.types.tasks import TaskId, TaskStatus, TextGeneration
|
||||
from exo.shared.types.text_generation import InputMessage, TextGenerationTaskParams
|
||||
from exo.shared.types.topology import Connection, SocketConnection
|
||||
from exo.shared.types.worker.downloads import (
|
||||
DownloadCompleted,
|
||||
DownloadFailed,
|
||||
DownloadOngoing,
|
||||
DownloadProgressData,
|
||||
ModelDownloadFailed,
|
||||
ModelDownloading,
|
||||
ModelReady,
|
||||
)
|
||||
from exo.shared.types.worker.instances import (
|
||||
Instance,
|
||||
@@ -624,7 +624,7 @@ def test_placement_prefers_cycle_with_downloaded_model(
|
||||
# node_b has the model fully downloaded, node_a does not
|
||||
download_status = {
|
||||
node_b: [
|
||||
DownloadCompleted(
|
||||
ModelReady(
|
||||
node_id=node_b,
|
||||
shard_metadata=shard_meta,
|
||||
total=model_card.storage_size,
|
||||
@@ -671,7 +671,7 @@ def test_placement_prefers_cycle_with_higher_download_progress(
|
||||
# node_a: 30% downloaded, node_b: 80% downloaded
|
||||
download_status = {
|
||||
node_a: [
|
||||
DownloadOngoing(
|
||||
ModelDownloading(
|
||||
node_id=node_a,
|
||||
shard_metadata=shard_meta,
|
||||
download_progress=DownloadProgressData(
|
||||
@@ -687,7 +687,7 @@ def test_placement_prefers_cycle_with_higher_download_progress(
|
||||
),
|
||||
],
|
||||
node_b: [
|
||||
DownloadOngoing(
|
||||
ModelDownloading(
|
||||
node_id=node_b,
|
||||
shard_metadata=shard_meta,
|
||||
download_progress=DownloadProgressData(
|
||||
@@ -744,7 +744,7 @@ def test_placement_does_not_prefer_cycle_with_failed_download(
|
||||
# node_b has a failed download — should not be preferred
|
||||
download_status = {
|
||||
node_b: [
|
||||
DownloadFailed(
|
||||
ModelDownloadFailed(
|
||||
node_id=node_b,
|
||||
shard_metadata=shard_meta,
|
||||
error_message="connection reset",
|
||||
|
||||
Reference in New Issue
Block a user