IPAM: duplikaatti-IP-tarkistus, vapaat lohkot, asiakas-kentän poisto + varattu oletus
- Duplikaatti-IP/verkko -tarkistus: estää saman verkko-osoitteen lisäämisen kahdesti - Vapaan tilan näyttö: kun subnet avataan, näytetään vapaat osoitelohkot lasten välissä (vihreä "Vapaa"-rivi) - Asiakas-kenttä poistettu IPAM-näkymästä (taulukot, lomake, haku) - Varattu oletustilaksi verkkoa/VLANia lisättäessä Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
11
api.php
11
api.php
@@ -1836,6 +1836,17 @@ switch ($action) {
|
|||||||
'muokattu' => date('Y-m-d H:i:s'),
|
'muokattu' => date('Y-m-d H:i:s'),
|
||||||
'muokkaaja' => currentUser(),
|
'muokkaaja' => currentUser(),
|
||||||
];
|
];
|
||||||
|
// Duplikaatti-tarkistus: sama verkko/IP ei saa olla jo olemassa
|
||||||
|
if ($entry['verkko'] !== '' && $entry['tyyppi'] !== 'vlan') {
|
||||||
|
$existingAll = dbLoadIpam($companyId);
|
||||||
|
foreach ($existingAll as $ex) {
|
||||||
|
if ($ex['verkko'] === $entry['verkko'] && $ex['id'] !== $entry['id'] && $ex['tyyppi'] !== 'vlan') {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'IP-osoite tai verkko "' . $entry['verkko'] . '" on jo olemassa (' . ($ex['nimi'] ?: 'nimetön') . ')']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
dbSaveIpam($companyId, $entry);
|
dbSaveIpam($companyId, $entry);
|
||||||
// Auto-VLAN: jos subnet/ip:llä on vlan_id, luo VLAN automaattisesti jos ei vielä ole
|
// Auto-VLAN: jos subnet/ip:llä on vlan_id, luo VLAN automaattisesti jos ei vielä ole
|
||||||
if ($entry['tyyppi'] !== 'vlan' && !empty($entry['vlan_id'])) {
|
if ($entry['tyyppi'] !== 'vlan' && !empty($entry['vlan_id'])) {
|
||||||
|
|||||||
@@ -285,7 +285,6 @@
|
|||||||
<th>VLAN</th>
|
<th>VLAN</th>
|
||||||
<th>Sijainti</th>
|
<th>Sijainti</th>
|
||||||
<th>Tila</th>
|
<th>Tila</th>
|
||||||
<th>Asiakas</th>
|
|
||||||
<th>Toiminnot</th>
|
<th>Toiminnot</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -314,7 +313,6 @@
|
|||||||
<th>Nimi / Kuvaus</th>
|
<th>Nimi / Kuvaus</th>
|
||||||
<th>Sijainti</th>
|
<th>Sijainti</th>
|
||||||
<th>Tila</th>
|
<th>Tila</th>
|
||||||
<th>Asiakas</th>
|
|
||||||
<th>Toiminnot</th>
|
<th>Toiminnot</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -1036,12 +1034,6 @@
|
|||||||
<option value="varattu">Varattu</option>
|
<option value="varattu">Varattu</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
|
||||||
<label for="ipam-form-asiakas">Asiakas</label>
|
|
||||||
<select id="ipam-form-asiakas">
|
|
||||||
<option value="">— Ei asiakasta —</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group full-width">
|
<div class="form-group full-width">
|
||||||
<label for="ipam-form-lisatiedot">Lisätiedot</label>
|
<label for="ipam-form-lisatiedot">Lisätiedot</label>
|
||||||
<textarea id="ipam-form-lisatiedot" rows="2"></textarea>
|
<textarea id="ipam-form-lisatiedot" rows="2"></textarea>
|
||||||
|
|||||||
136
script.js
136
script.js
@@ -3022,6 +3022,80 @@ function isSubnetOf(childNet, childPrefix, childBits, parentNet, parentPrefix, p
|
|||||||
const shift = BigInt(parentBits - parentPrefix);
|
const shift = BigInt(parentBits - parentPrefix);
|
||||||
return (childNet >> shift) === (parentNet >> shift);
|
return (childNet >> shift) === (parentNet >> shift);
|
||||||
}
|
}
|
||||||
|
// BigInt -> IP-osoite merkkijono
|
||||||
|
function biToIpv4(bi) {
|
||||||
|
return [Number((bi >> 24n) & 0xFFn), Number((bi >> 16n) & 0xFFn), Number((bi >> 8n) & 0xFFn), Number(bi & 0xFFn)].join('.');
|
||||||
|
}
|
||||||
|
function biToIpv6(bi) {
|
||||||
|
const parts = [];
|
||||||
|
for (let i = 7; i >= 0; i--) parts.push(Number((bi >> BigInt(i * 16)) & 0xFFFFn).toString(16));
|
||||||
|
// Yksinkertaistettu: ei :: kompressointia
|
||||||
|
return parts.join(':');
|
||||||
|
}
|
||||||
|
function biToIp(bi, v6) { return v6 ? biToIpv6(bi) : biToIpv4(bi); }
|
||||||
|
|
||||||
|
// Laske vapaat lohkot parent-subnetin sisällä (aukot lasten välissä)
|
||||||
|
function findFreeSpaces(parentNode, maxEntries = 30) {
|
||||||
|
if (!parentNode || parentNode.bits === 0 || parentNode.entry.tyyppi !== 'subnet') return [];
|
||||||
|
const pNet = parentNode.net;
|
||||||
|
const pPrefix = parentNode.prefix;
|
||||||
|
const pBits = parentNode.bits;
|
||||||
|
const hostBits = BigInt(pBits - pPrefix);
|
||||||
|
const parentStart = (pNet >> hostBits) << hostBits;
|
||||||
|
const parentSize = 1n << hostBits;
|
||||||
|
const parentEnd = parentStart + parentSize;
|
||||||
|
|
||||||
|
// Kerää lapset samasta osoiteperheestä, järjestä osoitteen mukaan
|
||||||
|
const children = parentNode.children
|
||||||
|
.filter(c => c.bits === pBits)
|
||||||
|
.sort((a, b) => a.net < b.net ? -1 : a.net > b.net ? 1 : 0);
|
||||||
|
|
||||||
|
const result = [];
|
||||||
|
let pos = parentStart;
|
||||||
|
|
||||||
|
for (const child of children) {
|
||||||
|
const cHostBits = BigInt(pBits - child.prefix);
|
||||||
|
const childStart = (child.net >> cHostBits) << cHostBits;
|
||||||
|
const childEnd = childStart + (1n << cHostBits);
|
||||||
|
if (pos < childStart) {
|
||||||
|
addAlignedBlocks(result, pos, childStart, pBits, pBits === 128, maxEntries - result.length);
|
||||||
|
}
|
||||||
|
if (childEnd > pos) pos = childEnd;
|
||||||
|
}
|
||||||
|
if (pos < parentEnd) {
|
||||||
|
addAlignedBlocks(result, pos, parentEnd, pBits, pBits === 128, maxEntries - result.length);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addAlignedBlocks(result, start, end, totalBits, v6, maxAdd) {
|
||||||
|
let pos = start;
|
||||||
|
const tb = BigInt(totalBits);
|
||||||
|
while (pos < end && maxAdd > 0) {
|
||||||
|
const space = end - pos;
|
||||||
|
// Alignment: kuinka monta trailing nollaa pos:ssa
|
||||||
|
let alignBits = 0n;
|
||||||
|
if (pos === 0n) {
|
||||||
|
alignBits = tb;
|
||||||
|
} else {
|
||||||
|
let tmp = pos;
|
||||||
|
while ((tmp & 1n) === 0n && alignBits < tb) { alignBits++; tmp >>= 1n; }
|
||||||
|
}
|
||||||
|
// Suurin 2^n joka mahtuu tilaan
|
||||||
|
let spaceBits = 0n;
|
||||||
|
let tmp = space >> 1n;
|
||||||
|
while (tmp > 0n) { spaceBits++; tmp >>= 1n; }
|
||||||
|
// Tarkista ettei ylitä
|
||||||
|
if ((1n << spaceBits) > space) spaceBits--;
|
||||||
|
const blockBits = alignBits < spaceBits ? alignBits : spaceBits;
|
||||||
|
if (blockBits < 0n) break;
|
||||||
|
const prefix = totalBits - Number(blockBits);
|
||||||
|
result.push({ net: pos, prefix, bits: totalBits, v6, verkko: biToIp(pos, v6) + '/' + prefix });
|
||||||
|
pos += (1n << blockBits);
|
||||||
|
maxAdd--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Laske subnetin käyttöaste: kuinka monta lasta (direct children) vs kapasiteetti
|
// Laske subnetin käyttöaste: kuinka monta lasta (direct children) vs kapasiteetti
|
||||||
function subnetUsageHtml(node) {
|
function subnetUsageHtml(node) {
|
||||||
if (node.entry.tyyppi !== 'subnet' || node.children.length === 0) return '';
|
if (node.entry.tyyppi !== 'subnet' || node.children.length === 0) return '';
|
||||||
@@ -3107,9 +3181,30 @@ function flattenTree(nodes, depth, drillId) {
|
|||||||
for (const node of list) {
|
for (const node of list) {
|
||||||
const hasChildren = node.children.length > 0;
|
const hasChildren = node.children.length > 0;
|
||||||
const expanded = ipamExpandedIds.has(node.entry.id);
|
const expanded = ipamExpandedIds.has(node.entry.id);
|
||||||
rows.push({ entry: node.entry, depth: d, hasChildren, expanded, node });
|
rows.push({ entry: node.entry, depth: d, hasChildren, expanded, node, isFree: false });
|
||||||
if (hasChildren && expanded) {
|
if (hasChildren && expanded) {
|
||||||
render(node.children, d + 1);
|
// Laske vapaat lohkot ja sekoita lasten sekaan osoitejärjestyksessä
|
||||||
|
const freeSpaces = findFreeSpaces(node);
|
||||||
|
if (freeSpaces.length > 0) {
|
||||||
|
// Yhdistä lapset + vapaat, järjestä osoitteen mukaan
|
||||||
|
const allItems = [
|
||||||
|
...node.children.map(c => ({ type: 'node', item: c, sortKey: c.net })),
|
||||||
|
...freeSpaces.map(f => ({ type: 'free', item: f, sortKey: f.net }))
|
||||||
|
];
|
||||||
|
allItems.sort((a, b) => a.sortKey < b.sortKey ? -1 : a.sortKey > b.sortKey ? 1 : 0);
|
||||||
|
for (const ai of allItems) {
|
||||||
|
if (ai.type === 'node') {
|
||||||
|
render([ai.item], d + 1);
|
||||||
|
} else {
|
||||||
|
rows.push({
|
||||||
|
entry: { verkko: ai.item.verkko, tyyppi: 'free', nimi: '', tila: 'vapaa' },
|
||||||
|
depth: d + 1, hasChildren: false, expanded: false, node: ai.item, isFree: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
render(node.children, d + 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -3135,7 +3230,6 @@ function renderIpam() {
|
|||||||
(e.verkko || '').toLowerCase().includes(query) ||
|
(e.verkko || '').toLowerCase().includes(query) ||
|
||||||
(e.nimi || '').toLowerCase().includes(query) ||
|
(e.nimi || '').toLowerCase().includes(query) ||
|
||||||
(e.site_name || '').toLowerCase().includes(query) ||
|
(e.site_name || '').toLowerCase().includes(query) ||
|
||||||
(e.asiakas || '').toLowerCase().includes(query) ||
|
|
||||||
(e.lisatiedot || '').toLowerCase().includes(query) ||
|
(e.lisatiedot || '').toLowerCase().includes(query) ||
|
||||||
String(e.vlan_id || '').includes(query)
|
String(e.vlan_id || '').includes(query)
|
||||||
);
|
);
|
||||||
@@ -3177,6 +3271,18 @@ function renderIpam() {
|
|||||||
tbody.innerHTML = rows.map(r => {
|
tbody.innerHTML = rows.map(r => {
|
||||||
const e = r.entry;
|
const e = r.entry;
|
||||||
const indent = r.depth * 1.5;
|
const indent = r.depth * 1.5;
|
||||||
|
|
||||||
|
// Vapaa-lohko (ei oikea entry, vaan laskettu vapaa tila)
|
||||||
|
if (r.isFree) {
|
||||||
|
return `<tr class="ipam-tree-row ipam-free-row">
|
||||||
|
<td style="padding-left:${indent}rem;white-space:nowrap;">
|
||||||
|
<span class="ipam-toggle-placeholder"></span> <span class="ipam-type-free">Vapaa</span>
|
||||||
|
</td>
|
||||||
|
<td><code class="ipam-network ipam-network-free">${esc(e.verkko)}</code></td>
|
||||||
|
<td colspan="4" style="color:#999;font-style:italic;">Käytettävissä</td>
|
||||||
|
</tr>`;
|
||||||
|
}
|
||||||
|
|
||||||
const toggleIcon = r.hasChildren
|
const toggleIcon = r.hasChildren
|
||||||
? `<span class="ipam-toggle" onclick="event.stopPropagation();ipamToggle('${e.id}')">${r.expanded ? '▼' : '▶'}</span> `
|
? `<span class="ipam-toggle" onclick="event.stopPropagation();ipamToggle('${e.id}')">${r.expanded ? '▼' : '▶'}</span> `
|
||||||
: '<span class="ipam-toggle-placeholder"></span> ';
|
: '<span class="ipam-toggle-placeholder"></span> ';
|
||||||
@@ -3195,7 +3301,6 @@ function renderIpam() {
|
|||||||
<td>${vlanRefHtml(e.vlan_id)}</td>
|
<td>${vlanRefHtml(e.vlan_id)}</td>
|
||||||
<td>${e.site_name ? esc(e.site_name) : '<span style="color:#ccc;">—</span>'}</td>
|
<td>${e.site_name ? esc(e.site_name) : '<span style="color:#ccc;">—</span>'}</td>
|
||||||
<td><span class="ipam-tila ${tilaClass[e.tila] || ''}">${tilaLabel[e.tila] || e.tila}</span></td>
|
<td><span class="ipam-tila ${tilaClass[e.tila] || ''}">${tilaLabel[e.tila] || e.tila}</span></td>
|
||||||
<td>${esc(e.asiakas || '-')}</td>
|
|
||||||
<td class="actions-cell" onclick="event.stopPropagation()">
|
<td class="actions-cell" onclick="event.stopPropagation()">
|
||||||
<button class="btn-link" onclick="editIpam('${e.id}')">✎</button>
|
<button class="btn-link" onclick="editIpam('${e.id}')">✎</button>
|
||||||
<button class="btn-link" style="color:#dc2626;" onclick="deleteIpam('${e.id}','${esc(e.nimi || e.verkko)}')">🗑</button>
|
<button class="btn-link" style="color:#dc2626;" onclick="deleteIpam('${e.id}','${esc(e.nimi || e.verkko)}')">🗑</button>
|
||||||
@@ -3218,8 +3323,7 @@ function renderIpamVlans(query) {
|
|||||||
String(e.vlan_id || '').includes(query) ||
|
String(e.vlan_id || '').includes(query) ||
|
||||||
(e.nimi || '').toLowerCase().includes(query) ||
|
(e.nimi || '').toLowerCase().includes(query) ||
|
||||||
(e.verkko || '').toLowerCase().includes(query) ||
|
(e.verkko || '').toLowerCase().includes(query) ||
|
||||||
(e.site_name || '').toLowerCase().includes(query) ||
|
(e.site_name || '').toLowerCase().includes(query)
|
||||||
(e.asiakas || '').toLowerCase().includes(query)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
vlans.sort((a, b) => (a.vlan_id || 0) - (b.vlan_id || 0));
|
vlans.sort((a, b) => (a.vlan_id || 0) - (b.vlan_id || 0));
|
||||||
@@ -3233,7 +3337,7 @@ function renderIpamVlans(query) {
|
|||||||
|
|
||||||
if (section) section.style.display = '';
|
if (section) section.style.display = '';
|
||||||
if (vlans.length === 0) {
|
if (vlans.length === 0) {
|
||||||
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;color:#aaa;padding:1rem;">Ei VLANeja vielä.</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;color:#aaa;padding:1rem;">Ei VLANeja vielä.</td></tr>';
|
||||||
} else {
|
} else {
|
||||||
tbody.innerHTML = vlans.map(e => `<tr>
|
tbody.innerHTML = vlans.map(e => `<tr>
|
||||||
<td><strong>${e.vlan_id || '-'}</strong></td>
|
<td><strong>${e.vlan_id || '-'}</strong></td>
|
||||||
@@ -3241,7 +3345,6 @@ function renderIpamVlans(query) {
|
|||||||
<td>${esc(e.nimi || '-')}</td>
|
<td>${esc(e.nimi || '-')}</td>
|
||||||
<td>${e.site_name ? esc(e.site_name) : '<span style="color:#ccc;">—</span>'}</td>
|
<td>${e.site_name ? esc(e.site_name) : '<span style="color:#ccc;">—</span>'}</td>
|
||||||
<td><span class="ipam-tila ${tilaClass[e.tila] || ''}">${tilaLabel[e.tila] || e.tila}</span></td>
|
<td><span class="ipam-tila ${tilaClass[e.tila] || ''}">${tilaLabel[e.tila] || e.tila}</span></td>
|
||||||
<td>${esc(e.asiakas || '-')}</td>
|
|
||||||
<td class="actions-cell">
|
<td class="actions-cell">
|
||||||
<button class="btn-link" onclick="editIpam('${e.id}')">✎</button>
|
<button class="btn-link" onclick="editIpam('${e.id}')">✎</button>
|
||||||
<button class="btn-link" style="color:#dc2626;" onclick="deleteIpam('${e.id}','${esc(e.nimi || e.verkko)}')">🗑</button>
|
<button class="btn-link" style="color:#dc2626;" onclick="deleteIpam('${e.id}','${esc(e.nimi || e.verkko)}')">🗑</button>
|
||||||
@@ -3259,17 +3362,6 @@ function vlanRefHtml(vlanId) {
|
|||||||
return `<strong>${vlanId}</strong>${label ? ` <small style="color:#888;">${label}</small>` : ''}`;
|
return `<strong>${vlanId}</strong>${label ? ` <small style="color:#888;">${label}</small>` : ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Asiakas-dropdown populointi ---
|
|
||||||
async function populateIpamCustomerDropdown(selectedName) {
|
|
||||||
if (!customers || customers.length === 0) {
|
|
||||||
try { customers = await apiCall('customers'); } catch(e) {}
|
|
||||||
}
|
|
||||||
const sel = document.getElementById('ipam-form-asiakas');
|
|
||||||
const sorted = [...customers].sort((a, b) => (a.nimi || '').localeCompare(b.nimi || ''));
|
|
||||||
sel.innerHTML = '<option value="">— Ei asiakasta —</option>' +
|
|
||||||
sorted.map(c => `<option value="${esc(c.nimi)}" ${c.nimi === selectedName ? 'selected' : ''}>${esc(c.nimi)}</option>`).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Toggle & Drill ---
|
// --- Toggle & Drill ---
|
||||||
function ipamToggle(id) {
|
function ipamToggle(id) {
|
||||||
if (ipamExpandedIds.has(id)) ipamExpandedIds.delete(id);
|
if (ipamExpandedIds.has(id)) ipamExpandedIds.delete(id);
|
||||||
@@ -3310,7 +3402,6 @@ async function editIpam(id) {
|
|||||||
document.getElementById('ipam-form-tila').value = e.tila || 'vapaa';
|
document.getElementById('ipam-form-tila').value = e.tila || 'vapaa';
|
||||||
document.getElementById('ipam-form-lisatiedot').value = e.lisatiedot || '';
|
document.getElementById('ipam-form-lisatiedot').value = e.lisatiedot || '';
|
||||||
document.getElementById('ipam-form-vlan').value = e.vlan_id || '';
|
document.getElementById('ipam-form-vlan').value = e.vlan_id || '';
|
||||||
await populateIpamCustomerDropdown(e.asiakas || '');
|
|
||||||
await loadIpamSitesDropdown();
|
await loadIpamSitesDropdown();
|
||||||
document.getElementById('ipam-form-site').value = e.site_id || '';
|
document.getElementById('ipam-form-site').value = e.site_id || '';
|
||||||
document.getElementById('ipam-modal-title').textContent = e.tyyppi === 'vlan' ? 'Muokkaa VLANia' : 'Muokkaa verkkoa / IP:tä';
|
document.getElementById('ipam-modal-title').textContent = e.tyyppi === 'vlan' ? 'Muokkaa VLANia' : 'Muokkaa verkkoa / IP:tä';
|
||||||
@@ -3329,7 +3420,7 @@ document.getElementById('btn-add-ipam')?.addEventListener('click', async () => {
|
|||||||
document.getElementById('ipam-form-id').value = '';
|
document.getElementById('ipam-form-id').value = '';
|
||||||
document.getElementById('ipam-form').reset();
|
document.getElementById('ipam-form').reset();
|
||||||
document.getElementById('ipam-form-tyyppi').value = 'subnet';
|
document.getElementById('ipam-form-tyyppi').value = 'subnet';
|
||||||
await populateIpamCustomerDropdown('');
|
document.getElementById('ipam-form-tila').value = 'varattu';
|
||||||
await loadIpamSitesDropdown();
|
await loadIpamSitesDropdown();
|
||||||
document.getElementById('ipam-modal-title').textContent = 'Lisää verkko / IP';
|
document.getElementById('ipam-modal-title').textContent = 'Lisää verkko / IP';
|
||||||
document.getElementById('ipam-modal').style.display = 'flex';
|
document.getElementById('ipam-modal').style.display = 'flex';
|
||||||
@@ -3339,7 +3430,7 @@ document.getElementById('btn-add-vlan')?.addEventListener('click', async () => {
|
|||||||
document.getElementById('ipam-form-id').value = '';
|
document.getElementById('ipam-form-id').value = '';
|
||||||
document.getElementById('ipam-form').reset();
|
document.getElementById('ipam-form').reset();
|
||||||
document.getElementById('ipam-form-tyyppi').value = 'vlan';
|
document.getElementById('ipam-form-tyyppi').value = 'vlan';
|
||||||
await populateIpamCustomerDropdown('');
|
document.getElementById('ipam-form-tila').value = 'varattu';
|
||||||
await loadIpamSitesDropdown();
|
await loadIpamSitesDropdown();
|
||||||
document.getElementById('ipam-modal-title').textContent = 'Lisää VLAN';
|
document.getElementById('ipam-modal-title').textContent = 'Lisää VLAN';
|
||||||
document.getElementById('ipam-modal').style.display = 'flex';
|
document.getElementById('ipam-modal').style.display = 'flex';
|
||||||
@@ -3362,7 +3453,6 @@ document.getElementById('ipam-form')?.addEventListener('submit', async (e) => {
|
|||||||
nimi: document.getElementById('ipam-form-nimi').value.trim(),
|
nimi: document.getElementById('ipam-form-nimi').value.trim(),
|
||||||
site_id: document.getElementById('ipam-form-site').value || null,
|
site_id: document.getElementById('ipam-form-site').value || null,
|
||||||
tila: document.getElementById('ipam-form-tila').value,
|
tila: document.getElementById('ipam-form-tila').value,
|
||||||
asiakas: document.getElementById('ipam-form-asiakas').value.trim(),
|
|
||||||
lisatiedot: document.getElementById('ipam-form-lisatiedot').value.trim(),
|
lisatiedot: document.getElementById('ipam-form-lisatiedot').value.trim(),
|
||||||
};
|
};
|
||||||
if (id) data.id = id;
|
if (id) data.id = id;
|
||||||
|
|||||||
@@ -1090,6 +1090,10 @@ span.empty {
|
|||||||
.ipam-bc-current { color:#333; font-weight:600; cursor:default; }
|
.ipam-bc-current { color:#333; font-weight:600; cursor:default; }
|
||||||
.ipam-bc-current:hover { text-decoration:none; }
|
.ipam-bc-current:hover { text-decoration:none; }
|
||||||
.ipam-usage { display:inline-block; margin-left:0.5rem; padding:1px 6px; border-radius:8px; font-size:0.72rem; font-weight:600; background:#f0f4ff; color:#4338ca; }
|
.ipam-usage { display:inline-block; margin-left:0.5rem; padding:1px 6px; border-radius:8px; font-size:0.72rem; font-weight:600; background:#f0f4ff; color:#4338ca; }
|
||||||
|
.ipam-free-row { background:#f0fdf4 !important; }
|
||||||
|
.ipam-free-row:hover { background:#dcfce7 !important; }
|
||||||
|
.ipam-type-free { display:inline-block; padding:1px 8px; border-radius:4px; font-size:0.75rem; font-weight:700; background:#d1fae5; color:#065f46; }
|
||||||
|
.ipam-network-free { color:#059669; background:#ecfdf5; }
|
||||||
|
|
||||||
/* Role badge */
|
/* Role badge */
|
||||||
.role-badge {
|
.role-badge {
|
||||||
|
|||||||
Reference in New Issue
Block a user