добавлен редактор локальных репозиториев и удалено лишнее событие
This commit is contained in:
parent
aa06678220
commit
1bcf317c8f
|
|
@ -68,54 +68,8 @@ public class NetworkManager implements RPC {
|
||||||
return JSONUtility.serializeStructure(context.getServiceManager().getService(NetworkingService.class).getOutputResources());
|
return JSONUtility.serializeStructure(context.getServiceManager().getService(NetworkingService.class).getOutputResources());
|
||||||
}
|
}
|
||||||
|
|
||||||
@JRPCMethod
|
|
||||||
@ProtectedMethod
|
|
||||||
public void createLocalResourceFile(
|
|
||||||
@JRPCArgument(name = "name") String name,
|
|
||||||
@JRPCArgument(name = "domain") String domain,
|
|
||||||
@JRPCArgument(name = "ASN") JSONArray ASN,
|
|
||||||
@JRPCArgument(name = "subnets") JSONArray subnets,
|
|
||||||
@JRPCArgument(name = "addresses") JSONArray addresses) throws IOException {
|
|
||||||
|
|
||||||
|
|
||||||
var optional = context.getConfig().getSubscriptions().stream().filter(repositoryConfig -> repositoryConfig.getType().equals(LocalFilesystemSubscription.class)).findFirst();
|
|
||||||
if (optional.isEmpty()) {
|
|
||||||
throw new IllegalStateException("Unable to find any local repository of subscriptions");
|
|
||||||
}
|
|
||||||
|
|
||||||
var repositoryConfig = optional.get();
|
|
||||||
|
|
||||||
var directory = new File(repositoryConfig.getSource());
|
|
||||||
if (!directory.exists()) {
|
|
||||||
throw new FileNotFoundException("Unable to find directory " + directory.getAbsolutePath());
|
|
||||||
}
|
|
||||||
|
|
||||||
try (var writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(directory, name + ".json"))))) {
|
|
||||||
var merged = JSONUtility.deserializeCollection(subnets, IPv4Subnet.class, null).collect(Collectors.toList());
|
|
||||||
addresses.forEach(address -> {
|
|
||||||
merged.add(new IPv4Subnet(address.toString(), 32));
|
|
||||||
});
|
|
||||||
//noinspection rawtypes
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
var asnList = (List<Integer>) JSONUtility.deserializeCollection(ASN, Integer.class, (Class) DefaultPropertySerializer.class).toList();
|
|
||||||
writer.write(JSONUtility.serializeStructure(
|
|
||||||
NetworkResourceBundle.builder()
|
|
||||||
.ASN(asnList)
|
|
||||||
.subnets(merged)
|
|
||||||
.domains(List.of(domain))
|
|
||||||
.description("Unlocked resource: " + name + " (" + domain + ")")
|
|
||||||
.build()
|
|
||||||
).toString(2));
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
var subscribedResources = context.getConfig().getSubscribedResources();
|
|
||||||
subscribedResources.add(repositoryConfig.getName() + ":" + name);
|
|
||||||
|
|
||||||
context.getServiceManager().getService(SubscriptionService.class).triggerUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
@JRPCMethod
|
@JRPCMethod
|
||||||
@ProtectedMethod
|
@ProtectedMethod
|
||||||
public JSONObject discoverBlockedDomainResources(@JRPCArgument(name = "domain") String domain, @JRPCArgument(name = "server") String nameserver) {
|
public JSONObject discoverBlockedDomainResources(@JRPCArgument(name = "domain") String domain, @JRPCArgument(name = "server") String nameserver) {
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,21 @@ package ru.kirillius.pf.sdn.web.RPC;
|
||||||
|
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
import ru.kirillius.json.DefaultPropertySerializer;
|
||||||
import ru.kirillius.json.JSONUtility;
|
import ru.kirillius.json.JSONUtility;
|
||||||
import ru.kirillius.json.rpc.Annotations.JRPCArgument;
|
import ru.kirillius.json.rpc.Annotations.JRPCArgument;
|
||||||
import ru.kirillius.json.rpc.Annotations.JRPCMethod;
|
import ru.kirillius.json.rpc.Annotations.JRPCMethod;
|
||||||
|
import ru.kirillius.pf.sdn.External.API.LocalFilesystemSubscription;
|
||||||
import ru.kirillius.pf.sdn.core.Context;
|
import ru.kirillius.pf.sdn.core.Context;
|
||||||
|
import ru.kirillius.pf.sdn.core.Networking.IPv4Subnet;
|
||||||
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
|
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
|
||||||
|
import ru.kirillius.pf.sdn.core.Subscription.RepositoryConfig;
|
||||||
import ru.kirillius.pf.sdn.core.Subscription.SubscriptionService;
|
import ru.kirillius.pf.sdn.core.Subscription.SubscriptionService;
|
||||||
import ru.kirillius.pf.sdn.web.ProtectedMethod;
|
import ru.kirillius.pf.sdn.web.ProtectedMethod;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -70,4 +77,97 @@ public class SubscriptionManager implements RPC {
|
||||||
context.getConfig().setSubscribedResources(JSONUtility.deserializeCollection(subscribedResources, String.class, null).collect(Collectors.toList()));
|
context.getConfig().setSubscribedResources(JSONUtility.deserializeCollection(subscribedResources, String.class, null).collect(Collectors.toList()));
|
||||||
triggerUpdate();
|
triggerUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JRPCMethod
|
||||||
|
@ProtectedMethod
|
||||||
|
public void writeLocalResourceFile(
|
||||||
|
@JRPCArgument(name = "name") String name,
|
||||||
|
@JRPCArgument(name = "description") String description,
|
||||||
|
@JRPCArgument(name = "domains") JSONArray domains,
|
||||||
|
@JRPCArgument(name = "ASN") JSONArray ASN,
|
||||||
|
@JRPCArgument(name = "subnets") JSONArray subnets,
|
||||||
|
@JRPCArgument(name = "addresses") JSONArray addresses,
|
||||||
|
@JRPCArgument(name = "storage") String storage,
|
||||||
|
@JRPCArgument(name = "subscribe") boolean subscribe) throws IOException {
|
||||||
|
var repositoryConfig = findLocalRepo(storage);
|
||||||
|
var directory = new File(repositoryConfig.getSource());
|
||||||
|
if (!directory.exists()) {
|
||||||
|
throw new FileNotFoundException("Unable to find directory " + directory.getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
var domainList = new ArrayList<String>();
|
||||||
|
|
||||||
|
domains.forEach(d->domainList.add(d.toString()));
|
||||||
|
|
||||||
|
try (var writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(directory, name + ".json"))))) {
|
||||||
|
var merged = JSONUtility.deserializeCollection(subnets, IPv4Subnet.class, null).collect(Collectors.toList());
|
||||||
|
addresses.forEach(address -> {
|
||||||
|
merged.add(new IPv4Subnet(address.toString(), 32));
|
||||||
|
});
|
||||||
|
//noinspection rawtypes
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
var asnList = (List<Integer>) JSONUtility.deserializeCollection(ASN, Integer.class, (Class) DefaultPropertySerializer.class).toList();
|
||||||
|
writer.write(JSONUtility.serializeStructure(
|
||||||
|
NetworkResourceBundle.builder()
|
||||||
|
.ASN(asnList)
|
||||||
|
.subnets(merged)
|
||||||
|
.domains(domainList)
|
||||||
|
.description(description)
|
||||||
|
.build()
|
||||||
|
).toString(2));
|
||||||
|
}
|
||||||
|
if (subscribe) {
|
||||||
|
var subscribedResources = context.getConfig().getSubscribedResources();
|
||||||
|
subscribedResources.add(repositoryConfig.getName() + ":" + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.getServiceManager().getService(SubscriptionService.class).triggerUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private RepositoryConfig findLocalRepo(String storage) throws FileNotFoundException {
|
||||||
|
var optional = context.getConfig().getSubscriptions().stream().filter(repositoryConfig -> repositoryConfig.getType().equals(LocalFilesystemSubscription.class) && repositoryConfig.getName().equals(storage)).findFirst();
|
||||||
|
if (optional.isEmpty()) {
|
||||||
|
throw new IllegalStateException("Unable to find local repository with name " + storage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return optional.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@JRPCMethod
|
||||||
|
@ProtectedMethod
|
||||||
|
public void removeLocalResourceFile(
|
||||||
|
@JRPCArgument(name = "name") String name,
|
||||||
|
@JRPCArgument(name = "storage") String storage) throws IOException {
|
||||||
|
|
||||||
|
var repositoryConfig = findLocalRepo(storage);
|
||||||
|
|
||||||
|
var directory = new File(repositoryConfig.getSource());
|
||||||
|
if (!directory.exists()) {
|
||||||
|
throw new FileNotFoundException("Unable to find directory " + directory.getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
var localFile = new File(directory, name + ".json");
|
||||||
|
if (!localFile.delete()) {
|
||||||
|
throw new IOException("Unable to delete local file " + localFile.getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
var subscribedResources = context.getConfig().getSubscribedResources();
|
||||||
|
subscribedResources.remove(repositoryConfig.getName() + ":" + name);
|
||||||
|
|
||||||
|
context.getServiceManager().getService(SubscriptionService.class).triggerUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@JRPCMethod
|
||||||
|
@ProtectedMethod
|
||||||
|
public JSONArray getLocalRepositories(
|
||||||
|
) {
|
||||||
|
var names = context.getConfig().getSubscriptions().stream()
|
||||||
|
.filter(repositoryConfig ->
|
||||||
|
repositoryConfig.getType().equals(LocalFilesystemSubscription.class)
|
||||||
|
).map(RepositoryConfig::getName).toList();
|
||||||
|
|
||||||
|
return JSONUtility.serializeCollection(names, String.class, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -140,12 +140,6 @@ public class WebService extends AppService {
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("Unable to start web server", e);
|
throw new RuntimeException("Unable to start web server", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
context.getEventsHandler().getRPCInitEvent().invoke(JSONRPC);
|
|
||||||
} catch (Exception e) {
|
|
||||||
SystemLogger.error("Error on RPC init event", CTX, e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private final static String CTX = WebService.class.getSimpleName();
|
private final static String CTX = WebService.class.getSimpleName();
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import lombok.Builder;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import ru.kirillius.java.utils.events.ConcurrentEventHandler;
|
import ru.kirillius.java.utils.events.ConcurrentEventHandler;
|
||||||
import ru.kirillius.java.utils.events.EventHandler;
|
import ru.kirillius.java.utils.events.EventHandler;
|
||||||
import ru.kirillius.json.rpc.Servlet.JSONRPCServlet;
|
|
||||||
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
|
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -21,11 +20,6 @@ public final class ContextEventsHandler {
|
||||||
*/
|
*/
|
||||||
@Getter
|
@Getter
|
||||||
private final EventHandler<NetworkResourceBundle> subscriptionsUpdateEvent = new ConcurrentEventHandler<>();
|
private final EventHandler<NetworkResourceBundle> subscriptionsUpdateEvent = new ConcurrentEventHandler<>();
|
||||||
/**
|
|
||||||
* Event fired after the RPC servlet is initialised and ready.
|
|
||||||
*/
|
|
||||||
@Getter
|
|
||||||
private final EventHandler<JSONRPCServlet> RPCInitEvent = new ConcurrentEventHandler<>();
|
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
private final EventHandler<ConfigChangeContext> configChangeEvent = new ConcurrentEventHandler<>();
|
private final EventHandler<ConfigChangeContext> configChangeEvent = new ConcurrentEventHandler<>();
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,14 @@ public class SubscriptionService extends AppService {
|
||||||
private final Map<String, NetworkResourceBundle> availableResources = new ConcurrentHashMap<>();
|
private final Map<String, NetworkResourceBundle> availableResources = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private <T extends SubscriptionProvider> T getProvider(Class<T> providerType) {
|
||||||
|
if (!providerCache.containsKey(providerType)) {
|
||||||
|
providerCache.put(providerType, SubscriptionProvider.instantiate(providerType, context));
|
||||||
|
}
|
||||||
|
return (T) providerCache.get(providerType);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts a background task that refreshes subscription repositories and aggregates resources.
|
* Starts a background task that refreshes subscription repositories and aggregates resources.
|
||||||
*/
|
*/
|
||||||
|
|
@ -62,18 +70,9 @@ public class SubscriptionService extends AppService {
|
||||||
var subscribedResources = config.getSubscribedResources();
|
var subscribedResources = config.getSubscribedResources();
|
||||||
for (var repoConfig : config.getSubscriptions()) {
|
for (var repoConfig : config.getSubscriptions()) {
|
||||||
var providerType = repoConfig.getType();
|
var providerType = repoConfig.getType();
|
||||||
var provider = providerCache.get(providerType);
|
@SuppressWarnings({"unchecked", "rawtypes"}) var provider = getProvider((Class) providerType);
|
||||||
try {
|
try {
|
||||||
if (provider == null) {
|
|
||||||
//noinspection unchecked
|
|
||||||
provider = SubscriptionProvider.instantiate((Class<? extends SubscriptionProvider>) providerType, context);
|
|
||||||
//noinspection unchecked
|
|
||||||
providerCache.put((Class<? extends SubscriptionProvider>) providerType, provider);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var resources = provider.getResources(repoConfig);
|
var resources = provider.getResources(repoConfig);
|
||||||
|
|
||||||
resources.keySet().forEach(key -> {
|
resources.keySet().forEach(key -> {
|
||||||
var resourceName = repoConfig.getName() + ":" + key;
|
var resourceName = repoConfig.getName() + ":" + key;
|
||||||
//добавляем только выбранные ресурсы
|
//добавляем только выбранные ресурсы
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import { SettingsPage } from '../pages/Settings.js';
|
||||||
import { LogsPage } from '../pages/Logs.js';
|
import { LogsPage } from '../pages/Logs.js';
|
||||||
import { NetworkResourcesPage } from '../pages/NetworkResources.js';
|
import { NetworkResourcesPage } from '../pages/NetworkResources.js';
|
||||||
import { UnlockSitePage } from '../pages/UnlockSite.js';
|
import { UnlockSitePage } from '../pages/UnlockSite.js';
|
||||||
|
import { LocalStoragesPage } from '../pages/LocalStorages.js';
|
||||||
|
|
||||||
|
|
||||||
// Переменная для отслеживания текущего активного хеша (для корректного unmount)
|
// Переменная для отслеживания текущего активного хеша (для корректного unmount)
|
||||||
|
|
@ -32,6 +33,7 @@ const allMenuItems = [
|
||||||
{ label: 'Журнал', path: 'logs', component: null },
|
{ label: 'Журнал', path: 'logs', component: null },
|
||||||
{ label: 'Сетевые ресурсы', path: 'network-resources', component: null },
|
{ label: 'Сетевые ресурсы', path: 'network-resources', component: null },
|
||||||
{ label: 'Разблокировка сайта', path: 'unlock-site', component: null },
|
{ label: 'Разблокировка сайта', path: 'unlock-site', component: null },
|
||||||
|
{ label: 'Локальные хранилища', path: 'local-storages', component: null },
|
||||||
];
|
];
|
||||||
|
|
||||||
// 2. Определение страниц
|
// 2. Определение страниц
|
||||||
|
|
@ -90,6 +92,11 @@ const routes = {
|
||||||
render: UnlockSitePage.render,
|
render: UnlockSitePage.render,
|
||||||
mount: UnlockSitePage.mount,
|
mount: UnlockSitePage.mount,
|
||||||
unmount: UnlockSitePage.unmount
|
unmount: UnlockSitePage.unmount
|
||||||
|
},
|
||||||
|
'#local-storages': {
|
||||||
|
render: LocalStoragesPage.render,
|
||||||
|
mount: LocalStoragesPage.mount,
|
||||||
|
unmount: LocalStoragesPage.unmount
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,535 @@
|
||||||
|
import $ from 'jquery';
|
||||||
|
import { JSONRPC } from '@/json-rpc.js';
|
||||||
|
|
||||||
|
const FIELD_IDS = {
|
||||||
|
storageSelect: 'local-storages-storage-select',
|
||||||
|
storageStatus: 'local-storages-storage-status',
|
||||||
|
createButton: 'local-storages-create-btn',
|
||||||
|
listContainer: 'local-storages-list',
|
||||||
|
modalOverlay: 'local-storages-modal',
|
||||||
|
modalTitle: 'local-storages-modal-title',
|
||||||
|
modalForm: 'local-storages-form',
|
||||||
|
nameInput: 'local-storages-name',
|
||||||
|
descriptionInput: 'local-storages-description',
|
||||||
|
domainsInput: 'local-storages-domains',
|
||||||
|
asnInput: 'local-storages-asn',
|
||||||
|
subnetsInput: 'local-storages-subnets',
|
||||||
|
modalStatus: 'local-storages-modal-status',
|
||||||
|
modalSaveButton: 'local-storages-save-btn',
|
||||||
|
modalCancelButton: 'local-storages-cancel-btn'
|
||||||
|
};
|
||||||
|
|
||||||
|
const SELECTORS = Object.entries(FIELD_IDS).reduce((acc, [key, value]) => {
|
||||||
|
acc[key] = `#${value}`;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const REFRESH_DELAY_MS = 5000;
|
||||||
|
|
||||||
|
let repositories = [];
|
||||||
|
let availableResources = {};
|
||||||
|
let selectedRepository = '';
|
||||||
|
let currentModalMode = 'create';
|
||||||
|
let currentModalResourceName = '';
|
||||||
|
let resourcesRequestId = 0;
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitLines(text) {
|
||||||
|
if (!text) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map(line => line.trim())
|
||||||
|
.filter(line => line.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function joinLines(items) {
|
||||||
|
if (!Array.isArray(items)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return items.map(item => String(item ?? '').trim()).filter(item => item.length > 0).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAsn(list) {
|
||||||
|
const values = splitLines(list);
|
||||||
|
const parsed = [];
|
||||||
|
for (const item of values) {
|
||||||
|
if (!/^\d+$/.test(item)) {
|
||||||
|
return { success: false, error: `Некорректное значение ASN: ${item}` };
|
||||||
|
}
|
||||||
|
parsed.push(Number(item));
|
||||||
|
}
|
||||||
|
return { success: true, value: parsed };
|
||||||
|
}
|
||||||
|
|
||||||
|
function delay(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getResourceKey(storage, name) {
|
||||||
|
return `${storage}:${name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getResourcesForStorage(storage) {
|
||||||
|
if (!storage) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return Object.keys(availableResources)
|
||||||
|
.filter(key => key.startsWith(`${storage}:`))
|
||||||
|
.map(key => ({
|
||||||
|
key,
|
||||||
|
name: key.substring(storage.length + 1),
|
||||||
|
data: availableResources[key]
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name, 'ru', { sensitivity: 'base' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStorageStatus(message, type) {
|
||||||
|
const $status = $(SELECTORS.storageStatus);
|
||||||
|
if (!$status.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!message) {
|
||||||
|
$status.hide().text('').removeClass('error-message success-message');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$status
|
||||||
|
.text(message)
|
||||||
|
.removeClass('success-message error-message')
|
||||||
|
.addClass(type === 'success' ? 'success-message' : 'error-message')
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setModalStatus(message, type) {
|
||||||
|
const $status = $(SELECTORS.modalStatus);
|
||||||
|
if (!$status.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!message) {
|
||||||
|
$status.hide().text('').removeClass('error-message success-message');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$status
|
||||||
|
.text(message)
|
||||||
|
.removeClass('success-message error-message')
|
||||||
|
.addClass(type === 'success' ? 'success-message' : 'error-message')
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderStorageOptions() {
|
||||||
|
const $select = $(SELECTORS.storageSelect);
|
||||||
|
if (!$select.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!repositories.length) {
|
||||||
|
$select
|
||||||
|
.html('<option value="">Нет доступных хранилищ</option>')
|
||||||
|
.prop('disabled', true);
|
||||||
|
$(SELECTORS.createButton).prop('disabled', true);
|
||||||
|
renderResourceList();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const optionsHtml = repositories
|
||||||
|
.map(repo => `<option value="${escapeHtml(repo)}"${repo === selectedRepository ? ' selected' : ''}>${escapeHtml(repo)}</option>`)
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
$select
|
||||||
|
.html(optionsHtml)
|
||||||
|
.prop('disabled', false);
|
||||||
|
|
||||||
|
if (!selectedRepository || !repositories.includes(selectedRepository)) {
|
||||||
|
selectedRepository = repositories[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
$(SELECTORS.createButton).prop('disabled', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLoadingList() {
|
||||||
|
const $list = $(SELECTORS.listContainer);
|
||||||
|
$list.html('<p>Загрузка содержимого...</p>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderUpdatingPlaceholder() {
|
||||||
|
const $list = $(SELECTORS.listContainer);
|
||||||
|
$list.html('<p>Обновление...</p>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderResourceList() {
|
||||||
|
const $list = $(SELECTORS.listContainer);
|
||||||
|
if (!$list.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!repositories.length) {
|
||||||
|
$list.html('<p>Нет локальных хранилищ.</p>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedRepository) {
|
||||||
|
$list.html('<p>Выберите хранилище, чтобы просмотреть содержимое.</p>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resources = getResourcesForStorage(selectedRepository);
|
||||||
|
|
||||||
|
if (!resources.length) {
|
||||||
|
$list.html('<p>В хранилище нет ресурсов.</p>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowsHtml = resources.map(resource => {
|
||||||
|
const safeName = escapeHtml(resource.name);
|
||||||
|
return `
|
||||||
|
<div class="subscription-item" style="display: flex; align-items: center; justify-content: space-between; gap: 12px;">
|
||||||
|
<span style="flex-grow: 1;">${safeName}</span>
|
||||||
|
<div style="display: flex; gap: 8px;">
|
||||||
|
<button type="button" class="btn-secondary local-storage-edit" data-name="${safeName}">Редактировать</button>
|
||||||
|
<button type="button" class="btn-link local-storage-delete" data-name="${safeName}">Удалить</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
$list.html(`
|
||||||
|
<div class="subscription-list" style="padding: 0; margin: 0; border: none; background: transparent; display: flex; flex-direction: column; gap: 12px;">${rowsHtml}</div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadRepositories() {
|
||||||
|
try {
|
||||||
|
repositories = await JSONRPC.SubscriptionManager.getLocalRepositories();
|
||||||
|
if (!Array.isArray(repositories)) {
|
||||||
|
repositories = [];
|
||||||
|
}
|
||||||
|
renderStorageOptions();
|
||||||
|
setStorageStatus('', '');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка получения хранилищ:', error);
|
||||||
|
repositories = [];
|
||||||
|
selectedRepository = '';
|
||||||
|
renderStorageOptions();
|
||||||
|
setStorageStatus('Не удалось получить список хранилищ.', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAvailableResources(currentRequestId) {
|
||||||
|
try {
|
||||||
|
const data = await JSONRPC.SubscriptionManager.getAvailableResources();
|
||||||
|
if (currentRequestId !== resourcesRequestId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
availableResources = typeof data === 'object' && data !== null ? data : {};
|
||||||
|
renderResourceList();
|
||||||
|
} catch (error) {
|
||||||
|
if (currentRequestId !== resourcesRequestId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.error('Ошибка получения содержимого хранилища:', error);
|
||||||
|
availableResources = {};
|
||||||
|
const $list = $(SELECTORS.listContainer);
|
||||||
|
$list.html('<p class="error-message">Не удалось загрузить содержимое.</p>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openModal(mode, resourceName = '') {
|
||||||
|
currentModalMode = mode;
|
||||||
|
currentModalResourceName = resourceName;
|
||||||
|
|
||||||
|
const $overlay = $(SELECTORS.modalOverlay);
|
||||||
|
const $title = $(SELECTORS.modalTitle);
|
||||||
|
const $nameInput = $(SELECTORS.nameInput);
|
||||||
|
const $descriptionInput = $(SELECTORS.descriptionInput);
|
||||||
|
const $domainsInput = $(SELECTORS.domainsInput);
|
||||||
|
const $asnInput = $(SELECTORS.asnInput);
|
||||||
|
const $subnetsInput = $(SELECTORS.subnetsInput);
|
||||||
|
const $saveButton = $(SELECTORS.modalSaveButton);
|
||||||
|
|
||||||
|
setModalStatus('', '');
|
||||||
|
|
||||||
|
if (mode === 'edit') {
|
||||||
|
const key = getResourceKey(selectedRepository, resourceName);
|
||||||
|
const resource = availableResources[key] || {};
|
||||||
|
$title.text(`Редактирование ресурса ${resourceName}`);
|
||||||
|
$nameInput.val(resourceName).prop('disabled', true);
|
||||||
|
$descriptionInput.val(resource.description || '');
|
||||||
|
$domainsInput.val(joinLines(resource.domains));
|
||||||
|
$asnInput.val(joinLines(resource.ASN));
|
||||||
|
$subnetsInput.val(joinLines(resource.subnets));
|
||||||
|
} else {
|
||||||
|
$title.text('Создание ресурса');
|
||||||
|
$nameInput.val('').prop('disabled', false);
|
||||||
|
$descriptionInput.val('');
|
||||||
|
$domainsInput.val('');
|
||||||
|
$asnInput.val('');
|
||||||
|
$subnetsInput.val('');
|
||||||
|
}
|
||||||
|
|
||||||
|
$saveButton.prop('disabled', false).text('Сохранить');
|
||||||
|
$overlay.css('display', 'flex');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
const $overlay = $(SELECTORS.modalOverlay);
|
||||||
|
const $saveButton = $(SELECTORS.modalSaveButton);
|
||||||
|
const $form = $(SELECTORS.modalForm);
|
||||||
|
setModalStatus('', '');
|
||||||
|
$saveButton.prop('disabled', false).text('Сохранить');
|
||||||
|
if ($form.length) {
|
||||||
|
$form[0].reset();
|
||||||
|
}
|
||||||
|
$overlay.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateName(name) {
|
||||||
|
if (!name.trim()) {
|
||||||
|
return 'Имя обязательно для заполнения.';
|
||||||
|
}
|
||||||
|
if (!/^[A-Za-z0-9.-]+$/.test(name)) {
|
||||||
|
return 'Допускаются латинские буквы, цифры, точка и дефис.';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleModalSubmit(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!selectedRepository) {
|
||||||
|
setModalStatus('Выберите хранилище.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const $saveButton = $(SELECTORS.modalSaveButton);
|
||||||
|
const $nameInput = $(SELECTORS.nameInput);
|
||||||
|
const $descriptionInput = $(SELECTORS.descriptionInput);
|
||||||
|
const $domainsInput = $(SELECTORS.domainsInput);
|
||||||
|
const $asnInput = $(SELECTORS.asnInput);
|
||||||
|
const $subnetsInput = $(SELECTORS.subnetsInput);
|
||||||
|
|
||||||
|
const nameValue = ($nameInput.val() || '').trim();
|
||||||
|
const validationError = validateName(nameValue);
|
||||||
|
if (validationError) {
|
||||||
|
setModalStatus(validationError, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentModalMode === 'create') {
|
||||||
|
const existing = getResourcesForStorage(selectedRepository).some(resource => resource.name === nameValue);
|
||||||
|
if (existing) {
|
||||||
|
setModalStatus('Ресурс с таким именем уже существует.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const descriptionValue = ($descriptionInput.val() || '').trim();
|
||||||
|
const domainsValue = splitLines($domainsInput.val());
|
||||||
|
const subnetsValue = splitLines($subnetsInput.val());
|
||||||
|
const parsedAsn = parseAsn($asnInput.val());
|
||||||
|
if (!parsedAsn.success) {
|
||||||
|
setModalStatus(parsedAsn.error, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$saveButton.prop('disabled', true).text('Сохранение...');
|
||||||
|
setModalStatus('', '');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await JSONRPC.SubscriptionManager.writeLocalResourceFile(
|
||||||
|
nameValue,
|
||||||
|
descriptionValue,
|
||||||
|
domainsValue,
|
||||||
|
parsedAsn.value,
|
||||||
|
subnetsValue,
|
||||||
|
[],
|
||||||
|
selectedRepository,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
closeModal();
|
||||||
|
setStorageStatus('Изменения успешно сохранены.', 'success');
|
||||||
|
renderUpdatingPlaceholder();
|
||||||
|
await delay(REFRESH_DELAY_MS);
|
||||||
|
await refreshResourceList();
|
||||||
|
setTimeout(() => setStorageStatus('', ''), 3000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка сохранения ресурса:', error);
|
||||||
|
setModalStatus('Не удалось сохранить ресурс.', 'error');
|
||||||
|
$saveButton.prop('disabled', false).text('Сохранить');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeleteResource(name) {
|
||||||
|
if (!selectedRepository) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const confirmed = window.confirm(`Удалить ресурс "${name}"?`);
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStorageStatus('', '');
|
||||||
|
const nameValue = String(name);
|
||||||
|
try {
|
||||||
|
await JSONRPC.SubscriptionManager.removeLocalResourceFile(nameValue, selectedRepository);
|
||||||
|
setStorageStatus('Ресурс удалён.', 'success');
|
||||||
|
renderUpdatingPlaceholder();
|
||||||
|
await delay(REFRESH_DELAY_MS);
|
||||||
|
await refreshResourceList();
|
||||||
|
setTimeout(() => setStorageStatus('', ''), 3000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка удаления ресурса:', error);
|
||||||
|
setStorageStatus('Не удалось удалить ресурс.', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshResourceList() {
|
||||||
|
renderLoadingList();
|
||||||
|
resourcesRequestId += 1;
|
||||||
|
const requestId = resourcesRequestId;
|
||||||
|
await loadAvailableResources(requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachEventHandlers() {
|
||||||
|
$(SELECTORS.storageSelect).on('change', async function () {
|
||||||
|
selectedRepository = $(this).val() || '';
|
||||||
|
if (!selectedRepository) {
|
||||||
|
renderResourceList();
|
||||||
|
$(SELECTORS.createButton).prop('disabled', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$(SELECTORS.createButton).prop('disabled', false);
|
||||||
|
await refreshResourceList();
|
||||||
|
});
|
||||||
|
|
||||||
|
$(SELECTORS.createButton).on('click', function () {
|
||||||
|
if (!selectedRepository) {
|
||||||
|
setStorageStatus('Выберите хранилище.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
openModal('create');
|
||||||
|
});
|
||||||
|
|
||||||
|
$(SELECTORS.listContainer).on('click', '.local-storage-edit', function () {
|
||||||
|
const name = $(this).data('name');
|
||||||
|
if (!name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
openModal('edit', name);
|
||||||
|
});
|
||||||
|
|
||||||
|
$(SELECTORS.listContainer).on('click', '.local-storage-delete', function () {
|
||||||
|
const name = $(this).data('name');
|
||||||
|
if (!name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleDeleteResource(name);
|
||||||
|
});
|
||||||
|
|
||||||
|
$(SELECTORS.modalForm).on('submit', handleModalSubmit);
|
||||||
|
|
||||||
|
$(SELECTORS.modalCancelButton).on('click', function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
closeModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
$(SELECTORS.modalOverlay).on('click', function (event) {
|
||||||
|
if (event.target === this) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function detachEventHandlers() {
|
||||||
|
$(SELECTORS.storageSelect).off('change');
|
||||||
|
$(SELECTORS.createButton).off('click');
|
||||||
|
$(SELECTORS.listContainer).off('click');
|
||||||
|
$(SELECTORS.modalForm).off('submit');
|
||||||
|
$(SELECTORS.modalCancelButton).off('click');
|
||||||
|
$(SELECTORS.modalOverlay).off('click');
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetState() {
|
||||||
|
repositories = [];
|
||||||
|
availableResources = {};
|
||||||
|
selectedRepository = '';
|
||||||
|
currentModalMode = 'create';
|
||||||
|
currentModalResourceName = '';
|
||||||
|
resourcesRequestId = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LocalStoragesPage = {
|
||||||
|
render: () => `
|
||||||
|
<div>
|
||||||
|
<h1 class="page-title">Локальные хранилища</h1>
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 20px;">
|
||||||
|
<div class="card" style="padding: 20px; max-width: 480px;">
|
||||||
|
<div class="form-group" style="margin-bottom: 0;">
|
||||||
|
<label for="${FIELD_IDS.storageSelect}">Репозиторий</label>
|
||||||
|
<select id="${FIELD_IDS.storageSelect}" class="form-control" disabled>
|
||||||
|
<option>Загрузка...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div id="${FIELD_IDS.storageStatus}" class="error-message" style="display: none; margin-top: 16px;"></div>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; justify-content: flex-end;">
|
||||||
|
<button type="button" id="${FIELD_IDS.createButton}" class="btn-primary" style="width: auto; padding: 10px 18px;" disabled>Создать</button>
|
||||||
|
</div>
|
||||||
|
<div id="${FIELD_IDS.listContainer}" class="card" style="padding: 20px;">
|
||||||
|
<p>Загрузка содержимого...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="${FIELD_IDS.modalOverlay}" style="display: none; position: fixed; inset: 0; background: rgba(0, 0, 0, 0.6); z-index: 999; align-items: center; justify-content: center;">
|
||||||
|
<div class="card" style="max-width: 640px; width: 90%; padding: 24px; position: relative;">
|
||||||
|
<form id="${FIELD_IDS.modalForm}" style="display: flex; flex-direction: column; gap: 16px;">
|
||||||
|
<h2 id="${FIELD_IDS.modalTitle}" style="margin: 0 0 10px 0;">Создание ресурса</h2>
|
||||||
|
<div class="form-group" style="margin-bottom: 0;">
|
||||||
|
<label for="${FIELD_IDS.nameInput}">Имя</label>
|
||||||
|
<input type="text" id="${FIELD_IDS.nameInput}" class="form-control" placeholder="example-resource">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom: 0;">
|
||||||
|
<label for="${FIELD_IDS.descriptionInput}">Описание</label>
|
||||||
|
<textarea id="${FIELD_IDS.descriptionInput}" class="form-control" rows="3" placeholder="Описание ресурса"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom: 0;">
|
||||||
|
<label for="${FIELD_IDS.domainsInput}">Домены (каждый с новой строки)</label>
|
||||||
|
<textarea id="${FIELD_IDS.domainsInput}" class="form-control" rows="4" placeholder="example.com"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom: 0;">
|
||||||
|
<label for="${FIELD_IDS.asnInput}">ASN (каждый с новой строки)</label>
|
||||||
|
<textarea id="${FIELD_IDS.asnInput}" class="form-control" rows="3" placeholder="12345"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom: 0;">
|
||||||
|
<label for="${FIELD_IDS.subnetsInput}">Подсети (каждая с новой строки)</label>
|
||||||
|
<textarea id="${FIELD_IDS.subnetsInput}" class="form-control" rows="4" placeholder="192.0.2.0/24"></textarea>
|
||||||
|
</div>
|
||||||
|
<div id="${FIELD_IDS.modalStatus}" class="error-message" style="display: none;"></div>
|
||||||
|
<div style="display: flex; gap: 12px; justify-content: flex-end; margin-top: 8px;">
|
||||||
|
<button type="button" id="${FIELD_IDS.modalCancelButton}" class="btn-secondary" style="padding: 10px 18px;">Отмена</button>
|
||||||
|
<button type="submit" id="${FIELD_IDS.modalSaveButton}" class="btn-primary" style="width: auto; padding: 10px 18px;">Сохранить</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
mount: async () => {
|
||||||
|
attachEventHandlers();
|
||||||
|
await loadRepositories();
|
||||||
|
if (repositories.length && !selectedRepository) {
|
||||||
|
selectedRepository = repositories[0];
|
||||||
|
renderStorageOptions();
|
||||||
|
}
|
||||||
|
await refreshResourceList();
|
||||||
|
},
|
||||||
|
unmount: () => {
|
||||||
|
detachEventHandlers();
|
||||||
|
resetState();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -13,6 +13,7 @@ const FIELD_IDS = {
|
||||||
subnetsCheckbox: 'unlock-site-subnets-checkbox',
|
subnetsCheckbox: 'unlock-site-subnets-checkbox',
|
||||||
asnCheckbox: 'unlock-site-asn-checkbox',
|
asnCheckbox: 'unlock-site-asn-checkbox',
|
||||||
nameInput: 'unlock-site-name',
|
nameInput: 'unlock-site-name',
|
||||||
|
storageSelect: 'unlock-site-storage',
|
||||||
addButton: 'unlock-site-add-button',
|
addButton: 'unlock-site-add-button',
|
||||||
nameError: 'unlock-site-name-error',
|
nameError: 'unlock-site-name-error',
|
||||||
domainError: 'unlock-site-domain-error',
|
domainError: 'unlock-site-domain-error',
|
||||||
|
|
@ -28,6 +29,8 @@ const DEFAULT_DOMAIN = 'google.com';
|
||||||
const DEFAULT_SERVER = '8.8.8.8';
|
const DEFAULT_SERVER = '8.8.8.8';
|
||||||
|
|
||||||
let currentResults = { addresses: [], subnets: [], ASN: [] };
|
let currentResults = { addresses: [], subnets: [], ASN: [] };
|
||||||
|
let availableStorages = [];
|
||||||
|
let currentStorage = '';
|
||||||
|
|
||||||
function escapeHtml(value) {
|
function escapeHtml(value) {
|
||||||
return String(value)
|
return String(value)
|
||||||
|
|
@ -47,6 +50,16 @@ function normalizeListing(values) {
|
||||||
.filter(item => item.length > 0);
|
.filter(item => item.length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadLocalRepositories() {
|
||||||
|
try {
|
||||||
|
const repositories = await JSONRPC.SubscriptionManager.getLocalRepositories();
|
||||||
|
return normalizeListing(repositories);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка получения списка хранилищ:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function setStatus(message, type) {
|
function setStatus(message, type) {
|
||||||
const $status = $(SELECTORS.status);
|
const $status = $(SELECTORS.status);
|
||||||
if (!$status.length) {
|
if (!$status.length) {
|
||||||
|
|
@ -126,10 +139,21 @@ function buildAsnItems(items) {
|
||||||
return `<div style="margin-top: 12px;">${items.map(item => escapeHtml(item)).join(', ')}</div>`;
|
return `<div style="margin-top: 12px;">${items.map(item => escapeHtml(item)).join(', ')}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderResults(domainValue) {
|
function renderResults(domainValue, storages) {
|
||||||
const addresses = normalizeListing(currentResults.addresses);
|
const addresses = normalizeListing(currentResults.addresses);
|
||||||
const subnets = normalizeListing(currentResults.subnets);
|
const subnets = normalizeListing(currentResults.subnets);
|
||||||
const ASN = normalizeListing(currentResults.ASN);
|
const ASN = normalizeListing(currentResults.ASN);
|
||||||
|
const storageOptions = Array.isArray(storages) ? storages : [];
|
||||||
|
|
||||||
|
if (!storageOptions.includes(currentStorage)) {
|
||||||
|
currentStorage = storageOptions.length ? storageOptions[0] : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const storageSelectDisabled = storageOptions.length === 0;
|
||||||
|
const storageOptionsHtml = storageOptions.length
|
||||||
|
? storageOptions.map(storage => `<option value="${escapeHtml(storage)}"${storage === currentStorage ? ' selected' : ''}>${escapeHtml(storage)}</option>`).join('')
|
||||||
|
: '<option value="">Нет доступных хранилищ</option>';
|
||||||
|
const storageHint = storageOptions.length ? '' : '<div class="hint-text" style="margin-top: 8px;">Не найдено доступных хранилищ.</div>';
|
||||||
|
|
||||||
const addressesDisabled = addresses.length === 0;
|
const addressesDisabled = addresses.length === 0;
|
||||||
const subnetsDisabled = subnets.length === 0;
|
const subnetsDisabled = subnets.length === 0;
|
||||||
|
|
@ -158,6 +182,13 @@ function renderResults(domainValue) {
|
||||||
</label>
|
</label>
|
||||||
${buildAsnItems(ASN)}
|
${buildAsnItems(ASN)}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="${FIELD_IDS.storageSelect}">Хранилище</label>
|
||||||
|
<select id="${FIELD_IDS.storageSelect}" class="form-control" ${storageSelectDisabled ? 'disabled' : ''}>
|
||||||
|
${storageOptionsHtml}
|
||||||
|
</select>
|
||||||
|
${storageHint}
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="${FIELD_IDS.nameInput}">Имя списка</label>
|
<label for="${FIELD_IDS.nameInput}">Имя списка</label>
|
||||||
<input type="text" id="${FIELD_IDS.nameInput}" class="form-control" value="${escapeHtml(domainValue)}">
|
<input type="text" id="${FIELD_IDS.nameInput}" class="form-control" value="${escapeHtml(domainValue)}">
|
||||||
|
|
@ -179,13 +210,15 @@ function updateAddButtonState() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const $nameInput = $(SELECTORS.nameInput);
|
const $nameInput = $(SELECTORS.nameInput);
|
||||||
|
const $storageSelect = $(SELECTORS.storageSelect);
|
||||||
const { valid } = validateListName($nameInput.val() || '');
|
const { valid } = validateListName($nameInput.val() || '');
|
||||||
const hasSelection = [
|
const hasSelection = [
|
||||||
$(SELECTORS.addressesCheckbox),
|
$(SELECTORS.addressesCheckbox),
|
||||||
$(SELECTORS.subnetsCheckbox),
|
$(SELECTORS.subnetsCheckbox),
|
||||||
$(SELECTORS.asnCheckbox)
|
$(SELECTORS.asnCheckbox)
|
||||||
].some($checkbox => $checkbox.length && !$checkbox.prop('disabled') && $checkbox.prop('checked'));
|
].some($checkbox => $checkbox.length && !$checkbox.prop('disabled') && $checkbox.prop('checked'));
|
||||||
$addButton.prop('disabled', !(hasSelection && valid));
|
const storageValue = $storageSelect.length && !$storageSelect.prop('disabled') ? ($storageSelect.val() || '') : '';
|
||||||
|
$addButton.prop('disabled', !(hasSelection && valid && storageValue.length > 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSearch(event) {
|
async function handleSearch(event) {
|
||||||
|
|
@ -227,7 +260,8 @@ async function handleSearch(event) {
|
||||||
subnets: normalizeListing(result?.subnets),
|
subnets: normalizeListing(result?.subnets),
|
||||||
ASN: normalizeListing(result?.ASN)
|
ASN: normalizeListing(result?.ASN)
|
||||||
};
|
};
|
||||||
renderResults($domainInput.val().trim());
|
availableStorages = await loadLocalRepositories();
|
||||||
|
renderResults($domainInput.val().trim(), availableStorages);
|
||||||
updateAddButtonState();
|
updateAddButtonState();
|
||||||
clearFieldError(SELECTORS.nameError);
|
clearFieldError(SELECTORS.nameError);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -257,13 +291,24 @@ function handleAdd() {
|
||||||
const selectedAddresses = $(SELECTORS.addressesCheckbox).prop('checked') ? currentResults.addresses : [];
|
const selectedAddresses = $(SELECTORS.addressesCheckbox).prop('checked') ? currentResults.addresses : [];
|
||||||
const selectedSubnets = $(SELECTORS.subnetsCheckbox).prop('checked') ? currentResults.subnets : [];
|
const selectedSubnets = $(SELECTORS.subnetsCheckbox).prop('checked') ? currentResults.subnets : [];
|
||||||
const selectedAsn = $(SELECTORS.asnCheckbox).prop('checked') ? currentResults.ASN : [];
|
const selectedAsn = $(SELECTORS.asnCheckbox).prop('checked') ? currentResults.ASN : [];
|
||||||
|
const storage = $(SELECTORS.storageSelect).val() || '';
|
||||||
|
|
||||||
JSONRPC.NetworkManager.createLocalResourceFile(
|
if (!storage) {
|
||||||
|
setStatus('Выберите хранилище.', 'error');
|
||||||
|
$addButton.prop('disabled', false).text('Добавить');
|
||||||
|
updateAddButtonState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONRPC.SubscriptionManager.writeLocalResourceFile(
|
||||||
$nameInput.val().trim(),
|
$nameInput.val().trim(),
|
||||||
domain,
|
"Unlocked resource: " + $nameInput.val().trim() + " (" + domain + ")",
|
||||||
|
[domain],
|
||||||
selectedAsn,
|
selectedAsn,
|
||||||
selectedSubnets,
|
selectedSubnets,
|
||||||
selectedAddresses
|
selectedAddresses,
|
||||||
|
storage,
|
||||||
|
true
|
||||||
).then(() => {
|
).then(() => {
|
||||||
setStatus('Ресурсы успешно добавлены.', 'success');
|
setStatus('Ресурсы успешно добавлены.', 'success');
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
|
|
@ -290,11 +335,17 @@ function handleNameInputChange() {
|
||||||
updateAddButtonState();
|
updateAddButtonState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleStorageChange() {
|
||||||
|
currentStorage = $(SELECTORS.storageSelect).val() || '';
|
||||||
|
updateAddButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
function attachEventHandlers() {
|
function attachEventHandlers() {
|
||||||
$(SELECTORS.form).on('submit', handleSearch);
|
$(SELECTORS.form).on('submit', handleSearch);
|
||||||
$(SELECTORS.domainInput).on('input', () => clearFieldError(SELECTORS.domainError));
|
$(SELECTORS.domainInput).on('input', () => clearFieldError(SELECTORS.domainError));
|
||||||
$(SELECTORS.serverInput).on('input', () => clearFieldError(SELECTORS.serverError));
|
$(SELECTORS.serverInput).on('input', () => clearFieldError(SELECTORS.serverError));
|
||||||
$(SELECTORS.results).on('change', `input[type="checkbox"]`, handleCheckboxChange);
|
$(SELECTORS.results).on('change', `input[type="checkbox"]`, handleCheckboxChange);
|
||||||
|
$(SELECTORS.results).on('change', SELECTORS.storageSelect, handleStorageChange);
|
||||||
$(SELECTORS.results).on('input', SELECTORS.nameInput, handleNameInputChange);
|
$(SELECTORS.results).on('input', SELECTORS.nameInput, handleNameInputChange);
|
||||||
$(SELECTORS.results).on('click', SELECTORS.addButton, handleAdd);
|
$(SELECTORS.results).on('click', SELECTORS.addButton, handleAdd);
|
||||||
}
|
}
|
||||||
|
|
@ -310,6 +361,8 @@ function detachEventHandlers() {
|
||||||
|
|
||||||
function resetState() {
|
function resetState() {
|
||||||
currentResults = { addresses: [], subnets: [], ASN: [] };
|
currentResults = { addresses: [], subnets: [], ASN: [] };
|
||||||
|
currentStorage = '';
|
||||||
|
availableStorages = [];
|
||||||
setStatus('', '');
|
setStatus('', '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue