Added checkin feature, fixed Engine Dockerfile to not warn about running as root, added ability to create External Network from CIDR size, removed unnecessary permissions from deployment script and optimized code flow for closing dialog boxes in UI

This commit is contained in:
Matthew Garrett 2024-05-29 23:42:51 -07:00
Родитель b74f467630
Коммит 8ec14b0b08
15 изменённых файлов: 351 добавлений и 253 удалений

Просмотреть файл

@ -388,12 +388,12 @@ process {
}
)
$uiWebSettings = @{
ImplicitGrantSetting = @{
EnableAccessTokenIssuance = $true
EnableIdTokenIssuance = $true
}
}
# $uiWebSettings = @{
# ImplicitGrantSetting = @{
# EnableAccessTokenIssuance = $true
# EnableIdTokenIssuance = $true
# }
# }
# Create IPAM UI Application (If -UI:$false not specified)
if (-not $DisableUI) {
@ -401,8 +401,8 @@ process {
$uiApp = New-AzADApplication `
-DisplayName $UiAppName `
-SPARedirectUri "https://replace-this-value.azurewebsites.net" `
-Web $uiWebSettings
-SPARedirectUri "https://replace-this-value.azurewebsites.net"
# -Web $uiWebSettings
}
$engineResourceMap = @{

Просмотреть файл

@ -6,6 +6,9 @@ ARG PORT=80
# Set Environment Variable
ENV PORT=${PORT}
# Disable PIP Root Warnings
ENV PIP_ROOT_USER_ACTION=ignore
# Set the Working Directory
WORKDIR /ipam

Просмотреть файл

@ -114,5 +114,33 @@ class Globals:
@property
def SHARED_TRANSPORT(self):
return self.shared_transport
@property
def DEPLOYMENT_STACK(self):
ipam_stack = ""
if os.environ.get('WEBSITE_SITE_NAME'):
if os.environ.get('FUNCTIONS_WORKER_RUNTIME'):
ipam_stack = "Function"
else:
ipam_stack = "App"
if os.environ.get('WEBSITE_STACK'):
if os.environ.get('WEBSITE_STACK') == 'DOCKER':
ipam_stack += "Container"
else:
ipam_stack += "Native"
else:
ipam_stack = "LegacyCompose"
elif os.environ.get('CONTAINER_APP_HOSTNAME'):
ipam_stack = "ContainerApp"
elif os.environ.get('KUBERNETES_SERVICE_HOST'):
ipam_stack = "Kubernetes"
elif os.path.exists('/.dockerenv'):
ipam_stack = "Docker"
else:
ipam_stack = "Unknown"
return ipam_stack
globals = Globals()

Просмотреть файл

@ -4,7 +4,6 @@ from fastapi.staticfiles import StaticFiles
from fastapi.exceptions import HTTPException as StarletteHTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from fastapi_restful.tasks import repeat_every
from fastapi.encoders import jsonable_encoder
from azure.identity.aio import ManagedIdentityCredential
@ -33,8 +32,11 @@ import json
import shutil
import tempfile
import traceback
import requests
from pathlib import Path
from urllib.parse import urlparse
from contextlib import asynccontextmanager
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from app.globals import globals
@ -58,126 +60,101 @@ description = """
Azure IPAM is a lightweight solution developed on top of the Azure platform designed to help Azure customers manage their enterprise IP Address space easily and effectively.
"""
app = FastAPI(
title = "Azure IPAM",
description = description,
version = globals.IPAM_VERSION,
contact = {
"name": "Azure IPAM Team",
"url": "https://github.com/azure/ipam",
"email": "ipam@microsoft.com",
},
openapi_url = "/api/openapi.json",
docs_url = "/api/docs",
redoc_url = "/api/redoc"
)
async def ipam_init():
global BUILD_DIR
app.logger = logger
release_data = {}
app.include_router(
azure.router,
prefix = "/api",
include_in_schema = False
)
path = '/etc/os-release' if os.path.exists('/etc/os-release') else '/usr/lib/os-release'
app.include_router(
internal.router,
prefix = "/api",
include_in_schema = False
)
release_info = open(path, 'r')
release_values = release_info.read().splitlines()
cleaned_values = [i for i in release_values if i]
app.include_router(
admin.router,
prefix = "/api"
)
for value in cleaned_values:
clean_value = value.strip()
value_parts = clean_value.split('=')
release_data[value_parts[0]] = value_parts[1].replace('"', '')
app.include_router(
user.router,
prefix = "/api"
)
os.environ['VITE_CONTAINER_IMAGE_ID'] = release_data['ID']
os.environ['VITE_CONTAINER_IMAGE_VERSION'] = release_data['VERSION_ID']
os.environ['VITE_CONTAINER_IMAGE_CODENAME'] = release_data['VERSION'].split(" ")[1][1:-1].lower()
os.environ['VITE_CONTAINER_IMAGE_PRETTY_NAME'] = release_data['PRETTY_NAME']
app.include_router(
tool.router,
prefix = "/api"
)
if os.path.exists(BUILD_DIR):
if(os.environ.get('FUNCTIONS_WORKER_RUNTIME') or (not os.access(BUILD_DIR, os.W_OK))):
new_build_dir = os.path.join(tempfile.gettempdir(), "dist")
app.include_router(
space.router,
prefix = "/api"
)
shutil.copytree(BUILD_DIR, new_build_dir)
app.include_router(
status.router,
prefix = "/api"
)
BUILD_DIR = new_build_dir
@app.get(
"/api/{full_path:path}",
include_in_schema = False
)
async def serve_react_app(request: Request):
"""
Catch-All Path for /api Route
"""
env_data = {
'VITE_AZURE_ENV': os.environ.get('AZURE_ENV'),
'VITE_UI_ID': os.environ.get('UI_APP_ID'),
'VITE_ENGINE_ID': os.environ.get('ENGINE_APP_ID'),
'VITE_TENANT_ID': os.environ.get('TENANT_ID'),
'VITE_OS_NAME': release_data['PRETTY_NAME']
}
raise HTTPException(status_code=404, detail="Invalid API path.")
env_data_js = "window.env = " + json.dumps(env_data, indent=4) + "\n"
origins = [
"http://localhost:3000"
]
env_file = os.path.join(BUILD_DIR, "env.js")
if os.environ.get('WEBSITE_HOSTNAME'):
origins.append("https://" + os.environ.get('WEBSITE_HOSTNAME'))
with open(env_file, "w") as env_file:
env_file.write(env_data_js)
if os.environ.get('IPAM_UI_URL'):
ui_url = urlparse(os.environ.get('IPAM_UI_URL'))
if (ui_url.scheme and ui_url.netloc):
origins.append(ui_url.scheme + "://" + ui_url.netloc)
app.add_middleware(
CORSMiddleware,
allow_origins = origins,
allow_credentials = True,
allow_methods = ["*"],
allow_headers = ["*"],
)
app.add_middleware(
GZipMiddleware,
minimum_size = 500
)
if os.path.isdir(BUILD_DIR) and UI_APP_ID and VALID_APP_ID:
app.mount(
"/assets/",
StaticFiles(directory = Path(BUILD_DIR) / "assets"),
name = "static"
managed_identity_credential = ManagedIdentityCredential(
client_id = globals.MANAGED_IDENTITY_ID
)
@app.get(
"/",
response_class = FileResponse,
include_in_schema = False
cosmos_client = CosmosClient(
globals.COSMOS_URL,
credential=globals.COSMOS_KEY if globals.COSMOS_KEY else managed_identity_credential,
transport=globals.SHARED_TRANSPORT
)
def read_index(request: Request):
return FileResponse(BUILD_DIR + "/index.html")
@app.get(
"/{full_path:path}",
response_class = FileResponse,
include_in_schema = False
)
def read_index(request: Request, full_path: str):
target_file = BUILD_DIR + "/" + full_path
database_name = globals.DATABASE_NAME
print('look for: ', full_path, target_file)
if os.path.exists(target_file):
return FileResponse(target_file)
try:
logger.info('Verifying Database Exists...')
database = await cosmos_client.create_database_if_not_exists(
id = database_name
)
except CosmosHttpResponseError as e:
logger.error('Cosmos database does not exist, error initializing Azure IPAM!')
raise e
database = cosmos_client.get_database_client(database_name)
return FileResponse(BUILD_DIR + "/index.html")
container_name = globals.CONTAINER_NAME
async def db_upgrade():
try:
logger.info('Verifying Container Exists...')
container = await database.create_container_if_not_exists(
id = container_name,
partition_key = PartitionKey(path = "/tenant_id")
)
except CosmosHttpResponseError as e:
logger.error('Cosmos container does not exist, error initializing Azure IPAM!')
raise e
container = database.get_container_client(container_name)
await cosmos_client.close()
await managed_identity_credential.close()
hb_message = {
"tenantId": globals.TENANT_ID,
"version": globals.IPAM_VERSION,
"type": globals.DEPLOYMENT_STACK,
"env": globals.AZURE_ENV
}
requests.post(url = "https://azureipammetrics.azurewebsites.net/api/heartbeat", json = hb_message)
async def upgrade_db():
managed_identity_credential = ManagedIdentityCredential(
client_id = globals.MANAGED_IDENTITY_ID
)
@ -410,97 +387,7 @@ async def db_upgrade():
await cosmos_client.close()
await managed_identity_credential.close()
@app.on_event("startup")
async def ipam_startup():
global BUILD_DIR
if os.path.exists(BUILD_DIR):
if(os.environ.get('FUNCTIONS_WORKER_RUNTIME') or (not os.access(BUILD_DIR, os.W_OK))):
new_build_dir = os.path.join(tempfile.gettempdir(), "dist")
shutil.copytree(BUILD_DIR, new_build_dir)
BUILD_DIR = new_build_dir
release_data = {}
path = '/etc/os-release' if os.path.exists('/etc/os-release') else '/usr/lib/os-release'
release_info = open(path, 'r')
release_values = release_info.read().splitlines()
cleaned_values = [i for i in release_values if i]
for value in cleaned_values:
clean_value = value.strip()
value_parts = clean_value.split('=')
release_data[value_parts[0]] = value_parts[1].replace('"', '')
os.environ['VITE_CONTAINER_IMAGE_ID'] = release_data['ID']
os.environ['VITE_CONTAINER_IMAGE_VERSION'] = release_data['VERSION_ID']
os.environ['VITE_CONTAINER_IMAGE_CODENAME'] = release_data['VERSION'].split(" ")[1][1:-1].lower()
os.environ['VITE_CONTAINER_IMAGE_PRETTY_NAME'] = release_data['PRETTY_NAME']
env_data = {
'VITE_AZURE_ENV': os.environ.get('AZURE_ENV'),
'VITE_UI_ID': os.environ.get('UI_APP_ID'),
'VITE_ENGINE_ID': os.environ.get('ENGINE_APP_ID'),
'VITE_TENANT_ID': os.environ.get('TENANT_ID'),
'VITE_OS_NAME': release_data['PRETTY_NAME']
}
env_data_js = "window.env = " + json.dumps(env_data, indent=4) + "\n"
env_file = os.path.join(BUILD_DIR, "env.js")
with open(env_file, "w") as env_file:
env_file.write(env_data_js)
managed_identity_credential = ManagedIdentityCredential(
client_id = globals.MANAGED_IDENTITY_ID
)
cosmos_client = CosmosClient(
globals.COSMOS_URL,
credential=globals.COSMOS_KEY if globals.COSMOS_KEY else managed_identity_credential,
transport=globals.SHARED_TRANSPORT
)
database_name = globals.DATABASE_NAME
try:
logger.info('Verifying Database Exists...')
database = await cosmos_client.create_database_if_not_exists(
id = database_name
)
except CosmosHttpResponseError as e:
logger.error('Cosmos database does not exist, error initializing Azure IPAM!')
raise e
database = cosmos_client.get_database_client(database_name)
container_name = globals.CONTAINER_NAME
try:
logger.info('Verifying Container Exists...')
container = await database.create_container_if_not_exists(
id = container_name,
partition_key = PartitionKey(path = "/tenant_id")
)
except CosmosHttpResponseError as e:
logger.error('Cosmos container does not exist, error initializing Azure IPAM!')
raise e
container = database.get_container_client(container_name)
await cosmos_client.close()
await managed_identity_credential.close()
await db_upgrade()
@app.on_event("startup")
@repeat_every(seconds = 60, wait_first = True)
async def find_reservations() -> None:
async def find_reservations():
if not os.environ.get("FUNCTIONS_WORKER_RUNTIME"):
try:
await azure.match_resv_to_vnets()
@ -510,6 +397,142 @@ async def find_reservations() -> None:
logger.debug(tb)
raise e
@asynccontextmanager
async def lifespan(app: FastAPI):
# IPAM Startup Tasks
await ipam_init()
await upgrade_db()
# Schedule Recurring Tasks
scheduler = AsyncIOScheduler()
scheduler.add_job(func=find_reservations, trigger='interval', minutes=1)
scheduler.start()
yield
# IPAM Shutdown Tasks
scheduler.shutdown()
app = FastAPI(
title = "Azure IPAM",
description = description,
version = globals.IPAM_VERSION,
contact = {
"name": "Azure IPAM Team",
"url": "https://github.com/azure/ipam",
"email": "ipam@microsoft.com",
},
openapi_url = "/api/openapi.json",
docs_url = "/api/docs",
redoc_url = "/api/redoc",
lifespan = lifespan
)
app.logger = logger
app.include_router(
azure.router,
prefix = "/api",
include_in_schema = False
)
app.include_router(
internal.router,
prefix = "/api",
include_in_schema = False
)
app.include_router(
admin.router,
prefix = "/api"
)
app.include_router(
user.router,
prefix = "/api"
)
app.include_router(
tool.router,
prefix = "/api"
)
app.include_router(
space.router,
prefix = "/api"
)
app.include_router(
status.router,
prefix = "/api"
)
@app.get(
"/api/{full_path:path}",
include_in_schema = False
)
async def serve_react_app(request: Request):
"""
Catch-All Path for /api Route
"""
raise HTTPException(status_code=404, detail="Invalid API path.")
origins = [
"http://localhost:3000"
]
if os.environ.get('WEBSITE_HOSTNAME'):
origins.append("https://" + os.environ.get('WEBSITE_HOSTNAME'))
if os.environ.get('IPAM_UI_URL'):
ui_url = urlparse(os.environ.get('IPAM_UI_URL'))
if (ui_url.scheme and ui_url.netloc):
origins.append(ui_url.scheme + "://" + ui_url.netloc)
app.add_middleware(
CORSMiddleware,
allow_origins = origins,
allow_credentials = True,
allow_methods = ["*"],
allow_headers = ["*"],
)
app.add_middleware(
GZipMiddleware,
minimum_size = 500
)
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request, exc):
return JSONResponse({"error": str(exc.detail)}, status_code=exc.status_code)
if os.path.isdir(BUILD_DIR) and UI_APP_ID and VALID_APP_ID:
app.mount(
"/assets/",
StaticFiles(directory = Path(BUILD_DIR) / "assets"),
name = "static"
)
@app.get(
"/",
response_class = FileResponse,
include_in_schema = False
)
def read_index(request: Request):
return FileResponse(BUILD_DIR + "/index.html")
@app.get(
"/{full_path:path}",
response_class = FileResponse,
include_in_schema = False
)
def read_index(request: Request, full_path: str):
target_file = BUILD_DIR + "/" + full_path
print('look for: ', full_path, target_file)
if os.path.exists(target_file):
return FileResponse(target_file)
return FileResponse(BUILD_DIR + "/index.html")

Просмотреть файл

@ -149,6 +149,16 @@ class ExtNet(BaseModel):
cidr: IPv4Network
subnets: List[ExtSubnet]
class ExtNetExpand(BaseModel):
"""DOCSTRING"""
name: str
desc: str
space: str
block: str
cidr: IPv4Network
subnets: List[ExtSubnet]
class VNets(BaseModel):
"""DOCSTRING"""
@ -399,15 +409,28 @@ class ExtNetReq(BaseModel):
"""DOCSTRING"""
name: str
desc: str
cidr: IPv4Network
desc: Optional[str] = None
cidr: Optional[IPv4Network] = None
size: Optional[int] = None
@model_validator(mode='before')
@classmethod
def validate_request(cls, data: Any) -> Any:
if isinstance(data, dict):
if 'cidr' in data and 'size' in data:
if data['cidr'] is not None:
raise AssertionError("the 'cidr' and 'size' parameters can only be used alternatively")
if 'cidr' not in data and 'size' not in data:
raise AssertionError("it is required to provide either the 'cidr' or 'size' parameter")
return data
class ExtSubnetReq(BaseModel):
"""DOCSTRING"""
name: str
desc: Optional[str] = None
cidr: Optional[str] = None
cidr: Optional[IPv4Network] = None
size: Optional[int] = None
@model_validator(mode='before')

Просмотреть файл

@ -1409,7 +1409,7 @@ async def get_external_networks(
@router.post(
"/{space}/blocks/{block}/externals",
summary = "Create External Network",
response_model = ExtNet,
response_model = ExtNetExpand,
status_code = 201
)
@cosmos_retry(
@ -1417,7 +1417,7 @@ async def get_external_networks(
error_msg = "Error adding external network to block, please try again."
)
async def create_external_network(
external: ExtNetReq,
req: ExtNetReq,
space: str = Path(..., description="Name of the target Space"),
block: str = Path(..., description="Name of the target Block"),
authorization: str = Header(None, description="Azure Bearer token"),
@ -1435,10 +1435,10 @@ async def create_external_network(
if not is_admin:
raise HTTPException(status_code=403, detail="API restricted to admins.")
if not re.match(EXTERNAL_NAME_REGEX, external.name, re.IGNORECASE):
if not re.match(EXTERNAL_NAME_REGEX, req.name, re.IGNORECASE):
raise HTTPException(status_code=400, detail="External network name can be a maximum of 64 characters and may contain alphanumerics, underscores, hypens, and periods.")
if not re.match(EXTERNAL_DESC_REGEX, external.desc, re.IGNORECASE):
if not re.match(EXTERNAL_DESC_REGEX, req.desc, re.IGNORECASE):
raise HTTPException(status_code=400, detail="External network description can be a maximum of 128 characters and may contain alphanumerics, spaces, underscores, hypens, slashes, and periods.")
space_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}')".format(space), tenant_id)
@ -1453,22 +1453,12 @@ async def create_external_network(
if not target_block:
raise HTTPException(status_code=400, detail="Invalid block name.")
if external.name in [x['name'] for x in target_block['externals']]:
if req.name in [x['name'] for x in target_block['externals']]:
raise HTTPException(status_code=400, detail="External network name already exists in block.")
if str(IPNetwork(external.cidr).cidr) != external.cidr:
raise HTTPException(status_code=400, detail="External network cidr invalid, should be {}".format(IPNetwork(external.cidr).cidr))
net_list = await get_network(authorization, True)
ext_cidr_in_block = IPNetwork(external.cidr) in IPNetwork(target_block['cidr'])
if not ext_cidr_in_block:
raise HTTPException(status_code=400, detail="External network CIDR not within block CIDR.")
block_net_cidrs = []
resv_cidrs = IPSet(x['cidr'] for x in target_block['resv'])
ext_cidrs = IPSet(x['cidr'] for x in target_block['externals'])
for v in target_block['vnets']:
target = next((x for x in net_list if x['id'].lower() == v['id'].lower()), None)
@ -1477,19 +1467,43 @@ async def create_external_network(
prefixes = list(filter(lambda x: IPNetwork(x) in IPNetwork(target_block['cidr']), target['prefixes']))
block_net_cidrs += prefixes
if IPSet([external.cidr]) & ext_cidrs:
raise HTTPException(status_code=400, detail="Block contains external network(s) which overlap the target external network.")
block_set = IPSet(block_net_cidrs)
resv_set = IPSet(x['cidr'] for x in target_block['resv'])
external_set = IPSet(x['cidr'] for x in target_block['externals'])
available_set = IPSet([target_block['cidr']]) ^ (resv_set | external_set | block_set)
if IPSet([external.cidr]) & resv_cidrs:
raise HTTPException(status_code=400, detail="Block contains unfulfilled reservation(s) which overlap the target external network.")
if req.cidr is not None:
try:
next_cidr = IPNetwork(req.cidr)
except:
raise HTTPException(status_code=400, detail="Invalid CIDR, please ensure CIDR is in valid IPv4 CIDR notation (x.x.x.x/x).")
if str(IPNetwork(req.cidr).cidr) != req.cidr:
raise HTTPException(status_code=400, detail="External network cidr invalid, should be {}".format(IPNetwork(req.cidr).cidr))
if IPNetwork(req.cidr) not in IPNetwork(target_block['cidr']):
raise HTTPException(status_code=400, detail="External network CIDR not within block CIDR.")
if IPSet([req.cidr]) & external_set:
raise HTTPException(status_code=400, detail="Block contains external network(s) which overlap the target external network.")
if IPSet([req.cidr]) & resv_set:
raise HTTPException(status_code=400, detail="Block contains unfulfilled reservation(s) which overlap the target external network.")
if IPSet([req.cidr]) & block_set:
raise HTTPException(status_code=400, detail="Block contains a virtual network(s) or hub(s) which overlap the target external network.")
else:
available_network = next((net for net in list(available_set.iter_cidrs()) if net.prefixlen <= req.size), None)
if not available_network:
raise HTTPException(status_code=500, detail="Network of requested size unavailable in target block.")
next_cidr = list(available_network.subnet(req.size))[0]
if IPSet([external.cidr]) & IPSet(block_net_cidrs):
raise HTTPException(status_code=400, detail="Block contains a virtual network(s) or hub(s) which overlap the target external network.")
new_external = {
"name": external.name,
"desc": external.desc,
"cidr": external.cidr,
"name": req.name,
"desc": req.desc,
"cidr": str(next_cidr),
"subnets": []
}
@ -1497,6 +1511,9 @@ async def create_external_network(
await cosmos_replace(space_query[0], target_space)
new_external['space'] = target_space['name']
new_external['block'] = target_block['name']
return new_external
@router.get(
@ -1688,10 +1705,7 @@ async def create_external_subnet(
if req.name in [x['name'] for x in target_external['subnets']]:
raise HTTPException(status_code=400, detail="Subnet name already exists in external network.")
subnet_cidrs = []
for s in (s for s in target_external['subnets']):
subnet_cidrs.append(s['cidr'])
subnet_cidrs = [s['cidr'] for s in target_external['subnets']]
external_set = IPSet([target_external['cidr']])
subnet_set = IPSet(subnet_cidrs)
@ -1701,11 +1715,14 @@ async def create_external_subnet(
try:
next_cidr = IPNetwork(req.cidr)
except:
raise HTTPException(status_code=400, detail="Invalid network CIDR format.")
raise HTTPException(status_code=400, detail="Invalid CIDR, please ensure CIDR is in valid IPv4 CIDR notation (x.x.x.x/x).")
if str(next_cidr.cidr) != req.cidr:
raise HTTPException(status_code=400, detail="External subnet CIDR invalid, should be {}".format(IPNetwork(req.cidr).cidr))
if IPNetwork(req.cidr) not in IPNetwork(target_external['cidr']):
raise HTTPException(status_code=400, detail="External subnet CIDR not within external network CIDR.")
if next_cidr not in available_set:
raise HTTPException(status_code=409, detail="Requested subnet CIDR overlaps existing subnet(s).")
else:

Просмотреть файл

@ -20,16 +20,16 @@ router = APIRouter(
status_code = 200
)
async def get_status():
is_container = os.environ.get('WEBSITE_STACK') == 'DOCKER'
is_function = os.environ.get('FUNCTIONS_WORKER_RUNTIME') is not None
stack_type = 'Function' if is_function else 'App'
stack_type += 'Container' if is_container else ''
is_container = (
os.environ.get('WEBSITE_STACK') == 'DOCKER' or
os.environ.get('KUBERNETES_SERVICE_HOST') or
os.path.exists('/.dockerenv')
)
status_message = {
"status": "OK",
"version": globals.IPAM_VERSION,
"stack": stack_type,
"stack": globals.DEPLOYMENT_STACK,
"environment": globals.AZURE_ENV
}

Просмотреть файл

@ -1,6 +1,5 @@
fastapi[all]>=0.103.0
pydantic[email]>=2.3.0
fastapi-restful[all]>=0.5.0
msal
pyjwt
cryptography
@ -24,4 +23,4 @@ azure-mgmt-datafactory
azure-keyvault-secrets
azure-cosmos
azure-functions
nest_asyncio
apscheduler

Просмотреть файл

@ -58,9 +58,10 @@ export default function AddBlock(props) {
&& !cidr.error ? false : true;
function onCancel() {
handleClose();
setBlockName({ value: "", error: false });
setCidr({ value: "", error: false });
handleClose();
}
function onSubmit() {
@ -124,7 +125,7 @@ export default function AddBlock(props) {
<div>
<Dialog
open={open}
onClose={handleClose}
onClose={onCancel}
PaperComponent={DraggablePaper}
maxWidth="xs"
fullWidth

Просмотреть файл

@ -87,9 +87,10 @@ export default function EditBlock(props) {
}, [block]);
function onCancel() {
handleClose();
setBlockName({ value: block.name, error: false });
setCidr({ value: block.cidr, error: false });
handleClose();
}
function onSubmit() {
@ -154,7 +155,7 @@ export default function EditBlock(props) {
<div>
<Dialog
open={open}
onClose={handleClose}
onClose={onCancel}
PaperComponent={DraggablePaper}
maxWidth="xs"
fullWidth

Просмотреть файл

@ -58,9 +58,10 @@ export default function AddSpace(props) {
&& !description.error ? false : true;
function onCancel() {
handleClose();
setSpaceName({ value: "", error: false });
setDescription({ value: "", error: false });
handleClose();
}
function onSubmit() {
@ -126,7 +127,7 @@ export default function AddSpace(props) {
<div>
<Dialog
open={open}
onClose={handleClose}
onClose={onCancel}
PaperComponent={DraggablePaper}
maxWidth="xs"
fullWidth

Просмотреть файл

@ -87,9 +87,10 @@ export default function EditSpace(props) {
}, [space]);
function onCancel() {
handleClose();
setSpaceName({ value: space.name, error: false });
setDescription({ value: space.desc, error: false });
handleClose();
}
function onSubmit() {
@ -154,7 +155,7 @@ export default function EditSpace(props) {
<div>
<Dialog
open={open}
onClose={handleClose}
onClose={onCancel}
PaperComponent={DraggablePaper}
maxWidth="xs"
fullWidth

Просмотреть файл

@ -236,7 +236,7 @@ export default function AddExtNetwork(props) {
<div>
<Dialog
open={open}
onClose={handleClose}
onClose={onCancel}
PaperComponent={DraggablePaper}
maxWidth="xs"
fullWidth

Просмотреть файл

@ -209,7 +209,7 @@ export default function AddExtSubnet(props) {
<div>
<Dialog
open={open}
onClose={handleClose}
onClose={onCancel}
PaperComponent={DraggablePaper}
maxWidth="xs"
fullWidth

Просмотреть файл

@ -104,6 +104,7 @@ export default function NewReservation(props) {
function onCancel() {
handleClose();
setChecked(true);
setDescription({ value: "", error: false });
setMask(maskOptions[0]);
@ -195,7 +196,7 @@ export default function NewReservation(props) {
<div>
<Dialog
open={open}
onClose={handleClose}
onClose={onCancel}
PaperComponent={DraggablePaper}
maxWidth="md"
fullWidth