This commit is contained in:
James Read 2026-01-11 23:28:46 +00:00 committed by GitHub
commit 54471d8ea3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 482 additions and 257 deletions

View File

@ -124,7 +124,10 @@ actions:
#
# Docs: https://docs.olivetin.app/reference/reference_themes_for_users.html
- title: Get OliveTin Theme
shell: olivetin-get-theme {{ themeGitRepo }} {{ themeFolderName }}
exec:
- "olivetin-get-theme"
- "{{ themeGitRepo }}"
- "{{ themeFolderName }}"
icon: theme
arguments:
- name: themeGitRepo

View File

@ -8,7 +8,6 @@
<title>OliveTin</title>
<link rel = "stylesheet" type = "text/css" href = "/theme.css" />
<link rel = "stylesheet" href = "node_modules/@xterm/xterm/css/xterm.css" />
<link rel = "shortcut icon" type = "image/png" href = "OliveTinLogo.png" />

View File

@ -1451,6 +1451,13 @@ export declare type InitResponse = Message<"olivetin.api.v1.InitResponse"> & {
* @generated from field: bool login_required = 23;
*/
loginRequired: boolean;
/**
* List of available theme names
*
* @generated from field: repeated string available_themes = 24;
*/
availableThemes: string[];
};
/**

File diff suppressed because one or more lines are too long

View File

@ -318,109 +318,111 @@ watch(
</script>
<style scoped>
.action-button {
display: flex;
flex-direction: column;
flex-grow: 1;
}
<style>
.action-button button {
display: flex;
flex-direction: column;
flex-grow: 1;
justify-content: center;
padding: 0.5em;
border: 1px solid #ccc;
border-radius: 4px;
background: #fff;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 0 .6em #aaa;
font-size: .85em;
border-radius: .7em;
}
@layer components {
.action-button {
display: flex;
flex-direction: column;
flex-grow: 1;
}
.action-button button:hover:not(:disabled) {
background: #f5f5f5;
border-color: #999;
}
.action-button button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.action-button button .icon {
font-size: 3em;
flex-grow: 1;
align-content: center;
}
.action-button button .title {
font-weight: 500;
padding: 0.2em;
}
.action-button button .rate-limit-message {
font-size: 0.75em;
color: #856404;
padding: 0.2em;
font-weight: normal;
}
/* Animation classes */
.action-button button.action-timeout {
background: #fff3cd;
border-color: #ffeaa7;
color: #856404;
}
.action-button button.action-blocked {
background: #f8d7da !important;
border-color: #f5c6cb;
color: #721c24;
}
.action-button button.action-nonzero-exit {
background: #f8d7da !important;
border-color: #f5c6cb;
color: #721c24;
}
.action-button button.action-success {
background: #d4edda !important;
border-color: #c3e6cb;
color: #155724;
}
.action-button-footer {
margin-top: 0.5em;
}
.navigate-on-start-container {
position: relative;
margin-left: auto;
height: 0;
right: 0;
top: 0;
}
@media (prefers-color-scheme: dark) {
.action-button button {
background: #111;
border-color: #000;
box-shadow: 0 0 6px #000;
color: #fff;
display: flex;
flex-direction: column;
flex-grow: 1;
justify-content: center;
padding: 0.5em;
border: 1px solid #ccc;
border-radius: 4px;
background: #fff;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 0 .6em #aaa;
font-size: .85em;
border-radius: .7em;
}
.action-button button:hover:not(:disabled) {
background: #222;
border-color: #000;
box-shadow: 0 0 6px #444;
color: #fff;
background: #f5f5f5;
border-color: #999;
}
.action-button button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.action-button button .icon {
font-size: 3em;
flex-grow: 1;
align-content: center;
}
.action-button button .title {
font-weight: 500;
padding: 0.2em;
}
.action-button button .rate-limit-message {
font-size: 0.75em;
color: #856404;
padding: 0.2em;
font-weight: normal;
}
/* Animation classes */
.action-button button.action-timeout {
background: #fff3cd;
border-color: #ffeaa7;
color: #856404;
}
.action-button button.action-blocked {
background: #f8d7da !important;
border-color: #f5c6cb;
color: #721c24;
}
.action-button button.action-nonzero-exit {
background: #f8d7da !important;
border-color: #f5c6cb;
color: #721c24;
}
.action-button button.action-success {
background: #d4edda !important;
border-color: #c3e6cb;
color: #155724;
}
.action-button-footer {
margin-top: 0.5em;
}
.navigate-on-start-container {
position: relative;
margin-left: auto;
height: 0;
right: 0;
top: 0;
}
@media (prefers-color-scheme: dark) {
.action-button button {
background: #111;
border-color: #000;
box-shadow: 0 0 6px #000;
color: #fff;
}
.action-button button:hover:not(:disabled) {
background: #222;
border-color: #000;
box-shadow: 0 0 6px #444;
color: #fff;
}
}
}
</style>

View File

@ -46,6 +46,10 @@
<a href="#" @click.prevent="openLanguageDialog">{{ currentLanguageName }}</a>
</span>
<span v-if="availableThemes.length > 1">
<a href="#" @click.prevent="openThemeDialog">{{ currentThemeName }}</a>
</span>
<span>{{ t('connected') }}</span>
</p>
<p>
@ -55,7 +59,7 @@
</div>
</div>
<dialog ref="languageDialog" class="language-dialog" @click="handleDialogClick">
<dialog ref="languageDialog" class="language-dialog" @click="handleLanguageDialogClick">
<div class="dialog-content" @click.stop>
<h2>{{ t('language-dialog.title') }}</h2>
<select v-model="selectedLanguage" @change="changeLanguage" class="language-select">
@ -73,6 +77,21 @@
</div>
</div>
</dialog>
<dialog ref="themeDialog" class="theme-dialog" @click="handleThemeDialogClick">
<div class="dialog-content" @click.stop>
<h2>{{ t('theme-dialog.title') }}</h2>
<select v-model="selectedTheme" @change="changeTheme" class="language-select">
<option value="">{{ t('theme-dialog.default') }}</option>
<option v-for="theme in availableThemes" :key="theme" :value="theme">
{{ theme }}
</option>
</select>
<div class="dialog-buttons">
<button @click="closeThemeDialog">{{ t('theme-dialog.close') }}</button>
</div>
</div>
</dialog>
</template>
<script setup>
@ -117,6 +136,12 @@ const initialLanguagePreference = typeof window !== 'undefined' ? localStorage.g
const languagePreference = ref(initialLanguagePreference || 'auto')
const selectedLanguage = ref(languagePreference.value)
const themeDialog = ref(null)
const availableThemes = ref([])
const initialThemePreference = typeof window !== 'undefined' ? localStorage.getItem('olivetin-theme') : null
const themePreference = ref(initialThemePreference || '')
const selectedTheme = ref(themePreference.value)
// Available languages with display names
const availableLanguages = {
'auto': 'Browser Language',
@ -136,6 +161,14 @@ const currentLanguageName = computed(() => {
return availableLanguages[languagePreference.value] || languagePreference.value
})
// Computed property to get current theme display name
const currentThemeName = computed(() => {
if (!themePreference.value || themePreference.value === '') {
return t('theme-dialog.default')
}
return themePreference.value
})
// Computed properties for navigation style
const topbarEnabled = computed(() => {
return sectionNavigationStyle.value === 'topbar'
@ -191,12 +224,16 @@ function updateHeaderFromInit() {
showLogs.value = window.initResponse.showLogList
showDiagnostics.value = window.initResponse.showDiagnostics
sectionNavigationStyle.value = window.initResponse.sectionNavigationStyle || 'sidebar'
availableThemes.value = window.initResponse.availableThemes || []
if (!window.initResponse.authLocalLogin && window.initResponse.oAuth2Providers.length === 0) {
showLoginLink.value = false
}
applyStyleMods()
renderNavigation()
applyTheme()
if (window.initResponse.loginRequired) {
router.push('/login')
@ -280,13 +317,75 @@ function changeLanguage() {
closeLanguageDialog()
}
function handleDialogClick(event) {
function handleLanguageDialogClick(event) {
// Close dialog when clicking on the backdrop
if (event.target === languageDialog.value) {
closeLanguageDialog()
}
}
function openThemeDialog() {
selectedTheme.value = themePreference.value || ''
if (themeDialog.value) {
themeDialog.value.showModal()
}
}
function closeThemeDialog() {
if (themeDialog.value) {
themeDialog.value.close()
}
}
function changeTheme() {
if (!selectedTheme.value || selectedTheme.value === '') {
localStorage.removeItem('olivetin-theme')
themePreference.value = ''
} else {
localStorage.setItem('olivetin-theme', selectedTheme.value)
themePreference.value = selectedTheme.value
}
applyTheme()
closeThemeDialog()
}
function applyTheme() {
let themeStyle = document.getElementById('theme-style')
if (!themeStyle) {
themeStyle = document.createElement('style')
themeStyle.id = 'theme-style'
themeStyle.type = 'text/css'
document.head.appendChild(themeStyle)
}
if (themePreference.value && themePreference.value !== '') {
themeStyle.textContent = `@import url('/custom-webui/themes/${themePreference.value}/theme.css') layer(theme);`
} else {
themeStyle.textContent = ''
}
}
function applyStyleMods() {
if (!window.initResponse || !window.initResponse.styleMods) {
return
}
for (const styleMod of window.initResponse.styleMods) {
if (styleMod) {
document.body.classList.add(styleMod)
}
}
}
function handleThemeDialogClick(event) {
if (event.target === themeDialog.value) {
closeThemeDialog()
}
}
window.updateHeaderFromInit = updateHeaderFromInit
onMounted(() => {
@ -296,6 +395,9 @@ onMounted(() => {
// Initialize selected language from stored preference
selectedLanguage.value = languagePreference.value
// Initialize selected theme from stored preference
selectedTheme.value = themePreference.value || ''
if (typeof navigator !== 'undefined' && Array.isArray(navigator.languages)) {
browserLanguages.value = navigator.languages
}
@ -316,7 +418,8 @@ onMounted(() => {
text-decoration: underline;
}
.language-dialog {
.language-dialog,
.theme-dialog {
border: 1px solid var(--border-color, #ccc);
border-radius: 0.5rem;
padding: 0;
@ -324,7 +427,8 @@ onMounted(() => {
width: 90%;
}
.language-dialog::backdrop {
.language-dialog::backdrop,
.theme-dialog::backdrop {
background-color: rgba(0, 0, 0, 0.5);
}

View File

@ -1,3 +1,5 @@
@layer components, karma, theme;
header {
position: fixed;
width: 100%;
@ -37,3 +39,27 @@ div.buttons button svg {
section.small {
border-radius: .4em;
}
.sm-side-icons .action-button button {
display: flex;
flex-direction: row;
align-items: center;
}
.sm-imageicons-fullwidth .action-button button .icon img {
width: 100%;
}
.sm-transparent-header header {
background-color: transparent;
box-shadow: none;
border-color: transparent;
}
.sm-transparent-header #sidebar-button {
border-color: transparent;
}
.sm-transparent-footer footer span {
background-color: transparent;
}

View File

@ -18,6 +18,15 @@ export default defineConfig({
target: 'http://localhost:1337',
changeOrigin: true,
secure: false,
},
'/theme.css': {
target: 'http://localhost:1337',
changeOrigin: true,
secure: false,
},
"/custom-webui": {
target: "http://localhost:1337",
changeOrigin: true,
}
},
},

View File

@ -1,14 +0,0 @@
/* Custom theme for integration testing */
body {
background-color: #ff6b6b;
}
/* Add a distinctive style that we can test for */
#content {
border: 5px solid #4ecdc4;
}

View File

@ -1,82 +0,0 @@
import { describe, it, before, after } from 'mocha'
import { expect } from 'chai'
import { By } from 'selenium-webdriver'
import {
getRootAndWait,
takeScreenshotOnFailure,
} from '../../lib/elements.js'
describe('config: customTheme', function () {
before(async function () {
await runner.start('customTheme')
})
after(async () => {
await runner.stop()
})
afterEach(function () {
takeScreenshotOnFailure(this.currentTest, webdriver)
})
it('Custom theme CSS is loaded and accessible', async function () {
await getRootAndWait()
// Fetch the theme CSS directly
const themeCssUrl = runner.baseUrl() + 'theme.css'
await webdriver.get(themeCssUrl)
// Wait for the page to load
await webdriver.sleep(500)
// Get the page source (should be CSS content)
const pageSource = await webdriver.getPageSource()
// Verify the theme CSS contains our custom styles
expect(pageSource).to.include('background-color: #ff6b6b')
expect(pageSource).to.include('border: 5px solid #4ecdc4')
expect(pageSource).to.include('Custom theme for integration testing')
})
it('Custom theme styles are applied to the page', async function () {
await getRootAndWait()
// Wait a bit for CSS to be fully loaded
await webdriver.sleep(500)
// Get computed background color of body
const backgroundColor = await webdriver.executeScript(
'return window.getComputedStyle(document.body).backgroundColor'
)
// The background color should be rgb(255, 107, 107) which is #ff6b6b
// Different browsers might return different formats, so we check for the RGB values
expect(backgroundColor).to.include('255, 107, 107')
// Get computed border color of content element
const borderColor = await webdriver.executeScript(
'const el = document.getElementById("content"); return el ? window.getComputedStyle(el).borderColor : null'
)
// The border color should be rgb(78, 205, 196) which is #4ecdc4
// borderColor might return "rgb(78, 205, 196)" or similar format
expect(borderColor).to.include('78, 205, 196')
})
it('Theme CSS link is present in the HTML', async function () {
await getRootAndWait()
// Find the theme.css link in the head
const themeLink = await webdriver.findElement(By.css('link[href="/theme.css"]'))
expect(themeLink).to.not.be.null
// Verify it's a stylesheet
const rel = await themeLink.getAttribute('rel')
expect(rel).to.equal('stylesheet')
const type = await themeLink.getAttribute('type')
expect(type).to.equal('text/css')
})
})

View File

@ -4,14 +4,8 @@ listenAddressSingleHTTPFrontend: 0.0.0.0:1337
logLevel: "DEBUG"
checkForUpdates: false
themeName: "test-theme"
actions:
- title: Test action
shell: "echo 'Hello from themed OliveTin'"
shell: "echo 'Hello from theme loading test'"
icon: ping

View File

@ -0,0 +1,5 @@
/* Theme One CSS */
body {
background-color: #theme-one;
}

View File

@ -0,0 +1,5 @@
/* Theme Two CSS */
body {
background-color: #theme-two;
}

View File

@ -0,0 +1,73 @@
import { describe, it, before, after } from 'mocha'
import { expect } from 'chai'
import {
getRootAndWait,
takeScreenshotOnFailure,
} from '../../lib/elements.js'
describe('config: themeLoading', function () {
before(async function () {
await runner.start('themeLoading')
})
after(async () => {
await runner.stop()
})
afterEach(function () {
takeScreenshotOnFailure(this.currentTest, webdriver)
})
it('Available themes are discovered and returned in Init response', async function () {
await getRootAndWait()
// Wait for initResponse to be available
await webdriver.wait(async () => {
const hasInitResponse = await webdriver.executeScript(
'return typeof window.initResponse !== "undefined" && window.initResponse !== null'
)
return hasInitResponse
}, 5000, 'Init response should be available')
// Get available themes from the Init response
const availableThemes = await webdriver.executeScript(
'return window.initResponse ? (window.initResponse.availableThemes || []) : []'
)
// Verify themes array exists and is an array
expect(availableThemes).to.be.an('array')
// Verify that themes with theme.css are discovered
// theme-one and theme-two have theme.css, invalid-theme does not
expect(availableThemes).to.include('theme-one')
expect(availableThemes).to.include('theme-two')
// Verify that themes without theme.css are not included
expect(availableThemes).to.not.include('invalid-theme')
// Verify themes are sorted alphabetically
const sortedThemes = [...availableThemes].sort()
expect(availableThemes).to.deep.equal(sortedThemes)
})
it('Available themes list is accessible via JavaScript', async function () {
await getRootAndWait()
// Wait for initResponse to be available
await webdriver.wait(async () => {
const hasInitResponse = await webdriver.executeScript(
'return typeof window.initResponse !== "undefined" && window.initResponse !== null'
)
return hasInitResponse
}, 5000, 'Init response should be available')
// Verify availableThemes is accessible
const availableThemes = await webdriver.executeScript(
'return window.initResponse ? (window.initResponse.availableThemes || []) : []'
)
expect(availableThemes).to.be.an('array')
expect(availableThemes.length).to.be.at.least(2)
})
})

View File

@ -49,6 +49,9 @@
"raise-issue": "Ein Problem melden auf GitHub",
"return-to-index": "Zurück zur Startseite",
"search-filter": "Filter aktuelle Seite",
"theme-dialog.close": "Schließen",
"theme-dialog.default": "Standard-Design",
"theme-dialog.title": "Design auswählen",
"welcome": "Willkommen bei OliveTin"
},
"en": {
@ -99,6 +102,9 @@
"raise-issue": "Raise an issue on GitHub",
"return-to-index": "Return to index",
"search-filter": "Filter current page",
"theme-dialog.close": "Close",
"theme-dialog.default": "Default Theme",
"theme-dialog.title": "Select Theme",
"welcome": "Welcome to OliveTin"
},
"es-ES": {
@ -149,6 +155,9 @@
"raise-issue": "Reportar un problema en GitHub",
"return-to-index": "Volver a la página principal",
"search-filter": "Filtrar página actual",
"theme-dialog.close": "Cerrar",
"theme-dialog.default": "Tema Predeterminado",
"theme-dialog.title": "Seleccionar tema",
"welcome": "Bienvenido a OliveTin"
},
"it-IT": {
@ -199,6 +208,9 @@
"raise-issue": "Segnala un problema su GitHub",
"return-to-index": "Torna alla pagina principale",
"search-filter": "Filtra la pagina corrente",
"theme-dialog.close": "Chiudi",
"theme-dialog.default": "Tema Predefinito",
"theme-dialog.title": "Seleziona tema",
"welcome": "Benvenuto in OliveTin"
},
"zh-Hans-CN": {
@ -249,6 +261,9 @@
"raise-issue": "在 GitHub 上报告问题",
"return-to-index": "返回首页",
"search-filter": "过滤当前页面",
"theme-dialog.close": "关闭",
"theme-dialog.default": "默认主题",
"theme-dialog.title": "选择主题",
"welcome": "欢迎使用 OliveTin"
}
}

View File

@ -48,3 +48,6 @@ translations:
language-dialog.browser-languages: Browser-Sprachen
language-dialog.not-available: Nicht verfügbar
language-dialog.close: Schließen
theme-dialog.title: Design auswählen
theme-dialog.default: Standard-Design
theme-dialog.close: Schließen

View File

@ -48,3 +48,6 @@ translations:
language-dialog.browser-languages: Browser languages
language-dialog.not-available: Not available
language-dialog.close: Close
theme-dialog.title: Select Theme
theme-dialog.default: Default Theme
theme-dialog.close: Close

View File

@ -48,3 +48,6 @@ translations:
language-dialog.browser-languages: Idiomas del navegador
language-dialog.not-available: No disponible
language-dialog.close: Cerrar
theme-dialog.title: Seleccionar tema
theme-dialog.default: Tema Predeterminado
theme-dialog.close: Cerrar

View File

@ -48,3 +48,6 @@ translations:
language-dialog.browser-languages: Lingue del browser
language-dialog.not-available: Non disponibile
language-dialog.close: Chiudi
theme-dialog.title: Seleziona tema
theme-dialog.default: Tema Predefinito
theme-dialog.close: Chiudi

View File

@ -22,6 +22,9 @@ translations:
language-dialog.browser-languages: 浏览器语言
language-dialog.not-available: 不可用
language-dialog.close: 关闭
theme-dialog.title: 选择主题
theme-dialog.default: 默认主题
theme-dialog.close: 关闭
logs.timed-out: 超时
logs.blocked: 阻塞
logs.exit-code: 退出代码

View File

@ -332,6 +332,7 @@ message InitResponse {
bool show_diagnostics = 21;
bool show_log_list = 22;
bool login_required = 23;
repeated string available_themes = 24; // List of available theme names
}
message AdditionalLink {

View File

@ -3236,6 +3236,7 @@ type InitResponse struct {
ShowDiagnostics bool `protobuf:"varint,21,opt,name=show_diagnostics,json=showDiagnostics,proto3" json:"show_diagnostics,omitempty"`
ShowLogList bool `protobuf:"varint,22,opt,name=show_log_list,json=showLogList,proto3" json:"show_log_list,omitempty"`
LoginRequired bool `protobuf:"varint,23,opt,name=login_required,json=loginRequired,proto3" json:"login_required,omitempty"`
AvailableThemes []string `protobuf:"bytes,24,rep,name=available_themes,json=availableThemes,proto3" json:"available_themes,omitempty"` // List of available theme names
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -3431,6 +3432,13 @@ func (x *InitResponse) GetLoginRequired() bool {
return false
}
func (x *InitResponse) GetAvailableThemes() []string {
if x != nil {
return x.AvailableThemes
}
return nil
}
type AdditionalLink struct {
state protoimpl.MessageState `protogen:"open.v1"`
Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
@ -4088,7 +4096,7 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" +
"\x16GetDiagnosticsResponse\x12 \n" +
"\vSshFoundKey\x18\x01 \x01(\tR\vSshFoundKey\x12&\n" +
"\x0eSshFoundConfig\x18\x02 \x01(\tR\x0eSshFoundConfig\"\r\n" +
"\vInitRequest\"\xa2\b\n" +
"\vInitRequest\"\xcd\b\n" +
"\fInitResponse\x12\x1e\n" +
"\n" +
"showFooter\x18\x01 \x01(\bR\n" +
@ -4116,7 +4124,8 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" +
"banner_css\x18\x14 \x01(\tR\tbannerCss\x12)\n" +
"\x10show_diagnostics\x18\x15 \x01(\bR\x0fshowDiagnostics\x12\"\n" +
"\rshow_log_list\x18\x16 \x01(\bR\vshowLogList\x12%\n" +
"\x0elogin_required\x18\x17 \x01(\bR\rloginRequired\"8\n" +
"\x0elogin_required\x18\x17 \x01(\bR\rloginRequired\x12)\n" +
"\x10available_themes\x18\x18 \x03(\tR\x0favailableThemes\"8\n" +
"\x0eAdditionalLink\x12\x14\n" +
"\x05title\x18\x01 \x01(\tR\x05title\x12\x10\n" +
"\x03url\x18\x02 \x01(\tR\x03url\"L\n" +

View File

@ -3,6 +3,8 @@ package api
import (
ctx "context"
"encoding/json"
"os"
"path"
"sort"
"connectrpc.com/connect"
@ -902,11 +904,62 @@ func (api *oliveTinAPI) Init(ctx ctx.Context, req *connect.Request[apiv1.InitReq
ShowDiagnostics: user.EffectivePolicy.ShowDiagnostics,
ShowLogList: user.EffectivePolicy.ShowLogList,
LoginRequired: loginRequired,
AvailableThemes: discoverAvailableThemes(api.cfg),
}
return connect.NewResponse(res), nil
}
// discoverAvailableThemes finds all available themes in the custom-webui/themes directory.
// A theme is considered available if it has a theme.css file.
func discoverAvailableThemes(cfg *config.Config) []string {
configDir := cfg.GetDir()
if configDir == "" {
return []string{}
}
themesDir := path.Join(configDir, "custom-webui", "themes")
entries, err := os.ReadDir(themesDir)
if err != nil {
log.WithFields(log.Fields{
"themesDir": themesDir,
"error": err,
}).Tracef("Could not read themes directory")
return []string{}
}
themes := collectValidThemes(themesDir, entries)
sort.Strings(themes)
return themes
}
// collectValidThemes collects theme names from directory entries that have a theme.css file.
func collectValidThemes(themesDir string, entries []os.DirEntry) []string {
var themes []string
for _, entry := range entries {
if themeName := getValidThemeName(themesDir, entry); themeName != "" {
themes = append(themes, themeName)
}
}
return themes
}
// getValidThemeName returns the theme name if the entry is a valid theme directory with theme.css, otherwise returns empty string.
func getValidThemeName(themesDir string, entry os.DirEntry) string {
if !entry.IsDir() {
return ""
}
themeName := entry.Name()
themeCssPath := path.Join(themesDir, themeName, "theme.css")
if _, err := os.Stat(themeCssPath); err != nil {
return ""
}
return themeName
}
func (api *oliveTinAPI) buildRootDashboards(user *authpublic.AuthenticatedUser, dashboards []*config.DashboardComponent) []string {
var rootDashboards []string
dashboardRenderRequest := api.createDashboardRenderRequest(user, "", "")

View File

@ -58,5 +58,8 @@ func (cfg *Config) SetDir(dir string) {
}
func (cfg *Config) GetDir() string {
if len(cfg.sourceFiles) == 0 {
return ""
}
return cfg.sourceFiles[len(cfg.sourceFiles)-1]
}

View File

@ -224,58 +224,14 @@ func (e *Executor) GetLogTrackingIds(startOffset int64, pageCount int64) ([]*Int
return trackingIds, pagingResult
}
// isValidLogEntryForACL checks if a log entry has all required fields for ACL checking.
func isValidLogEntryForACL(entry *InternalLogEntry) bool {
return entry != nil && entry.Binding != nil && entry.Binding.Action != nil
}
// isLogEntryAllowedByACL checks if a log entry is allowed to be viewed by the user.
func isLogEntryAllowedByACL(cfg *config.Config, user *authpublic.AuthenticatedUser, entry *InternalLogEntry) bool {
return acl.IsAllowedLogs(cfg, user, entry.Binding.Action)
}
// parseDateFilter parses a date filter string and returns the parsed date and validity.
func parseDateFilter(dateFilter string) (time.Time, bool) {
if dateFilter == "" {
return time.Time{}, false
}
parsedDate, err := time.Parse("2006-01-02", dateFilter)
if err != nil {
log.WithFields(log.Fields{
"dateFilter": dateFilter,
"error": err,
}).Errorf("Failed to parse date filter, expected format YYYY-MM-DD")
return time.Time{}, false
}
return parsedDate, true
}
// matchesDateFilter checks if an entry matches the date filter criteria.
func matchesDateFilter(entry *InternalLogEntry, filterDate time.Time, hasDateFilter bool) bool {
if !hasDateFilter {
return true
}
entryDate := entry.DatetimeStarted.UTC().Truncate(24 * time.Hour)
filterDateUTC := filterDate.UTC().Truncate(24 * time.Hour)
return entryDate.Equal(filterDateUTC)
}
// shouldIncludeLogEntry determines if a log entry should be included in filtered results.
func shouldIncludeLogEntry(cfg *config.Config, user *authpublic.AuthenticatedUser, entry *InternalLogEntry, filterDate time.Time, hasDateFilter bool) bool {
if !isValidLogEntryForACL(entry) {
return false
}
if !isLogEntryAllowedByACL(cfg, user, entry) {
return false
}
return matchesDateFilter(entry, filterDate, hasDateFilter)
}
func (e *Executor) filterLogsByACL(cfg *config.Config, user *authpublic.AuthenticatedUser, dateFilter string) []*InternalLogEntry {
e.logmutex.RLock()
defer e.logmutex.RUnlock()
@ -294,6 +250,48 @@ func (e *Executor) filterLogsByACL(cfg *config.Config, user *authpublic.Authenti
return filtered
}
// parseDateFilter parses the date filter string and returns filter information.
func parseDateFilter(dateFilter string) (filterDate time.Time, hasDateFilter bool) {
if dateFilter == "" {
return time.Time{}, false
}
parsedDate, err := time.Parse("2006-01-02", dateFilter)
if err != nil {
log.WithFields(log.Fields{
"dateFilter": dateFilter,
"error": err,
}).Errorf("Failed to parse date filter, expected format YYYY-MM-DD")
return time.Time{}, false
}
return parsedDate, true
}
// shouldIncludeLogEntry determines if a log entry should be included based on ACL and date filter.
func shouldIncludeLogEntry(cfg *config.Config, user *authpublic.AuthenticatedUser, entry *InternalLogEntry, filterDate time.Time, hasDateFilter bool) bool {
if !isValidLogEntryForACL(entry) {
return false
}
if !isLogEntryAllowedByACL(cfg, user, entry) {
return false
}
return matchesDateFilter(entry, filterDate, hasDateFilter)
}
// matchesDateFilter checks if the log entry matches the date filter.
func matchesDateFilter(entry *InternalLogEntry, filterDate time.Time, hasDateFilter bool) bool {
if !hasDateFilter {
return true
}
entryDate := entry.DatetimeStarted.UTC().Truncate(24 * time.Hour)
filterDateUTC := filterDate.UTC().Truncate(24 * time.Hour)
return entryDate.Equal(filterDateUTC)
}
// paginateFilteredLogs applies pagination to a filtered list of logs and returns
// the paginated results along with pagination metadata.
func paginateFilteredLogs(filtered []*InternalLogEntry, startOffset int64, pageCount int64) ([]*InternalLogEntry, *PagingResult) {