Initial
This commit is contained in:
@@ -511,24 +511,24 @@
|
|||||||
<label v-if="runPlanForm.visibility === 'group'"><span>Group</span><select v-model="runPlanForm.groupId"><option :value="null">Choose group</option><option v-for="group in groups" :key="group.id" :value="group.id">{{ group.name }}</option></select></label>
|
<label v-if="runPlanForm.visibility === 'group'"><span>Group</span><select v-model="runPlanForm.groupId"><option :value="null">Choose group</option><option v-for="group in groups" :key="group.id" :value="group.id">{{ group.name }}</option></select></label>
|
||||||
<label class="toggle modal-toggle"><input v-model="runPlanForm.parallel" type="checkbox" /><span><strong>Parallel execution</strong><small>Run against selected hosts concurrently.</small></span></label>
|
<label class="toggle modal-toggle"><input v-model="runPlanForm.parallel" type="checkbox" /><span><strong>Parallel execution</strong><small>Run against selected hosts concurrently.</small></span></label>
|
||||||
<label class="full"><span>Description</span><textarea v-model="runPlanForm.description" rows="3" placeholder="Purpose, operator notes, approval context"></textarea></label>
|
<label class="full"><span>Description</span><textarea v-model="runPlanForm.description" rows="3" placeholder="Purpose, operator notes, approval context"></textarea></label>
|
||||||
<div class="full modal-host-picker">
|
<TargetBrowser
|
||||||
<div class="list-header"><span>Target host groups</span><strong>{{ runPlanForm.hostGroupIds.length }} selected</strong></div>
|
v-model="runPlanForm.hostGroupIds"
|
||||||
<div class="host-picker">
|
:items="hostGroups"
|
||||||
<label v-for="hostGroup in hostGroups" :key="hostGroup.id">
|
kind="group"
|
||||||
<input v-model="runPlanForm.hostGroupIds" type="checkbox" :value="hostGroup.id" />
|
eyebrow="HOST GROUP TARGETS"
|
||||||
<span><strong>{{ hostGroup.name }}</strong><small>{{ hostGroup.mode }} · {{ hostGroup.memberCount || 0 }} host(s)</small></span>
|
title="Target host groups"
|
||||||
</label>
|
description="Select dynamic or manual collections. Dynamic groups use their latest synced membership when the RunPlan executes."
|
||||||
</div>
|
search-placeholder="Search groups, mode, rule text..."
|
||||||
</div>
|
/>
|
||||||
<div class="full modal-host-picker">
|
<TargetBrowser
|
||||||
<div class="list-header"><span>Target hosts</span><strong>{{ runPlanForm.hostIds.length }} selected</strong></div>
|
v-model="runPlanForm.hostIds"
|
||||||
<div class="host-picker">
|
:items="hosts"
|
||||||
<label v-for="host in hosts" :key="host.id">
|
kind="host"
|
||||||
<input v-model="runPlanForm.hostIds" type="checkbox" :value="host.id" />
|
eyebrow="DIRECT HOST TARGETS"
|
||||||
<span><strong>{{ host.name }}</strong><small>{{ host.transport }} · {{ host.address }}</small></span>
|
title="Target hosts"
|
||||||
</label>
|
description="Select individual systems when this RunPlan needs exact one-off targets in addition to host groups."
|
||||||
</div>
|
search-placeholder="Search host, FQDN, address, tags..."
|
||||||
</div>
|
/>
|
||||||
<div class="form-actions modal-actions full">
|
<div class="form-actions modal-actions full">
|
||||||
<button class="ghost-button" type="button" @click="runPlanModalOpen = false">Cancel</button>
|
<button class="ghost-button" type="button" @click="runPlanModalOpen = false">Cancel</button>
|
||||||
<button class="primary-action" type="submit"><Save :size="17" />{{ runPlanForm.id ? 'Update RunPlan' : 'Create RunPlan' }}</button>
|
<button class="primary-action" type="submit"><Save :size="17" />{{ runPlanForm.id ? 'Update RunPlan' : 'Create RunPlan' }}</button>
|
||||||
@@ -971,6 +971,7 @@ import HelpCenter from './components/help/HelpCenter.vue';
|
|||||||
import ParticleBackdrop from './components/ParticleBackdrop.vue';
|
import ParticleBackdrop from './components/ParticleBackdrop.vue';
|
||||||
import BaseModal from './components/ui/BaseModal.vue';
|
import BaseModal from './components/ui/BaseModal.vue';
|
||||||
import ResourceTable from './components/ui/ResourceTable.vue';
|
import ResourceTable from './components/ui/ResourceTable.vue';
|
||||||
|
import TargetBrowser from './components/ui/TargetBrowser.vue';
|
||||||
import ScriptExplorer from './components/scripts/ScriptExplorer.vue';
|
import ScriptExplorer from './components/scripts/ScriptExplorer.vue';
|
||||||
import ScriptTestRunModal from './components/scripts/ScriptTestRunModal.vue';
|
import ScriptTestRunModal from './components/scripts/ScriptTestRunModal.vue';
|
||||||
import VariableEditorModal from './components/scripts/VariableEditorModal.vue';
|
import VariableEditorModal from './components/scripts/VariableEditorModal.vue';
|
||||||
|
|||||||
256
client/src/components/ui/TargetBrowser.vue
Normal file
256
client/src/components/ui/TargetBrowser.vue
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
<template>
|
||||||
|
<section class="full host-member-browser target-browser">
|
||||||
|
<div class="member-browser-header">
|
||||||
|
<div>
|
||||||
|
<span class="eyebrow">{{ eyebrow }}</span>
|
||||||
|
<h4>{{ title }}</h4>
|
||||||
|
<p>{{ description }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="member-selection-meter">
|
||||||
|
<strong>{{ selectedIds.length }}</strong>
|
||||||
|
<span>selected</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div :class="['member-toolbar', { 'target-browser-toolbar-compact': kind !== 'host' }]">
|
||||||
|
<label class="member-search">
|
||||||
|
<Search :size="15" />
|
||||||
|
<input v-model="search" :placeholder="searchPlaceholder" />
|
||||||
|
<button v-if="search" type="button" :aria-label="`Clear ${title} search`" @click="search = ''">
|
||||||
|
<X :size="13" />
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
<template v-if="kind === 'host'">
|
||||||
|
<label>
|
||||||
|
<span>Source</span>
|
||||||
|
<select v-model="source">
|
||||||
|
<option value="">All sources</option>
|
||||||
|
<option v-for="option in sourceOptions" :key="option" :value="option">{{ sourceLabel(option) }}</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>Transport</span>
|
||||||
|
<select v-model="transport">
|
||||||
|
<option value="">All transports</option>
|
||||||
|
<option v-for="option in transportOptions" :key="option" :value="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
<button :class="['ghost-button compact', { active: selectedOnly }]" type="button" @click="selectedOnly = !selectedOnly">
|
||||||
|
<ListChecks :size="15" />Selected
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="member-results-bar">
|
||||||
|
<span>{{ filteredItems.length }} matching {{ itemLabel }}</span>
|
||||||
|
<div>
|
||||||
|
<button class="ghost-button compact" type="button" :disabled="!pagedItems.length || pageAllSelected" @click="selectPage">Select page</button>
|
||||||
|
<button class="ghost-button compact" type="button" :disabled="!pagedItems.some((item) => selectedIds.includes(item.id))" @click="clearPage">Clear page</button>
|
||||||
|
<button class="ghost-button compact danger-text" type="button" :disabled="!selectedIds.length" @click="updateSelection([])">Clear all</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="member-table-wrap">
|
||||||
|
<table class="member-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="select-col">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="pageAllSelected"
|
||||||
|
:disabled="!pagedItems.length"
|
||||||
|
:aria-label="`Select all ${itemLabel} on this page`"
|
||||||
|
@change="$event.target.checked ? selectPage() : clearPage()"
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
<th v-for="column in columns" :key="column.key">
|
||||||
|
<button v-if="column.sortable !== false" type="button" @click="toggleSort(column.key)">
|
||||||
|
{{ column.label }} <component :is="sortIcon(column.key)" :size="13" />
|
||||||
|
</button>
|
||||||
|
<template v-else>{{ column.label }}</template>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="item in pagedItems" :key="item.id" :class="{ selected: selectedIds.includes(item.id) }">
|
||||||
|
<td class="select-col">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="selectedIds.includes(item.id)"
|
||||||
|
:aria-label="`Select ${item.name}`"
|
||||||
|
@change="toggleItem(item.id, $event.target.checked)"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td v-for="column in columns" :key="column.key">
|
||||||
|
<template v-if="column.key === 'name'">
|
||||||
|
<strong>{{ item.name }}</strong>
|
||||||
|
<small>{{ secondaryText(item) }}</small>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'address'"><code>{{ item.address || '-' }}</code></template>
|
||||||
|
<template v-else-if="column.key === 'transport'"><span class="status-pill neutral">{{ item.transport || '-' }}</span></template>
|
||||||
|
<template v-else-if="column.key === 'sourceType'"><span :class="['status-pill', item.sourceType === 'vmware' ? 'dynamic' : 'manual']">{{ sourceLabel(item.sourceType) }}</span></template>
|
||||||
|
<template v-else-if="column.key === 'mode'"><span :class="['status-pill', item.mode]">{{ item.mode || '-' }}</span></template>
|
||||||
|
<template v-else-if="column.key === 'memberCount'">{{ item.memberCount || item.hostIds?.length || 0 }} host(s)</template>
|
||||||
|
<template v-else-if="column.key === 'tags'"><span class="member-tags">{{ item.tags?.length ? item.tags.slice(0, 3).join(', ') : '-' }}</span></template>
|
||||||
|
<template v-else-if="column.key === 'rules'"><span class="member-tags">{{ groupRuleSummary(item) }}</span></template>
|
||||||
|
<template v-else>{{ item[column.key] || '-' }}</template>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="!pagedItems.length">
|
||||||
|
<td :colspan="columns.length + 1" class="member-empty">No {{ itemLabel }} match the current filters.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="member-pagination">
|
||||||
|
<label>
|
||||||
|
<span>Rows</span>
|
||||||
|
<select v-model.number="pageSize">
|
||||||
|
<option :value="8">8</option>
|
||||||
|
<option :value="12">12</option>
|
||||||
|
<option :value="20">20</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<span>Page {{ Math.min(page, pageCount) }} of {{ pageCount }}</span>
|
||||||
|
<div>
|
||||||
|
<button class="ghost-button compact" type="button" :disabled="page <= 1" @click="page -= 1">
|
||||||
|
<ChevronLeft :size="15" />Previous
|
||||||
|
</button>
|
||||||
|
<button class="ghost-button compact" type="button" :disabled="page >= pageCount" @click="page += 1">
|
||||||
|
Next<ChevronRight :size="15" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import { ArrowDownAZ, ArrowUpAZ, ChevronsUpDown, ChevronLeft, ChevronRight, ListChecks, Search, X } from '@lucide/vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: Array, default: () => [] },
|
||||||
|
items: { type: Array, default: () => [] },
|
||||||
|
kind: { type: String, default: 'host' },
|
||||||
|
eyebrow: { type: String, default: 'TARGETS' },
|
||||||
|
title: { type: String, required: true },
|
||||||
|
description: { type: String, default: '' },
|
||||||
|
searchPlaceholder: { type: String, default: 'Search targets...' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
const search = ref('');
|
||||||
|
const source = ref('');
|
||||||
|
const transport = ref('');
|
||||||
|
const selectedOnly = ref(false);
|
||||||
|
const sort = ref('name');
|
||||||
|
const direction = ref('asc');
|
||||||
|
const page = ref(1);
|
||||||
|
const pageSize = ref(8);
|
||||||
|
|
||||||
|
const selectedIds = computed(() => props.modelValue || []);
|
||||||
|
const itemLabel = computed(() => props.kind === 'group' ? 'host group(s)' : 'host(s)');
|
||||||
|
const columns = computed(() => props.kind === 'group'
|
||||||
|
? [
|
||||||
|
{ key: 'name', label: 'Group' },
|
||||||
|
{ key: 'mode', label: 'Mode' },
|
||||||
|
{ key: 'memberCount', label: 'Members' },
|
||||||
|
{ key: 'rules', label: 'Rules', sortable: false }
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{ key: 'name', label: 'Host' },
|
||||||
|
{ key: 'address', label: 'Address' },
|
||||||
|
{ key: 'transport', label: 'Transport' },
|
||||||
|
{ key: 'sourceType', label: 'Source' },
|
||||||
|
{ key: 'tags', label: 'Tags', sortable: false }
|
||||||
|
]);
|
||||||
|
const sourceOptions = computed(() => uniqueOptions(props.items.map((item) => item.sourceType || 'manual')));
|
||||||
|
const transportOptions = computed(() => uniqueOptions(props.items.map((item) => item.transport).filter(Boolean)));
|
||||||
|
const filteredItems = computed(() => {
|
||||||
|
const term = search.value.trim().toLowerCase();
|
||||||
|
const selected = new Set(selectedIds.value);
|
||||||
|
return props.items.filter((item) => {
|
||||||
|
if (selectedOnly.value && !selected.has(item.id)) return false;
|
||||||
|
if (props.kind === 'host' && source.value && (item.sourceType || 'manual') !== source.value) return false;
|
||||||
|
if (props.kind === 'host' && transport.value && item.transport !== transport.value) return false;
|
||||||
|
if (!term) return true;
|
||||||
|
const values = props.kind === 'group'
|
||||||
|
? [item.name, item.mode, item.description, groupRuleSummary(item)]
|
||||||
|
: [item.name, item.fqdn, item.address, item.transport, item.sourceType, ...(item.tags || [])];
|
||||||
|
return values.filter(Boolean).some((value) => String(value).toLowerCase().includes(term));
|
||||||
|
}).sort((a, b) => {
|
||||||
|
const result = sortableValue(a, sort.value).localeCompare(sortableValue(b, sort.value), undefined, { numeric: true, sensitivity: 'base' });
|
||||||
|
return direction.value === 'asc' ? result : -result;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const pageCount = computed(() => Math.max(1, Math.ceil(filteredItems.value.length / pageSize.value)));
|
||||||
|
const pagedItems = computed(() => filteredItems.value.slice((page.value - 1) * pageSize.value, (page.value - 1) * pageSize.value + pageSize.value));
|
||||||
|
const pageAllSelected = computed(() => Boolean(pagedItems.value.length) && pagedItems.value.every((item) => selectedIds.value.includes(item.id)));
|
||||||
|
|
||||||
|
watch([search, source, transport, selectedOnly, pageSize], () => {
|
||||||
|
page.value = 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(pageCount, (next) => {
|
||||||
|
if (page.value > next) page.value = next;
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateSelection(ids) {
|
||||||
|
emit('update:modelValue', ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleItem(id, checked) {
|
||||||
|
const next = new Set(selectedIds.value);
|
||||||
|
if (checked) next.add(id);
|
||||||
|
else next.delete(id);
|
||||||
|
updateSelection([...next]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectPage() {
|
||||||
|
updateSelection([...new Set([...selectedIds.value, ...pagedItems.value.map((item) => item.id)])]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearPage() {
|
||||||
|
const pageIds = new Set(pagedItems.value.map((item) => item.id));
|
||||||
|
updateSelection(selectedIds.value.filter((id) => !pageIds.has(id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSort(key) {
|
||||||
|
if (sort.value === key) {
|
||||||
|
direction.value = direction.value === 'asc' ? 'desc' : 'asc';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sort.value = key;
|
||||||
|
direction.value = 'asc';
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortIcon(key) {
|
||||||
|
if (sort.value !== key) return ChevronsUpDown;
|
||||||
|
return direction.value === 'asc' ? ArrowUpAZ : ArrowDownAZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
function uniqueOptions(values) {
|
||||||
|
return [...new Set(values.filter(Boolean))].sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortableValue(item, key) {
|
||||||
|
if (key === 'sourceType') return item.sourceType || 'manual';
|
||||||
|
return String(item[key] || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function secondaryText(item) {
|
||||||
|
return props.kind === 'group'
|
||||||
|
? `${item.mode || 'manual'} / ${item.memberCount || item.hostIds?.length || 0} host(s)`
|
||||||
|
: item.fqdn || 'No FQDN recorded';
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceLabel(value = 'manual') {
|
||||||
|
return value === 'vmware' ? 'VMware' : 'Manual';
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupRuleSummary(item) {
|
||||||
|
if (item.mode === 'manual') return 'Manual membership';
|
||||||
|
return (item.rules || []).map((rule) => `${rule.field} ${rule.operator} ${rule.value}`).join(` ${item.matchMode || 'all'} `) || 'No rules';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -2818,6 +2818,7 @@ textarea:focus {
|
|||||||
|
|
||||||
.psadt-function-list button {
|
.psadt-function-list button {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border: 1px solid rgba(156, 199, 232, .1);
|
border: 1px solid rgba(156, 199, 232, .1);
|
||||||
border-radius: 11px;
|
border-radius: 11px;
|
||||||
@@ -2833,14 +2834,22 @@ textarea:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.psadt-function-list code {
|
.psadt-function-list code {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
color: var(--cyan);
|
color: var(--cyan);
|
||||||
font: 800 10px "SFMono-Regular", Consolas, monospace;
|
font: 800 10px "SFMono-Regular", Consolas, monospace;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.psadt-function-list span {
|
.psadt-function-list span {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
color: var(--subtle);
|
color: var(--subtle);
|
||||||
font: 800 8px "SFMono-Regular", Consolas, monospace;
|
font: 800 8px "SFMono-Regular", Consolas, monospace;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -4771,6 +4780,10 @@ body {
|
|||||||
align-items: end;
|
align-items: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.member-toolbar.target-browser-toolbar-compact {
|
||||||
|
grid-template-columns: minmax(280px, 1fr) auto;
|
||||||
|
}
|
||||||
|
|
||||||
.member-toolbar label,
|
.member-toolbar label,
|
||||||
.member-pagination label {
|
.member-pagination label {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -4878,6 +4891,8 @@ body {
|
|||||||
|
|
||||||
.member-table th button {
|
.member-table th button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
min-height: 30px;
|
||||||
|
padding: 6px 0;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
@@ -6896,6 +6911,94 @@ input:read-only {
|
|||||||
min-width: 118px;
|
min-width: 118px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-modal .toggle,
|
||||||
|
.modal-form .toggle,
|
||||||
|
.grid-form .toggle {
|
||||||
|
min-height: 58px;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 11px 12px !important;
|
||||||
|
border: 1px solid rgba(156, 199, 232, .14) !important;
|
||||||
|
border-radius: 15px !important;
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, rgba(255, 255, 255, .055), rgba(88, 221, 255, .025)),
|
||||||
|
rgba(5, 13, 28, .46) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-modal .toggle input[type="checkbox"],
|
||||||
|
.modal-form .toggle input[type="checkbox"],
|
||||||
|
.grid-form .toggle input[type="checkbox"] {
|
||||||
|
appearance: none;
|
||||||
|
position: relative;
|
||||||
|
flex: 0 0 42px;
|
||||||
|
width: 42px !important;
|
||||||
|
min-width: 42px !important;
|
||||||
|
max-width: 42px !important;
|
||||||
|
height: 24px !important;
|
||||||
|
min-height: 24px !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
border: 1px solid rgba(156, 199, 232, .24) !important;
|
||||||
|
border-radius: 999px !important;
|
||||||
|
background: rgba(3, 9, 20, .8) !important;
|
||||||
|
box-shadow: inset 0 1px 3px rgba(0, 0, 0, .34);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-modal .toggle input[type="checkbox"]::after,
|
||||||
|
.modal-form .toggle input[type="checkbox"]::after,
|
||||||
|
.grid-form .toggle input[type="checkbox"]::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 3px;
|
||||||
|
left: 3px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(218, 236, 255, .78);
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, .32);
|
||||||
|
transition: transform .18s ease, background .18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-modal .toggle input[type="checkbox"]:checked,
|
||||||
|
.modal-form .toggle input[type="checkbox"]:checked,
|
||||||
|
.grid-form .toggle input[type="checkbox"]:checked {
|
||||||
|
border-color: rgba(88, 221, 255, .54) !important;
|
||||||
|
background: linear-gradient(135deg, rgba(88, 221, 255, .85), rgba(166, 122, 255, .72)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-modal .toggle input[type="checkbox"]:checked::after,
|
||||||
|
.modal-form .toggle input[type="checkbox"]:checked::after,
|
||||||
|
.grid-form .toggle input[type="checkbox"]:checked::after {
|
||||||
|
transform: translateX(18px);
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-modal .host-picker input[type="checkbox"],
|
||||||
|
.modal-form .host-picker input[type="checkbox"],
|
||||||
|
.grid-form .host-picker input[type="checkbox"],
|
||||||
|
.form-modal .host-member-browser input[type="checkbox"],
|
||||||
|
.modal-form .host-member-browser input[type="checkbox"],
|
||||||
|
.grid-form .host-member-browser input[type="checkbox"] {
|
||||||
|
appearance: auto;
|
||||||
|
flex: 0 0 16px;
|
||||||
|
width: 16px !important;
|
||||||
|
min-width: 16px !important;
|
||||||
|
max-width: 16px !important;
|
||||||
|
height: 16px !important;
|
||||||
|
min-height: 16px !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
accent-color: #58ddff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-toolbar .ghost-button.compact,
|
||||||
|
.resource-toolbar .primary-action.compact {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
min-width: max-content;
|
||||||
|
}
|
||||||
|
|
||||||
.form-modal.wide-form-modal {
|
.form-modal.wide-form-modal {
|
||||||
width: min(1120px, calc(100vw - 34px));
|
width: min(1120px, calc(100vw - 34px));
|
||||||
}
|
}
|
||||||
@@ -7528,6 +7631,33 @@ input:read-only {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1180px) {
|
@media (max-width: 1180px) {
|
||||||
|
.topbar {
|
||||||
|
grid-template-columns: minmax(160px, 250px) minmax(180px, 1fr) auto;
|
||||||
|
gap: 10px;
|
||||||
|
padding-inline: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-actions .topbar-action-button {
|
||||||
|
width: 40px;
|
||||||
|
min-width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
padding: 0;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-actions .topbar-action-button svg {
|
||||||
|
width: 17px;
|
||||||
|
height: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-center {
|
||||||
|
left: 12px !important;
|
||||||
|
top: 12px !important;
|
||||||
|
width: calc(100vw - 24px);
|
||||||
|
height: calc(100vh - 24px);
|
||||||
|
}
|
||||||
|
|
||||||
.deployment-section-body.three,
|
.deployment-section-body.three,
|
||||||
.deployment-section-body.two,
|
.deployment-section-body.two,
|
||||||
.return-code-row,
|
.return-code-row,
|
||||||
@@ -7575,3 +7705,53 @@ input:read-only {
|
|||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Last-mile control guard: modal input rules must not resize checkboxes/switches. */
|
||||||
|
.dashboard-widget {
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-widget::before {
|
||||||
|
inset: -80px 0 auto auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-widget > *,
|
||||||
|
.widget-toolbar,
|
||||||
|
.dashboard-stat-widget {
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-modal .toggle input[type="checkbox"],
|
||||||
|
.modal-form .toggle input[type="checkbox"],
|
||||||
|
.grid-form .toggle input[type="checkbox"] {
|
||||||
|
appearance: none !important;
|
||||||
|
flex: 0 0 42px !important;
|
||||||
|
width: 42px !important;
|
||||||
|
min-width: 42px !important;
|
||||||
|
max-width: 42px !important;
|
||||||
|
height: 24px !important;
|
||||||
|
min-height: 24px !important;
|
||||||
|
max-height: 24px !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-modal .host-picker input[type="checkbox"],
|
||||||
|
.modal-form .host-picker input[type="checkbox"],
|
||||||
|
.grid-form .host-picker input[type="checkbox"],
|
||||||
|
.form-modal .host-member-browser input[type="checkbox"],
|
||||||
|
.modal-form .host-member-browser input[type="checkbox"],
|
||||||
|
.grid-form .host-member-browser input[type="checkbox"] {
|
||||||
|
appearance: auto !important;
|
||||||
|
flex: 0 0 16px !important;
|
||||||
|
width: 16px !important;
|
||||||
|
min-width: 16px !important;
|
||||||
|
max-width: 16px !important;
|
||||||
|
height: 16px !important;
|
||||||
|
min-height: 16px !important;
|
||||||
|
max-height: 16px !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user