new changes
This commit is contained in:
@@ -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};
|
||||
@@ -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};
|
||||
@@ -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};
|
||||
@@ -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
1
YakPanel-server/frontend/dist/assets/BackupPlansPage-CgzFMegr.js
vendored
Normal file
1
YakPanel-server/frontend/dist/assets/BackupPlansPage-CgzFMegr.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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};
|
||||
@@ -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
1
YakPanel-server/frontend/dist/assets/FilesPage-DLgTKzsa.js
vendored
Normal file
1
YakPanel-server/frontend/dist/assets/FilesPage-DLgTKzsa.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -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};
|
||||
File diff suppressed because one or more lines are too long
@@ -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};
|
||||
File diff suppressed because one or more lines are too long
@@ -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};
|
||||
File diff suppressed because one or more lines are too long
@@ -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};
|
||||
@@ -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};
|
||||
1
YakPanel-server/frontend/dist/assets/PluginsPage-C4H8wPuq.js
vendored
Normal file
1
YakPanel-server/frontend/dist/assets/PluginsPage-C4H8wPuq.js
vendored
Normal 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};
|
||||
File diff suppressed because one or more lines are too long
@@ -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};
|
||||
File diff suppressed because one or more lines are too long
@@ -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};
|
||||
File diff suppressed because one or more lines are too long
@@ -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};
|
||||
1
YakPanel-server/frontend/dist/assets/UsersPage-DqvygXkC.js
vendored
Normal file
1
YakPanel-server/frontend/dist/assets/UsersPage-DqvygXkC.js
vendored
Normal 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};
|
||||
File diff suppressed because one or more lines are too long
2
YakPanel-server/frontend/dist/index.html
vendored
2
YakPanel-server/frontend/dist/index.html
vendored
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user