New menu/page for configuration sync and cloning (pro feature).

Now displays DHCP server details along with historical lease information(pro feature)).
Added a  ping status component for monitoring device connectivity.
Device pages now include logs, accounting, and authorization data for improved tracking.
New component in the device page to display active sessions with the ability to terminate them.
Redesigned Device Detail Page
Reload Buttons for device details page Tabs
Device detail pages now refresh every 30 seconds.
Restored the missing device logs action menu on the devices list page.
Improved handling of duplicate serial numbers when the license serial is not set, with better registration status details on the dashboard.
Various minor stability and performance improvements.
This commit is contained in:
sepehr 2025-02-03 12:35:12 +03:00
parent d8aa93f7ec
commit 10d4cff4a4
55 changed files with 2412 additions and 271 deletions

View file

@ -0,0 +1,21 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ClonerComponent } from './cloner.component';
const routes: Routes = [
{
path: '',
component: ClonerComponent,
data: {
title: $localize`synchronization and cloner`
}
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ClonerRoutingModule {
}

View file

@ -0,0 +1,239 @@
<c-row>
<c-col xs>
<c-card class="mb-4">
<c-card-header>
<c-row>
<c-col xs [lg]="10">
Config synchronization and cloners
</c-col>
<c-col xs [lg]="2" style="text-align: right;">
<button cButton color="primary" (click)="editAddCloner({},'showadd')"><i
class="fa-solid fa-plus"></i></button>
</c-col>
</c-row>
</c-card-header>
<c-card-body>
<gui-grid [autoResizeWidth]="true" [source]="source" [columnMenu]="columnMenu" [sorting]="sorting"
[infoPanel]="infoPanel" [autoResizeWidth]=true>
<gui-grid-column header="Description" field="description">
<ng-template let-value="item.description" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="Members type" field="pair_type">
<ng-template let-value="item.pair_type" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="Runtime" field="desc_cron">
<ng-template let-value="item.desc_cron" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="Actions" width="120" field="action">
<ng-template let-value="item.id" let-item="item" let-index="index">
<button cButton color="warning" size="sm" (click)="editAddCloner(item,'edit');"><i
class="fa-regular fa-pen-to-square"></i></button>
<!-- <button cButton color="info" size="sm" (click)="confirm_run(item);" class="mx-1"><i
class="fa-solid fa-bolt"></i></button> -->
<button class=" mx-1" cButton color="danger" size="sm" (click)="confirm_delete(item);"><i
class="fa-regular fa-trash-can"></i></button>
</ng-template>
</gui-grid-column>
</gui-grid>
</c-card-body>
</c-card>
</c-col>
</c-row>
<c-modal #EditClonerModal backdrop="static" size="xl" [(visible)]="EditClonerModalVisible" id="EditClonerModal">
<c-modal-header>
<h5 *ngIf="SelectedCloner['action']=='edit'" cModalTitle>Editing Cloner {{SelectedCloner['name']}}</h5>
<h5 *ngIf="SelectedCloner['action']=='add'" cModalTitle>Adding new task</h5>
<button [cModalToggle]="EditClonerModal.id" cButtonClose></button>
</c-modal-header>
<c-modal-body>
<label cLabel >General data:</label>
<c-input-group class="mb-3">
<label cInputGroupText for="floatingInput">Name</label>
<input cFormControl id="floatingInput" placeholder="SelectedCloner['name']" [(ngModel)]="SelectedCloner['name']" />
<label cInputGroupText for="floatingInput">Description</label>
<input cFormControl id="floatingInput" placeholder="SelectedCloner['description']"
[(ngModel)]="SelectedCloner['description']" />
</c-input-group>
<label cLabel >Sync config and Oprating Modes:</label>
<c-input-group class="mb-3">
<label cInputGroupText for="Direction">
Direction
</label>
<select cSelect id="Direction" [(ngModel)]="SelectedCloner['direction']">
<option value="twoway">Two way</option>
<option value="oneway">Master mode</option>
</select>
<label cInputGroupText for="inputGroupSelect01">
Live Mode
</label>
<select cSelect id="inputGroupSelect01" [(ngModel)]="SelectedCloner['live_mode']">
<option [ngValue]="false">Deactive</option>
<option [ngValue]="true">Active</option>
</select>
<label *ngIf="SelectedCloner['direction']=='oneway'" cInputGroupText for="inputGroupSelect02">
Schedule
</label>
<select *ngIf="SelectedCloner['direction']=='oneway'" cSelect id="inputGroupSelect02" [(ngModel)]="SelectedCloner['schedule']">
<option [ngValue]="false">Deactive</option>
<option [ngValue]="true">Active</option>
</select>
<label cInputGroupText *ngIf="SelectedCloner['schedule'] && SelectedCloner['direction']=='oneway'" for="cron">cron</label>
<input cFormControl *ngIf="SelectedCloner['schedule'] && SelectedCloner['direction']=='oneway'" id="cron" placeholder="Cron" [(ngModel)]="SelectedCloner['cron']" />
</c-input-group>
<label cLabel >Peers Setting:</label>
<c-input-group class="mb-3">
<label cInputGroupText for="inputGroupSelect03">
Peers type
</label>
<select cSelect id="inputGroupSelect03" (change)="form_changed()" [(ngModel)]="SelectedCloner['pair_type']">
<option value="devices">Devices</option>
<option value="groups" *ngIf="SelectedCloner['direction']=='oneway'">Groups</option>
</select>
</c-input-group>
<c-col xs style="border: 1px solid #ddd; border-radius: 4px; padding: 0;">
<div class="nav nav-underline" style="background: #fff; border-bottom: 2px solid #2c3e50;">
<div class="nav-item" *ngFor="let tab of tabs; let i = index">
<a class="nav-link" [active]="i==0" [cTabContent]="tabContent" [tabPaneIdx]="i">{{ tab.name }}</a>
</div>
</div>
<c-tab-content class="command-sections" style="padding: 10px!important;min-height: 150px;" #tabContent="cTabContent">
<c-tab-pane *ngFor="let tab of tabs; let i = index">
<div class="section" *ngFor="let section of tab.sections">
<h5 class="cloner-sections">{{ section.title }}</h5>
<div class="row">
<div class="col-4" *ngFor="let command of section.commands">
<c-card style="margin-bottom: 5px;">
<c-card-body class="p-2">
<h6 class="card-title mb-1">{{ command }}</h6>
<div class="custom-switch">
<input type="checkbox" class="custom-control-input" [checked]="in_active_commands(command)" (click)="activate_command(command)" [id]="command.replace('/', '')">
<label class="custom-control-label" [for]="command.replace('/', '')"></label>
</div>
</c-card-body>
</c-card>
</div>
</div>
</div>
</c-tab-pane>
</c-tab-content>
</c-col>
<h5>Peers :</h5>
<gui-grid [autoResizeWidth]="true" [source]="SelectedMembers" [columnMenu]="columnMenu" [sorting]="sorting"
[rowSelection]="rowSelection" [autoResizeWidth]=true [paging]="paging">
<gui-grid-column header="Name" field="name">
<ng-template let-value="item.name" let-item="item" let-index="index">
<i class="fa-solid fa-m" style="color: #ff3300;" *ngIf="SelectedCloner['direction']=='oneway' && item.id==master"></i>
&nbsp; {{value}} </ng-template>
</gui-grid-column>
<gui-grid-column *ngIf="SelectedCloner['pair_type']=='devices'" header="MAC" field="mac">
<ng-template let-value="item.mac" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="Actions" width="120" field="action">
<ng-template let-value="item.id" let-item="item" let-index="index">
<button cButton color="danger" size="sm" [cTooltip]="'Delete Member'" (click)="remove_member(item)"><i
class="fa-regular fa-trash-can"></i></button>
<button *ngIf="SelectedCloner['direction']=='oneway'" cButton color="success" size="sm" style="margin-left: 5px;" [cTooltip]="'Set as Master'" (click)="set_master(item.id)"><i class="fa-regular fa-star"></i></button>
</ng-template>
</gui-grid-column>
</gui-grid>
<hr />
<button cButton color="primary" (click)="show_new_member_form()">+ Add new Members</button>
</c-modal-body>
<c-modal-footer>
<button *ngIf="SelectedCloner['action']=='add'" (click)="submit('add')" cButton color="primary">Add</button>
<button *ngIf="SelectedCloner['action']=='edit'" (click)="submit('edit')" cButton color="primary">save</button>
<button [cModalToggle]="EditClonerModal.id" cButton color="secondary">
Close
</button>
</c-modal-footer>
</c-modal>
<c-modal #NewMemberModal backdrop="static" size="lg" [(visible)]="NewMemberModalVisible" id="NewMemberModal">
<c-modal-header>
<h5 cModalTitle>Editing Group </h5>
<button (click)="NewMemberModalVisible=!NewMemberModalVisible" cButtonClose></button>
</c-modal-header>
<c-modal-body>
<c-input-group class="mb-3">
<h5>Group Members :</h5>
<gui-grid [autoResizeWidth]="true" *ngIf="NewMemberModalVisible" [searching]="searching"
[source]="availbleMembers" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel"
[rowSelection]="rowSelection" (selectedRows)="onSelectedRowsNewMembers($event)" [autoResizeWidth]=true
[paging]="paging">
<gui-grid-column header="Member Name" field="name">
<ng-template let-value="item.name" let-item="item" let-index="index">
&nbsp; {{value}} </ng-template>
</gui-grid-column>
<gui-grid-column *ngIf="SelectedCloner['pair_type']=='devices'" header="IP Address" field="ip">
<ng-template let-value="item.ip" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column *ngIf="SelectedCloner['pair_type']=='devices'" header="MAC Address" field="mac">
<ng-template let-value="item.mac" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
</gui-grid>
<br />
</c-input-group>
<hr />
</c-modal-body>
<c-modal-footer>
<button *ngIf="NewMemberRows.length!= 0" (click)="add_new_members()" cButton color="primary">Add {{
NewMemberRows.length }}</button>
<button (click)="NewMemberModalVisible=!NewMemberModalVisible" cButton color="secondary">
Close
</button>
</c-modal-footer>
</c-modal>
<c-modal #DeleteConfirmModal backdrop="static" [(visible)]="DeleteConfirmModalVisible" id="DeleteConfirmModal">
<c-modal-header>
<h5 cModalTitle>Confirm delete {{ SelectedCloner['name'] }}</h5>
<button [cModalToggle]="DeleteConfirmModal.id" cButtonClose></button>
</c-modal-header>
<c-modal-body>
Are you sure that You want to delete following task ?
<br />
<br />
<table style="width: 100%;">
<tr>
<td><b>Taks name : </b></td>
<td>{{ SelectedCloner['name'] }}</td>
</tr>
<tr>
<td><b>Description : </b></td>
<td>{{ SelectedCloner['description'] }}</td>
</tr>
<tr>
<td><b>Cron exec : </b></td>
<td>{{ SelectedCloner['desc_cron'] }}</td>
</tr>
</table>
</c-modal-body>
<c-modal-footer>
<button (click)="confirm_delete('',true)" cButton color="danger">
Yes,Delete!
</button>
<button [cModalToggle]="DeleteConfirmModal.id" cButton color="info">
Close
</button>
</c-modal-footer>
</c-modal>
<c-toaster position="fixed" placement="top-end"></c-toaster>

View file

@ -0,0 +1,458 @@
import { Component, OnInit, OnDestroy, ViewChildren ,QueryList } from "@angular/core";
import { dataProvider } from "../../providers/mikrowizard/data";
import { Router } from "@angular/router";
import { loginChecker } from "../../providers/login_checker";
import {
GuiSelectedRow,
GuiSearching,
GuiInfoPanel,
GuiColumn,
GuiColumnMenu,
GuiPaging,
GuiPagingDisplay,
GuiRowSelectionMode,
GuiRowSelection,
GuiRowSelectionType,
} from "@generic-ui/ngx-grid";
import { NgxSuperSelectOptions } from "ngx-super-select";
import { _getFocusedElementPierceShadowDom } from "@angular/cdk/platform";
import { ToasterComponent } from "@coreui/angular";
import { AppToastComponent } from "../toast-simple/toast.component";
@Component({
templateUrl: "cloner.component.html",
styleUrls: ["cloner.scss"],
})
export class ClonerComponent implements OnInit {
public uid: number;
public uname: string;
public ispro: boolean = false;
constructor(
private data_provider: dataProvider,
private router: Router,
private login_checker: loginChecker
) {
var _self = this;
if (!this.login_checker.isLoggedIn()) {
setTimeout(function () {
_self.router.navigate(["login"]);
}, 100);
}
this.data_provider.getSessionInfo().then((res) => {
_self.uid = res.uid;
_self.uname = res.name;
_self.ispro = res['ISPRO']
const userId = _self.uid;
if (res.role != "admin") {
setTimeout(function () {
_self.router.navigate(["/user/dashboard"]);
}, 100);
}
});
//get datagrid data
function isNotEmpty(value: any): boolean {
return value !== undefined && value !== null && value !== "";
}
}
@ViewChildren(ToasterComponent) viewChildren!: QueryList<ToasterComponent>;
public source: Array<any> = [];
public columns: Array<GuiColumn> = [];
public loading: boolean = true;
public rows: any = [];
public SelectedCloner: any = {};
public SelectedClonerItems: any = "";
public EditClonerModalVisible: boolean = false;
public DeleteConfirmModalVisible: boolean = false;
public Members: any = "";
public SelectedMembers: any = [];
public NewMemberModalVisible: boolean = false;
public availbleMembers: any = [];
public NewMemberRows: any = [];
public SelectedNewMemberRows: any;
public master: number = 0;
public active_commands:any=[];
public tabs:any=[
{
"name": "Network",
"sections": [
{
"title": "IP Management",
"commands": [
"/ip address",
"/ip cloud",
"/ip dhcp-server",
"/ip dns",
"/ip pool",
"/ip route",
"/ip vrf"
]
},
{
"title": "IP Services",
"commands": [
"/ip service"
]
}
]
},
{
"name": "Security",
"sections": [
{
"title": "Firewall",
"commands": [
"/ip firewall address-list",
"/ip firewall connection",
"/ip firewall layer7-protocol",
"/ip firewall nat",
"/ip firewall service-port",
"/ip firewall calea",
"/ip firewall filter",
"/ip firewall mangle",
"/ip firewall raw"
]
},
{
"title": "IPSec",
"commands": [
"/ip ipsec identity",
"/ip ipsec key",
"/ip ipsec peer",
"/ip ipsec profile",
"/ip ipsec settings",
"/ip ipsec statistics",
"/ip ipsec proposal",
"/ip ipsec policy",
"/ip ipsec mode-config",
"/ip ipsec active-peers",
"/ip ipsec installed-sa"
]
}
]
},
{
"name": "System",
"sections": [
{
"title": "Scripts & Scheduling",
"commands": [
"/system script",
"/system scheduler"
]
},
{
"title": "Time Management",
"commands": [
"/system ntp client servers",
"/system ntp client",
"/system ntp server",
"/system clock"
]
},
{
"title": "RADIUS",
"commands": [
"/radius"
]
}
]
},
{
"name": "MPLS",
"sections": [
{
"title": "MPLS Configuration",
"commands": [
"/mpls forwarding-table",
"/mpls interface",
"/mpls ldp",
"/mpls settings",
"/mpls traffic-eng"
]
},
{
"title": "VPLS",
"commands": [
"/interface vpls"
]
}
]
}
];
public sorting = {
enabled: true,
multiSorting: true,
};
searching: GuiSearching = {
enabled: true,
placeholder: "Search Devices",
};
options: Partial<NgxSuperSelectOptions> = {
selectionMode: "single",
actionsEnabled: false,
displayExpr: "name",
valueExpr: "id",
placeholder: "Snippet",
searchEnabled: true,
enableDarkMode: false,
};
public paging: GuiPaging = {
enabled: true,
page: 1,
pageSize: 10,
pageSizes: [5, 10, 25, 50],
display: GuiPagingDisplay.ADVANCED,
};
public columnMenu: GuiColumnMenu = {
enabled: true,
sort: true,
columnsManager: true,
};
toasterForm = {
autohide: true,
delay: 3000,
position: "fixed",
fade: true,
closeButton: true,
};
public infoPanel: GuiInfoPanel = {
enabled: true,
infoDialog: false,
columnsManager: true,
schemaManager: true,
};
public rowSelection: boolean | GuiRowSelection = {
enabled: true,
type: GuiRowSelectionType.CHECKBOX,
mode: GuiRowSelectionMode.MULTIPLE,
};
activate_command(command:string){
// add to active_commands if it not added before
if(!this.active_commands.includes(command)){
this.active_commands.push(command);
}
else{
this.active_commands=this.active_commands.filter((x:any)=>x!=command);
}
}
show_toast(title: string, body: string, color: string) {
const { ...props } = { ...this.toasterForm, color, title, body };
const componentRef = this.viewChildren.first.addToast(
AppToastComponent,
props,
{}
);
componentRef.instance["closeButton"] = props.closeButton;
}
show_new_member_form() {
this.NewMemberModalVisible = true;
var _self = this;
_self.availbleMembers = [];
this.SelectedNewMemberRows = [];
this.NewMemberRows = [];
var data = {
group_id: false,
search: false,
page: false,
size: 10000,
};
if (this.SelectedCloner["pair_type"] == "devices")
_self.data_provider.get_dev_list(data).then((res) => {
_self.availbleMembers = res.filter(
(x: any) => !_self.SelectedClonerItems.includes(x.id)
);
_self.NewMemberModalVisible = true;
});
else
_self.data_provider.get_devgroup_list().then((res) => {
_self.availbleMembers = res.filter(
(x: any) => !_self.SelectedClonerItems.includes(x.id)
);
_self.NewMemberModalVisible = true;
});
}
ngOnInit(): void {
this.initGridTable();
}
submit(action: string) {
var _self = this;
if(_self.master==0 && _self.SelectedCloner['direction']=='oneway'){
_self.show_toast(
"Error",
"Master device is not selected",
"danger"
);
return;
}
if(_self.SelectedCloner['direction']=='twoway' && _self.SelectedCloner['pair_type']=='groups'){
_self.show_toast(
"Error",
"Using Groups is only allowed with Master Mode",
"danger"
);
return;
}
if (_self.master!=0 && _self.SelectedCloner['direction']=='oneway'){
_self.SelectedCloner["masterid"]=_self.master;
}
_self.SelectedCloner["active_commands"]=_self.active_commands;
if (action == "add") {
this.data_provider
.Add_cloner(_self.SelectedCloner, _self.SelectedClonerItems)
.then((res) => {
_self.initGridTable();
});
} else {
this.data_provider
.Edit_cloner(_self.SelectedCloner, _self.SelectedClonerItems)
.then((res) => {
_self.initGridTable();
});
}
this.EditClonerModalVisible = false;
}
onSelectedRowsNewMembers(rows: Array<GuiSelectedRow>): void {
this.NewMemberRows = rows;
this.SelectedNewMemberRows = rows.map((m: GuiSelectedRow) => m.source);
}
add_new_members() {
var _self = this;
_self.SelectedMembers = [
...new Set(_self.SelectedMembers.concat(_self.SelectedNewMemberRows)),
];
_self.SelectedClonerItems = _self.SelectedMembers.map((x: any) => {
return x.id;
});
this.NewMemberModalVisible = false;
}
set_master(id:number){
var _self=this;
this.master=id;
// sort SelectedMembers and put master on top
this.SelectedMembers=
[
...new Set(
this.SelectedMembers.sort((a:any,b:any)=>{
if(a.id==_self.master) return -1;
if(b.id==_self.master) return 1;
return 0;
})
),
];
}
editAddCloner(item: any, action: string) {
if (action == "showadd") {
this.SelectedCloner = {
id: 0,
name: "",
description: "",
pair_type: "devices",
live_mode: false,
schedule: false,
cron: "",
desc_cron: "",
direction: "oneway",
members: "",
action: "add",
};
this.SelectedMembers = [];
this.SelectedClonerItems = [];
this.EditClonerModalVisible = true;
return;
}
var _self = this;
this.SelectedCloner = { ...item };
this.active_commands=item['active_commands']?JSON.parse(item['active_commands']):[];
if (action != "select_change" ) {
this.SelectedCloner["action"] = "edit";
this.data_provider.get_cloner_members(_self.SelectedCloner.id).then((res) => {
_self.SelectedMembers = res;
if(_self.SelectedCloner["master"] && _self.SelectedCloner['direction']=='oneway'){
_self.set_master(_self.SelectedCloner["master"]);
}
_self.EditClonerModalVisible = true;
_self.SelectedClonerItems = res.map((x: any) => {
return x.id;
});
});
} else {
_self.SelectedMembers = [];
this.SelectedClonerItems = [];
}
}
in_active_commands(command:string){
return this.active_commands.includes(command);
}
remove_member(item: any) {
var _self = this;
_self.SelectedMembers = _self.SelectedMembers.filter(
(x: any) => x.id != item.id
);
_self.SelectedClonerItems = _self.SelectedMembers.map((x: any) => {
return x.id;
});
}
get_member_by_id(id: string) {
return this.Members.find((x: any) => x.id == id);
}
confirm_delete(item: any = "", del: boolean = false) {
if (!del) {
this.SelectedCloner = { ...item };
this.DeleteConfirmModalVisible = true;
} else {
var _self = this;
this.data_provider.Delete_cloner(_self.SelectedCloner["id"]).then((res) => {
_self.initGridTable();
_self.DeleteConfirmModalVisible = false;
});
}
}
form_changed() {
this.editAddCloner(this.SelectedCloner, "select_change");
}
logger(item: any) {
console.dir(item);
}
initGridTable(): void {
var _self = this;
this.data_provider.get_cloner_list().then((res) => {
_self.source = res.map((x: any) => {
return x;
});
_self.loading = false;
});
}
}

View file

@ -0,0 +1,44 @@
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { FormsModule,ReactiveFormsModule } from "@angular/forms";
import {
ButtonModule,
CardModule,
FormModule,
GridModule,
ModalModule,
ButtonGroupModule,
ToastModule,
TooltipModule,
NavModule,
TabsModule,
} from "@coreui/angular";
import { ClonerRoutingModule } from "./cloner-routing.module";
import { ClonerComponent } from "./cloner.component";
import { GuiGridModule } from "@generic-ui/ngx-grid";
import { NgxSuperSelectModule} from "ngx-super-select";
@NgModule({
imports: [
ClonerRoutingModule,
CardModule,
CommonModule,
GridModule,
FormModule,
ButtonModule,
ButtonGroupModule,
GuiGridModule,
ModalModule,
ReactiveFormsModule,
FormsModule,
NgxSuperSelectModule,
ToastModule,
TooltipModule,
NavModule,
TabsModule,
],
declarations: [ClonerComponent],
})
export class ClonerModule {}

View file

@ -0,0 +1,133 @@
.nav-underline {
border-bottom: 2px solid var(--cui-nav-underline-border-color, #c4c9d0)
}
.nav-underline .nav-item {
margin-bottom: -2px;
cursor: pointer;
}
.nav-underline .nav-link {
color: var(--cui-nav-underline-link-color, #768192);
border-style: none none solid!important;
border-width:2px;
position:relative;
bottom:-1px;
cursor: pointer;
}
.nav-underline .nav-link:hover,.nav-underline .nav-link:focus {
border-color: var(--cui-nav-underline-link-active-border-color, #321fdb)
}
.nav-underline .nav-link.active,.nav-underline .show>.nav-link {
color: var(--cui-nav-underline-link-active-color, #321fdb);
background: transparent;
border-color: var(--cui-nav-underline-link-active-border-color, #321fdb)
}
.custom-control-label::before, .custom-control-label::after {
top: 0.1rem;
width: 2rem;
height: 1rem;
}
.custom-control-input:checked ~ .custom-control-label::before {
color: #fff;
border-color: #3498db;
background-color: #3498db;
}
.custom-control-input:focus ~ .custom-control-label::before {
box-shadow: 0 0 0 0.2rem rgba(52, 152, 219, 0.25);
}
h5.cloner-sections {
color: #3498db;
margin-bottom: 5px;
font-size: 15px;
font-weight: 600; }
.nav-link {
color: #333;
}
.nav-link.active {
color: #3498db;
border-bottom: 2px solid #3498db;
}
.command-sections c-card-body{
display: flex!important;
justify-content: space-between!important;
padding:5px!important;
align-items: center!important;
justify-content: space-between!important;
border-radius: 4px;
}
.command-sections c-card-body h6{
margin: 0!important;
font-size: 12px!important;
color: var(--primary-color);
white-space: nowrap!important;
overflow: hidden!important;
text-overflow: ellipsis!important;
width: 80%!important;
font-weight: bold;
}
.command-sections c-card{
border: 1px solid #e0e0e0!important;;
border-radius: 4px!important;;
}
/* Checkbox Styling */
.custom-switch {
position: relative;
width: 40px;
height: 20px;
}
.custom-switch input {
display: none;
}
.custom-control-label {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 20px;
background: #ccc;
cursor: pointer;
transition: all 0.3s ease;
}
.custom-control-label::after {
content: '';
position: absolute;
left: 2px;
top: 2px;
width: 16px;
height: 16px;
background: #fff;
border-radius: 50%;
transition: all 0.3s ease;
}
.custom-switch input:checked + .custom-control-label {
background: #3498db;
}
.custom-switch input:checked + .custom-control-label::after {
left: calc(100% - 18px);
}