Revamp navbar - implement new sidepanel navigation

This commit is contained in:
2026-01-04 14:00:03 +08:00
parent b9cfc45aa2
commit 94a55cf2b7
3 changed files with 1174 additions and 120 deletions

View File

@@ -73,7 +73,6 @@ html {
}
.message-stack { position: sticky; top: 1rem; z-index: 100; }
/* Mobile-friendly table improvements */
.table-responsive table {
min-width: 600px;
}
@@ -83,7 +82,6 @@ html {
white-space: nowrap;
}
/* Allow text wrapping for description columns */
.table-responsive table td.text-wrap {
white-space: normal;
min-width: 200px;
@@ -166,14 +164,772 @@ code {
border-color: var(--myfsio-card-border);
}
.myfsio-nav {
:root {
--sidebar-width: 260px;
--sidebar-collapsed-width: 72px;
--mobile-header-height: 56px;
--sidebar-bg: linear-gradient(180deg, #0f172a 0%, #1e293b 100%);
--sidebar-border: rgba(255, 255, 255, 0.08);
--sidebar-link-color: rgba(255, 255, 255, 0.7);
--sidebar-link-hover: rgba(255, 255, 255, 0.95);
--sidebar-link-active-bg: rgba(59, 130, 246, 0.2);
--sidebar-link-active-border: #3b82f6;
--sidebar-section-color: rgba(255, 255, 255, 0.4);
}
[data-theme='dark'] {
--sidebar-bg: linear-gradient(180deg, #020617 0%, #0f172a 100%);
--sidebar-border: rgba(255, 255, 255, 0.06);
--sidebar-link-active-bg: rgba(59, 130, 246, 0.25);
}
body {
display: flex;
min-height: 100vh;
}
.main-wrapper {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
transition: margin-left 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
@media (min-width: 992px) {
.main-wrapper {
margin-left: var(--sidebar-width);
}
body.sidebar-is-collapsed .main-wrapper {
margin-left: var(--sidebar-collapsed-width);
}
}
.main-content {
flex: 1;
padding: 1.5rem;
max-width: 100%;
overflow-x: hidden;
}
@media (min-width: 992px) {
.main-content {
padding: 2rem 2.5rem;
}
}
.mobile-header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: var(--mobile-header-height);
background: var(--sidebar-bg);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 1rem;
z-index: 1030;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.sidebar-toggle-btn {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.1);
border: none;
border-radius: 10px;
color: white;
cursor: pointer;
transition: all 0.2s ease;
}
.sidebar-toggle-btn:hover {
background: rgba(255, 255, 255, 0.15);
transform: scale(1.05);
}
.mobile-brand {
display: flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
color: white;
font-weight: 600;
font-size: 1.1rem;
}
.mobile-brand img {
border-radius: 8px;
}
.theme-toggle-mobile {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.1);
border: none;
border-radius: 10px;
color: white;
cursor: pointer;
transition: all 0.2s ease;
}
.theme-toggle-mobile:hover {
background: rgba(255, 255, 255, 0.15);
}
@media (max-width: 991.98px) {
.main-wrapper {
padding-top: var(--mobile-header-height);
}
}
.sidebar {
position: fixed;
top: 0;
left: 0;
width: var(--sidebar-width);
height: 100vh;
background: var(--sidebar-bg);
display: flex;
flex-direction: column;
z-index: 1040;
border-right: 1px solid var(--sidebar-border);
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: visible;
}
.sidebar-collapsed {
width: var(--sidebar-collapsed-width);
}
html.sidebar-will-collapse .sidebar {
width: var(--sidebar-collapsed-width);
}
html.sidebar-will-collapse .main-wrapper {
margin-left: var(--sidebar-collapsed-width);
}
html.sidebar-will-collapse .sidebar,
html.sidebar-will-collapse .sidebar *,
html.sidebar-will-collapse .main-wrapper {
transition: none !important;
}
html.sidebar-will-collapse .sidebar-title,
html.sidebar-will-collapse .sidebar-link-text,
html.sidebar-will-collapse .nav-section-title,
html.sidebar-will-collapse .theme-toggle-text,
html.sidebar-will-collapse .sidebar-user .user-info,
html.sidebar-will-collapse .sidebar-logout-btn .logout-text,
html.sidebar-will-collapse .sidebar-collapse-btn {
display: none !important;
opacity: 0 !important;
width: 0 !important;
}
html.sidebar-will-collapse .sidebar-header {
justify-content: center;
padding: 1rem 0;
}
html.sidebar-will-collapse .sidebar-brand {
justify-content: center;
}
html.sidebar-will-collapse .nav-section {
padding: 0;
width: 100%;
}
html.sidebar-will-collapse .sidebar-nav {
align-items: center;
}
html.sidebar-will-collapse .sidebar-link {
justify-content: center;
align-items: center;
width: 48px;
height: 48px;
padding: 0;
margin: 0.25rem auto;
gap: 0;
}
html.sidebar-will-collapse .sidebar-footer {
padding: 0.75rem 0.5rem;
align-items: center;
}
html.sidebar-will-collapse .theme-toggle-sidebar,
html.sidebar-will-collapse .sidebar-user,
html.sidebar-will-collapse .sidebar-logout-btn {
justify-content: center;
width: 48px;
height: 48px;
padding: 0;
margin: 0 auto;
}
html.sidebar-will-collapse .sidebar-user {
background: transparent;
}
.sidebar-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.25rem 1rem;
border-bottom: 1px solid var(--sidebar-border);
min-height: 70px;
gap: 0.5rem;
overflow: visible;
}
.sidebar-collapsed .sidebar-header {
justify-content: center;
padding: 1rem 0;
min-height: 70px;
}
.sidebar-collapsed .sidebar-logo {
width: 36px;
height: 36px;
}
.sidebar-collapsed .sidebar-brand {
justify-content: center;
cursor: pointer;
position: relative;
gap: 0;
}
.sidebar-collapsed .sidebar-brand::after {
content: 'Click to expand';
position: absolute;
left: calc(100% + 12px);
top: 50%;
transform: translateY(-50%);
background: #1e293b;
color: white;
padding: 0.5rem 0.875rem;
border-radius: 8px;
font-size: 0.8rem;
font-weight: 500;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: all 0.2s ease;
z-index: 1050;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.sidebar-collapsed .sidebar-brand:hover::after {
opacity: 1;
visibility: visible;
}
.sidebar-collapsed .sidebar-collapse-btn {
display: none;
}
.sidebar-brand {
display: flex;
align-items: center;
gap: 0.75rem;
text-decoration: none;
color: white;
overflow: visible;
}
.sidebar-logo {
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
flex-shrink: 0;
transition: transform 0.2s ease;
}
.sidebar-brand:hover .sidebar-logo {
transform: scale(1.05);
}
.sidebar-title {
font-weight: 700;
font-size: 1.25rem;
letter-spacing: -0.02em;
white-space: nowrap;
opacity: 1;
transition: opacity 0.2s ease;
}
.sidebar-collapsed .sidebar-title {
opacity: 0;
width: 0;
}
.sidebar-collapse-btn {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.1);
border: none;
border-radius: 8px;
color: rgba(255, 255, 255, 0.7);
cursor: pointer;
transition: all 0.2s ease;
flex-shrink: 0;
}
.sidebar-collapse-btn:hover {
background: rgba(255, 255, 255, 0.15);
color: white;
}
.sidebar-collapse-btn svg {
transition: transform 0.3s ease;
}
.sidebar-collapsed .sidebar-collapse-btn svg {
transform: rotate(180deg);
}
.sidebar-body {
flex: 1;
padding: 1rem 0;
}
.sidebar-nav {
display: flex;
flex-direction: column;
gap: 0.5rem;
overflow: visible;
}
.nav-section {
padding: 0 0.75rem;
margin-bottom: 0.5rem;
overflow: visible;
}
.nav-section-title {
display: block;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--sidebar-section-color);
padding: 0.5rem 0.75rem;
white-space: nowrap;
overflow: hidden;
transition: opacity 0.2s ease;
}
.sidebar-collapsed .nav-section-title {
opacity: 0;
height: 0;
padding: 0;
margin: 0;
}
.sidebar-link {
display: flex;
align-items: center;
gap: 0.875rem;
padding: 0.75rem 1rem;
color: var(--sidebar-link-color);
text-decoration: none;
border-radius: 10px;
transition: all 0.2s ease;
position: relative;
overflow: visible;
margin: 0.5rem;
}
.sidebar-link::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 0;
background: var(--sidebar-link-active-border);
border-radius: 0 2px 2px 0;
transition: height 0.2s ease;
}
.sidebar-link:hover {
color: var(--sidebar-link-hover);
background: rgba(255, 255, 255, 0.08);
}
.sidebar-link.active {
color: white;
background: var(--sidebar-link-active-bg);
}
.sidebar-link.active::before {
height: 60%;
}
.sidebar-link svg {
flex-shrink: 0;
opacity: 0.9;
transition: transform 0.2s ease;
}
.sidebar-link:hover svg {
transform: scale(1.1);
}
.sidebar-link-text {
white-space: nowrap;
overflow: hidden;
transition: opacity 0.2s ease, width 0.2s ease;
}
.sidebar-collapsed .sidebar-link-text {
opacity: 0;
width: 0;
}
.sidebar-collapsed .sidebar-link {
justify-content: center;
align-items: center;
padding: 0;
margin: 0.25rem auto;
width: 48px;
height: 48px;
gap: 0;
}
.sidebar-collapsed .sidebar-link svg {
width: 20px;
height: 20px;
flex-shrink: 0;
}
.sidebar-collapsed .nav-section {
padding: 0;
width: 100%;
}
.sidebar-collapsed .sidebar-nav {
align-items: center;
}
.sidebar-collapsed .sidebar-link[data-tooltip]::after {
content: attr(data-tooltip);
position: absolute;
left: calc(100% + 12px);
top: 50%;
transform: translateY(-50%);
background: #1e293b;
color: white;
padding: 0.5rem 0.875rem;
border-radius: 8px;
font-size: 0.85rem;
font-weight: 500;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: all 0.2s ease;
z-index: 1050;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.sidebar-collapsed .sidebar-link[data-tooltip]:hover::after {
opacity: 1;
visibility: visible;
}
.sidebar-footer {
padding: 1rem;
border-top: 1px solid var(--sidebar-border);
display: flex;
flex-direction: column;
gap: 0.75rem;
overflow: visible;
}
.sidebar-collapsed .sidebar-footer {
padding: 0.75rem 0.5rem;
align-items: center;
}
.theme-toggle-sidebar {
display: flex;
align-items: center;
gap: 0.75rem;
width: 100%;
padding: 0.75rem 1rem;
background: rgba(255, 255, 255, 0.08);
border: none;
border-radius: 10px;
color: rgba(255, 255, 255, 0.8);
cursor: pointer;
transition: all 0.2s ease;
}
.theme-toggle-sidebar:hover {
background: rgba(255, 255, 255, 0.12);
color: white;
}
.theme-toggle-text {
font-size: 0.875rem;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
transition: opacity 0.2s ease;
}
.sidebar-collapsed .theme-toggle-text {
opacity: 0;
width: 0;
display: none;
}
.sidebar-collapsed .theme-toggle-sidebar {
justify-content: center;
padding: 0;
width: 48px;
height: 48px;
margin: 0 auto;
position: relative;
}
.sidebar-collapsed .theme-toggle-sidebar svg {
width: 18px;
height: 18px;
flex-shrink: 0;
}
.sidebar-collapsed .theme-toggle-sidebar::after {
content: 'Toggle theme';
position: absolute;
left: calc(100% + 12px);
top: 50%;
transform: translateY(-50%);
background: #1e293b;
color: white;
padding: 0.5rem 0.875rem;
border-radius: 8px;
font-size: 0.8rem;
font-weight: 500;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: all 0.2s ease;
z-index: 1050;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.sidebar-collapsed .theme-toggle-sidebar:hover::after {
opacity: 1;
visibility: visible;
}
.sidebar-user {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
background: rgba(255, 255, 255, 0.05);
border-radius: 10px;
overflow: hidden;
}
.sidebar-user .user-avatar {
width: 36px;
height: 36px;
border-radius: 10px;
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
display: flex;
align-items: center;
justify-content: center;
color: white;
flex-shrink: 0;
}
.sidebar-user .user-info {
overflow: hidden;
transition: opacity 0.2s ease, width 0.2s ease;
}
.sidebar-collapsed .sidebar-user .user-info {
opacity: 0;
width: 0;
display: none;
}
.sidebar-collapsed .sidebar-user {
justify-content: center;
padding: 0;
width: 48px;
height: 48px;
margin: 0 auto;
background: transparent;
position: relative;
}
.sidebar-collapsed .sidebar-user .user-avatar {
width: 36px;
height: 36px;
}
.sidebar-collapsed .sidebar-user::after {
content: attr(data-username);
position: absolute;
left: calc(100% + 12px);
top: 50%;
transform: translateY(-50%);
background: #1e293b;
color: white;
padding: 0.5rem 0.875rem;
border-radius: 8px;
font-size: 0.8rem;
font-weight: 500;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: all 0.2s ease;
z-index: 1050;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.sidebar-collapsed .sidebar-user:hover::after {
opacity: 1;
visibility: visible;
}
.sidebar-user .user-name {
font-size: 0.875rem;
font-weight: 600;
color: white;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sidebar-user .user-key {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.5);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sidebar-logout-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
width: 100%;
padding: 0.75rem 1rem;
background: rgba(239, 68, 68, 0.15);
border: 1px solid rgba(239, 68, 68, 0.3);
border-radius: 10px;
color: #fca5a5;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.sidebar-logout-btn:hover {
background: rgba(239, 68, 68, 0.25);
border-color: rgba(239, 68, 68, 0.5);
color: #fecaca;
}
.sidebar-collapsed .sidebar-logout-btn .logout-text {
display: none;
}
.sidebar-collapsed .sidebar-logout-btn {
justify-content: center;
padding: 0;
width: 48px;
height: 48px;
margin: 0 auto;
position: relative;
}
.sidebar-collapsed .sidebar-logout-btn svg {
width: 18px;
height: 18px;
flex-shrink: 0;
}
.sidebar-collapsed .sidebar-logout-btn::after {
content: 'Sign out';
position: absolute;
left: calc(100% + 12px);
top: 50%;
transform: translateY(-50%);
background: #1e293b;
color: white;
padding: 0.5rem 0.875rem;
border-radius: 8px;
font-size: 0.8rem;
font-weight: 500;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: all 0.2s ease;
z-index: 1050;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.sidebar-collapsed .sidebar-logout-btn:hover::after {
opacity: 1;
visibility: visible;
}
.sidebar-offcanvas {
width: 280px !important;
background: var(--sidebar-bg) !important;
border-right: none !important;
}
.sidebar-offcanvas .sidebar-header {
background: transparent;
}
.sidebar-offcanvas .sidebar-body {
display: flex;
flex-direction: column;
padding: 0;
}
.sidebar-offcanvas .sidebar-nav {
flex: 1;
padding: 1rem 0;
}
.sidebar-offcanvas .sidebar-footer {
margin-top: auto;
}
.myfsio-nav {
background: var(--myfsio-nav-gradient);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
.myfsio-nav .navbar-brand {
.myfsio-nav .navbar-brand {
color: #fff;
font-weight: 600;
letter-spacing: -0.02em;
@@ -192,24 +948,24 @@ code {
display: inline-block;
}
.myfsio-nav .nav-link {
.myfsio-nav .nav-link {
color: var(--myfsio-nav-link);
transition: color 0.2s ease;
}
.myfsio-nav .nav-link:hover {
.myfsio-nav .nav-link:hover {
color: var(--myfsio-nav-link-hover);
}
.myfsio-nav .nav-link.nav-link-muted { opacity: 0.75; }
.myfsio-nav .nav-link.nav-link-muted .badge {
.myfsio-nav .nav-link.nav-link-muted .badge {
color: #0f172a;
background-color: #fef08a;
}
[data-theme='dark'] .myfsio-nav .nav-link.nav-link-muted .badge {
color: #0f172a;
[data-theme='dark'] .myfsio-nav .nav-link.nav-link-muted .badge {
color: #0f172a;
background-color: #fde047;
}
@@ -382,14 +1138,12 @@ code {
min-width: 0;
}
/* Ensure pre blocks don't overflow on mobile */
.alert pre {
max-width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
/* IAM User Cards */
.iam-user-card {
border: 1px solid var(--myfsio-card-border);
border-radius: 0.75rem;
@@ -1773,7 +2527,6 @@ pre code {
top: 0 !important;
}
/* Ensure tables are scrollable on mobile */
.card-body .table-responsive {
margin: -1rem;
padding: 0;
@@ -1784,13 +2537,11 @@ pre code {
margin-bottom: 0;
}
/* IAM users table mobile adjustments */
.table th,
.table td {
padding: 0.5rem 0.75rem;
}
/* Better touch scrolling indicator */
.table-responsive::after {
content: '';
position: absolute;
@@ -1969,3 +2720,76 @@ body.theme-transitioning * {
flex: 1;
}
}
.modal {
z-index: 1055;
}
.modal-backdrop {
z-index: 1050;
}
@media (min-width: 992px) {
.toast-container {
right: 1.5rem;
}
}
@media (min-width: 992px) {
.floating-upload-progress {
right: 1.5rem;
}
}
.sidebar-body::-webkit-scrollbar {
width: 4px;
}
.sidebar-body::-webkit-scrollbar-track {
background: transparent;
}
.sidebar-body::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 2px;
}
.sidebar-body::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
@keyframes sidebarLinkPulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.4); }
50% { box-shadow: 0 0 0 4px rgba(59, 130, 246, 0); }
}
.sidebar-link.active {
animation: sidebarLinkPulse 2s ease-in-out infinite;
}
.offcanvas-backdrop.show {
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
}
body:has(.login-card) .sidebar,
body:has(.login-card) .mobile-header {
display: none !important;
}
body:has(.login-card) .main-wrapper {
margin-left: 0 !important;
padding-top: 0 !important;
}
@media print {
.sidebar,
.mobile-header {
display: none !important;
}
.main-wrapper {
margin-left: 0 !important;
padding-top: 0 !important;
}
}

View File

@@ -24,105 +24,216 @@
document.documentElement.dataset.bsTheme = 'light';
document.documentElement.dataset.theme = 'light';
}
try {
if (localStorage.getItem('myfsio-sidebar-collapsed') === 'true') {
document.documentElement.classList.add('sidebar-will-collapse');
}
} catch (err) {}
})();
</script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}" />
</head>
<body>
<nav class="navbar navbar-expand-lg myfsio-nav shadow-sm">
<div class="container-fluid">
<a class="navbar-brand fw-semibold" href="{{ url_for('ui.buckets_overview') }}">
<img
src="{{ url_for('static', filename='images/MyFSIO.png') }}"
alt="MyFSIO logo"
class="myfsio-logo"
width="32"
height="32"
decoding="async"
/>
<span class="myfsio-title">MyFSIO</span>
<header class="mobile-header d-lg-none">
<button class="sidebar-toggle-btn" type="button" data-bs-toggle="offcanvas" data-bs-target="#mobileSidebar" aria-controls="mobileSidebar" aria-label="Toggle navigation">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
</svg>
</button>
<a class="mobile-brand" href="{{ url_for('ui.buckets_overview') }}">
<img src="{{ url_for('static', filename='images/MyFSIO.png') }}" alt="MyFSIO logo" width="28" height="28" />
<span>MyFSIO</span>
</a>
<button class="theme-toggle-mobile" type="button" id="themeToggleMobile" aria-label="Toggle dark mode">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="theme-icon-mobile" id="themeToggleSunMobile" viewBox="0 0 16 16">
<path d="M8 11.5a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7zm0 1.5a5 5 0 1 0 0-10 5 5 0 0 0 0 10zM8 0a.5.5 0 0 1 .5.5v1.555a.5.5 0 0 1-1 0V.5A.5.5 0 0 1 8 0zm0 12.945a.5.5 0 0 1 .5.5v2.055a.5.5 0 0 1-1 0v-2.055a.5.5 0 0 1 .5-.5zM2.343 2.343a.5.5 0 0 1 .707 0l1.1 1.1a.5.5 0 1 1-.708.707l-1.1-1.1a.5.5 0 0 1 0-.707zm9.507 9.507a.5.5 0 0 1 .707 0l1.1 1.1a.5.5 0 1 1-.707.708l-1.1-1.1a.5.5 0 0 1 0-.708zM0 8a.5.5 0 0 1 .5-.5h1.555a.5.5 0 0 1 0 1H.5A.5.5 0 0 1 0 8zm12.945 0a.5.5 0 0 1 .5-.5H15.5a.5.5 0 0 1 0 1h-2.055a.5.5 0 0 1-.5-.5zM2.343 13.657a.5.5 0 0 1 0-.707l1.1-1.1a.5.5 0 1 1 .708.707l-1.1 1.1a.5.5 0 0 1-.708 0zm9.507-9.507a.5.5 0 0 1 0-.707l1.1-1.1a.5.5 0 0 1 .707.708l-1.1 1.1a.5.5 0 0 1-.707 0z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="theme-icon-mobile d-none" id="themeToggleMoonMobile" viewBox="0 0 16 16">
<path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z"/>
<path d="M10.794 3.148a.217.217 0 0 1 .412 0l.387 1.162c.173.518.579.924 1.097 1.097l1.162.387a.217.217 0 0 1 0 .412l-1.162.387a1.734 1.734 0 0 0-1.097 1.097l-.387 1.162a.217.217 0 0 1-.412 0l-.387-1.162A1.734 1.734 0 0 0 9.31 6.593l-1.162-.387a.217.217 0 0 1 0-.412l1.162-.387a1.734 1.734 0 0 0 1.097-1.097l.387-1.162zM13.863.099a.145.145 0 0 1 .274 0l.258.774c.115.346.386.617.732.732l.774.258a.145.145 0 0 1 0 .274l-.774.258a1.156 1.156 0 0 0-.732.732l-.258.774a.145.145 0 0 1-.274 0l-.258-.774a1.156 1.156 0 0 0-.732-.732l-.774-.258a.145.145 0 0 1 0-.274l.774-.258c.346-.115.617-.386.732-.732L13.863.1z"/>
</svg>
</button>
</header>
<div class="offcanvas offcanvas-start sidebar-offcanvas" tabindex="-1" id="mobileSidebar" aria-labelledby="mobileSidebarLabel">
<div class="offcanvas-header sidebar-header">
<a class="sidebar-brand" href="{{ url_for('ui.buckets_overview') }}">
<img src="{{ url_for('static', filename='images/MyFSIO.png') }}" alt="MyFSIO logo" class="sidebar-logo" width="36" height="36" />
<span class="sidebar-title">MyFSIO</span>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navContent" aria-controls="navContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
{% if principal %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('ui.buckets_overview') }}">Buckets</a>
</li>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body sidebar-body">
<nav class="sidebar-nav">
{% if principal %}
<div class="nav-section">
<span class="nav-section-title">Navigation</span>
<a href="{{ url_for('ui.buckets_overview') }}" class="sidebar-link {% if request.endpoint == 'ui.buckets_overview' or request.endpoint == 'ui.bucket_detail' %}active{% endif %}">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
<path d="M2.522 5H2a.5.5 0 0 0-.494.574l1.372 9.149A1.5 1.5 0 0 0 4.36 16h7.278a1.5 1.5 0 0 0 1.483-1.277l1.373-9.149A.5.5 0 0 0 14 5h-.522A5.5 5.5 0 0 0 2.522 5zm1.005 0a4.5 4.5 0 0 1 8.945 0H3.527z"/>
</svg>
<span>Buckets</span>
</a>
{% if can_manage_iam %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('ui.iam_dashboard') }}">IAM</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('ui.connections_dashboard') }}">Connections</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('ui.metrics_dashboard') }}">Metrics</a>
</li>
{% endif %}
{% endif %}
{% if principal %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('ui.docs_page') }}">Docs</a>
</li>
{% endif %}
</ul>
<div class="ms-lg-auto d-flex align-items-center gap-3 text-light flex-wrap">
<button
class="btn btn-outline-light btn-sm theme-toggle"
type="button"
id="themeToggle"
aria-pressed="false"
aria-label="Toggle dark mode"
>
<span id="themeToggleLabel" class="visually-hidden">Toggle dark mode</span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
class="theme-icon"
id="themeToggleSun"
viewBox="0 0 16 16"
aria-hidden="true"
>
<path
d="M8 11.5a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7zm0 1.5a5 5 0 1 0 0-10 5 5 0 0 0 0 10zM8 0a.5.5 0 0 1 .5.5v1.555a.5.5 0 0 1-1 0V.5A.5.5 0 0 1 8 0zm0 12.945a.5.5 0 0 1 .5.5v2.055a.5.5 0 0 1-1 0v-2.055a.5.5 0 0 1 .5-.5zM2.343 2.343a.5.5 0 0 1 .707 0l1.1 1.1a.5.5 0 1 1-.708.707l-1.1-1.1a.5.5 0 0 1 0-.707zm9.507 9.507a.5.5 0 0 1 .707 0l1.1 1.1a.5.5 0 1 1-.707.708l-1.1-1.1a.5.5 0 0 1 0-.708zM0 8a.5.5 0 0 1 .5-.5h1.555a.5.5 0 0 1 0 1H.5A.5.5 0 0 1 0 8zm12.945 0a.5.5 0 0 1 .5-.5H15.5a.5.5 0 0 1 0 1h-2.055a.5.5 0 0 1-.5-.5zM2.343 13.657a.5.5 0 0 1 0-.707l1.1-1.1a.5.5 0 1 1 .708.707l-1.1 1.1a.5.5 0 0 1-.708 0zm9.507-9.507a.5.5 0 0 1 0-.707l1.1-1.1a.5.5 0 0 1 .707.708l-1.1 1.1a.5.5 0 0 1-.707 0z"
/>
<a href="{{ url_for('ui.iam_dashboard') }}" class="sidebar-link {% if request.endpoint == 'ui.iam_dashboard' %}active{% endif %}">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
<path d="M15 14s1 0 1-1-1-4-5-4-5 3-5 4 1 1 1 1h8zm-7.978-1A.261.261 0 0 1 7 12.996c.001-.264.167-1.03.76-1.72C8.312 10.629 9.282 10 11 10c1.717 0 2.687.63 3.24 1.276.593.69.758 1.457.76 1.72l-.008.002a.274.274 0 0 1-.014.002H7.022zM11 7a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm3-2a3 3 0 1 1-6 0 3 3 0 0 1 6 0zM6.936 9.28a5.88 5.88 0 0 0-1.23-.247A7.35 7.35 0 0 0 5 9c-4 0-5 3-5 4 0 .667.333 1 1 1h4.216A2.238 2.238 0 0 1 5 13c0-1.01.377-2.042 1.09-2.904.243-.294.526-.569.846-.816zM4.92 10A5.493 5.493 0 0 0 4 13H1c0-.26.164-1.03.76-1.724.545-.636 1.492-1.256 3.16-1.275zM1.5 5.5a3 3 0 1 1 6 0 3 3 0 0 1-6 0zm3-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4z"/>
</svg>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
class="theme-icon d-none"
id="themeToggleMoon"
viewBox="0 0 16 16"
aria-hidden="true"
>
<path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z"/>
<path d="M10.794 3.148a.217.217 0 0 1 .412 0l.387 1.162c.173.518.579.924 1.097 1.097l1.162.387a.217.217 0 0 1 0 .412l-1.162.387a1.734 1.734 0 0 0-1.097 1.097l-.387 1.162a.217.217 0 0 1-.412 0l-.387-1.162A1.734 1.734 0 0 0 9.31 6.593l-1.162-.387a.217.217 0 0 1 0-.412l1.162-.387a1.734 1.734 0 0 0 1.097-1.097l.387-1.162zM13.863.099a.145.145 0 0 1 .274 0l.258.774c.115.346.386.617.732.732l.774.258a.145.145 0 0 1 0 .274l-.774.258a1.156 1.156 0 0 0-.732.732l-.258.774a.145.145 0 0 1-.274 0l-.258-.774a1.156 1.156 0 0 0-.732-.732l-.774-.258a.145.145 0 0 1 0-.274l.774-.258c.346-.115.617-.386.732-.732L13.863.1z"/>
<span>IAM</span>
</a>
<a href="{{ url_for('ui.connections_dashboard') }}" class="sidebar-link {% if request.endpoint == 'ui.connections_dashboard' %}active{% endif %}">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
<path d="M6 0a.5.5 0 0 1 .5.5V3h3V.5a.5.5 0 0 1 1 0V3h1a.5.5 0 0 1 .5.5v3A3.5 3.5 0 0 1 8.5 10c-.002.434-.01.845-.04 1.22-.041.514-.126 1.003-.317 1.424a2.083 2.083 0 0 1-.97 1.028C6.725 13.9 6.169 14 5.5 14c-.998 0-1.61.33-1.974.718A1.922 1.922 0 0 0 3 16H2c0-.616.232-1.367.797-1.968C3.374 13.42 4.261 13 5.5 13c.581 0 .962-.088 1.218-.219.241-.123.4-.3.514-.55.121-.266.193-.621.23-1.09.027-.34.035-.718.037-1.141A3.5 3.5 0 0 1 4 6.5v-3a.5.5 0 0 1 .5-.5h1V.5A.5.5 0 0 1 6 0z"/>
</svg>
</button>
{% if principal %}
<div class="text-end small">
<div class="fw-semibold" title="{{ principal.display_name }}">{{ principal.display_name | truncate(20, true) }}</div>
<div class="opacity-75">{{ principal.access_key }}</div>
</div>
<form method="post" action="{{ url_for('ui.logout') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<button class="btn btn-outline-light btn-sm" type="submit">Sign out</button>
</form>
<span>Connections</span>
</a>
<a href="{{ url_for('ui.metrics_dashboard') }}" class="sidebar-link {% if request.endpoint == 'ui.metrics_dashboard' %}active{% endif %}">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
<path d="M4 11H2v3h2v-3zm5-4H7v7h2V7zm5-5h-2v12h2V2zm-2-1a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1h-2zM6 7a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7zm-5 4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1v-3z"/>
</svg>
<span>Metrics</span>
</a>
{% endif %}
</div>
<div class="nav-section">
<span class="nav-section-title">Resources</span>
<a href="{{ url_for('ui.docs_page') }}" class="sidebar-link {% if request.endpoint == 'ui.docs_page' %}active{% endif %}">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
<path d="M1 2.828c.885-.37 2.154-.769 3.388-.893 1.33-.134 2.458.063 3.112.752v9.746c-.935-.53-2.12-.603-3.213-.493-1.18.12-2.37.461-3.287.811V2.828zm7.5-.141c.654-.689 1.782-.886 3.112-.752 1.234.124 2.503.523 3.388.893v9.923c-.918-.35-2.107-.692-3.287-.81-1.094-.111-2.278-.039-3.213.492V2.687zM8 1.783C7.015.936 5.587.81 4.287.94c-1.514.153-3.042.672-3.994 1.105A.5.5 0 0 0 0 2.5v11a.5.5 0 0 0 .707.455c.882-.4 2.303-.881 3.68-1.02 1.409-.142 2.59.087 3.223.877a.5.5 0 0 0 .78 0c.633-.79 1.814-1.019 3.222-.877 1.378.139 2.8.62 3.681 1.02A.5.5 0 0 0 16 13.5v-11a.5.5 0 0 0-.293-.455c-.952-.433-2.48-.952-3.994-1.105C10.413.809 8.985.936 8 1.783z"/>
</svg>
<span>Documentation</span>
</a>
</div>
{% endif %}
</nav>
{% if principal %}
<div class="sidebar-footer">
<div class="sidebar-user">
<div class="user-avatar">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
<path d="M11 6a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/>
<path fill-rule="evenodd" d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm8-7a7 7 0 0 0-5.468 11.37C3.242 11.226 4.805 10 8 10s4.757 1.225 5.468 2.37A7 7 0 0 0 8 1z"/>
</svg>
</div>
<div class="user-info">
<div class="user-name" title="{{ principal.display_name }}">{{ principal.display_name | truncate(16, true) }}</div>
<div class="user-key">{{ principal.access_key | truncate(12, true) }}</div>
</div>
</div>
<form method="post" action="{{ url_for('ui.logout') }}" class="w-100">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<button class="sidebar-logout-btn" type="submit">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M10 12.5a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v2a.5.5 0 0 0 1 0v-2A1.5 1.5 0 0 0 9.5 2h-8A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-2a.5.5 0 0 0-1 0v2z"/>
<path fill-rule="evenodd" d="M15.854 8.354a.5.5 0 0 0 0-.708l-3-3a.5.5 0 0 0-.708.708L14.293 7.5H5.5a.5.5 0 0 0 0 1h8.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3z"/>
</svg>
<span>Sign out</span>
</button>
</form>
</div>
{% endif %}
</div>
</nav>
<main class="container py-4">
{% block content %}{% endblock %}
</main>
</div>
<aside class="sidebar d-none d-lg-flex" id="desktopSidebar">
<div class="sidebar-header">
<div class="sidebar-brand" id="sidebarBrand">
<img src="{{ url_for('static', filename='images/MyFSIO.png') }}" alt="MyFSIO logo" class="sidebar-logo" width="36" height="36" />
<span class="sidebar-title">MyFSIO</span>
</div>
<button class="sidebar-collapse-btn" type="button" id="sidebarCollapseBtn" aria-label="Collapse sidebar">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z"/>
</svg>
</button>
</div>
<div class="sidebar-body">
<nav class="sidebar-nav">
{% if principal %}
<div class="nav-section">
<span class="nav-section-title">Navigation</span>
<a href="{{ url_for('ui.buckets_overview') }}" class="sidebar-link {% if request.endpoint == 'ui.buckets_overview' or request.endpoint == 'ui.bucket_detail' %}active{% endif %}" data-tooltip="Buckets">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
<path d="M2.522 5H2a.5.5 0 0 0-.494.574l1.372 9.149A1.5 1.5 0 0 0 4.36 16h7.278a1.5 1.5 0 0 0 1.483-1.277l1.373-9.149A.5.5 0 0 0 14 5h-.522A5.5 5.5 0 0 0 2.522 5zm1.005 0a4.5 4.5 0 0 1 8.945 0H3.527z"/>
</svg>
<span class="sidebar-link-text">Buckets</span>
</a>
{% if can_manage_iam %}
<a href="{{ url_for('ui.iam_dashboard') }}" class="sidebar-link {% if request.endpoint == 'ui.iam_dashboard' %}active{% endif %}" data-tooltip="IAM">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
<path d="M15 14s1 0 1-1-1-4-5-4-5 3-5 4 1 1 1 1h8zm-7.978-1A.261.261 0 0 1 7 12.996c.001-.264.167-1.03.76-1.72C8.312 10.629 9.282 10 11 10c1.717 0 2.687.63 3.24 1.276.593.69.758 1.457.76 1.72l-.008.002a.274.274 0 0 1-.014.002H7.022zM11 7a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm3-2a3 3 0 1 1-6 0 3 3 0 0 1 6 0zM6.936 9.28a5.88 5.88 0 0 0-1.23-.247A7.35 7.35 0 0 0 5 9c-4 0-5 3-5 4 0 .667.333 1 1 1h4.216A2.238 2.238 0 0 1 5 13c0-1.01.377-2.042 1.09-2.904.243-.294.526-.569.846-.816zM4.92 10A5.493 5.493 0 0 0 4 13H1c0-.26.164-1.03.76-1.724.545-.636 1.492-1.256 3.16-1.275zM1.5 5.5a3 3 0 1 1 6 0 3 3 0 0 1-6 0zm3-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4z"/>
</svg>
<span class="sidebar-link-text">IAM</span>
</a>
<a href="{{ url_for('ui.connections_dashboard') }}" class="sidebar-link {% if request.endpoint == 'ui.connections_dashboard' %}active{% endif %}" data-tooltip="Connections">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
<path d="M6 0a.5.5 0 0 1 .5.5V3h3V.5a.5.5 0 0 1 1 0V3h1a.5.5 0 0 1 .5.5v3A3.5 3.5 0 0 1 8.5 10c-.002.434-.01.845-.04 1.22-.041.514-.126 1.003-.317 1.424a2.083 2.083 0 0 1-.97 1.028C6.725 13.9 6.169 14 5.5 14c-.998 0-1.61.33-1.974.718A1.922 1.922 0 0 0 3 16H2c0-.616.232-1.367.797-1.968C3.374 13.42 4.261 13 5.5 13c.581 0 .962-.088 1.218-.219.241-.123.4-.3.514-.55.121-.266.193-.621.23-1.09.027-.34.035-.718.037-1.141A3.5 3.5 0 0 1 4 6.5v-3a.5.5 0 0 1 .5-.5h1V.5A.5.5 0 0 1 6 0z"/>
</svg>
<span class="sidebar-link-text">Connections</span>
</a>
<a href="{{ url_for('ui.metrics_dashboard') }}" class="sidebar-link {% if request.endpoint == 'ui.metrics_dashboard' %}active{% endif %}" data-tooltip="Metrics">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
<path d="M4 11H2v3h2v-3zm5-4H7v7h2V7zm5-5h-2v12h2V2zm-2-1a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1h-2zM6 7a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7zm-5 4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1v-3z"/>
</svg>
<span class="sidebar-link-text">Metrics</span>
</a>
{% endif %}
</div>
<div class="nav-section">
<span class="nav-section-title">Resources</span>
<a href="{{ url_for('ui.docs_page') }}" class="sidebar-link {% if request.endpoint == 'ui.docs_page' %}active{% endif %}" data-tooltip="Documentation">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
<path d="M1 2.828c.885-.37 2.154-.769 3.388-.893 1.33-.134 2.458.063 3.112.752v9.746c-.935-.53-2.12-.603-3.213-.493-1.18.12-2.37.461-3.287.811V2.828zm7.5-.141c.654-.689 1.782-.886 3.112-.752 1.234.124 2.503.523 3.388.893v9.923c-.918-.35-2.107-.692-3.287-.81-1.094-.111-2.278-.039-3.213.492V2.687zM8 1.783C7.015.936 5.587.81 4.287.94c-1.514.153-3.042.672-3.994 1.105A.5.5 0 0 0 0 2.5v11a.5.5 0 0 0 .707.455c.882-.4 2.303-.881 3.68-1.02 1.409-.142 2.59.087 3.223.877a.5.5 0 0 0 .78 0c.633-.79 1.814-1.019 3.222-.877 1.378.139 2.8.62 3.681 1.02A.5.5 0 0 0 16 13.5v-11a.5.5 0 0 0-.293-.455c-.952-.433-2.48-.952-3.994-1.105C10.413.809 8.985.936 8 1.783z"/>
</svg>
<span class="sidebar-link-text">Documentation</span>
</a>
</div>
{% endif %}
</nav>
</div>
<div class="sidebar-footer">
<button class="theme-toggle-sidebar" type="button" id="themeToggle" aria-label="Toggle dark mode">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="theme-icon" id="themeToggleSun" viewBox="0 0 16 16">
<path d="M8 11.5a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7zm0 1.5a5 5 0 1 0 0-10 5 5 0 0 0 0 10zM8 0a.5.5 0 0 1 .5.5v1.555a.5.5 0 0 1-1 0V.5A.5.5 0 0 1 8 0zm0 12.945a.5.5 0 0 1 .5.5v2.055a.5.5 0 0 1-1 0v-2.055a.5.5 0 0 1 .5-.5zM2.343 2.343a.5.5 0 0 1 .707 0l1.1 1.1a.5.5 0 1 1-.708.707l-1.1-1.1a.5.5 0 0 1 0-.707zm9.507 9.507a.5.5 0 0 1 .707 0l1.1 1.1a.5.5 0 1 1-.707.708l-1.1-1.1a.5.5 0 0 1 0-.708zM0 8a.5.5 0 0 1 .5-.5h1.555a.5.5 0 0 1 0 1H.5A.5.5 0 0 1 0 8zm12.945 0a.5.5 0 0 1 .5-.5H15.5a.5.5 0 0 1 0 1h-2.055a.5.5 0 0 1-.5-.5zM2.343 13.657a.5.5 0 0 1 0-.707l1.1-1.1a.5.5 0 1 1 .708.707l-1.1 1.1a.5.5 0 0 1-.708 0zm9.507-9.507a.5.5 0 0 1 0-.707l1.1-1.1a.5.5 0 0 1 .707.708l-1.1 1.1a.5.5 0 0 1-.707 0z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="theme-icon d-none" id="themeToggleMoon" viewBox="0 0 16 16">
<path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z"/>
<path d="M10.794 3.148a.217.217 0 0 1 .412 0l.387 1.162c.173.518.579.924 1.097 1.097l1.162.387a.217.217 0 0 1 0 .412l-1.162.387a1.734 1.734 0 0 0-1.097 1.097l-.387 1.162a.217.217 0 0 1-.412 0l-.387-1.162A1.734 1.734 0 0 0 9.31 6.593l-1.162-.387a.217.217 0 0 1 0-.412l1.162-.387a1.734 1.734 0 0 0 1.097-1.097l.387-1.162zM13.863.099a.145.145 0 0 1 .274 0l.258.774c.115.346.386.617.732.732l.774.258a.145.145 0 0 1 0 .274l-.774.258a1.156 1.156 0 0 0-.732.732l-.258.774a.145.145 0 0 1-.274 0l-.258-.774a1.156 1.156 0 0 0-.732-.732l-.774-.258a.145.145 0 0 1 0-.274l.774-.258c.346-.115.617-.386.732-.732L13.863.1z"/>
</svg>
<span class="theme-toggle-text">Toggle theme</span>
</button>
{% if principal %}
<div class="sidebar-user" data-username="{{ principal.display_name }}">
<div class="user-avatar">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
<path d="M11 6a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/>
<path fill-rule="evenodd" d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm8-7a7 7 0 0 0-5.468 11.37C3.242 11.226 4.805 10 8 10s4.757 1.225 5.468 2.37A7 7 0 0 0 8 1z"/>
</svg>
</div>
<div class="user-info">
<div class="user-name" title="{{ principal.display_name }}">{{ principal.display_name | truncate(16, true) }}</div>
<div class="user-key">{{ principal.access_key | truncate(12, true) }}</div>
</div>
</div>
<form method="post" action="{{ url_for('ui.logout') }}" class="w-100">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<button class="sidebar-logout-btn" type="submit">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M10 12.5a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v2a.5.5 0 0 0 1 0v-2A1.5 1.5 0 0 0 9.5 2h-8A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-2a.5.5 0 0 0-1 0v2z"/>
<path fill-rule="evenodd" d="M15.854 8.354a.5.5 0 0 0 0-.708l-3-3a.5.5 0 0 0-.708.708L14.293 7.5H5.5a.5.5 0 0 0 0 1h8.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3z"/>
</svg>
<span class="logout-text">Sign out</span>
</button>
</form>
{% endif %}
</div>
</aside>
<div class="main-wrapper">
<main class="main-content">
{% block content %}{% endblock %}
</main>
</div>
<div class="toast-container position-fixed bottom-0 end-0 p-3">
<div id="liveToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header">
@@ -162,9 +273,11 @@
(function () {
const storageKey = 'myfsio-theme';
const toggle = document.getElementById('themeToggle');
const label = document.getElementById('themeToggleLabel');
const toggleMobile = document.getElementById('themeToggleMobile');
const sunIcon = document.getElementById('themeToggleSun');
const moonIcon = document.getElementById('themeToggleMoon');
const sunIconMobile = document.getElementById('themeToggleSunMobile');
const moonIconMobile = document.getElementById('themeToggleMoonMobile');
const applyTheme = (theme) => {
document.documentElement.dataset.bsTheme = theme;
@@ -172,29 +285,74 @@
try {
localStorage.setItem(storageKey, theme);
} catch (err) {
/* localStorage unavailable */
}
if (label) {
label.textContent = theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode';
}
if (toggle) {
toggle.setAttribute('aria-pressed', theme === 'dark' ? 'true' : 'false');
toggle.setAttribute('title', theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode');
toggle.setAttribute('aria-label', theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode');
console.log("Error: local storage not available, cannot save theme preference.");
}
const isDark = theme === 'dark';
if (sunIcon && moonIcon) {
const isDark = theme === 'dark';
sunIcon.classList.toggle('d-none', !isDark);
moonIcon.classList.toggle('d-none', isDark);
}
if (sunIconMobile && moonIconMobile) {
sunIconMobile.classList.toggle('d-none', !isDark);
moonIconMobile.classList.toggle('d-none', isDark);
}
[toggle, toggleMobile].forEach(btn => {
if (btn) {
btn.setAttribute('aria-pressed', isDark ? 'true' : 'false');
btn.setAttribute('title', isDark ? 'Switch to light mode' : 'Switch to dark mode');
btn.setAttribute('aria-label', isDark ? 'Switch to light mode' : 'Switch to dark mode');
}
});
};
const current = document.documentElement.dataset.bsTheme || 'light';
applyTheme(current);
toggle?.addEventListener('click', () => {
const handleToggle = () => {
const next = document.documentElement.dataset.bsTheme === 'dark' ? 'light' : 'dark';
applyTheme(next);
};
toggle?.addEventListener('click', handleToggle);
toggleMobile?.addEventListener('click', handleToggle);
})();
</script>
<script>
(function () {
const sidebar = document.getElementById('desktopSidebar');
const collapseBtn = document.getElementById('sidebarCollapseBtn');
const sidebarBrand = document.getElementById('sidebarBrand');
const storageKey = 'myfsio-sidebar-collapsed';
if (!sidebar || !collapseBtn) return;
const applyCollapsed = (collapsed) => {
sidebar.classList.toggle('sidebar-collapsed', collapsed);
document.body.classList.toggle('sidebar-is-collapsed', collapsed);
document.documentElement.classList.remove('sidebar-will-collapse');
try {
localStorage.setItem(storageKey, collapsed ? 'true' : 'false');
} catch (err) {}
};
try {
const stored = localStorage.getItem(storageKey);
applyCollapsed(stored === 'true');
} catch (err) {
document.documentElement.classList.remove('sidebar-will-collapse');
}
collapseBtn.addEventListener('click', () => {
const isCollapsed = sidebar.classList.contains('sidebar-collapsed');
applyCollapsed(!isCollapsed);
});
sidebarBrand?.addEventListener('click', (e) => {
const isCollapsed = sidebar.classList.contains('sidebar-collapsed');
if (isCollapsed) {
e.preventDefault();
applyCollapsed(false);
}
});
})();
</script>

View File

@@ -3276,6 +3276,19 @@
instance.hide();
}
});
const iconEl = document.getElementById('messageModalIcon');
if (iconEl) {
const iconPaths = {
success: '<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/>',
danger: '<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>',
warning: '<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>',
info: '<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-1 4.705c-.07.34.029.533.304.533.194 0 .487-.07.686-.246l-.088.416c-.287.346-.92.598-1.465.598-.703 0-1.002-.422-.808-1.319l.738-3.468c.064-.293.006-.399-.287-.47l-.451-.081.082-.381 2.29-.287zM8 5.5a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/>'
};
const iconColors = { success: 'text-success', danger: 'text-danger', warning: 'text-warning', info: 'text-primary' };
iconEl.innerHTML = iconPaths[variant] || iconPaths.info;
iconEl.classList.remove('text-success', 'text-danger', 'text-warning', 'text-primary');
iconEl.classList.add(iconColors[variant] || 'text-primary');
}
messageModalTitle.textContent = title;
if (bodyHtml) {
messageModalBody.innerHTML = bodyHtml;
@@ -5167,14 +5180,34 @@
<td class="small">${expiration}</td>
<td class="small">${noncurrent}</td>
<td class="text-end">
<button class="btn btn-outline-danger btn-sm" onclick="deleteLifecycleRule(${idx})" title="Delete rule">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"><path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/><path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/></svg>
</button>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-secondary" onclick="editLifecycleRule(${idx})" title="Edit rule">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"><path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/></svg>
</button>
<button class="btn btn-outline-danger" onclick="deleteLifecycleRule(${idx})" title="Delete rule">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"><path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/><path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/></svg>
</button>
</div>
</td>
</tr>`;
}).join('');
};
window.editLifecycleRule = (idx) => {
const rule = lifecycleRules[idx];
if (!rule) return;
document.getElementById('lifecycleRuleId').value = rule.ID || '';
document.getElementById('lifecycleRuleStatus').value = rule.Status || 'Enabled';
document.getElementById('lifecycleRulePrefix').value = rule.Filter?.Prefix || '';
document.getElementById('lifecycleExpirationDays').value = rule.Expiration?.Days || '';
document.getElementById('lifecycleNoncurrentDays').value = rule.NoncurrentVersionExpiration?.NoncurrentDays || '';
document.getElementById('lifecycleAbortMpuDays').value = rule.AbortIncompleteMultipartUpload?.DaysAfterInitiation || '';
window.editingLifecycleIdx = idx;
addLifecycleRuleModal?.show();
};
window.editingLifecycleIdx = null;
window.deleteLifecycleRule = async (idx) => {
lifecycleRules.splice(idx, 1);
await saveLifecycleRules();
@@ -5210,7 +5243,12 @@
if (expDays > 0) rule.Expiration = { Days: expDays };
if (ncDays > 0) rule.NoncurrentVersionExpiration = { NoncurrentDays: ncDays };
if (abortDays > 0) rule.AbortIncompleteMultipartUpload = { DaysAfterInitiation: abortDays };
lifecycleRules.push(rule);
if (typeof window.editingLifecycleIdx === 'number' && window.editingLifecycleIdx !== null) {
lifecycleRules[window.editingLifecycleIdx] = rule;
window.editingLifecycleIdx = null;
} else {
lifecycleRules.push(rule);
}
await saveLifecycleRules();
addLifecycleRuleModal?.hide();
document.getElementById('lifecycleRuleId').value = '';
@@ -5218,6 +5256,7 @@
document.getElementById('lifecycleExpirationDays').value = '';
document.getElementById('lifecycleNoncurrentDays').value = '';
document.getElementById('lifecycleAbortMpuDays').value = '';
document.getElementById('lifecycleRuleStatus').value = 'Enabled';
});
const corsCard = document.getElementById('cors-rules-card');
@@ -5257,14 +5296,37 @@
<td>${headers || '<span class="text-muted">*</span>'}</td>
<td>${rule.MaxAgeSeconds || '<span class="text-muted">-</span>'}</td>
<td class="text-end">
<button class="btn btn-outline-danger btn-sm" onclick="deleteCorsRule(${idx})">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"><path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/><path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/></svg>
</button>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-secondary" onclick="editCorsRule(${idx})" title="Edit rule">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"><path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/></svg>
</button>
<button class="btn btn-outline-danger" onclick="deleteCorsRule(${idx})" title="Delete rule">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"><path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/><path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/></svg>
</button>
</div>
</td>
</tr>`;
}).join('');
};
window.editCorsRule = (idx) => {
const rule = corsRules[idx];
if (!rule) return;
document.getElementById('corsAllowedOrigins').value = (rule.AllowedOrigins || []).join('\n');
document.getElementById('corsAllowedHeaders').value = (rule.AllowedHeaders || []).join('\n');
document.getElementById('corsExposeHeaders').value = (rule.ExposeHeaders || []).join('\n');
document.getElementById('corsMaxAge').value = rule.MaxAgeSeconds || '';
document.getElementById('corsMethodGet').checked = (rule.AllowedMethods || []).includes('GET');
document.getElementById('corsMethodPut').checked = (rule.AllowedMethods || []).includes('PUT');
document.getElementById('corsMethodPost').checked = (rule.AllowedMethods || []).includes('POST');
document.getElementById('corsMethodDelete').checked = (rule.AllowedMethods || []).includes('DELETE');
document.getElementById('corsMethodHead').checked = (rule.AllowedMethods || []).includes('HEAD');
window.editingCorsIdx = idx;
addCorsRuleModal?.show();
};
window.editingCorsIdx = null;
window.deleteCorsRule = async (idx) => {
corsRules.splice(idx, 1);
await saveCorsRules();
@@ -5307,13 +5369,23 @@
if (headers.length > 0) rule.AllowedHeaders = headers;
if (expose.length > 0) rule.ExposeHeaders = expose;
if (maxAge > 0) rule.MaxAgeSeconds = maxAge;
corsRules.push(rule);
if (typeof window.editingCorsIdx === 'number' && window.editingCorsIdx !== null) {
corsRules[window.editingCorsIdx] = rule;
window.editingCorsIdx = null;
} else {
corsRules.push(rule);
}
await saveCorsRules();
addCorsRuleModal?.hide();
document.getElementById('corsAllowedOrigins').value = '';
document.getElementById('corsAllowedHeaders').value = '';
document.getElementById('corsExposeHeaders').value = '';
document.getElementById('corsMaxAge').value = '';
document.getElementById('corsMethodGet').checked = false;
document.getElementById('corsMethodPut').checked = false;
document.getElementById('corsMethodPost').checked = false;
document.getElementById('corsMethodDelete').checked = false;
document.getElementById('corsMethodHead').checked = false;
});
const aclCard = document.getElementById('bucket-acl-card');