Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<link rel="apple-touch-icon" href="logo192.png" />
<link rel="manifest" href="/manifest.json" />
<title>KopiaUI</title>
<script src="/theme-detector.js"></script>
</head>

<body id="kopia">
Expand Down
106 changes: 102 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@
"@tanstack/react-table": "^8.21.3",
"bootstrap": "^5.3.6",
"http-proxy-middleware": "^3.0.5",
"i18next": "^25.8.14",
"i18next-browser-languagedetector": "^8.2.1",
"moment": "^2.30.1",
"prop-types": "^15.8.1",
"react": "^19.1.0",
"react-bootstrap": "^2.10.10",
"react-dom": "^19.1.0",
"react-i18next": "^16.5.5",
"react-router-dom": "^7.12.0"
},
"scripts": {
Expand Down
17 changes: 17 additions & 0 deletions public/theme-detector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Detect and apply theme immediately
(function() {
try {
// First try to load saved theme from localStorage
const saved = localStorage.getItem('ui-theme');
if (saved) {
document.documentElement.classList.add(saved);
return;
}

// If no saved theme, use system preference
const isDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.classList.add(isDark ? 'dark' : 'light');
} catch (e) {
document.documentElement.classList.add('light');
}
})();
50 changes: 37 additions & 13 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import PropTypes from 'prop-types';
import { withTranslation } from 'react-i18next';
import "bootstrap/dist/css/bootstrap.min.css";
import "./css/Theme.css";
import "./css/App.css";
import axios from "axios";
import { React, Component } from "react";
import { Navbar, Nav, Container } from "react-bootstrap";
import { Navbar, Nav, Container, NavDropdown } from "react-bootstrap";
import { BrowserRouter as Router, NavLink, Navigate, Route, Routes } from "react-router-dom";
import { Policy } from "./pages/Policy";
import { Preferences } from "./pages/Preferences";
import Preferences from "./pages/Preferences";
import { Policies } from "./pages/Policies";
import { Repository } from "./pages/Repository";
import { Task } from "./pages/Task";
Expand All @@ -19,7 +21,7 @@ import { SnapshotRestore } from "./pages/SnapshotRestore";
import { AppContext } from "./contexts/AppContext";
import { UIPreferenceProvider } from "./contexts/UIPreferencesContext";

export default class App extends Component {
class App extends Component {
constructor() {
super();

Expand Down Expand Up @@ -125,48 +127,62 @@ export default class App extends Component {
<NavLink
data-testid="tab-snapshots"
title=""
data-title="Snapshots"
data-title={this.props.t('app.snapshots')}
className={isRepositoryConnected ? "nav-link" : "nav-link disabled"}
to="/snapshots"
>
Snapshots
{this.props.t('app.snapshots')}
</NavLink>
</span>
<span className="d-inline-block" data-toggle="tooltip" title="Repository is not connected">
<NavLink
data-testid="tab-policies"
title=""
data-title="Policies"
data-title={this.props.t('app.policies')}
className={isRepositoryConnected ? "nav-link" : "nav-link disabled"}
to="/policies"
>
Policies
{this.props.t('app.policies')}
</NavLink>
</span>
<span className="d-inline-block" data-toggle="tooltip" title="Repository is not connected">
<NavLink
data-testid="tab-tasks"
title=""
data-title="Tasks"
data-title={this.props.t('app.tasks')}
className={isRepositoryConnected ? "nav-link" : "nav-link disabled"}
to="/tasks"
>
Tasks
{this.props.t('app.tasks')}
<>{runningTaskCount > 0 && <>({runningTaskCount})</>}</>
</NavLink>
</span>
<NavLink data-testid="tab-repo" data-title="Repository" className="nav-link" to="/repo">
Repository
<NavLink data-testid="tab-repo" data-title={this.props.t('app.repository')} className="nav-link" to="/repo">
{this.props.t('app.repository')}
</NavLink>
<NavLink
data-testid="tab-preferences"
data-title="Preferences"
data-title={this.props.t('app.preferences')}
className="nav-link"
to="/preferences"
>
Preferences
{this.props.t('app.preferences')}
</NavLink>
</Nav>
<Nav className="ms-auto">
<NavDropdown
id="language-dropdown"
title={this.props.t('language.' + this.props.i18n.language)}
align="end"
>
<NavDropdown.Item onClick={() => this.props.i18n.changeLanguage('en')}>
{this.props.t('language.en')}
</NavDropdown.Item>
<NavDropdown.Item onClick={() => this.props.i18n.changeLanguage('ru')}>
{this.props.t('language.ru')}
</NavDropdown.Item>
</NavDropdown>
</Nav>
</Navbar.Collapse>
</Navbar>

Expand Down Expand Up @@ -196,3 +212,11 @@ export default class App extends Component {
);
}
}
App.propTypes = {
t: PropTypes.func.isRequired,
i18n: PropTypes.shape({
language: PropTypes.string,
changeLanguage: PropTypes.func
}).isRequired
};
export default withTranslation()(App);
12 changes: 7 additions & 5 deletions src/components/notifications/NotificationEditor.jsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { withTranslation } from 'react-i18next';
import axios from "axios";
import React, { Component } from "react";
import { EmailNotificationMethod } from "./EmailNotificationMethod";
Expand Down Expand Up @@ -33,7 +34,7 @@ function severityName(severity) {
return opt ? opt.label : "Unknown";
}

export class NotificationEditor extends Component {
class NotificationEditorBase extends Component {
constructor() {
super();

Expand Down Expand Up @@ -292,19 +293,19 @@ export class NotificationEditor extends Component {
<Row>
<p>
<Badge bg="warning" text="dark">
Important
{this.props.t('notifications.important')}
</Badge>
&nbsp;You don&apos;t have any notification profiles defined.
&nbsp;{this.props.t('notifications.noProfiles')}
<br />
<br />
Click the button below to add a new profile to receive notifications from Kopia.
{this.props.t('notifications.clickToAdd')}
</p>
</Row>
)}
<Row>
<Dropdown>
<Dropdown.Toggle size="sm" variant="primary" id="newProfileButton">
Create New Profile
{this.props.t('notifications.createProfile')}
</Dropdown.Toggle>
<Dropdown.Menu>
{Object.keys(notificationMethods).map((k) => (
Expand Down Expand Up @@ -340,3 +341,4 @@ export class NotificationEditor extends Component {
return this.renderList();
}
}
export const NotificationEditor = withTranslation()(NotificationEditorBase);
5 changes: 4 additions & 1 deletion src/contexts/UIPreferencesContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ export function UIPreferenceProvider(props: UIPreferenceProviderProps) {
(theme: Theme) =>
setPreferences((oldPreferences) => {
syncTheme(theme, oldPreferences.fontSize);
// Save theme to localStorage
localStorage.setItem('ui-theme', theme);
return { ...oldPreferences, theme };
}),
[],
Expand Down Expand Up @@ -153,7 +155,8 @@ export function UIPreferenceProvider(props: UIPreferenceProviderProps) {
*/
const syncTheme = (theme: Theme, fontSize: FontSize) => {
const doc = document.querySelector("html")!;
doc.classList.remove(...doc.classList);
// Remove only theme and font-size classes, preserve others
doc.classList.remove('light', 'dark', 'pastel', 'ocean', 'fs-6', 'fs-5', 'fs-4');
doc.classList.add(theme, fontSize);
};

Expand Down
Loading