new changes

This commit is contained in:
Niranjan
2026-04-07 09:46:22 +05:30
parent b679cc3bb5
commit 5e86cc7e40
36 changed files with 1077 additions and 208 deletions

View File

@@ -1 +1 @@
import{j as t}from"./index-cE9w-Kq7.js";function o({variant:l="danger",children:r,className:a="",dismissible:e,onDismiss:s}){return t.jsxs("div",{className:`alert alert-${l} ${e?"alert-dismissible fade show":""} ${a}`.trim(),role:"alert",children:[r,e?t.jsx("button",{type:"button",className:"btn-close","aria-label":"Close",onClick:s}):null]})}export{o as A};
import{j as t}from"./index-CRR9sQ49.js";function o({variant:l="danger",children:r,className:a="",dismissible:e,onDismiss:s}){return t.jsxs("div",{className:`alert alert-${l} ${e?"alert-dismissible fade show":""} ${a}`.trim(),role:"alert",children:[r,e?t.jsx("button",{type:"button",className:"btn-close","aria-label":"Close",onClick:s}):null]})}export{o as A};

View File

@@ -1 +1 @@
import{j as a}from"./index-cE9w-Kq7.js";function i({children:n,variant:r="primary",size:t,type:o="button",disabled:m,className:s="",...u}){return a.jsx("button",{type:o,disabled:m,className:`btn btn-${r}${t?` btn-${t}`:""} ${s}`.trim(),...u,children:n})}export{i as A};
import{j as a}from"./index-CRR9sQ49.js";function i({children:n,variant:r="primary",size:t,type:o="button",disabled:m,className:s="",...u}){return a.jsx("button",{type:o,disabled:m,className:`btn btn-${r}${t?` btn-${t}`:""} ${s}`.trim(),...u,children:n})}export{i as A};

View File

@@ -1 +1 @@
import{j as e}from"./index-cE9w-Kq7.js";function c({title:s,iconClass:a,children:i,headerExtra:r,className:d="",bodyClassName:l=""}){return e.jsxs("div",{className:`card flex-fill ${d}`.trim(),children:[(s||r)&&e.jsxs("div",{className:"card-header border-0 pb-0 d-flex align-items-center justify-content-between flex-wrap gap-2",children:[e.jsxs("h4",{className:"mb-0 d-flex align-items-center gap-2",children:[a?e.jsx("i",{className:a,"aria-hidden":!0}):null,s]}),r]}),e.jsx("div",{className:`card-body ${l}`.trim(),children:i})]})}export{c as A};
import{j as e}from"./index-CRR9sQ49.js";function c({title:s,iconClass:a,children:i,headerExtra:r,className:d="",bodyClassName:l=""}){return e.jsxs("div",{className:`card flex-fill ${d}`.trim(),children:[(s||r)&&e.jsxs("div",{className:"card-header border-0 pb-0 d-flex align-items-center justify-content-between flex-wrap gap-2",children:[e.jsxs("h4",{className:"mb-0 d-flex align-items-center gap-2",children:[a?e.jsx("i",{className:a,"aria-hidden":!0}):null,s]}),r]}),e.jsx("div",{className:`card-body ${l}`.trim(),children:i})]})}export{c as A};

View File

@@ -1 +1 @@
import{j as t}from"./index-cE9w-Kq7.js";function l({children:r,className:s="",responsive:a=!0}){const e=t.jsx("table",{className:`table table-hover ${s}`.trim(),children:r});return a?t.jsx("div",{className:"table-responsive",children:e}):e}export{l as A};
import{j as t}from"./index-CRR9sQ49.js";function l({children:r,className:s="",responsive:a=!0}){const e=t.jsx("table",{className:`table table-hover ${s}`.trim(),children:r});return a?t.jsx("div",{className:"table-responsive",children:e}):e}export{l as A};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
import{r as i,g as m,j as s}from"./index-cE9w-Kq7.js";import{A as n}from"./AdminAlert-yrdXFH0e.js";import{A as o}from"./AdminCard-BvdkSQBp.js";import{P as d}from"./PageHeader-HdM4gpcn.js";function p(){const[e,r]=i.useState(null),[a,l]=i.useState("");return i.useEffect(()=>{m().then(r).catch(c=>l(c.message))},[]),a?s.jsxs(s.Fragment,{children:[s.jsx(d,{}),s.jsx(n,{variant:"danger",children:a})]}):e?s.jsxs(s.Fragment,{children:[s.jsx(d,{}),s.jsxs("div",{className:"row g-3 mb-4",children:[s.jsx("div",{className:"col-md-4 d-flex",children:s.jsx(t,{iconClass:"ti ti-world",title:"Websites",value:e.site_count})}),s.jsx("div",{className:"col-md-4 d-flex",children:s.jsx(t,{iconClass:"ti ti-folder-share",title:"FTP Accounts",value:e.ftp_count})}),s.jsx("div",{className:"col-md-4 d-flex",children:s.jsx(t,{iconClass:"ti ti-database",title:"Databases",value:e.database_count})})]}),s.jsxs("div",{className:"row g-3",children:[s.jsx("div",{className:"col-md-4 d-flex",children:s.jsx(t,{iconClass:"ti ti-cpu",title:"CPU",value:`${e.system.cpu_percent}%`})}),s.jsx("div",{className:"col-md-4 d-flex",children:s.jsx(t,{iconClass:"ti ti-device-desktop",title:"Memory",value:`${e.system.memory_percent}%`,subtitle:`${e.system.memory_used_mb} / ${e.system.memory_total_mb} MB`})}),s.jsx("div",{className:"col-md-4 d-flex",children:s.jsx(t,{iconClass:"ti ti-database-export",title:"Disk",value:`${e.system.disk_percent}%`,subtitle:`${e.system.disk_used_gb} / ${e.system.disk_total_gb} GB`})})]})]}):s.jsxs(s.Fragment,{children:[s.jsx(d,{}),s.jsx("div",{className:"placeholder-glow",children:s.jsx("span",{className:"placeholder col-12 rounded",style:{height:"8rem"}})})]})}function t({iconClass:e,title:r,value:a,subtitle:l}){return s.jsxs(o,{className:"border-0 shadow-sm",bodyClassName:"d-flex align-items-center gap-3",children:[s.jsx("span",{className:"avatar avatar-md bg-primary-transparent text-primary rounded-circle d-flex align-items-center justify-content-center flex-shrink-0",children:s.jsx("i",{className:`${e} fs-4`,"aria-hidden":!0})}),s.jsxs("div",{children:[s.jsx("p",{className:"text-muted mb-0 small",children:r}),s.jsx("p",{className:"fs-4 fw-semibold mb-0",children:a}),l?s.jsx("p",{className:"text-muted small mb-0",children:l}):null]})]})}export{p as DashboardPage};
import{r as i,g as m,j as s}from"./index-CRR9sQ49.js";import{A as n}from"./AdminAlert-DW1IRWce.js";import{A as o}from"./AdminCard-DNA70pGd.js";import{P as d}from"./PageHeader-BcjNf7GG.js";function p(){const[e,r]=i.useState(null),[a,l]=i.useState("");return i.useEffect(()=>{m().then(r).catch(c=>l(c.message))},[]),a?s.jsxs(s.Fragment,{children:[s.jsx(d,{}),s.jsx(n,{variant:"danger",children:a})]}):e?s.jsxs(s.Fragment,{children:[s.jsx(d,{}),s.jsxs("div",{className:"row g-3 mb-4",children:[s.jsx("div",{className:"col-md-4 d-flex",children:s.jsx(t,{iconClass:"ti ti-world",title:"Websites",value:e.site_count})}),s.jsx("div",{className:"col-md-4 d-flex",children:s.jsx(t,{iconClass:"ti ti-folder-share",title:"FTP Accounts",value:e.ftp_count})}),s.jsx("div",{className:"col-md-4 d-flex",children:s.jsx(t,{iconClass:"ti ti-database",title:"Databases",value:e.database_count})})]}),s.jsxs("div",{className:"row g-3",children:[s.jsx("div",{className:"col-md-4 d-flex",children:s.jsx(t,{iconClass:"ti ti-cpu",title:"CPU",value:`${e.system.cpu_percent}%`})}),s.jsx("div",{className:"col-md-4 d-flex",children:s.jsx(t,{iconClass:"ti ti-device-desktop",title:"Memory",value:`${e.system.memory_percent}%`,subtitle:`${e.system.memory_used_mb} / ${e.system.memory_total_mb} MB`})}),s.jsx("div",{className:"col-md-4 d-flex",children:s.jsx(t,{iconClass:"ti ti-database-export",title:"Disk",value:`${e.system.disk_percent}%`,subtitle:`${e.system.disk_used_gb} / ${e.system.disk_total_gb} GB`})})]})]}):s.jsxs(s.Fragment,{children:[s.jsx(d,{}),s.jsx("div",{className:"placeholder-glow",children:s.jsx("span",{className:"placeholder col-12 rounded",style:{height:"8rem"}})})]})}function t({iconClass:e,title:r,value:a,subtitle:l}){return s.jsxs(o,{className:"border-0 shadow-sm",bodyClassName:"d-flex align-items-center gap-3",children:[s.jsx("span",{className:"avatar avatar-md bg-primary-transparent text-primary rounded-circle d-flex align-items-center justify-content-center flex-shrink-0",children:s.jsx("i",{className:`${e} fs-4`,"aria-hidden":!0})}),s.jsxs("div",{children:[s.jsx("p",{className:"text-muted mb-0 small",children:r}),s.jsx("p",{className:"fs-4 fw-semibold mb-0",children:a}),l?s.jsx("p",{className:"text-muted small mb-0",children:l}):null]})]})}export{p as DashboardPage};

View File

@@ -1 +1 @@
import{r as t,j as e,a as o}from"./index-cE9w-Kq7.js";import{M as l}from"./Modal-CL3xZqxR.js";import{A as R}from"./AdminAlert-yrdXFH0e.js";import{A as m}from"./AdminButton-ByutG8m-.js";import{P as N}from"./PageHeader-HdM4gpcn.js";function k(){const[d,y]=t.useState([]),[c,b]=t.useState([]),[v,u]=t.useState(!0),[x,h]=t.useState(""),[i,p]=t.useState(null),[a,n]=t.useState(null),[j,f]=t.useState(""),g=()=>{u(!0),Promise.all([o("/ssl/domains"),o("/ssl/certificates")]).then(([s,r])=>{y(s),b(r.certificates||[])}).catch(s=>h(s.message)).finally(()=>u(!1))};t.useEffect(()=>{g()},[]);const S=s=>{s.preventDefault(),a&&(p(a.name),o("/ssl/request",{method:"POST",body:JSON.stringify({domain:a.name,webroot:a.site_path,email:j})}).then(()=>{n(null),g()}).catch(r=>h(r.message)).finally(()=>p(null)))},q=s=>c.some(r=>r.name===s||r.name.startsWith(s+" "));return v?e.jsxs(e.Fragment,{children:[e.jsx(N,{title:"Domains & SSL"}),e.jsx("p",{className:"text-secondary",children:"Loading…"})]}):e.jsxs(e.Fragment,{children:[e.jsx(N,{title:"Domains & SSL"}),x?e.jsx(R,{className:"mb-3",children:x}):null,e.jsx("div",{className:"alert alert-warning small mb-4",children:"Request Let's Encrypt certificates for your site domains. Requires certbot and nginx configured for the domain."}),e.jsxs("div",{className:"row g-4",children:[e.jsx("div",{className:"col-lg-6",children:e.jsxs("div",{className:"card h-100",children:[e.jsx("div",{className:"card-header",children:"Domains (from sites)"}),e.jsx("div",{className:"list-group list-group-flush overflow-auto",style:{maxHeight:"20rem"},children:d.length===0?e.jsx("div",{className:"list-group-item text-secondary text-center py-4",children:"No domains. Add a site first."}):d.map(s=>e.jsxs("div",{className:"list-group-item d-flex align-items-center justify-content-between gap-2 flex-wrap",children:[e.jsxs("div",{className:"small",children:[e.jsx("span",{className:"font-monospace",children:s.name}),s.port!=="80"?e.jsxs("span",{className:"text-secondary ms-1",children:[":",s.port]}):null,e.jsxs("span",{className:"text-secondary ms-2",children:["(",s.site_name,")"]})]}),e.jsx("div",{children:q(s.name)?e.jsxs("span",{className:"text-success small",children:[e.jsx("i",{className:"ti ti-shield-check me-1","aria-hidden":!0}),"Cert"]}):e.jsx(m,{variant:"outline-primary",size:"sm",disabled:!!i,onClick:()=>{n(s),f("")},children:i===s.name?e.jsx("span",{className:"spinner-border spinner-border-sm",role:"status"}):"Request SSL"})})]},s.id))})]})}),e.jsx("div",{className:"col-lg-6",children:e.jsxs("div",{className:"card h-100",children:[e.jsx("div",{className:"card-header",children:"Certificates"}),e.jsx("div",{className:"list-group list-group-flush overflow-auto",style:{maxHeight:"20rem"},children:c.length===0?e.jsx("div",{className:"list-group-item text-secondary text-center py-4",children:"No certificates yet"}):c.map(s=>e.jsxs("div",{className:"list-group-item d-flex align-items-center gap-2",children:[e.jsx("i",{className:"ti ti-shield-check text-success flex-shrink-0","aria-hidden":!0}),e.jsx("span",{className:"font-monospace small text-break",children:s.name})]},s.name))})]})})]}),e.jsxs(l,{show:!!a,onHide:()=>n(null),centered:!0,children:[e.jsx(l.Header,{closeButton:!0,children:e.jsxs(l.Title,{children:["Request SSL for ",a==null?void 0:a.name]})}),a?e.jsxs("form",{onSubmit:S,children:[e.jsxs(l.Body,{children:[e.jsxs("div",{className:"mb-3",children:[e.jsx("label",{className:"form-label",children:"Domain"}),e.jsx("input",{type:"text",value:a.name,readOnly:!0,className:"form-control-plaintext border rounded px-3 py-2 bg-body-secondary"})]}),e.jsxs("div",{className:"mb-3",children:[e.jsx("label",{className:"form-label",children:"Webroot (site path)"}),e.jsx("input",{type:"text",value:a.site_path,readOnly:!0,className:"form-control-plaintext border rounded px-3 py-2 bg-body-secondary"})]}),e.jsxs("div",{className:"mb-0",children:[e.jsx("label",{className:"form-label",children:"Email (for Let's Encrypt)"}),e.jsx("input",{type:"email",value:j,onChange:s=>f(s.target.value),placeholder:"admin@example.com",className:"form-control",required:!0})]})]}),e.jsxs(l.Footer,{children:[e.jsx(m,{type:"button",variant:"secondary",onClick:()=>n(null),children:"Cancel"}),e.jsx(m,{type:"submit",variant:"primary",disabled:!!i,children:i?"Requesting…":"Request"})]})]}):null]})]})}export{k as DomainsPage};
import{r as t,j as e,a as o}from"./index-CRR9sQ49.js";import{M as l}from"./Modal-B7V4w_St.js";import{A as R}from"./AdminAlert-DW1IRWce.js";import{A as m}from"./AdminButton-Bd2cLTu3.js";import{P as N}from"./PageHeader-BcjNf7GG.js";function k(){const[d,y]=t.useState([]),[c,b]=t.useState([]),[v,u]=t.useState(!0),[x,h]=t.useState(""),[i,p]=t.useState(null),[a,n]=t.useState(null),[j,f]=t.useState(""),g=()=>{u(!0),Promise.all([o("/ssl/domains"),o("/ssl/certificates")]).then(([s,r])=>{y(s),b(r.certificates||[])}).catch(s=>h(s.message)).finally(()=>u(!1))};t.useEffect(()=>{g()},[]);const S=s=>{s.preventDefault(),a&&(p(a.name),o("/ssl/request",{method:"POST",body:JSON.stringify({domain:a.name,webroot:a.site_path,email:j})}).then(()=>{n(null),g()}).catch(r=>h(r.message)).finally(()=>p(null)))},q=s=>c.some(r=>r.name===s||r.name.startsWith(s+" "));return v?e.jsxs(e.Fragment,{children:[e.jsx(N,{title:"Domains & SSL"}),e.jsx("p",{className:"text-secondary",children:"Loading…"})]}):e.jsxs(e.Fragment,{children:[e.jsx(N,{title:"Domains & SSL"}),x?e.jsx(R,{className:"mb-3",children:x}):null,e.jsx("div",{className:"alert alert-warning small mb-4",children:"Request Let's Encrypt certificates for your site domains. Requires certbot and nginx configured for the domain."}),e.jsxs("div",{className:"row g-4",children:[e.jsx("div",{className:"col-lg-6",children:e.jsxs("div",{className:"card h-100",children:[e.jsx("div",{className:"card-header",children:"Domains (from sites)"}),e.jsx("div",{className:"list-group list-group-flush overflow-auto",style:{maxHeight:"20rem"},children:d.length===0?e.jsx("div",{className:"list-group-item text-secondary text-center py-4",children:"No domains. Add a site first."}):d.map(s=>e.jsxs("div",{className:"list-group-item d-flex align-items-center justify-content-between gap-2 flex-wrap",children:[e.jsxs("div",{className:"small",children:[e.jsx("span",{className:"font-monospace",children:s.name}),s.port!=="80"?e.jsxs("span",{className:"text-secondary ms-1",children:[":",s.port]}):null,e.jsxs("span",{className:"text-secondary ms-2",children:["(",s.site_name,")"]})]}),e.jsx("div",{children:q(s.name)?e.jsxs("span",{className:"text-success small",children:[e.jsx("i",{className:"ti ti-shield-check me-1","aria-hidden":!0}),"Cert"]}):e.jsx(m,{variant:"outline-primary",size:"sm",disabled:!!i,onClick:()=>{n(s),f("")},children:i===s.name?e.jsx("span",{className:"spinner-border spinner-border-sm",role:"status"}):"Request SSL"})})]},s.id))})]})}),e.jsx("div",{className:"col-lg-6",children:e.jsxs("div",{className:"card h-100",children:[e.jsx("div",{className:"card-header",children:"Certificates"}),e.jsx("div",{className:"list-group list-group-flush overflow-auto",style:{maxHeight:"20rem"},children:c.length===0?e.jsx("div",{className:"list-group-item text-secondary text-center py-4",children:"No certificates yet"}):c.map(s=>e.jsxs("div",{className:"list-group-item d-flex align-items-center gap-2",children:[e.jsx("i",{className:"ti ti-shield-check text-success flex-shrink-0","aria-hidden":!0}),e.jsx("span",{className:"font-monospace small text-break",children:s.name})]},s.name))})]})})]}),e.jsxs(l,{show:!!a,onHide:()=>n(null),centered:!0,children:[e.jsx(l.Header,{closeButton:!0,children:e.jsxs(l.Title,{children:["Request SSL for ",a==null?void 0:a.name]})}),a?e.jsxs("form",{onSubmit:S,children:[e.jsxs(l.Body,{children:[e.jsxs("div",{className:"mb-3",children:[e.jsx("label",{className:"form-label",children:"Domain"}),e.jsx("input",{type:"text",value:a.name,readOnly:!0,className:"form-control-plaintext border rounded px-3 py-2 bg-body-secondary"})]}),e.jsxs("div",{className:"mb-3",children:[e.jsx("label",{className:"form-label",children:"Webroot (site path)"}),e.jsx("input",{type:"text",value:a.site_path,readOnly:!0,className:"form-control-plaintext border rounded px-3 py-2 bg-body-secondary"})]}),e.jsxs("div",{className:"mb-0",children:[e.jsx("label",{className:"form-label",children:"Email (for Let's Encrypt)"}),e.jsx("input",{type:"email",value:j,onChange:s=>f(s.target.value),placeholder:"admin@example.com",className:"form-control",required:!0})]})]}),e.jsxs(l.Footer,{children:[e.jsx(m,{type:"button",variant:"secondary",onClick:()=>n(null),children:"Cancel"}),e.jsx(m,{type:"submit",variant:"primary",disabled:!!i,children:i?"Requesting…":"Request"})]})]}):null]})]})}export{k as DomainsPage};

View File

@@ -1 +1 @@
import{j as t}from"./index-cE9w-Kq7.js";function m({iconClass:s="ti ti-inbox",title:a,description:e,action:i}){return t.jsxs("div",{className:"text-center py-5 text-muted",children:[t.jsx("i",{className:`${s} display-4 d-block mb-3`,"aria-hidden":!0}),t.jsx("h5",{className:"text-body",children:a}),e?t.jsx("p",{className:"mb-3",children:e}):null,i]})}export{m as E};
import{j as t}from"./index-CRR9sQ49.js";function m({iconClass:s="ti ti-inbox",title:a,description:e,action:i}){return t.jsxs("div",{className:"text-center py-5 text-muted",children:[t.jsx("i",{className:`${s} display-4 d-block mb-3`,"aria-hidden":!0}),t.jsx("h5",{className:"text-body",children:a}),e?t.jsx("p",{className:"mb-3",children:e}):null,i]})}export{m as E};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
import{r as t,j as e,a as p,N as k}from"./index-cE9w-Kq7.js";import{M as r}from"./Modal-CL3xZqxR.js";import{A as y}from"./AdminAlert-yrdXFH0e.js";import{A as c}from"./AdminButton-ByutG8m-.js";import{A as D}from"./AdminTable-eCi7S__-.js";import{E as H}from"./EmptyState-CmnFWkSO.js";import{P as g}from"./PageHeader-HdM4gpcn.js";function W(){const[o,v]=t.useState([]),[A,x]=t.useState(!0),[u,d]=t.useState(""),[w,a]=t.useState(!1),[j,f]=t.useState(!1),[N,m]=t.useState(""),[h,b]=t.useState(!1),n=()=>{x(!0),p("/firewall/list").then(v).catch(s=>d(s.message)).finally(()=>x(!1))};t.useEffect(()=>{n()},[]);const S=s=>{s.preventDefault();const l=s.currentTarget,i=l.elements.namedItem("port").value.trim(),E=l.elements.namedItem("protocol").value,F=l.elements.namedItem("action").value,R=l.elements.namedItem("ps").value.trim();if(!i){m("Port is required");return}f(!0),m(""),p("/firewall/create",{method:"POST",body:JSON.stringify({port:i,protocol:E,action:F,ps:R})}).then(()=>{a(!1),l.reset(),n()}).catch(T=>m(T.message)).finally(()=>f(!1))},C=(s,l)=>{confirm(`Delete rule for port ${l}?`)&&p(`/firewall/${s}`,{method:"DELETE"}).then(n).catch(i=>d(i.message))},P=()=>{b(!0),k().then(()=>n()).catch(s=>d(s.message)).finally(()=>b(!1))};return A?e.jsxs(e.Fragment,{children:[e.jsx(g,{title:"Security / Firewall"}),e.jsx("p",{className:"text-secondary",children:"Loading…"})]}):e.jsxs(e.Fragment,{children:[e.jsx(g,{title:"Security / Firewall",actions:e.jsxs("div",{className:"d-flex flex-wrap gap-2",children:[e.jsxs(c,{variant:"success",disabled:h||o.length===0,onClick:P,children:[h?e.jsx("span",{className:"spinner-border spinner-border-sm me-1",role:"status"}):e.jsx("i",{className:"ti ti-bolt me-1","aria-hidden":!0}),h?"Applying…":"Apply to UFW"]}),e.jsxs(c,{variant:"primary",onClick:()=>a(!0),children:[e.jsx("i",{className:"ti ti-plus me-1","aria-hidden":!0}),"Add Rule"]})]})}),u?e.jsx(y,{className:"mb-3",children:u}):null,e.jsxs("div",{className:"alert alert-warning small mb-3",children:['Rules are stored in the panel. Click "Apply to UFW" to run ',e.jsx("code",{className:"font-monospace",children:"ufw allow/deny"})," for each rule."]}),e.jsxs(r,{show:w,onHide:()=>a(!1),centered:!0,children:[e.jsx(r.Header,{closeButton:!0,children:e.jsx(r.Title,{children:"Add Firewall Rule"})}),e.jsxs("form",{onSubmit:S,children:[e.jsxs(r.Body,{children:[N?e.jsx(y,{className:"mb-3",children:N}):null,e.jsxs("div",{className:"mb-3",children:[e.jsx("label",{className:"form-label",children:"Port"}),e.jsx("input",{name:"port",type:"text",placeholder:"80 or 80-90 or 80,443",className:"form-control",required:!0})]}),e.jsxs("div",{className:"mb-3",children:[e.jsx("label",{className:"form-label",children:"Protocol"}),e.jsxs("select",{name:"protocol",className:"form-select",children:[e.jsx("option",{value:"tcp",children:"TCP"}),e.jsx("option",{value:"udp",children:"UDP"})]})]}),e.jsxs("div",{className:"mb-3",children:[e.jsx("label",{className:"form-label",children:"Action"}),e.jsxs("select",{name:"action",className:"form-select",children:[e.jsx("option",{value:"accept",children:"Accept"}),e.jsx("option",{value:"drop",children:"Drop"}),e.jsx("option",{value:"reject",children:"Reject"})]})]}),e.jsxs("div",{className:"mb-0",children:[e.jsx("label",{className:"form-label",children:"Note (optional)"}),e.jsx("input",{name:"ps",type:"text",placeholder:"HTTP",className:"form-control"})]})]}),e.jsxs(r.Footer,{children:[e.jsx(c,{type:"button",variant:"secondary",onClick:()=>a(!1),children:"Cancel"}),e.jsx(c,{type:"submit",variant:"primary",disabled:j,children:j?"Adding…":"Add"})]})]})]}),e.jsx("div",{className:"card",children:e.jsxs(D,{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Port"}),e.jsx("th",{children:"Protocol"}),e.jsx("th",{children:"Action"}),e.jsx("th",{children:"Note"}),e.jsx("th",{className:"text-end",children:"Actions"})]})}),e.jsx("tbody",{children:o.length===0?e.jsx("tr",{children:e.jsx("td",{colSpan:5,className:"p-0",children:e.jsx(H,{title:"No rules",description:'Click "Add Rule" to create one.'})})}):o.map(s=>e.jsxs("tr",{children:[e.jsx("td",{className:"font-monospace",children:s.port}),e.jsx("td",{children:s.protocol}),e.jsx("td",{children:s.action}),e.jsx("td",{children:s.ps||"—"}),e.jsx("td",{className:"text-end",children:e.jsx("button",{type:"button",className:"btn btn-link btn-sm text-danger p-1",title:"Delete",onClick:()=>C(s.id,s.port),children:e.jsx("i",{className:"ti ti-trash","aria-hidden":!0})})})]},s.id))})]})})]})}export{W as FirewallPage};
import{r as t,j as e,a as p,V as k}from"./index-CRR9sQ49.js";import{M as r}from"./Modal-B7V4w_St.js";import{A as y}from"./AdminAlert-DW1IRWce.js";import{A as c}from"./AdminButton-Bd2cLTu3.js";import{A as D}from"./AdminTable-BLiLxfnS.js";import{E as H}from"./EmptyState-C61VdEFl.js";import{P as g}from"./PageHeader-BcjNf7GG.js";function W(){const[o,v]=t.useState([]),[A,x]=t.useState(!0),[u,d]=t.useState(""),[w,a]=t.useState(!1),[j,f]=t.useState(!1),[N,m]=t.useState(""),[h,b]=t.useState(!1),n=()=>{x(!0),p("/firewall/list").then(v).catch(s=>d(s.message)).finally(()=>x(!1))};t.useEffect(()=>{n()},[]);const S=s=>{s.preventDefault();const l=s.currentTarget,i=l.elements.namedItem("port").value.trim(),E=l.elements.namedItem("protocol").value,F=l.elements.namedItem("action").value,R=l.elements.namedItem("ps").value.trim();if(!i){m("Port is required");return}f(!0),m(""),p("/firewall/create",{method:"POST",body:JSON.stringify({port:i,protocol:E,action:F,ps:R})}).then(()=>{a(!1),l.reset(),n()}).catch(T=>m(T.message)).finally(()=>f(!1))},C=(s,l)=>{confirm(`Delete rule for port ${l}?`)&&p(`/firewall/${s}`,{method:"DELETE"}).then(n).catch(i=>d(i.message))},P=()=>{b(!0),k().then(()=>n()).catch(s=>d(s.message)).finally(()=>b(!1))};return A?e.jsxs(e.Fragment,{children:[e.jsx(g,{title:"Security / Firewall"}),e.jsx("p",{className:"text-secondary",children:"Loading…"})]}):e.jsxs(e.Fragment,{children:[e.jsx(g,{title:"Security / Firewall",actions:e.jsxs("div",{className:"d-flex flex-wrap gap-2",children:[e.jsxs(c,{variant:"success",disabled:h||o.length===0,onClick:P,children:[h?e.jsx("span",{className:"spinner-border spinner-border-sm me-1",role:"status"}):e.jsx("i",{className:"ti ti-bolt me-1","aria-hidden":!0}),h?"Applying…":"Apply to UFW"]}),e.jsxs(c,{variant:"primary",onClick:()=>a(!0),children:[e.jsx("i",{className:"ti ti-plus me-1","aria-hidden":!0}),"Add Rule"]})]})}),u?e.jsx(y,{className:"mb-3",children:u}):null,e.jsxs("div",{className:"alert alert-warning small mb-3",children:['Rules are stored in the panel. Click "Apply to UFW" to run ',e.jsx("code",{className:"font-monospace",children:"ufw allow/deny"})," for each rule."]}),e.jsxs(r,{show:w,onHide:()=>a(!1),centered:!0,children:[e.jsx(r.Header,{closeButton:!0,children:e.jsx(r.Title,{children:"Add Firewall Rule"})}),e.jsxs("form",{onSubmit:S,children:[e.jsxs(r.Body,{children:[N?e.jsx(y,{className:"mb-3",children:N}):null,e.jsxs("div",{className:"mb-3",children:[e.jsx("label",{className:"form-label",children:"Port"}),e.jsx("input",{name:"port",type:"text",placeholder:"80 or 80-90 or 80,443",className:"form-control",required:!0})]}),e.jsxs("div",{className:"mb-3",children:[e.jsx("label",{className:"form-label",children:"Protocol"}),e.jsxs("select",{name:"protocol",className:"form-select",children:[e.jsx("option",{value:"tcp",children:"TCP"}),e.jsx("option",{value:"udp",children:"UDP"})]})]}),e.jsxs("div",{className:"mb-3",children:[e.jsx("label",{className:"form-label",children:"Action"}),e.jsxs("select",{name:"action",className:"form-select",children:[e.jsx("option",{value:"accept",children:"Accept"}),e.jsx("option",{value:"drop",children:"Drop"}),e.jsx("option",{value:"reject",children:"Reject"})]})]}),e.jsxs("div",{className:"mb-0",children:[e.jsx("label",{className:"form-label",children:"Note (optional)"}),e.jsx("input",{name:"ps",type:"text",placeholder:"HTTP",className:"form-control"})]})]}),e.jsxs(r.Footer,{children:[e.jsx(c,{type:"button",variant:"secondary",onClick:()=>a(!1),children:"Cancel"}),e.jsx(c,{type:"submit",variant:"primary",disabled:j,children:j?"Adding…":"Add"})]})]})]}),e.jsx("div",{className:"card",children:e.jsxs(D,{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Port"}),e.jsx("th",{children:"Protocol"}),e.jsx("th",{children:"Action"}),e.jsx("th",{children:"Note"}),e.jsx("th",{className:"text-end",children:"Actions"})]})}),e.jsx("tbody",{children:o.length===0?e.jsx("tr",{children:e.jsx("td",{colSpan:5,className:"p-0",children:e.jsx(H,{title:"No rules",description:'Click "Add Rule" to create one.'})})}):o.map(s=>e.jsxs("tr",{children:[e.jsx("td",{className:"font-monospace",children:s.port}),e.jsx("td",{children:s.protocol}),e.jsx("td",{children:s.action}),e.jsx("td",{children:s.ps||"—"}),e.jsx("td",{className:"text-end",children:e.jsx("button",{type:"button",className:"btn btn-link btn-sm text-danger p-1",title:"Delete",onClick:()=>C(s.id,s.port),children:e.jsx("i",{className:"ti ti-trash","aria-hidden":!0})})})]},s.id))})]})})]})}export{W as FirewallPage};

View File

@@ -1 +1 @@
import{r as t,L as p,j as e,M as C}from"./index-cE9w-Kq7.js";import{A as P}from"./AdminAlert-yrdXFH0e.js";import{A as g}from"./AdminButton-ByutG8m-.js";import{P as E}from"./PageHeader-HdM4gpcn.js";function _(a){return a<1024?a+" B":a<1024*1024?(a/1024).toFixed(1)+" KB":(a/1024/1024).toFixed(1)+" MB"}function $(){const[a,j]=t.useState("/"),[m,N]=t.useState([]),[v,x]=t.useState(!0),[h,i]=t.useState(""),[l,y]=t.useState(null),[b,u]=t.useState(""),[o,r]=t.useState(!1),[c,w]=t.useState(500),d=s=>{x(!0),i(""),C(s).then(n=>{j(n.path),N(n.items.sort((f,F)=>f.is_dir===F.is_dir?0:f.is_dir?-1:1))}).catch(n=>i(n.message)).finally(()=>x(!1))};t.useEffect(()=>{d(a)},[]),t.useEffect(()=>{l&&(r(!0),p(l,c).then(s=>u(s.content)).catch(s=>i(s.message)).finally(()=>r(!1)))},[l,c]);const k=s=>{if(s.is_dir){const n=a==="/"?"/"+s.name:a+"/"+s.name;d(n)}else y(a==="/"?s.name:a+"/"+s.name)},L=()=>{const s=a.replace(/\/$/,"").split("/").filter(Boolean);if(s.length<=1)return;s.pop();const n=s.length===0?"/":"/"+s.join("/");d(n)},S=()=>{l&&(r(!0),p(l,c).then(s=>u(s.content)).catch(s=>i(s.message)).finally(()=>r(!1)))},B=a.split("/").filter(Boolean).length>0;return e.jsxs(e.Fragment,{children:[e.jsx(E,{title:"Logs"}),e.jsxs("div",{className:"d-flex flex-wrap align-items-center gap-2 mb-3",children:[e.jsxs(g,{variant:"secondary",size:"sm",onClick:L,disabled:!B,children:[e.jsx("i",{className:"ti ti-arrow-left me-1","aria-hidden":!0}),"Back"]}),e.jsxs("code",{className:"small bg-body-secondary px-2 py-1 rounded text-break",children:["Path: ",a||"/"]})]}),h?e.jsx(P,{className:"mb-3",children:h}):null,e.jsxs("div",{className:"row g-3",children:[e.jsx("div",{className:"col-lg-6",children:e.jsxs("div",{className:"card h-100",children:[e.jsx("div",{className:"card-header small fw-medium",children:"Log files"}),v?e.jsx("div",{className:"card-body text-center py-5",children:e.jsx("span",{className:"spinner-border text-secondary",role:"status"})}):e.jsx("div",{className:"list-group list-group-flush overflow-auto",style:{maxHeight:500},children:m.length===0?e.jsx("div",{className:"list-group-item text-secondary text-center py-4",children:"Empty directory"}):m.map(s=>e.jsxs("button",{type:"button",className:"list-group-item list-group-item-action d-flex gap-2 align-items-center",onClick:()=>k(s),children:[e.jsx("i",{className:`ti flex-shrink-0 ${s.is_dir?"ti-folder text-warning":"ti-file text-secondary"}`,"aria-hidden":!0}),e.jsx("span",{className:"text-truncate",children:s.name}),s.is_dir?null:e.jsx("span",{className:"small text-secondary ms-auto flex-shrink-0",children:_(s.size)})]},s.name))})]})}),e.jsx("div",{className:"col-lg-6",children:e.jsxs("div",{className:"card h-100 d-flex flex-column",style:{minHeight:400},children:[e.jsxs("div",{className:"card-header d-flex align-items-center justify-content-between gap-2 flex-wrap",children:[e.jsx("span",{className:"small fw-medium text-truncate",children:l||"Select a log file"}),l?e.jsxs("div",{className:"d-flex align-items-center gap-2 flex-shrink-0",children:[e.jsx("label",{className:"small text-secondary mb-0",children:"Lines:"}),e.jsxs("select",{value:c,onChange:s=>w(Number(s.target.value)),className:"form-select form-select-sm",style:{width:"auto"},children:[e.jsx("option",{value:100,children:"100"}),e.jsx("option",{value:500,children:"500"}),e.jsx("option",{value:1e3,children:"1000"}),e.jsx("option",{value:5e3,children:"5000"}),e.jsx("option",{value:1e4,children:"10000"})]}),e.jsx(g,{variant:"light",size:"sm",onClick:S,disabled:o,title:"Refresh",children:o?e.jsx("span",{className:"spinner-border spinner-border-sm",role:"status"}):e.jsx("i",{className:"ti ti-refresh","aria-hidden":!0})})]}):null]}),e.jsx("div",{className:"card-body flex-grow-1 overflow-auto",children:l?o?e.jsx("div",{className:"text-center py-5",children:e.jsx("span",{className:"spinner-border text-secondary",role:"status"})}):e.jsx("pre",{className:"font-monospace small mb-0 text-break",style:{whiteSpace:"pre-wrap"},children:b||"(empty)"}):e.jsx("p",{className:"text-secondary small mb-0",children:"Click a log file to view"})})]})})]})]})}export{$ as LogsPage};
import{r as t,T as p,j as e,U as C}from"./index-CRR9sQ49.js";import{A as P}from"./AdminAlert-DW1IRWce.js";import{A as g}from"./AdminButton-Bd2cLTu3.js";import{P as E}from"./PageHeader-BcjNf7GG.js";function _(a){return a<1024?a+" B":a<1024*1024?(a/1024).toFixed(1)+" KB":(a/1024/1024).toFixed(1)+" MB"}function $(){const[a,j]=t.useState("/"),[m,N]=t.useState([]),[v,x]=t.useState(!0),[h,i]=t.useState(""),[l,y]=t.useState(null),[b,u]=t.useState(""),[o,r]=t.useState(!1),[c,w]=t.useState(500),d=s=>{x(!0),i(""),C(s).then(n=>{j(n.path),N(n.items.sort((f,F)=>f.is_dir===F.is_dir?0:f.is_dir?-1:1))}).catch(n=>i(n.message)).finally(()=>x(!1))};t.useEffect(()=>{d(a)},[]),t.useEffect(()=>{l&&(r(!0),p(l,c).then(s=>u(s.content)).catch(s=>i(s.message)).finally(()=>r(!1)))},[l,c]);const k=s=>{if(s.is_dir){const n=a==="/"?"/"+s.name:a+"/"+s.name;d(n)}else y(a==="/"?s.name:a+"/"+s.name)},L=()=>{const s=a.replace(/\/$/,"").split("/").filter(Boolean);if(s.length<=1)return;s.pop();const n=s.length===0?"/":"/"+s.join("/");d(n)},S=()=>{l&&(r(!0),p(l,c).then(s=>u(s.content)).catch(s=>i(s.message)).finally(()=>r(!1)))},B=a.split("/").filter(Boolean).length>0;return e.jsxs(e.Fragment,{children:[e.jsx(E,{title:"Logs"}),e.jsxs("div",{className:"d-flex flex-wrap align-items-center gap-2 mb-3",children:[e.jsxs(g,{variant:"secondary",size:"sm",onClick:L,disabled:!B,children:[e.jsx("i",{className:"ti ti-arrow-left me-1","aria-hidden":!0}),"Back"]}),e.jsxs("code",{className:"small bg-body-secondary px-2 py-1 rounded text-break",children:["Path: ",a||"/"]})]}),h?e.jsx(P,{className:"mb-3",children:h}):null,e.jsxs("div",{className:"row g-3",children:[e.jsx("div",{className:"col-lg-6",children:e.jsxs("div",{className:"card h-100",children:[e.jsx("div",{className:"card-header small fw-medium",children:"Log files"}),v?e.jsx("div",{className:"card-body text-center py-5",children:e.jsx("span",{className:"spinner-border text-secondary",role:"status"})}):e.jsx("div",{className:"list-group list-group-flush overflow-auto",style:{maxHeight:500},children:m.length===0?e.jsx("div",{className:"list-group-item text-secondary text-center py-4",children:"Empty directory"}):m.map(s=>e.jsxs("button",{type:"button",className:"list-group-item list-group-item-action d-flex gap-2 align-items-center",onClick:()=>k(s),children:[e.jsx("i",{className:`ti flex-shrink-0 ${s.is_dir?"ti-folder text-warning":"ti-file text-secondary"}`,"aria-hidden":!0}),e.jsx("span",{className:"text-truncate",children:s.name}),s.is_dir?null:e.jsx("span",{className:"small text-secondary ms-auto flex-shrink-0",children:_(s.size)})]},s.name))})]})}),e.jsx("div",{className:"col-lg-6",children:e.jsxs("div",{className:"card h-100 d-flex flex-column",style:{minHeight:400},children:[e.jsxs("div",{className:"card-header d-flex align-items-center justify-content-between gap-2 flex-wrap",children:[e.jsx("span",{className:"small fw-medium text-truncate",children:l||"Select a log file"}),l?e.jsxs("div",{className:"d-flex align-items-center gap-2 flex-shrink-0",children:[e.jsx("label",{className:"small text-secondary mb-0",children:"Lines:"}),e.jsxs("select",{value:c,onChange:s=>w(Number(s.target.value)),className:"form-select form-select-sm",style:{width:"auto"},children:[e.jsx("option",{value:100,children:"100"}),e.jsx("option",{value:500,children:"500"}),e.jsx("option",{value:1e3,children:"1000"}),e.jsx("option",{value:5e3,children:"5000"}),e.jsx("option",{value:1e4,children:"10000"})]}),e.jsx(g,{variant:"light",size:"sm",onClick:S,disabled:o,title:"Refresh",children:o?e.jsx("span",{className:"spinner-border spinner-border-sm",role:"status"}):e.jsx("i",{className:"ti ti-refresh","aria-hidden":!0})})]}):null]}),e.jsx("div",{className:"card-body flex-grow-1 overflow-auto",children:l?o?e.jsx("div",{className:"text-center py-5",children:e.jsx("span",{className:"spinner-border text-secondary",role:"status"})}):e.jsx("pre",{className:"font-monospace small mb-0 text-break",style:{whiteSpace:"pre-wrap"},children:b||"(empty)"}):e.jsx("p",{className:"text-secondary small mb-0",children:"Click a log file to view"})})]})})]})]})}export{$ as LogsPage};

View File

@@ -1 +1 @@
import{r,j as s,a as b,G as g,H as v}from"./index-cE9w-Kq7.js";import{A as N}from"./AdminAlert-yrdXFH0e.js";import{A as y}from"./AdminTable-eCi7S__-.js";import{P as o}from"./PageHeader-HdM4gpcn.js";function M(){const[e,d]=r.useState(null),[c,l]=r.useState([]),[t,n]=r.useState(null),[i,p]=r.useState("");return r.useEffect(()=>{const a=()=>{b("/monitor/system").then(d).catch(m=>p(m.message))},h=()=>{g(50).then(m=>l(m.processes)).catch(()=>l([]))},j=()=>{v().then(n).catch(()=>n(null))};a(),h(),j();const u=setInterval(()=>{a(),h(),j()},3e3);return()=>clearInterval(u)},[]),i&&!e?s.jsxs(s.Fragment,{children:[s.jsx(o,{title:"Monitor"}),s.jsx(N,{children:i})]}):e?s.jsxs(s.Fragment,{children:[s.jsx(o,{title:"Monitor"}),i?s.jsx(N,{className:"mb-3",children:i}):null,s.jsx("p",{className:"small text-secondary mb-3",children:"Refreshes every 3 seconds"}),s.jsxs("div",{className:"row g-3 mb-3",children:[s.jsx("div",{className:"col-md-4",children:s.jsx(x,{iconClass:"ti ti-cpu",title:"CPU",value:`${e.cpu_percent}%`,subtitle:"Usage",percent:e.cpu_percent})}),s.jsx("div",{className:"col-md-4",children:s.jsx(x,{iconClass:"ti ti-device-sd-card",title:"Memory",value:`${e.memory_used_mb} / ${e.memory_total_mb} MB`,subtitle:`${e.memory_percent}% used`,percent:e.memory_percent})}),s.jsx("div",{className:"col-md-4",children:s.jsx(x,{iconClass:"ti ti-database",title:"Disk",value:`${e.disk_used_gb} / ${e.disk_total_gb} GB`,subtitle:`${e.disk_percent}% used`,percent:e.disk_percent})})]}),t?s.jsx("div",{className:"card mb-3",children:s.jsxs("div",{className:"card-body",children:[s.jsxs("div",{className:"d-flex align-items-center gap-2 mb-3",children:[s.jsx("i",{className:"ti ti-network fs-5","aria-hidden":!0}),s.jsx("span",{className:"fw-medium",children:"Network I/O"})]}),s.jsxs("div",{className:"row g-3 small",children:[s.jsxs("div",{className:"col-6",children:[s.jsx("span",{className:"text-secondary d-block",children:"Sent"}),s.jsxs("span",{className:"font-monospace fw-medium",children:[t.bytes_sent_mb," MB"]})]}),s.jsxs("div",{className:"col-6",children:[s.jsx("span",{className:"text-secondary d-block",children:"Received"}),s.jsxs("span",{className:"font-monospace fw-medium",children:[t.bytes_recv_mb," MB"]})]})]})]})}):null,s.jsxs("div",{className:"card",children:[s.jsxs("div",{className:"card-header d-flex align-items-center gap-2",children:[s.jsx("i",{className:"ti ti-cpu","aria-hidden":!0}),s.jsx("span",{className:"fw-medium",children:"Top Processes (by CPU)"})]}),s.jsx("div",{className:"table-responsive",style:{maxHeight:"20rem"},children:s.jsxs(y,{responsive:!1,children:[s.jsx("thead",{className:"sticky-top bg-body-secondary",children:s.jsxs("tr",{children:[s.jsx("th",{className:"small",children:"PID"}),s.jsx("th",{className:"small",children:"Name"}),s.jsx("th",{className:"small",children:"User"}),s.jsx("th",{className:"small text-end",children:"CPU %"}),s.jsx("th",{className:"small text-end",children:"Mem %"}),s.jsx("th",{className:"small",children:"Status"})]})}),s.jsx("tbody",{children:c.length===0?s.jsx("tr",{children:s.jsx("td",{colSpan:6,className:"text-center text-secondary small py-3",children:"No process data"})}):c.map(a=>s.jsxs("tr",{className:"small",children:[s.jsx("td",{className:"font-monospace",children:a.pid}),s.jsx("td",{className:"text-truncate",style:{maxWidth:120},title:a.name,children:a.name}),s.jsx("td",{children:a.username}),s.jsxs("td",{className:"text-end font-monospace",children:[a.cpu_percent,"%"]}),s.jsxs("td",{className:"text-end font-monospace",children:[a.memory_percent,"%"]}),s.jsx("td",{className:"text-secondary",children:a.status})]},a.pid))})]})})]}),s.jsxs("div",{className:"alert alert-warning small mt-3 mb-0",children:[s.jsxs("div",{className:"d-flex align-items-center gap-2 fw-medium mb-1",children:[s.jsx("i",{className:"ti ti-activity","aria-hidden":!0}),"Live monitoring"]}),"System metrics, processes, and network stats are polled every 3 seconds."]})]}):s.jsxs(s.Fragment,{children:[s.jsx(o,{title:"Monitor"}),s.jsx("p",{className:"text-secondary",children:"Loading…"})]})}function x({iconClass:e,title:d,value:c,subtitle:l,percent:t}){const n=t>90?"bg-danger":t>70?"bg-warning":"bg-primary";return s.jsx("div",{className:"card h-100",children:s.jsxs("div",{className:"card-body",children:[s.jsxs("div",{className:"d-flex align-items-center gap-3 mb-3",children:[s.jsx("div",{className:"p-3 rounded bg-primary-subtle text-primary",children:s.jsx("i",{className:`${e} fs-2`,"aria-hidden":!0})}),s.jsxs("div",{children:[s.jsx("p",{className:"small text-secondary mb-0",children:d}),s.jsx("p",{className:"h5 mb-0",children:c}),s.jsx("p",{className:"small text-secondary mb-0",children:l})]})]}),s.jsx("div",{className:"progress",style:{height:6},children:s.jsx("div",{className:`progress-bar ${n}`,role:"progressbar",style:{width:`${Math.min(t,100)}%`}})})]})})}export{M as MonitorPage};
import{r,j as s,a as b,O as g,P as v}from"./index-CRR9sQ49.js";import{A as N}from"./AdminAlert-DW1IRWce.js";import{A as y}from"./AdminTable-BLiLxfnS.js";import{P as o}from"./PageHeader-BcjNf7GG.js";function M(){const[e,d]=r.useState(null),[c,l]=r.useState([]),[t,n]=r.useState(null),[i,p]=r.useState("");return r.useEffect(()=>{const a=()=>{b("/monitor/system").then(d).catch(m=>p(m.message))},h=()=>{g(50).then(m=>l(m.processes)).catch(()=>l([]))},j=()=>{v().then(n).catch(()=>n(null))};a(),h(),j();const u=setInterval(()=>{a(),h(),j()},3e3);return()=>clearInterval(u)},[]),i&&!e?s.jsxs(s.Fragment,{children:[s.jsx(o,{title:"Monitor"}),s.jsx(N,{children:i})]}):e?s.jsxs(s.Fragment,{children:[s.jsx(o,{title:"Monitor"}),i?s.jsx(N,{className:"mb-3",children:i}):null,s.jsx("p",{className:"small text-secondary mb-3",children:"Refreshes every 3 seconds"}),s.jsxs("div",{className:"row g-3 mb-3",children:[s.jsx("div",{className:"col-md-4",children:s.jsx(x,{iconClass:"ti ti-cpu",title:"CPU",value:`${e.cpu_percent}%`,subtitle:"Usage",percent:e.cpu_percent})}),s.jsx("div",{className:"col-md-4",children:s.jsx(x,{iconClass:"ti ti-device-sd-card",title:"Memory",value:`${e.memory_used_mb} / ${e.memory_total_mb} MB`,subtitle:`${e.memory_percent}% used`,percent:e.memory_percent})}),s.jsx("div",{className:"col-md-4",children:s.jsx(x,{iconClass:"ti ti-database",title:"Disk",value:`${e.disk_used_gb} / ${e.disk_total_gb} GB`,subtitle:`${e.disk_percent}% used`,percent:e.disk_percent})})]}),t?s.jsx("div",{className:"card mb-3",children:s.jsxs("div",{className:"card-body",children:[s.jsxs("div",{className:"d-flex align-items-center gap-2 mb-3",children:[s.jsx("i",{className:"ti ti-network fs-5","aria-hidden":!0}),s.jsx("span",{className:"fw-medium",children:"Network I/O"})]}),s.jsxs("div",{className:"row g-3 small",children:[s.jsxs("div",{className:"col-6",children:[s.jsx("span",{className:"text-secondary d-block",children:"Sent"}),s.jsxs("span",{className:"font-monospace fw-medium",children:[t.bytes_sent_mb," MB"]})]}),s.jsxs("div",{className:"col-6",children:[s.jsx("span",{className:"text-secondary d-block",children:"Received"}),s.jsxs("span",{className:"font-monospace fw-medium",children:[t.bytes_recv_mb," MB"]})]})]})]})}):null,s.jsxs("div",{className:"card",children:[s.jsxs("div",{className:"card-header d-flex align-items-center gap-2",children:[s.jsx("i",{className:"ti ti-cpu","aria-hidden":!0}),s.jsx("span",{className:"fw-medium",children:"Top Processes (by CPU)"})]}),s.jsx("div",{className:"table-responsive",style:{maxHeight:"20rem"},children:s.jsxs(y,{responsive:!1,children:[s.jsx("thead",{className:"sticky-top bg-body-secondary",children:s.jsxs("tr",{children:[s.jsx("th",{className:"small",children:"PID"}),s.jsx("th",{className:"small",children:"Name"}),s.jsx("th",{className:"small",children:"User"}),s.jsx("th",{className:"small text-end",children:"CPU %"}),s.jsx("th",{className:"small text-end",children:"Mem %"}),s.jsx("th",{className:"small",children:"Status"})]})}),s.jsx("tbody",{children:c.length===0?s.jsx("tr",{children:s.jsx("td",{colSpan:6,className:"text-center text-secondary small py-3",children:"No process data"})}):c.map(a=>s.jsxs("tr",{className:"small",children:[s.jsx("td",{className:"font-monospace",children:a.pid}),s.jsx("td",{className:"text-truncate",style:{maxWidth:120},title:a.name,children:a.name}),s.jsx("td",{children:a.username}),s.jsxs("td",{className:"text-end font-monospace",children:[a.cpu_percent,"%"]}),s.jsxs("td",{className:"text-end font-monospace",children:[a.memory_percent,"%"]}),s.jsx("td",{className:"text-secondary",children:a.status})]},a.pid))})]})})]}),s.jsxs("div",{className:"alert alert-warning small mt-3 mb-0",children:[s.jsxs("div",{className:"d-flex align-items-center gap-2 fw-medium mb-1",children:[s.jsx("i",{className:"ti ti-activity","aria-hidden":!0}),"Live monitoring"]}),"System metrics, processes, and network stats are polled every 3 seconds."]})]}):s.jsxs(s.Fragment,{children:[s.jsx(o,{title:"Monitor"}),s.jsx("p",{className:"text-secondary",children:"Loading…"})]})}function x({iconClass:e,title:d,value:c,subtitle:l,percent:t}){const n=t>90?"bg-danger":t>70?"bg-warning":"bg-primary";return s.jsx("div",{className:"card h-100",children:s.jsxs("div",{className:"card-body",children:[s.jsxs("div",{className:"d-flex align-items-center gap-3 mb-3",children:[s.jsx("div",{className:"p-3 rounded bg-primary-subtle text-primary",children:s.jsx("i",{className:`${e} fs-2`,"aria-hidden":!0})}),s.jsxs("div",{children:[s.jsx("p",{className:"small text-secondary mb-0",children:d}),s.jsx("p",{className:"h5 mb-0",children:c}),s.jsx("p",{className:"small text-secondary mb-0",children:l})]})]}),s.jsx("div",{className:"progress",style:{height:6},children:s.jsx("div",{className:`progress-bar ${n}`,role:"progressbar",style:{width:`${Math.min(t,100)}%`}})})]})})}export{M as MonitorPage};

View File

@@ -1 +1 @@
import{an as c,j as e,ao as m}from"./index-cE9w-Kq7.js";const d={"/":"Dashboard","/site":"Website","/ftp":"FTP","/database":"Databases","/docker":"Docker","/control":"Monitor","/firewall":"Security","/files":"Files","/node":"Node","/logs":"Logs","/ssl_domain":"Domains","/xterm":"Terminal","/crontab":"Cron","/soft":"App Store","/config":"Settings","/services":"Services","/plugins":"Plugins","/backup-plans":"Backup Plans","/users":"Users","/login":"Login","/install":"Remote install"};function b(t){return d[t]||"YakPanel"}function p({title:t,breadcrumbs:i,actions:n}){const{pathname:o}=c(),r=t??b(o),s=i??[{label:"Home",path:"/"},{label:r}];return e.jsx("div",{className:"page-header mb-4",children:e.jsxs("div",{className:"row align-items-center",children:[e.jsxs("div",{className:"col-md-6",children:[e.jsx("h3",{className:"page-title",children:r}),e.jsx("nav",{"aria-label":"breadcrumb",children:e.jsx("ol",{className:"breadcrumb mb-0",children:s.map((a,l)=>e.jsx("li",{className:`breadcrumb-item${l===s.length-1?" active":""}`,...l===s.length-1?{"aria-current":"page"}:{},children:a.path&&l<s.length-1?e.jsx(m,{to:a.path,children:a.label}):a.label},`${a.label}-${l}`))})})]}),n?e.jsx("div",{className:"col-md-6 d-flex justify-content-md-end mt-2 mt-md-0",children:n}):null]})})}export{p as P};
import{av as c,j as e,aw as m}from"./index-CRR9sQ49.js";const d={"/":"Dashboard","/site":"Website","/ftp":"FTP","/database":"Databases","/docker":"Docker","/control":"Monitor","/firewall":"Security","/files":"Files","/node":"Node","/logs":"Logs","/ssl_domain":"Domains","/xterm":"Terminal","/crontab":"Cron","/soft":"App Store","/config":"Settings","/services":"Services","/plugins":"Plugins","/backup-plans":"Backup Plans","/users":"Users","/login":"Login","/install":"Remote install"};function b(t){return d[t]||"YakPanel"}function p({title:t,breadcrumbs:i,actions:n}){const{pathname:o}=c(),r=t??b(o),s=i??[{label:"Home",path:"/"},{label:r}];return e.jsx("div",{className:"page-header mb-4",children:e.jsxs("div",{className:"row align-items-center",children:[e.jsxs("div",{className:"col-md-6",children:[e.jsx("h3",{className:"page-title",children:r}),e.jsx("nav",{"aria-label":"breadcrumb",children:e.jsx("ol",{className:"breadcrumb mb-0",children:s.map((a,l)=>e.jsx("li",{className:`breadcrumb-item${l===s.length-1?" active":""}`,...l===s.length-1?{"aria-current":"page"}:{},children:a.path&&l<s.length-1?e.jsx(m,{to:a.path,children:a.label}):a.label},`${a.label}-${l}`))})})]}),n?e.jsx("div",{className:"col-md-6 d-flex justify-content-md-end mt-2 mt-md-0",children:n}):null]})})}export{p as P};

View File

@@ -1 +0,0 @@
import{r as a,j as e,a as R,T as U,U as w}from"./index-cE9w-Kq7.js";import{M as t}from"./Modal-CL3xZqxR.js";import{A as f}from"./AdminAlert-yrdXFH0e.js";import{A as r}from"./AdminButton-ByutG8m-.js";import{P as p}from"./PageHeader-HdM4gpcn.js";function F(){const[b,N]=a.useState([]),[v,c]=a.useState(!0),[o,m]=a.useState(""),[y,l]=a.useState(!1),[u,h]=a.useState(""),[x,j]=a.useState(!1),[g,i]=a.useState(""),d=()=>R("/plugin/list").then(s=>N(s.plugins||[])).catch(s=>m(s.message));a.useEffect(()=>{c(!0),d().finally(()=>c(!1))},[]);const A=s=>{s.preventDefault();const n=u.trim();n&&(j(!0),i(""),U(n).then(()=>{l(!1),h(""),d()}).catch(S=>i(S.message)).finally(()=>j(!1)))},P=s=>{confirm("Remove this plugin?")&&w(s).then(d).catch(n=>m(n.message))};return v?e.jsxs(e.Fragment,{children:[e.jsx(p,{title:"Plugins",actions:e.jsxs(r,{variant:"primary",disabled:!0,children:[e.jsx("i",{className:"ti ti-plus me-1","aria-hidden":!0}),"Add from URL"]})}),e.jsx("p",{className:"text-secondary",children:"Loading…"})]}):e.jsxs(e.Fragment,{children:[e.jsx(p,{title:"Plugins",actions:e.jsxs(r,{variant:"primary",onClick:()=>l(!0),children:[e.jsx("i",{className:"ti ti-plus me-1","aria-hidden":!0}),"Add from URL"]})}),o?e.jsx(f,{className:"mb-3",children:o}):null,e.jsxs("div",{className:"alert alert-secondary small mb-4",children:["Built-in extensions and third-party plugins. Add plugins from a JSON manifest URL (must include ",e.jsx("code",{children:"id"}),","," ",e.jsx("code",{children:"name"}),", and optionally ",e.jsx("code",{children:"version"}),", ",e.jsx("code",{children:"desc"}),")."]}),e.jsxs(t,{show:y,onHide:()=>{l(!1),i("")},centered:!0,children:[e.jsx(t.Header,{closeButton:!0,children:e.jsx(t.Title,{children:"Add Plugin from URL"})}),e.jsxs("form",{onSubmit:A,children:[e.jsxs(t.Body,{children:[g?e.jsx(f,{className:"mb-3",children:g}):null,e.jsx("label",{className:"form-label",children:"Manifest URL"}),e.jsx("input",{value:u,onChange:s=>h(s.target.value),placeholder:"https://example.com/plugin.json",className:"form-control",required:!0})]}),e.jsxs(t.Footer,{children:[e.jsx(r,{type:"button",variant:"secondary",onClick:()=>{l(!1),i("")},children:"Cancel"}),e.jsx(r,{type:"submit",variant:"primary",disabled:x,children:x?"Adding…":"Add"})]})]})]}),e.jsx("div",{className:"row g-3",children:b.map(s=>e.jsx("div",{className:"col-md-6 col-xl-4",children:e.jsx("div",{className:"card h-100",children:e.jsxs("div",{className:"card-body d-flex gap-3",children:[e.jsx("i",{className:"ti ti-puzzle text-primary fs-2 flex-shrink-0","aria-hidden":!0}),e.jsxs("div",{className:"min-w-0 flex-grow-1",children:[e.jsxs("div",{className:"d-flex flex-wrap align-items-center gap-2 mb-1",children:[e.jsx("h3",{className:"h6 mb-0",children:s.name}),s.enabled?e.jsxs("span",{className:"badge bg-success-subtle text-success small",children:[e.jsx("i",{className:"ti ti-check me-1","aria-hidden":!0}),"Enabled"]}):null,s.builtin?null:e.jsx("button",{type:"button",className:"btn btn-link btn-sm text-danger p-0 ms-auto",title:"Remove",onClick:()=>P(s.id),children:e.jsx("i",{className:"ti ti-trash","aria-hidden":!0})})]}),e.jsx("p",{className:"small text-secondary mb-1",children:s.desc}),e.jsxs("p",{className:"small text-muted mb-0",children:["v",s.version,s.builtin?" (built-in)":""]})]})]})})},s.id))})]})}export{F as PluginsPage};

View File

@@ -0,0 +1 @@
import{r as a,j as e,a as R,$ as U,a0 as w}from"./index-CRR9sQ49.js";import{M as t}from"./Modal-B7V4w_St.js";import{A as f}from"./AdminAlert-DW1IRWce.js";import{A as r}from"./AdminButton-Bd2cLTu3.js";import{P as p}from"./PageHeader-BcjNf7GG.js";function F(){const[b,N]=a.useState([]),[v,c]=a.useState(!0),[o,m]=a.useState(""),[y,l]=a.useState(!1),[u,h]=a.useState(""),[x,j]=a.useState(!1),[g,i]=a.useState(""),d=()=>R("/plugin/list").then(s=>N(s.plugins||[])).catch(s=>m(s.message));a.useEffect(()=>{c(!0),d().finally(()=>c(!1))},[]);const A=s=>{s.preventDefault();const n=u.trim();n&&(j(!0),i(""),U(n).then(()=>{l(!1),h(""),d()}).catch(S=>i(S.message)).finally(()=>j(!1)))},P=s=>{confirm("Remove this plugin?")&&w(s).then(d).catch(n=>m(n.message))};return v?e.jsxs(e.Fragment,{children:[e.jsx(p,{title:"Plugins",actions:e.jsxs(r,{variant:"primary",disabled:!0,children:[e.jsx("i",{className:"ti ti-plus me-1","aria-hidden":!0}),"Add from URL"]})}),e.jsx("p",{className:"text-secondary",children:"Loading…"})]}):e.jsxs(e.Fragment,{children:[e.jsx(p,{title:"Plugins",actions:e.jsxs(r,{variant:"primary",onClick:()=>l(!0),children:[e.jsx("i",{className:"ti ti-plus me-1","aria-hidden":!0}),"Add from URL"]})}),o?e.jsx(f,{className:"mb-3",children:o}):null,e.jsxs("div",{className:"alert alert-secondary small mb-4",children:["Built-in extensions and third-party plugins. Add plugins from a JSON manifest URL (must include ",e.jsx("code",{children:"id"}),","," ",e.jsx("code",{children:"name"}),", and optionally ",e.jsx("code",{children:"version"}),", ",e.jsx("code",{children:"desc"}),")."]}),e.jsxs(t,{show:y,onHide:()=>{l(!1),i("")},centered:!0,children:[e.jsx(t.Header,{closeButton:!0,children:e.jsx(t.Title,{children:"Add Plugin from URL"})}),e.jsxs("form",{onSubmit:A,children:[e.jsxs(t.Body,{children:[g?e.jsx(f,{className:"mb-3",children:g}):null,e.jsx("label",{className:"form-label",children:"Manifest URL"}),e.jsx("input",{value:u,onChange:s=>h(s.target.value),placeholder:"https://example.com/plugin.json",className:"form-control",required:!0})]}),e.jsxs(t.Footer,{children:[e.jsx(r,{type:"button",variant:"secondary",onClick:()=>{l(!1),i("")},children:"Cancel"}),e.jsx(r,{type:"submit",variant:"primary",disabled:x,children:x?"Adding…":"Add"})]})]})]}),e.jsx("div",{className:"row g-3",children:b.map(s=>e.jsx("div",{className:"col-md-6 col-xl-4",children:e.jsx("div",{className:"card h-100",children:e.jsxs("div",{className:"card-body d-flex gap-3",children:[e.jsx("i",{className:"ti ti-puzzle text-primary fs-2 flex-shrink-0","aria-hidden":!0}),e.jsxs("div",{className:"min-w-0 flex-grow-1",children:[e.jsxs("div",{className:"d-flex flex-wrap align-items-center gap-2 mb-1",children:[e.jsx("h3",{className:"h6 mb-0",children:s.name}),s.enabled?e.jsxs("span",{className:"badge bg-success-subtle text-success small",children:[e.jsx("i",{className:"ti ti-check me-1","aria-hidden":!0}),"Enabled"]}):null,s.builtin?null:e.jsx("button",{type:"button",className:"btn btn-link btn-sm text-danger p-0 ms-auto",title:"Remove",onClick:()=>P(s.id),children:e.jsx("i",{className:"ti ti-trash","aria-hidden":!0})})]}),e.jsx("p",{className:"small text-secondary mb-1",children:s.desc}),e.jsxs("p",{className:"small text-muted mb-0",children:["v",s.version,s.builtin?" (built-in)":""]})]})]})})},s.id))})]})}export{F as PluginsPage};

View File

@@ -1 +1 @@
import{r,j as e,a as l}from"./index-cE9w-Kq7.js";import{A as S}from"./AdminAlert-yrdXFH0e.js";import{A as g}from"./AdminButton-ByutG8m-.js";import{A as y}from"./AdminTable-eCi7S__-.js";import{P as x}from"./PageHeader-HdM4gpcn.js";function u({show:d}){return d?e.jsx("span",{className:"spinner-border spinner-border-sm",role:"status"}):null}function R(){const[d,j]=r.useState([]),[p,o]=r.useState(!0),[m,c]=r.useState(""),[n,s]=r.useState(null),i=()=>{o(!0),l("/service/list").then(t=>j(t.services||[])).catch(t=>c(t.message)).finally(()=>o(!1))};r.useEffect(()=>{i()},[]);const b=t=>{s(t),l(`/service/${t}/start`,{method:"POST"}).then(i).catch(a=>c(a.message)).finally(()=>s(null))},f=t=>{s(t),l(`/service/${t}/stop`,{method:"POST"}).then(i).catch(a=>c(a.message)).finally(()=>s(null))},v=t=>{s(t),l(`/service/${t}/restart`,{method:"POST"}).then(i).catch(a=>c(a.message)).finally(()=>s(null))},h=t=>t==="active"||t==="activating";return p?e.jsxs(e.Fragment,{children:[e.jsx(x,{title:"Services"}),e.jsx("p",{className:"text-secondary",children:"Loading…"})]}):e.jsxs(e.Fragment,{children:[e.jsx(x,{title:"Services",actions:e.jsxs(g,{variant:"secondary",size:"sm",onClick:i,children:[e.jsx("i",{className:"ti ti-rotate-clockwise me-1","aria-hidden":!0}),"Refresh"]})}),m?e.jsx(S,{className:"mb-3",children:m}):null,e.jsx("div",{className:"alert alert-secondary small mb-3",children:"Control system services via systemctl. Requires panel to run with sufficient privileges."}),e.jsx("div",{className:"card",children:e.jsxs(y,{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Service"}),e.jsx("th",{children:"Unit"}),e.jsx("th",{children:"Status"}),e.jsx("th",{className:"text-end",children:"Actions"})]})}),e.jsx("tbody",{children:d.map(t=>e.jsxs("tr",{children:[e.jsx("td",{children:t.name}),e.jsx("td",{className:"font-monospace small",children:t.unit}),e.jsx("td",{children:e.jsx("span",{className:h(t.status)?"text-success":"text-secondary",children:t.status})}),e.jsx("td",{className:"text-end",children:e.jsx("span",{className:"d-inline-flex gap-1 justify-content-end",children:h(t.status)?e.jsxs(e.Fragment,{children:[e.jsx("button",{type:"button",className:"btn btn-link btn-sm text-warning p-1",title:"Restart",disabled:!!n,onClick:()=>v(t.id),children:n===t.id?e.jsx(u,{show:!0}):e.jsx("i",{className:"ti ti-rotate-clockwise","aria-hidden":!0})}),e.jsx("button",{type:"button",className:"btn btn-link btn-sm text-danger p-1",title:"Stop",disabled:!!n,onClick:()=>f(t.id),children:e.jsx("i",{className:"ti ti-square","aria-hidden":!0})})]}):e.jsx("button",{type:"button",className:"btn btn-link btn-sm text-success p-1",title:"Start",disabled:!!n,onClick:()=>b(t.id),children:n===t.id?e.jsx(u,{show:!0}):e.jsx("i",{className:"ti ti-player-play","aria-hidden":!0})})})})]},t.id))})]})})]})}export{R as ServicesPage};
import{r,j as e,a as l}from"./index-CRR9sQ49.js";import{A as S}from"./AdminAlert-DW1IRWce.js";import{A as g}from"./AdminButton-Bd2cLTu3.js";import{A as y}from"./AdminTable-BLiLxfnS.js";import{P as x}from"./PageHeader-BcjNf7GG.js";function u({show:d}){return d?e.jsx("span",{className:"spinner-border spinner-border-sm",role:"status"}):null}function R(){const[d,j]=r.useState([]),[p,o]=r.useState(!0),[m,c]=r.useState(""),[n,s]=r.useState(null),i=()=>{o(!0),l("/service/list").then(t=>j(t.services||[])).catch(t=>c(t.message)).finally(()=>o(!1))};r.useEffect(()=>{i()},[]);const b=t=>{s(t),l(`/service/${t}/start`,{method:"POST"}).then(i).catch(a=>c(a.message)).finally(()=>s(null))},f=t=>{s(t),l(`/service/${t}/stop`,{method:"POST"}).then(i).catch(a=>c(a.message)).finally(()=>s(null))},v=t=>{s(t),l(`/service/${t}/restart`,{method:"POST"}).then(i).catch(a=>c(a.message)).finally(()=>s(null))},h=t=>t==="active"||t==="activating";return p?e.jsxs(e.Fragment,{children:[e.jsx(x,{title:"Services"}),e.jsx("p",{className:"text-secondary",children:"Loading…"})]}):e.jsxs(e.Fragment,{children:[e.jsx(x,{title:"Services",actions:e.jsxs(g,{variant:"secondary",size:"sm",onClick:i,children:[e.jsx("i",{className:"ti ti-rotate-clockwise me-1","aria-hidden":!0}),"Refresh"]})}),m?e.jsx(S,{className:"mb-3",children:m}):null,e.jsx("div",{className:"alert alert-secondary small mb-3",children:"Control system services via systemctl. Requires panel to run with sufficient privileges."}),e.jsx("div",{className:"card",children:e.jsxs(y,{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Service"}),e.jsx("th",{children:"Unit"}),e.jsx("th",{children:"Status"}),e.jsx("th",{className:"text-end",children:"Actions"})]})}),e.jsx("tbody",{children:d.map(t=>e.jsxs("tr",{children:[e.jsx("td",{children:t.name}),e.jsx("td",{className:"font-monospace small",children:t.unit}),e.jsx("td",{children:e.jsx("span",{className:h(t.status)?"text-success":"text-secondary",children:t.status})}),e.jsx("td",{className:"text-end",children:e.jsx("span",{className:"d-inline-flex gap-1 justify-content-end",children:h(t.status)?e.jsxs(e.Fragment,{children:[e.jsx("button",{type:"button",className:"btn btn-link btn-sm text-warning p-1",title:"Restart",disabled:!!n,onClick:()=>v(t.id),children:n===t.id?e.jsx(u,{show:!0}):e.jsx("i",{className:"ti ti-rotate-clockwise","aria-hidden":!0})}),e.jsx("button",{type:"button",className:"btn btn-link btn-sm text-danger p-1",title:"Stop",disabled:!!n,onClick:()=>f(t.id),children:e.jsx("i",{className:"ti ti-square","aria-hidden":!0})})]}):e.jsx("button",{type:"button",className:"btn btn-link btn-sm text-success p-1",title:"Start",disabled:!!n,onClick:()=>b(t.id),children:n===t.id?e.jsx(u,{show:!0}):e.jsx("i",{className:"ti ti-player-play","aria-hidden":!0})})})})]},t.id))})]})})]})}export{R as ServicesPage};

View File

@@ -1 +1 @@
import{r as t,j as e,a as c}from"./index-cE9w-Kq7.js";import{A as b}from"./AdminAlert-yrdXFH0e.js";import{A as v}from"./AdminButton-ByutG8m-.js";import{P as m}from"./PageHeader-HdM4gpcn.js";function A(){const[x,u]=t.useState([]),[p,d]=t.useState(!0),[o,a]=t.useState(""),[n,l]=t.useState(null),[f,h]=t.useState(""),r=()=>{d(!0),c("/soft/list").then(s=>{u(s.software||[]),h(s.package_manager||"")}).catch(s=>a(s.message)).finally(()=>d(!1))};t.useEffect(()=>{r()},[]);const j=s=>{l(s),a(""),c(`/soft/install/${s}`,{method:"POST"}).then(()=>r()).catch(i=>a(i.message)).finally(()=>l(null))},g=(s,i)=>{confirm(`Uninstall ${i}?`)&&(l(s),a(""),c(`/soft/uninstall/${s}`,{method:"POST"}).then(()=>r()).catch(N=>a(N.message)).finally(()=>l(null)))};return p?e.jsxs(e.Fragment,{children:[e.jsx(m,{title:"App Store"}),e.jsx("div",{className:"d-flex justify-content-center py-5",children:e.jsx("div",{className:"spinner-border text-primary",role:"status",children:e.jsx("span",{className:"visually-hidden",children:"Loading…"})})})]}):e.jsxs(e.Fragment,{children:[e.jsx(m,{title:"App Store",actions:e.jsxs(v,{variant:"secondary",onClick:r,children:[e.jsx("i",{className:"ti ti-refresh me-1"}),"Refresh"]})}),o?e.jsx(b,{variant:"danger",children:o}):null,e.jsxs("div",{className:"alert alert-warning",role:"note",children:["Installs use your server package manager (",f||"unknown","). Panel must run as root (or equivalent). Supported: apt, dnf/yum/microdnf, apk."]}),e.jsx("div",{className:"row g-3",children:x.map(s=>e.jsx("div",{className:"col-md-6 col-xl-4 d-flex",children:e.jsx("div",{className:"card flex-fill shadow-sm",children:e.jsxs("div",{className:"card-body d-flex flex-column",children:[e.jsxs("div",{className:"d-flex align-items-start justify-content-between gap-2 mb-2",children:[e.jsxs("div",{className:"d-flex align-items-start gap-2",children:[e.jsx("span",{className:"avatar avatar-md bg-primary-transparent text-primary rounded flex-shrink-0",children:e.jsx("i",{className:"ti ti-package fs-5","aria-hidden":!0})}),e.jsxs("div",{children:[e.jsx("h5",{className:"card-title mb-1",children:s.name}),e.jsx("p",{className:"text-muted small mb-0",children:s.desc})]})]}),s.installed?e.jsxs("span",{className:"text-success small text-nowrap",children:[e.jsx("i",{className:"ti ti-circle-check me-1"}),s.version||"Installed"]}):e.jsxs("span",{className:"text-muted small text-nowrap",children:[e.jsx("i",{className:"ti ti-x me-1"}),"Not installed"]})]}),e.jsx("div",{className:"mt-auto pt-3",children:s.installed?e.jsxs("button",{type:"button",onClick:()=>g(s.id,s.name),disabled:n===s.id,className:"btn btn-outline-danger btn-sm w-100 d-inline-flex align-items-center justify-content-center gap-2",children:[n===s.id?e.jsx("span",{className:"spinner-border spinner-border-sm",role:"status"}):null,"Uninstall"]}):e.jsxs("button",{type:"button",onClick:()=>j(s.id),disabled:n===s.id,className:"btn btn-primary btn-sm w-100 d-inline-flex align-items-center justify-content-center gap-2",children:[n===s.id?e.jsx("span",{className:"spinner-border spinner-border-sm",role:"status"}):null,"Install"]})})]})})},s.id))})]})}export{A as SoftPage};
import{r as t,j as e,a as c}from"./index-CRR9sQ49.js";import{A as b}from"./AdminAlert-DW1IRWce.js";import{A as v}from"./AdminButton-Bd2cLTu3.js";import{P as m}from"./PageHeader-BcjNf7GG.js";function A(){const[x,u]=t.useState([]),[p,d]=t.useState(!0),[o,a]=t.useState(""),[n,l]=t.useState(null),[f,h]=t.useState(""),r=()=>{d(!0),c("/soft/list").then(s=>{u(s.software||[]),h(s.package_manager||"")}).catch(s=>a(s.message)).finally(()=>d(!1))};t.useEffect(()=>{r()},[]);const j=s=>{l(s),a(""),c(`/soft/install/${s}`,{method:"POST"}).then(()=>r()).catch(i=>a(i.message)).finally(()=>l(null))},g=(s,i)=>{confirm(`Uninstall ${i}?`)&&(l(s),a(""),c(`/soft/uninstall/${s}`,{method:"POST"}).then(()=>r()).catch(N=>a(N.message)).finally(()=>l(null)))};return p?e.jsxs(e.Fragment,{children:[e.jsx(m,{title:"App Store"}),e.jsx("div",{className:"d-flex justify-content-center py-5",children:e.jsx("div",{className:"spinner-border text-primary",role:"status",children:e.jsx("span",{className:"visually-hidden",children:"Loading…"})})})]}):e.jsxs(e.Fragment,{children:[e.jsx(m,{title:"App Store",actions:e.jsxs(v,{variant:"secondary",onClick:r,children:[e.jsx("i",{className:"ti ti-refresh me-1"}),"Refresh"]})}),o?e.jsx(b,{variant:"danger",children:o}):null,e.jsxs("div",{className:"alert alert-warning",role:"note",children:["Installs use your server package manager (",f||"unknown","). Panel must run as root (or equivalent). Supported: apt, dnf/yum/microdnf, apk."]}),e.jsx("div",{className:"row g-3",children:x.map(s=>e.jsx("div",{className:"col-md-6 col-xl-4 d-flex",children:e.jsx("div",{className:"card flex-fill shadow-sm",children:e.jsxs("div",{className:"card-body d-flex flex-column",children:[e.jsxs("div",{className:"d-flex align-items-start justify-content-between gap-2 mb-2",children:[e.jsxs("div",{className:"d-flex align-items-start gap-2",children:[e.jsx("span",{className:"avatar avatar-md bg-primary-transparent text-primary rounded flex-shrink-0",children:e.jsx("i",{className:"ti ti-package fs-5","aria-hidden":!0})}),e.jsxs("div",{children:[e.jsx("h5",{className:"card-title mb-1",children:s.name}),e.jsx("p",{className:"text-muted small mb-0",children:s.desc})]})]}),s.installed?e.jsxs("span",{className:"text-success small text-nowrap",children:[e.jsx("i",{className:"ti ti-circle-check me-1"}),s.version||"Installed"]}):e.jsxs("span",{className:"text-muted small text-nowrap",children:[e.jsx("i",{className:"ti ti-x me-1"}),"Not installed"]})]}),e.jsx("div",{className:"mt-auto pt-3",children:s.installed?e.jsxs("button",{type:"button",onClick:()=>g(s.id,s.name),disabled:n===s.id,className:"btn btn-outline-danger btn-sm w-100 d-inline-flex align-items-center justify-content-center gap-2",children:[n===s.id?e.jsx("span",{className:"spinner-border spinner-border-sm",role:"status"}):null,"Uninstall"]}):e.jsxs("button",{type:"button",onClick:()=>j(s.id),disabled:n===s.id,className:"btn btn-primary btn-sm w-100 d-inline-flex align-items-center justify-content-center gap-2",children:[n===s.id?e.jsx("span",{className:"spinner-border spinner-border-sm",role:"status"}):null,"Install"]})})]})})},s.id))})]})}export{A as SoftPage};

View File

@@ -1 +0,0 @@
import{r as a,j as e,_,a as I,$ as D,a0 as P,a1 as L}from"./index-cE9w-Kq7.js";import{M as n}from"./Modal-CL3xZqxR.js";import{A as b}from"./AdminAlert-yrdXFH0e.js";import{A as o}from"./AdminButton-ByutG8m-.js";import{A as T}from"./AdminTable-eCi7S__-.js";import{P as g}from"./PageHeader-HdM4gpcn.js";function M(){const[h,v]=a.useState([]),[N,x]=a.useState(!0),[u,d]=a.useState(""),[y,l]=a.useState(!1),[j,p]=a.useState(!1),[f,i]=a.useState(""),[U,A]=a.useState(null),c=()=>{x(!0),_().then(s=>{v(s),I("/auth/me").then(t=>A(t.id))}).catch(s=>d(s.message)).finally(()=>x(!1))};a.useEffect(()=>{c()},[]);const C=s=>{s.preventDefault();const t=s.currentTarget,r=t.elements.namedItem("username").value.trim(),m=t.elements.namedItem("password").value,E=t.elements.namedItem("email").value.trim();if(!r||r.length<2){i("Username must be at least 2 characters");return}if(!m||m.length<6){i("Password must be at least 6 characters");return}p(!0),i(""),L({username:r,password:m,email:E}).then(()=>{l(!1),t.reset(),c()}).catch(k=>i(k.message)).finally(()=>p(!1))},w=(s,t)=>{confirm(`Delete user "${t}"?`)&&P(s).then(c).catch(r=>d(r.message))},S=s=>{D(s).then(c).catch(t=>d(t.message))};return N&&h.length===0?e.jsxs(e.Fragment,{children:[e.jsx(g,{title:"Users"}),e.jsx("p",{className:"text-secondary",children:"Loading…"})]}):e.jsxs(e.Fragment,{children:[e.jsx(g,{title:"Users",actions:e.jsxs(o,{variant:"primary",onClick:()=>l(!0),children:[e.jsx("i",{className:"ti ti-plus me-1","aria-hidden":!0}),"Add User"]})}),u?e.jsx(b,{className:"mb-3",children:u}):null,e.jsx("div",{className:"card",children:e.jsxs(T,{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Username"}),e.jsx("th",{children:"Email"}),e.jsx("th",{children:"Status"}),e.jsx("th",{children:"Role"}),e.jsx("th",{className:"text-end",children:"Actions"})]})}),e.jsx("tbody",{children:h.map(s=>e.jsxs("tr",{children:[e.jsx("td",{children:s.username}),e.jsx("td",{children:s.email||"—"}),e.jsx("td",{children:e.jsx("span",{className:s.is_active?"text-success":"text-secondary",children:s.is_active?"Active":"Inactive"})}),e.jsx("td",{children:s.is_superuser?"Admin":"User"}),e.jsx("td",{className:"text-end",children:s.id!==U?e.jsxs("span",{className:"d-inline-flex gap-1 justify-content-end",children:[e.jsx("button",{type:"button",className:`btn btn-link btn-sm p-1 ${s.is_active?"text-warning":"text-success"}`,title:s.is_active?"Deactivate":"Activate",onClick:()=>S(s.id),children:e.jsx("i",{className:s.is_active?"ti ti-user-x":"ti ti-user-check","aria-hidden":!0})}),e.jsx("button",{type:"button",className:"btn btn-link btn-sm text-danger p-1",title:"Delete",onClick:()=>w(s.id,s.username),children:e.jsx("i",{className:"ti ti-trash","aria-hidden":!0})})]}):null})]},s.id))})]})}),e.jsxs(n,{show:y,onHide:()=>l(!1),centered:!0,children:[e.jsx(n.Header,{closeButton:!0,children:e.jsx(n.Title,{children:"Add User"})}),e.jsxs("form",{onSubmit:C,children:[e.jsxs(n.Body,{children:[f?e.jsx(b,{className:"mb-3",children:f}):null,e.jsxs("div",{className:"mb-3",children:[e.jsx("label",{className:"form-label",children:"Username"}),e.jsx("input",{name:"username",type:"text",placeholder:"newuser",className:"form-control",required:!0,minLength:2})]}),e.jsxs("div",{className:"mb-3",children:[e.jsx("label",{className:"form-label",children:"Password"}),e.jsx("input",{name:"password",type:"password",placeholder:"••••••••",className:"form-control",required:!0,minLength:6})]}),e.jsxs("div",{className:"mb-0",children:[e.jsx("label",{className:"form-label",children:"Email (optional)"}),e.jsx("input",{name:"email",type:"email",placeholder:"user@example.com",className:"form-control"})]})]}),e.jsxs(n.Footer,{children:[e.jsx(o,{type:"button",variant:"secondary",onClick:()=>l(!1),children:"Cancel"}),e.jsx(o,{type:"submit",variant:"primary",disabled:j,children:j?"Creating…":"Create"})]})]})]})]})}export{M as UsersPage};

View File

@@ -0,0 +1 @@
import{r as a,j as e,a6 as I,a as _,a7 as D,a8 as P,a9 as L}from"./index-CRR9sQ49.js";import{M as n}from"./Modal-B7V4w_St.js";import{A as b}from"./AdminAlert-DW1IRWce.js";import{A as o}from"./AdminButton-Bd2cLTu3.js";import{A as T}from"./AdminTable-BLiLxfnS.js";import{P as g}from"./PageHeader-BcjNf7GG.js";function $(){const[h,v]=a.useState([]),[N,x]=a.useState(!0),[u,d]=a.useState(""),[y,l]=a.useState(!1),[j,p]=a.useState(!1),[f,i]=a.useState(""),[U,A]=a.useState(null),c=()=>{x(!0),I().then(s=>{v(s),_("/auth/me").then(t=>A(t.id))}).catch(s=>d(s.message)).finally(()=>x(!1))};a.useEffect(()=>{c()},[]);const C=s=>{s.preventDefault();const t=s.currentTarget,r=t.elements.namedItem("username").value.trim(),m=t.elements.namedItem("password").value,E=t.elements.namedItem("email").value.trim();if(!r||r.length<2){i("Username must be at least 2 characters");return}if(!m||m.length<6){i("Password must be at least 6 characters");return}p(!0),i(""),L({username:r,password:m,email:E}).then(()=>{l(!1),t.reset(),c()}).catch(k=>i(k.message)).finally(()=>p(!1))},w=(s,t)=>{confirm(`Delete user "${t}"?`)&&P(s).then(c).catch(r=>d(r.message))},S=s=>{D(s).then(c).catch(t=>d(t.message))};return N&&h.length===0?e.jsxs(e.Fragment,{children:[e.jsx(g,{title:"Users"}),e.jsx("p",{className:"text-secondary",children:"Loading…"})]}):e.jsxs(e.Fragment,{children:[e.jsx(g,{title:"Users",actions:e.jsxs(o,{variant:"primary",onClick:()=>l(!0),children:[e.jsx("i",{className:"ti ti-plus me-1","aria-hidden":!0}),"Add User"]})}),u?e.jsx(b,{className:"mb-3",children:u}):null,e.jsx("div",{className:"card",children:e.jsxs(T,{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Username"}),e.jsx("th",{children:"Email"}),e.jsx("th",{children:"Status"}),e.jsx("th",{children:"Role"}),e.jsx("th",{className:"text-end",children:"Actions"})]})}),e.jsx("tbody",{children:h.map(s=>e.jsxs("tr",{children:[e.jsx("td",{children:s.username}),e.jsx("td",{children:s.email||"—"}),e.jsx("td",{children:e.jsx("span",{className:s.is_active?"text-success":"text-secondary",children:s.is_active?"Active":"Inactive"})}),e.jsx("td",{children:s.is_superuser?"Admin":"User"}),e.jsx("td",{className:"text-end",children:s.id!==U?e.jsxs("span",{className:"d-inline-flex gap-1 justify-content-end",children:[e.jsx("button",{type:"button",className:`btn btn-link btn-sm p-1 ${s.is_active?"text-warning":"text-success"}`,title:s.is_active?"Deactivate":"Activate",onClick:()=>S(s.id),children:e.jsx("i",{className:s.is_active?"ti ti-user-x":"ti ti-user-check","aria-hidden":!0})}),e.jsx("button",{type:"button",className:"btn btn-link btn-sm text-danger p-1",title:"Delete",onClick:()=>w(s.id,s.username),children:e.jsx("i",{className:"ti ti-trash","aria-hidden":!0})})]}):null})]},s.id))})]})}),e.jsxs(n,{show:y,onHide:()=>l(!1),centered:!0,children:[e.jsx(n.Header,{closeButton:!0,children:e.jsx(n.Title,{children:"Add User"})}),e.jsxs("form",{onSubmit:C,children:[e.jsxs(n.Body,{children:[f?e.jsx(b,{className:"mb-3",children:f}):null,e.jsxs("div",{className:"mb-3",children:[e.jsx("label",{className:"form-label",children:"Username"}),e.jsx("input",{name:"username",type:"text",placeholder:"newuser",className:"form-control",required:!0,minLength:2})]}),e.jsxs("div",{className:"mb-3",children:[e.jsx("label",{className:"form-label",children:"Password"}),e.jsx("input",{name:"password",type:"password",placeholder:"••••••••",className:"form-control",required:!0,minLength:6})]}),e.jsxs("div",{className:"mb-0",children:[e.jsx("label",{className:"form-label",children:"Email (optional)"}),e.jsx("input",{name:"email",type:"email",placeholder:"user@example.com",className:"form-control"})]})]}),e.jsxs(n.Footer,{children:[e.jsx(o,{type:"button",variant:"secondary",onClick:()=>l(!1),children:"Cancel"}),e.jsx(o,{type:"submit",variant:"primary",disabled:j,children:j?"Creating…":"Create"})]})]})]})]})}export{$ as UsersPage};

View File

@@ -5,7 +5,7 @@
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>YakPanel</title>
<script type="module" crossorigin src="/assets/index-cE9w-Kq7.js"></script>
<script type="module" crossorigin src="/assets/index-CRR9sQ49.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BHS5Y1YN.css">
</head>
<body>

View File

@@ -163,12 +163,67 @@ export async function downloadSiteBackup(siteId: number, filename: string): Prom
URL.revokeObjectURL(url)
}
export interface FileListItem {
name: string
is_dir: boolean
size: number
mtime?: string
mtime_ts?: number
mode?: string
mode_symbolic?: string
owner?: string
group?: string
}
export async function listFiles(path: string) {
return apiRequest<{ path: string; items: { name: string; is_dir: boolean; size: number }[] }>(
`/files/list?path=${encodeURIComponent(path)}`
return apiRequest<{ path: string; items: FileListItem[] }>(`/files/list?path=${encodeURIComponent(path)}`)
}
export async function fileDirSize(path: string) {
return apiRequest<{ size: number }>(`/files/dir-size?path=${encodeURIComponent(path)}`)
}
export async function fileSearch(q: string, path: string, maxResults = 200) {
return apiRequest<{ path: string; query: string; results: { path: string; name: string; is_dir: boolean }[] }>(
`/files/search?q=${encodeURIComponent(q)}&path=${encodeURIComponent(path)}&max_results=${maxResults}`
)
}
export async function fileChmod(filePath: string, mode: string, recursive = false) {
return apiRequest<{ status: boolean; msg: string }>('/files/chmod', {
method: 'POST',
body: JSON.stringify({ file_path: filePath, mode, recursive }),
})
}
export async function fileTouch(parentPath: string, name: string) {
return apiRequest<{ status: boolean; msg: string }>('/files/touch', {
method: 'POST',
body: JSON.stringify({ path: parentPath, name }),
})
}
export async function fileCopy(sourceParent: string, name: string, destParent: string, destName?: string) {
return apiRequest<{ status: boolean; msg: string }>('/files/copy', {
method: 'POST',
body: JSON.stringify({ path: sourceParent, name, dest_path: destParent, dest_name: destName ?? null }),
})
}
export async function fileMove(sourceParent: string, name: string, destParent: string, destName?: string) {
return apiRequest<{ status: boolean; msg: string }>('/files/move', {
method: 'POST',
body: JSON.stringify({ path: sourceParent, name, dest_path: destParent, dest_name: destName ?? null }),
})
}
export async function fileCompress(parentPath: string, names: string[], archiveName: string) {
return apiRequest<{ status: boolean; msg: string; archive?: string }>('/files/compress', {
method: 'POST',
body: JSON.stringify({ path: parentPath, names, archive_name: archiveName }),
})
}
export async function uploadFile(path: string, file: File) {
const form = new FormData()
form.append('path', path)

View File

@@ -1,27 +1,58 @@
import { useEffect, useState, useRef } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import Modal from 'react-bootstrap/Modal'
import { listFiles, downloadFile, uploadFile, readFile, writeFile, mkdirFile, renameFile, deleteFile } from '../api/client'
import Dropdown from 'react-bootstrap/Dropdown'
import {
listFiles,
downloadFile,
uploadFile,
readFile,
writeFile,
mkdirFile,
renameFile,
deleteFile,
fileDirSize,
fileSearch,
fileChmod,
fileTouch,
fileCopy,
fileMove,
fileCompress,
type FileListItem,
} from '../api/client'
import { PageHeader, AdminButton, AdminAlert, AdminTable, EmptyState } from '../components/admin'
interface FileItem {
name: string
is_dir: boolean
size: number
function joinPath(dir: string, name: string): string {
if (dir === '/') return `/${name}`
return `${dir.replace(/\/$/, '')}/${name}`
}
function parentPath(p: string): string {
const parts = p.replace(/\/$/, '').split('/').filter(Boolean)
parts.pop()
return parts.length === 0 ? '/' : `/${parts.join('/')}`
}
function formatSize(bytes: number): string {
if (bytes < 1024) return bytes + ' B'
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
return (bytes / 1024 / 1024).toFixed(1) + ' MB'
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`
return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`
}
const TEXT_EXT = ['.txt', '.html', '.htm', '.css', '.js', '.json', '.xml', '.md', '.py', '.php', '.sh', '.conf', '.env']
const TEXT_EXT = ['.txt', '.html', '.htm', '.css', '.js', '.json', '.xml', '.md', '.py', '.php', '.sh', '.conf', '.env', '.ini', '.log', '.yml', '.yaml']
type Clip = { op: 'copy' | 'cut'; entries: { parent: string; name: string }[] }
export function FilesPage() {
const [path, setPath] = useState('/')
const [items, setItems] = useState<FileItem[]>([])
const [pathInput, setPathInput] = useState('/')
const [items, setItems] = useState<FileListItem[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [filter, setFilter] = useState('')
const [selected, setSelected] = useState<Set<string>>(() => new Set())
const [clipboard, setClipboard] = useState<Clip | null>(null)
const [dirSizes, setDirSizes] = useState<Record<string, number>>({})
const [downloading, setDownloading] = useState<string | null>(null)
const [uploading, setUploading] = useState(false)
const [editingFile, setEditingFile] = useState<string | null>(null)
@@ -29,44 +60,86 @@ export function FilesPage() {
const [savingEdit, setSavingEdit] = useState(false)
const [showMkdir, setShowMkdir] = useState(false)
const [mkdirName, setMkdirName] = useState('')
const [renaming, setRenaming] = useState<FileItem | null>(null)
const [showNewFile, setShowNewFile] = useState(false)
const [newFileName, setNewFileName] = useState('')
const [renaming, setRenaming] = useState<FileListItem | null>(null)
const [renameValue, setRenameValue] = useState('')
const [showChmod, setShowChmod] = useState(false)
const [chmodItem, setChmodItem] = useState<FileListItem | null>(null)
const [chmodMode, setChmodMode] = useState('0644')
const [chmodRecursive, setChmodRecursive] = useState(false)
const [showCompress, setShowCompress] = useState(false)
const [compressName, setCompressName] = useState('archive.zip')
const [showSearchModal, setShowSearchModal] = useState(false)
const [searchQ, setSearchQ] = useState('')
const [searchLoading, setSearchLoading] = useState(false)
const [searchResults, setSearchResults] = useState<{ path: string; name: string; is_dir: boolean }[]>([])
const fileInputRef = useRef<HTMLInputElement>(null)
const loadDir = (p: string) => {
const loadDir = useCallback((p: string) => {
setLoading(true)
setError('')
setSelected(new Set())
listFiles(p)
.then((data) => {
setPath(data.path)
setPathInput(data.path)
setItems(data.items.sort((a, b) => (a.is_dir === b.is_dir ? 0 : a.is_dir ? -1 : 1)))
setDirSizes({})
})
.catch((err) => setError(err.message))
.finally(() => setLoading(false))
}
}, [])
useEffect(() => {
loadDir(path)
}, [])
const handleNavigate = (item: FileItem) => {
if (item.is_dir) {
const newPath = path.endsWith('/') ? path + item.name : path + '/' + item.name
loadDir(newPath)
}
const filteredItems = useMemo(() => {
const q = filter.trim().toLowerCase()
if (!q) return items
return items.filter((i) => i.name.toLowerCase().includes(q))
}, [items, filter])
const pathSegments = path.replace(/\/$/, '').split('/').filter(Boolean)
const canGoBack = pathSegments.length > 0
const toggleSelect = (name: string) => {
setSelected((prev) => {
const next = new Set(prev)
if (next.has(name)) next.delete(name)
else next.add(name)
return next
})
}
const selectAll = () => {
if (selected.size === filteredItems.length) setSelected(new Set())
else setSelected(new Set(filteredItems.map((i) => i.name)))
}
const selectedList = useMemo(
() => filteredItems.filter((i) => selected.has(i.name)),
[filteredItems, selected]
)
const handleNavigate = (item: FileListItem) => {
if (item.is_dir) loadDir(joinPath(path, item.name))
}
const handleBack = () => {
const parts = path.replace(/\/$/, '').split('/').filter(Boolean)
if (parts.length === 0) return
parts.pop()
const newPath = parts.length === 0 ? '/' : '/' + parts.join('/')
loadDir(newPath)
if (!canGoBack) return
loadDir(parentPath(path))
}
const handleDownload = (item: FileItem) => {
const goPathInput = () => {
const p = pathInput.trim() || '/'
loadDir(p.startsWith('/') ? p : `/${p}`)
}
const handleDownload = (item: FileListItem) => {
if (item.is_dir) return
const fullPath = path.endsWith('/') ? path + item.name : path + '/' + item.name
const fullPath = joinPath(path, item.name)
setDownloading(item.name)
downloadFile(fullPath)
.catch((err) => setError(err.message))
@@ -87,8 +160,8 @@ export function FilesPage() {
})
}
const handleEdit = (item: FileItem) => {
const fullPath = path.endsWith('/') ? path + item.name : path + '/' + item.name
const handleEdit = (item: FileListItem) => {
const fullPath = joinPath(path, item.name)
readFile(fullPath)
.then((data) => {
setEditingFile(fullPath)
@@ -124,6 +197,19 @@ export function FilesPage() {
.catch((err) => setError(err.message))
}
const handleTouch = (e: React.FormEvent) => {
e.preventDefault()
const name = newFileName.trim()
if (!name) return
fileTouch(path, name)
.then(() => {
setShowNewFile(false)
setNewFileName('')
loadDir(path)
})
.catch((err) => setError(err.message))
}
const handleRename = () => {
if (!renaming || !renameValue.trim()) return
const newName = renameValue.trim()
@@ -140,44 +226,253 @@ export function FilesPage() {
.catch((err) => setError(err.message))
}
const handleDelete = (item: FileItem) => {
const handleDelete = (item: FileListItem) => {
if (!confirm(`Delete ${item.is_dir ? 'folder' : 'file'} "${item.name}"?`)) return
deleteFile(path, item.name, item.is_dir)
.then(() => loadDir(path))
.catch((err) => setError(err.message))
}
const pathSegments = path.replace(/\/$/, '').split('/').filter(Boolean)
const canGoBack = pathSegments.length > 0
const batchDelete = () => {
if (selectedList.length === 0) return
if (!confirm(`Delete ${selectedList.length} item(s)?`)) return
Promise.all(
selectedList.map((i) => deleteFile(path, i.name, i.is_dir))
)
.then(() => loadDir(path))
.catch((err) => setError(err.message))
}
const copySelection = () => {
if (selectedList.length === 0) return
setClipboard({
op: 'copy',
entries: selectedList.map((i) => ({ parent: path, name: i.name })),
})
}
const cutSelection = () => {
if (selectedList.length === 0) return
setClipboard({
op: 'cut',
entries: selectedList.map((i) => ({ parent: path, name: i.name })),
})
}
const pasteHere = () => {
if (!clipboard || clipboard.entries.length === 0) return
const tasks = clipboard.entries.map((e) => {
if (clipboard.op === 'copy') return fileCopy(e.parent, e.name, path)
return fileMove(e.parent, e.name, path)
})
Promise.all(tasks)
.then(() => {
if (clipboard?.op === 'cut') setClipboard(null)
loadDir(path)
})
.catch((err) => setError(err.message))
}
const openChmod = (item: FileListItem) => {
setChmodItem(item)
setChmodMode(item.mode ? item.mode.padStart(3, '0') : '0644')
setChmodRecursive(item.is_dir)
setShowChmod(true)
}
const submitChmod = () => {
if (!chmodItem) return
const fp = joinPath(path, chmodItem.name)
fileChmod(fp, chmodMode, chmodRecursive && chmodItem.is_dir)
.then(() => {
setShowChmod(false)
setChmodItem(null)
loadDir(path)
})
.catch((err) => setError(err.message))
}
const openCompress = () => {
const names = selectedList.length > 0 ? selectedList.map((i) => i.name) : []
if (names.length === 0) return
setCompressName(`archive-${Date.now()}.zip`)
setShowCompress(true)
}
const submitCompress = () => {
const names = selectedList.length > 0 ? selectedList.map((i) => i.name) : []
if (!compressName.trim() || names.length === 0) return
fileCompress(path, names, compressName.trim())
.then(() => {
setShowCompress(false)
loadDir(path)
})
.catch((err) => setError(err.message))
}
const calcDirSize = (item: FileListItem) => {
if (!item.is_dir) return
const fp = joinPath(path, item.name)
fileDirSize(fp)
.then((r) => setDirSizes((d) => ({ ...d, [item.name]: r.size })))
.catch((err) => setError(err.message))
}
const runDeepSearch = () => {
const q = searchQ.trim()
if (!q) return
setSearchLoading(true)
setSearchResults([])
fileSearch(q, path, 300)
.then((r) => {
setSearchResults(r.results)
setShowSearchModal(true)
})
.catch((err) => setError(err.message))
.finally(() => setSearchLoading(false))
}
const navigateToSearchHit = (hit: { path: string; is_dir: boolean }) => {
setShowSearchModal(false)
if (hit.is_dir) loadDir(hit.path)
else loadDir(parentPath(hit.path))
}
const breadcrumbNavigate = (idx: number) => {
if (idx < 0) {
loadDir('/')
return
}
const segs = pathSegments.slice(0, idx + 1)
loadDir(`/${segs.join('/')}`)
}
return (
<>
<PageHeader title="Files" />
<div className="d-flex flex-wrap align-items-center gap-2 mb-3">
<AdminButton variant="secondary" size="sm" onClick={handleBack} disabled={!canGoBack}>
<i className="ti ti-arrow-left me-1" aria-hidden />
Back
</AdminButton>
<input ref={fileInputRef} type="file" className="d-none" onChange={handleUpload} />
<AdminButton variant="success" size="sm" onClick={() => setShowMkdir(true)}>
<i className="ti ti-folder-plus me-1" aria-hidden />
New Folder
</AdminButton>
<AdminButton variant="primary" size="sm" onClick={() => fileInputRef.current?.click()} disabled={uploading}>
{uploading ? (
<span className="spinner-border spinner-border-sm me-1" role="status" />
) : (
<i className="ti ti-upload me-1" aria-hidden />
)}
Upload
</AdminButton>
<code className="small bg-body-secondary px-2 py-1 rounded ms-auto text-break">Path: {path || '/'}</code>
<div className="card mb-3">
<div className="card-body py-2">
<div className="d-flex flex-wrap align-items-center gap-2 mb-2">
<AdminButton variant="secondary" size="sm" onClick={handleBack} disabled={!canGoBack}>
<i className="ti ti-arrow-left me-1" aria-hidden />
Back
</AdminButton>
<AdminButton variant="outline-secondary" size="sm" onClick={() => loadDir(path)} disabled={loading}>
<i className="ti ti-refresh me-1" aria-hidden />
Refresh
</AdminButton>
<Dropdown as="span">
<Dropdown.Toggle variant="success" size="sm" id="files-new-dropdown">
<i className="ti ti-plus me-1" aria-hidden />
New
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item onClick={() => setShowMkdir(true)}>
<i className="ti ti-folder-plus me-2" />
Folder
</Dropdown.Item>
<Dropdown.Item onClick={() => setShowNewFile(true)}>
<i className="ti ti-file-plus me-2" />
File
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
<input ref={fileInputRef} type="file" className="d-none" onChange={handleUpload} />
<AdminButton variant="primary" size="sm" onClick={() => fileInputRef.current?.click()} disabled={uploading}>
{uploading ? <span className="spinner-border spinner-border-sm me-1" role="status" /> : <i className="ti ti-upload me-1" aria-hidden />}
Upload
</AdminButton>
{selectedList.length === 1 && !selectedList[0].is_dir ? (
<AdminButton variant="outline-primary" size="sm" onClick={() => handleDownload(selectedList[0])}>
<i className="ti ti-download me-1" aria-hidden />
Download
</AdminButton>
) : null}
<AdminButton variant="outline-secondary" size="sm" onClick={copySelection} disabled={selectedList.length === 0}>
<i className="ti ti-copy me-1" aria-hidden />
Copy
</AdminButton>
<AdminButton variant="outline-secondary" size="sm" onClick={cutSelection} disabled={selectedList.length === 0}>
<i className="ti ti-cut me-1" aria-hidden />
Cut
</AdminButton>
<AdminButton variant="outline-primary" size="sm" onClick={pasteHere} disabled={!clipboard || clipboard.entries.length === 0}>
<i className="ti ti-clipboard me-1" aria-hidden />
Paste
</AdminButton>
<AdminButton variant="warning" size="sm" onClick={openCompress} disabled={selectedList.length === 0}>
<i className="ti ti-file-zip me-1" aria-hidden />
Compress
</AdminButton>
<AdminButton variant="outline-danger" size="sm" onClick={batchDelete} disabled={selectedList.length === 0}>
<i className="ti ti-trash me-1" aria-hidden />
Delete
</AdminButton>
</div>
<div className="d-flex flex-wrap align-items-center gap-2">
<div className="input-group input-group-sm" style={{ minWidth: 240, maxWidth: 480 }}>
<span className="input-group-text">Path</span>
<input
className="form-control font-monospace small"
value={pathInput}
onChange={(e) => setPathInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && goPathInput()}
/>
<AdminButton variant="primary" size="sm" className="rounded-0 rounded-end" type="button" onClick={goPathInput}>
Go
</AdminButton>
</div>
<div className="input-group input-group-sm flex-grow-1" style={{ minWidth: 200 }}>
<span className="input-group-text">
<i className="ti ti-search" aria-hidden />
</span>
<input
className="form-control"
placeholder="Filter current folder…"
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>
</div>
<div className="input-group input-group-sm" style={{ minWidth: 200 }}>
<input
className="form-control"
placeholder="Search subfolders…"
value={searchQ}
onChange={(e) => setSearchQ(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && runDeepSearch()}
/>
<AdminButton variant="outline-secondary" size="sm" type="button" onClick={runDeepSearch} disabled={searchLoading}>
{searchLoading ? <span className="spinner-border spinner-border-sm" role="status" /> : 'Search'}
</AdminButton>
</div>
</div>
<nav aria-label="breadcrumb" className="mt-2 mb-0">
<ol className="breadcrumb mb-0 small py-1">
<li className="breadcrumb-item">
<button type="button" className="btn btn-link btn-sm p-0 text-decoration-none" onClick={() => loadDir('/')}>
/
</button>
</li>
{pathSegments.map((seg, i) => (
<li key={`${seg}-${i}`} className={`breadcrumb-item${i === pathSegments.length - 1 ? ' active' : ''}`}>
{i === pathSegments.length - 1 ? (
seg
) : (
<button type="button" className="btn btn-link btn-sm p-0 text-decoration-none" onClick={() => breadcrumbNavigate(i)}>
{seg}
</button>
)}
</li>
))}
</ol>
</nav>
</div>
</div>
<Modal show={showMkdir} onHide={() => { setShowMkdir(false); setMkdirName('') }} centered>
<Modal.Header closeButton>
<Modal.Title>New Folder</Modal.Title>
<Modal.Title>New folder</Modal.Title>
</Modal.Header>
<form onSubmit={handleMkdir}>
<Modal.Body>
@@ -200,6 +495,109 @@ export function FilesPage() {
</form>
</Modal>
<Modal show={showNewFile} onHide={() => { setShowNewFile(false); setNewFileName('') }} centered>
<Modal.Header closeButton>
<Modal.Title>New file</Modal.Title>
</Modal.Header>
<form onSubmit={handleTouch}>
<Modal.Body>
<input
value={newFileName}
onChange={(e) => setNewFileName(e.target.value)}
placeholder="filename.txt"
className="form-control"
autoFocus
/>
</Modal.Body>
<Modal.Footer>
<AdminButton type="button" variant="secondary" onClick={() => { setShowNewFile(false); setNewFileName('') }}>
Cancel
</AdminButton>
<AdminButton type="submit" variant="primary">
Create
</AdminButton>
</Modal.Footer>
</form>
</Modal>
<Modal show={showChmod} onHide={() => { setShowChmod(false); setChmodItem(null) }} centered>
<Modal.Header closeButton>
<Modal.Title>Permissions</Modal.Title>
</Modal.Header>
<Modal.Body>
{chmodItem ? (
<>
<p className="small font-monospace text-break mb-2">{joinPath(path, chmodItem.name)}</p>
<label className="form-label small">Mode (octal)</label>
<input className="form-control mb-2 font-monospace" value={chmodMode} onChange={(e) => setChmodMode(e.target.value)} placeholder="0644" />
{chmodItem.is_dir ? (
<div className="form-check">
<input
className="form-check-input"
type="checkbox"
id="chmod-rec"
checked={chmodRecursive}
onChange={(e) => setChmodRecursive(e.target.checked)}
/>
<label className="form-check-label small" htmlFor="chmod-rec">
Recursive (chmod entire tree)
</label>
</div>
) : null}
</>
) : null}
</Modal.Body>
<Modal.Footer>
<AdminButton variant="secondary" onClick={() => { setShowChmod(false); setChmodItem(null) }}>
Cancel
</AdminButton>
<AdminButton variant="primary" onClick={submitChmod}>
Apply
</AdminButton>
</Modal.Footer>
</Modal>
<Modal show={showCompress} onHide={() => setShowCompress(false)} centered>
<Modal.Header closeButton>
<Modal.Title>Compress to ZIP</Modal.Title>
</Modal.Header>
<Modal.Body>
<p className="small text-secondary mb-2">{selectedList.length} item(s) selected</p>
<label className="form-label small">Archive name</label>
<input className="form-control font-monospace" value={compressName} onChange={(e) => setCompressName(e.target.value)} />
</Modal.Body>
<Modal.Footer>
<AdminButton variant="secondary" onClick={() => setShowCompress(false)}>
Cancel
</AdminButton>
<AdminButton variant="primary" onClick={submitCompress}>
Create ZIP
</AdminButton>
</Modal.Footer>
</Modal>
<Modal show={showSearchModal} onHide={() => setShowSearchModal(false)} size="lg" scrollable>
<Modal.Header closeButton>
<Modal.Title>Search results{searchQ ? `: “${searchQ}` : ''}</Modal.Title>
</Modal.Header>
<Modal.Body>
{searchResults.length === 0 ? (
<p className="text-secondary small mb-0">No matches.</p>
) : (
<ul className="list-group list-group-flush">
{searchResults.map((r) => (
<li key={r.path} className="list-group-item d-flex justify-content-between align-items-center">
<span className="small font-monospace text-break me-2">{r.path}</span>
<AdminButton size="sm" variant="outline-primary" onClick={() => navigateToSearchHit(r)}>
Open
</AdminButton>
</li>
))}
</ul>
)}
</Modal.Body>
</Modal>
<Modal show={!!editingFile} onHide={() => setEditingFile(null)} fullscreen="lg-down" size="lg">
<Modal.Header closeButton>
<Modal.Title className="text-break small font-monospace">{editingFile}</Modal.Title>
@@ -231,116 +629,212 @@ export function FilesPage() {
<span className="spinner-border text-secondary" role="status" />
</div>
) : (
<AdminTable>
<thead>
<tr>
<th>Name</th>
<th>Size</th>
<th className="text-end">Actions</th>
</tr>
</thead>
<tbody>
{items.length === 0 ? (
<>
<AdminTable>
<thead>
<tr>
<td colSpan={3} className="p-0">
<EmptyState title="Empty directory" description="Upload files or create a folder." />
</td>
<th style={{ width: 40 }}>
<input
type="checkbox"
className="form-check-input"
checked={filteredItems.length > 0 && selected.size === filteredItems.length}
onChange={selectAll}
aria-label="Select all"
/>
</th>
<th>Name</th>
<th>Size</th>
<th className="d-none d-lg-table-cell">Modified</th>
<th className="d-none d-md-table-cell">Permission</th>
<th className="d-none d-xl-table-cell">Owner</th>
<th className="text-end" style={{ minWidth: 120 }}>
Operation
</th>
</tr>
) : (
items.map((item) => (
<tr key={item.name}>
<td>
<button
type="button"
onClick={() => handleNavigate(item)}
className="btn btn-link text-start text-decoration-none p-0 d-inline-flex align-items-center gap-2"
>
<i className={`ti ${item.is_dir ? 'ti-folder text-warning' : 'ti-file text-secondary'}`} aria-hidden />
<span>{item.name}</span>
</button>
</td>
<td className="text-secondary">{item.is_dir ? '—' : formatSize(item.size)}</td>
<td className="text-end">
{renaming?.name === item.name ? (
<span className="d-inline-flex gap-1 align-items-center flex-wrap justify-content-end">
<input
value={renameValue}
onChange={(e) => setRenameValue(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleRename()}
className="form-control form-control-sm"
style={{ width: '8rem' }}
autoFocus
/>
<button type="button" className="btn btn-link btn-sm text-success p-1" title="Save" onClick={handleRename}>
<i className="ti ti-check" aria-hidden />
</button>
<button
type="button"
className="btn btn-link btn-sm p-1"
onClick={() => {
setRenaming(null)
setRenameValue('')
}}
>
Cancel
</button>
</span>
) : (
<span className="d-inline-flex gap-1 justify-content-end">
<button
type="button"
className="btn btn-link btn-sm text-secondary p-1"
title="Rename"
onClick={() => {
setRenaming(item)
setRenameValue(item.name)
}}
>
<i className="ti ti-pencil" aria-hidden />
</button>
{!item.is_dir && canEdit(item.name) ? (
<button
type="button"
className="btn btn-link btn-sm text-warning p-1"
title="Edit"
onClick={() => handleEdit(item)}
>
<i className="ti ti-edit" aria-hidden />
</button>
) : null}
{!item.is_dir ? (
<button
type="button"
className="btn btn-link btn-sm text-primary p-1"
title="Download"
disabled={downloading === item.name}
onClick={() => handleDownload(item)}
>
{downloading === item.name ? (
<span className="spinner-border spinner-border-sm" role="status" />
) : (
<i className="ti ti-download" aria-hidden />
)}
</button>
) : null}
<button
type="button"
className="btn btn-link btn-sm text-danger p-1"
title="Delete"
onClick={() => handleDelete(item)}
>
<i className="ti ti-trash" aria-hidden />
</button>
</span>
)}
</thead>
<tbody>
{filteredItems.length === 0 ? (
<tr>
<td colSpan={7} className="p-0">
<EmptyState title="Empty directory" description="Upload files, create a folder, or adjust the path filter." />
</td>
</tr>
))
)}
</tbody>
</AdminTable>
) : (
filteredItems.map((item) => (
<tr key={item.name}>
<td onClick={(e) => e.stopPropagation()}>
<input
type="checkbox"
className="form-check-input"
checked={selected.has(item.name)}
onChange={() => toggleSelect(item.name)}
aria-label={`Select ${item.name}`}
/>
</td>
<td>
{item.is_dir ? (
<button
type="button"
onClick={() => handleNavigate(item)}
className="btn btn-link text-start text-decoration-none p-0 d-inline-flex align-items-center gap-2"
>
<i className="ti ti-folder text-warning" aria-hidden />
<span>{item.name}</span>
</button>
) : (
<span className="d-inline-flex align-items-center gap-2">
<i className="ti ti-file text-secondary" aria-hidden />
{item.name}
</span>
)}
</td>
<td className="text-secondary small">
{item.is_dir ? (
dirSizes[item.name] !== undefined ? (
formatSize(dirSizes[item.name])
) : (
<button type="button" className="btn btn-link btn-sm p-0" onClick={() => calcDirSize(item)}>
Calculate
</button>
)
) : (
formatSize(item.size)
)}
</td>
<td className="small text-secondary d-none d-lg-table-cell">{item.mtime ?? '—'}</td>
<td className="small font-monospace d-none d-md-table-cell">
{item.mode_symbolic ? (
<>
<span title={`${item.mode_symbolic} (${item.mode})`}>{item.mode_symbolic}</span>
</>
) : (
item.mode ?? '—'
)}
</td>
<td className="small d-none d-xl-table-cell">
{item.owner ? `${item.owner}:${item.group ?? ''}` : '—'}
</td>
<td className="text-end">
{renaming?.name === item.name ? (
<span className="d-inline-flex gap-1 align-items-center flex-wrap justify-content-end">
<input
value={renameValue}
onChange={(e) => setRenameValue(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleRename()}
className="form-control form-control-sm"
style={{ width: '7rem' }}
autoFocus
/>
<button type="button" className="btn btn-link btn-sm text-success p-1" title="Save" onClick={handleRename}>
<i className="ti ti-check" aria-hidden />
</button>
<button
type="button"
className="btn btn-link btn-sm p-1"
onClick={() => {
setRenaming(null)
setRenameValue('')
}}
>
Cancel
</button>
</span>
) : (
<Dropdown align="end" onClick={(e: React.MouseEvent) => e.stopPropagation()}>
<Dropdown.Toggle variant="light" size="sm" className="py-0 border">
More
</Dropdown.Toggle>
<Dropdown.Menu>
{item.is_dir ? (
<Dropdown.Item onClick={() => handleNavigate(item)}>
<i className="ti ti-folder-open me-2" />
Open
</Dropdown.Item>
) : (
<>
<Dropdown.Item onClick={() => handleDownload(item)} disabled={downloading === item.name}>
<i className="ti ti-download me-2" />
Download
</Dropdown.Item>
{canEdit(item.name) ? (
<Dropdown.Item onClick={() => handleEdit(item)}>
<i className="ti ti-edit me-2" />
Edit
</Dropdown.Item>
) : null}
</>
)}
<Dropdown.Divider />
<Dropdown.Item onClick={() => { setClipboard({ op: 'copy', entries: [{ parent: path, name: item.name }] }) }}>
<i className="ti ti-copy me-2" />
Copy
</Dropdown.Item>
<Dropdown.Item onClick={() => { setClipboard({ op: 'cut', entries: [{ parent: path, name: item.name }] }) }}>
<i className="ti ti-cut me-2" />
Cut
</Dropdown.Item>
<Dropdown.Item
onClick={() => {
setRenaming(item)
setRenameValue(item.name)
}}
>
<i className="ti ti-pencil me-2" />
Rename
</Dropdown.Item>
<Dropdown.Item onClick={() => openChmod(item)}>
<i className="ti ti-lock me-2" />
Permission
</Dropdown.Item>
<Dropdown.Item
onClick={() => {
setSelected(new Set([item.name]))
setCompressName(`${item.name.replace(/\.[^/.]+$/, '') || 'archive'}.zip`)
setShowCompress(true)
}}
>
<i className="ti ti-file-zip me-2" />
Compress
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item className="text-danger" onClick={() => handleDelete(item)}>
<i className="ti ti-trash me-2" />
Delete
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
)}
</td>
</tr>
))
)}
</tbody>
</AdminTable>
{!loading && filteredItems.length > 0 ? (
<div className="card-footer py-2 small text-secondary d-flex flex-wrap gap-3">
<span>
Directories:{' '}
<strong>{filteredItems.filter((i) => i.is_dir).length}</strong>
</span>
<span>
Files:{' '}
<strong>{filteredItems.filter((i) => !i.is_dir).length}</strong>
</span>
<span>
Showing <strong>{filteredItems.length}</strong> of <strong>{items.length}</strong> in folder
</span>
</div>
) : null}
</>
)}
</div>
{clipboard && clipboard.entries.length > 0 ? (
<div className="alert alert-light border mt-3 mb-0 small py-2">
<i className="ti ti-clipboard me-1" aria-hidden />
Clipboard: <strong>{clipboard.op}</strong> ({clipboard.entries.length} item) use <strong>Paste</strong> in the target folder.
</div>
) : null}
</>
)
}