Next (#823)
This commit is contained in:
commit
54471d8ea3
|
|
@ -124,7 +124,10 @@ actions:
|
||||||
#
|
#
|
||||||
# Docs: https://docs.olivetin.app/reference/reference_themes_for_users.html
|
# Docs: https://docs.olivetin.app/reference/reference_themes_for_users.html
|
||||||
- title: Get OliveTin Theme
|
- title: Get OliveTin Theme
|
||||||
shell: olivetin-get-theme {{ themeGitRepo }} {{ themeFolderName }}
|
exec:
|
||||||
|
- "olivetin-get-theme"
|
||||||
|
- "{{ themeGitRepo }}"
|
||||||
|
- "{{ themeFolderName }}"
|
||||||
icon: theme
|
icon: theme
|
||||||
arguments:
|
arguments:
|
||||||
- name: themeGitRepo
|
- name: themeGitRepo
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
<title>OliveTin</title>
|
<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 = "stylesheet" href = "node_modules/@xterm/xterm/css/xterm.css" />
|
||||||
|
|
||||||
<link rel = "shortcut icon" type = "image/png" href = "OliveTinLogo.png" />
|
<link rel = "shortcut icon" type = "image/png" href = "OliveTinLogo.png" />
|
||||||
|
|
|
||||||
|
|
@ -1451,6 +1451,13 @@ export declare type InitResponse = Message<"olivetin.api.v1.InitResponse"> & {
|
||||||
* @generated from field: bool login_required = 23;
|
* @generated from field: bool login_required = 23;
|
||||||
*/
|
*/
|
||||||
loginRequired: boolean;
|
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
|
|
@ -318,109 +318,111 @@ watch(
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style>
|
||||||
.action-button {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-button button {
|
@layer components {
|
||||||
display: flex;
|
.action-button {
|
||||||
flex-direction: column;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-direction: column;
|
||||||
justify-content: center;
|
flex-grow: 1;
|
||||||
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: #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 {
|
.action-button button {
|
||||||
background: #111;
|
display: flex;
|
||||||
border-color: #000;
|
flex-direction: column;
|
||||||
box-shadow: 0 0 6px #000;
|
flex-grow: 1;
|
||||||
color: #fff;
|
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) {
|
.action-button button:hover:not(:disabled) {
|
||||||
background: #222;
|
background: #f5f5f5;
|
||||||
border-color: #000;
|
border-color: #999;
|
||||||
box-shadow: 0 0 6px #444;
|
}
|
||||||
color: #fff;
|
|
||||||
|
.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>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,10 @@
|
||||||
<a href="#" @click.prevent="openLanguageDialog">{{ currentLanguageName }}</a>
|
<a href="#" @click.prevent="openLanguageDialog">{{ currentLanguageName }}</a>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<span v-if="availableThemes.length > 1">
|
||||||
|
<a href="#" @click.prevent="openThemeDialog">{{ currentThemeName }}</a>
|
||||||
|
</span>
|
||||||
|
|
||||||
<span>{{ t('connected') }}</span>
|
<span>{{ t('connected') }}</span>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
|
|
@ -55,7 +59,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<dialog ref="languageDialog" class="language-dialog" @click="handleDialogClick">
|
<dialog ref="languageDialog" class="language-dialog" @click="handleLanguageDialogClick">
|
||||||
<div class="dialog-content" @click.stop>
|
<div class="dialog-content" @click.stop>
|
||||||
<h2>{{ t('language-dialog.title') }}</h2>
|
<h2>{{ t('language-dialog.title') }}</h2>
|
||||||
<select v-model="selectedLanguage" @change="changeLanguage" class="language-select">
|
<select v-model="selectedLanguage" @change="changeLanguage" class="language-select">
|
||||||
|
|
@ -73,6 +77,21 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|
@ -117,6 +136,12 @@ const initialLanguagePreference = typeof window !== 'undefined' ? localStorage.g
|
||||||
const languagePreference = ref(initialLanguagePreference || 'auto')
|
const languagePreference = ref(initialLanguagePreference || 'auto')
|
||||||
const selectedLanguage = ref(languagePreference.value)
|
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
|
// Available languages with display names
|
||||||
const availableLanguages = {
|
const availableLanguages = {
|
||||||
'auto': 'Browser Language',
|
'auto': 'Browser Language',
|
||||||
|
|
@ -136,6 +161,14 @@ const currentLanguageName = computed(() => {
|
||||||
return availableLanguages[languagePreference.value] || languagePreference.value
|
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
|
// Computed properties for navigation style
|
||||||
const topbarEnabled = computed(() => {
|
const topbarEnabled = computed(() => {
|
||||||
return sectionNavigationStyle.value === 'topbar'
|
return sectionNavigationStyle.value === 'topbar'
|
||||||
|
|
@ -191,12 +224,16 @@ function updateHeaderFromInit() {
|
||||||
showLogs.value = window.initResponse.showLogList
|
showLogs.value = window.initResponse.showLogList
|
||||||
showDiagnostics.value = window.initResponse.showDiagnostics
|
showDiagnostics.value = window.initResponse.showDiagnostics
|
||||||
sectionNavigationStyle.value = window.initResponse.sectionNavigationStyle || 'sidebar'
|
sectionNavigationStyle.value = window.initResponse.sectionNavigationStyle || 'sidebar'
|
||||||
|
availableThemes.value = window.initResponse.availableThemes || []
|
||||||
|
|
||||||
if (!window.initResponse.authLocalLogin && window.initResponse.oAuth2Providers.length === 0) {
|
if (!window.initResponse.authLocalLogin && window.initResponse.oAuth2Providers.length === 0) {
|
||||||
showLoginLink.value = false
|
showLoginLink.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyStyleMods()
|
||||||
|
|
||||||
renderNavigation()
|
renderNavigation()
|
||||||
|
applyTheme()
|
||||||
|
|
||||||
if (window.initResponse.loginRequired) {
|
if (window.initResponse.loginRequired) {
|
||||||
router.push('/login')
|
router.push('/login')
|
||||||
|
|
@ -280,13 +317,75 @@ function changeLanguage() {
|
||||||
closeLanguageDialog()
|
closeLanguageDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDialogClick(event) {
|
function handleLanguageDialogClick(event) {
|
||||||
// Close dialog when clicking on the backdrop
|
// Close dialog when clicking on the backdrop
|
||||||
if (event.target === languageDialog.value) {
|
if (event.target === languageDialog.value) {
|
||||||
closeLanguageDialog()
|
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
|
window.updateHeaderFromInit = updateHeaderFromInit
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|
@ -296,6 +395,9 @@ onMounted(() => {
|
||||||
// Initialize selected language from stored preference
|
// Initialize selected language from stored preference
|
||||||
selectedLanguage.value = languagePreference.value
|
selectedLanguage.value = languagePreference.value
|
||||||
|
|
||||||
|
// Initialize selected theme from stored preference
|
||||||
|
selectedTheme.value = themePreference.value || ''
|
||||||
|
|
||||||
if (typeof navigator !== 'undefined' && Array.isArray(navigator.languages)) {
|
if (typeof navigator !== 'undefined' && Array.isArray(navigator.languages)) {
|
||||||
browserLanguages.value = navigator.languages
|
browserLanguages.value = navigator.languages
|
||||||
}
|
}
|
||||||
|
|
@ -316,7 +418,8 @@ onMounted(() => {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.language-dialog {
|
.language-dialog,
|
||||||
|
.theme-dialog {
|
||||||
border: 1px solid var(--border-color, #ccc);
|
border: 1px solid var(--border-color, #ccc);
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
@ -324,7 +427,8 @@ onMounted(() => {
|
||||||
width: 90%;
|
width: 90%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.language-dialog::backdrop {
|
.language-dialog::backdrop,
|
||||||
|
.theme-dialog::backdrop {
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
@layer components, karma, theme;
|
||||||
|
|
||||||
header {
|
header {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
@ -37,3 +39,27 @@ div.buttons button svg {
|
||||||
section.small {
|
section.small {
|
||||||
border-radius: .4em;
|
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;
|
||||||
|
}
|
||||||
|
|
@ -18,6 +18,15 @@ export default defineConfig({
|
||||||
target: 'http://localhost:1337',
|
target: 'http://localhost:1337',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
|
},
|
||||||
|
'/theme.css': {
|
||||||
|
target: 'http://localhost:1337',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
},
|
||||||
|
"/custom-webui": {
|
||||||
|
target: "http://localhost:1337",
|
||||||
|
changeOrigin: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
@ -4,14 +4,8 @@ listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
||||||
logLevel: "DEBUG"
|
logLevel: "DEBUG"
|
||||||
checkForUpdates: false
|
checkForUpdates: false
|
||||||
|
|
||||||
themeName: "test-theme"
|
|
||||||
|
|
||||||
actions:
|
actions:
|
||||||
- title: Test action
|
- title: Test action
|
||||||
shell: "echo 'Hello from themed OliveTin'"
|
shell: "echo 'Hello from theme loading test'"
|
||||||
icon: ping
|
icon: ping
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
/* Theme One CSS */
|
||||||
|
body {
|
||||||
|
background-color: #theme-one;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
/* Theme Two CSS */
|
||||||
|
body {
|
||||||
|
background-color: #theme-two;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
@ -49,6 +49,9 @@
|
||||||
"raise-issue": "Ein Problem melden auf GitHub",
|
"raise-issue": "Ein Problem melden auf GitHub",
|
||||||
"return-to-index": "Zurück zur Startseite",
|
"return-to-index": "Zurück zur Startseite",
|
||||||
"search-filter": "Filter aktuelle Seite",
|
"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"
|
"welcome": "Willkommen bei OliveTin"
|
||||||
},
|
},
|
||||||
"en": {
|
"en": {
|
||||||
|
|
@ -99,6 +102,9 @@
|
||||||
"raise-issue": "Raise an issue on GitHub",
|
"raise-issue": "Raise an issue on GitHub",
|
||||||
"return-to-index": "Return to index",
|
"return-to-index": "Return to index",
|
||||||
"search-filter": "Filter current page",
|
"search-filter": "Filter current page",
|
||||||
|
"theme-dialog.close": "Close",
|
||||||
|
"theme-dialog.default": "Default Theme",
|
||||||
|
"theme-dialog.title": "Select Theme",
|
||||||
"welcome": "Welcome to OliveTin"
|
"welcome": "Welcome to OliveTin"
|
||||||
},
|
},
|
||||||
"es-ES": {
|
"es-ES": {
|
||||||
|
|
@ -149,6 +155,9 @@
|
||||||
"raise-issue": "Reportar un problema en GitHub",
|
"raise-issue": "Reportar un problema en GitHub",
|
||||||
"return-to-index": "Volver a la página principal",
|
"return-to-index": "Volver a la página principal",
|
||||||
"search-filter": "Filtrar página actual",
|
"search-filter": "Filtrar página actual",
|
||||||
|
"theme-dialog.close": "Cerrar",
|
||||||
|
"theme-dialog.default": "Tema Predeterminado",
|
||||||
|
"theme-dialog.title": "Seleccionar tema",
|
||||||
"welcome": "Bienvenido a OliveTin"
|
"welcome": "Bienvenido a OliveTin"
|
||||||
},
|
},
|
||||||
"it-IT": {
|
"it-IT": {
|
||||||
|
|
@ -199,6 +208,9 @@
|
||||||
"raise-issue": "Segnala un problema su GitHub",
|
"raise-issue": "Segnala un problema su GitHub",
|
||||||
"return-to-index": "Torna alla pagina principale",
|
"return-to-index": "Torna alla pagina principale",
|
||||||
"search-filter": "Filtra la pagina corrente",
|
"search-filter": "Filtra la pagina corrente",
|
||||||
|
"theme-dialog.close": "Chiudi",
|
||||||
|
"theme-dialog.default": "Tema Predefinito",
|
||||||
|
"theme-dialog.title": "Seleziona tema",
|
||||||
"welcome": "Benvenuto in OliveTin"
|
"welcome": "Benvenuto in OliveTin"
|
||||||
},
|
},
|
||||||
"zh-Hans-CN": {
|
"zh-Hans-CN": {
|
||||||
|
|
@ -249,6 +261,9 @@
|
||||||
"raise-issue": "在 GitHub 上报告问题",
|
"raise-issue": "在 GitHub 上报告问题",
|
||||||
"return-to-index": "返回首页",
|
"return-to-index": "返回首页",
|
||||||
"search-filter": "过滤当前页面",
|
"search-filter": "过滤当前页面",
|
||||||
|
"theme-dialog.close": "关闭",
|
||||||
|
"theme-dialog.default": "默认主题",
|
||||||
|
"theme-dialog.title": "选择主题",
|
||||||
"welcome": "欢迎使用 OliveTin"
|
"welcome": "欢迎使用 OliveTin"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,3 +48,6 @@ translations:
|
||||||
language-dialog.browser-languages: Browser-Sprachen
|
language-dialog.browser-languages: Browser-Sprachen
|
||||||
language-dialog.not-available: Nicht verfügbar
|
language-dialog.not-available: Nicht verfügbar
|
||||||
language-dialog.close: Schließen
|
language-dialog.close: Schließen
|
||||||
|
theme-dialog.title: Design auswählen
|
||||||
|
theme-dialog.default: Standard-Design
|
||||||
|
theme-dialog.close: Schließen
|
||||||
|
|
@ -48,3 +48,6 @@ translations:
|
||||||
language-dialog.browser-languages: Browser languages
|
language-dialog.browser-languages: Browser languages
|
||||||
language-dialog.not-available: Not available
|
language-dialog.not-available: Not available
|
||||||
language-dialog.close: Close
|
language-dialog.close: Close
|
||||||
|
theme-dialog.title: Select Theme
|
||||||
|
theme-dialog.default: Default Theme
|
||||||
|
theme-dialog.close: Close
|
||||||
|
|
@ -48,3 +48,6 @@ translations:
|
||||||
language-dialog.browser-languages: Idiomas del navegador
|
language-dialog.browser-languages: Idiomas del navegador
|
||||||
language-dialog.not-available: No disponible
|
language-dialog.not-available: No disponible
|
||||||
language-dialog.close: Cerrar
|
language-dialog.close: Cerrar
|
||||||
|
theme-dialog.title: Seleccionar tema
|
||||||
|
theme-dialog.default: Tema Predeterminado
|
||||||
|
theme-dialog.close: Cerrar
|
||||||
|
|
@ -48,3 +48,6 @@ translations:
|
||||||
language-dialog.browser-languages: Lingue del browser
|
language-dialog.browser-languages: Lingue del browser
|
||||||
language-dialog.not-available: Non disponibile
|
language-dialog.not-available: Non disponibile
|
||||||
language-dialog.close: Chiudi
|
language-dialog.close: Chiudi
|
||||||
|
theme-dialog.title: Seleziona tema
|
||||||
|
theme-dialog.default: Tema Predefinito
|
||||||
|
theme-dialog.close: Chiudi
|
||||||
|
|
@ -22,6 +22,9 @@ translations:
|
||||||
language-dialog.browser-languages: 浏览器语言
|
language-dialog.browser-languages: 浏览器语言
|
||||||
language-dialog.not-available: 不可用
|
language-dialog.not-available: 不可用
|
||||||
language-dialog.close: 关闭
|
language-dialog.close: 关闭
|
||||||
|
theme-dialog.title: 选择主题
|
||||||
|
theme-dialog.default: 默认主题
|
||||||
|
theme-dialog.close: 关闭
|
||||||
logs.timed-out: 超时
|
logs.timed-out: 超时
|
||||||
logs.blocked: 阻塞
|
logs.blocked: 阻塞
|
||||||
logs.exit-code: 退出代码
|
logs.exit-code: 退出代码
|
||||||
|
|
|
||||||
|
|
@ -332,6 +332,7 @@ message InitResponse {
|
||||||
bool show_diagnostics = 21;
|
bool show_diagnostics = 21;
|
||||||
bool show_log_list = 22;
|
bool show_log_list = 22;
|
||||||
bool login_required = 23;
|
bool login_required = 23;
|
||||||
|
repeated string available_themes = 24; // List of available theme names
|
||||||
}
|
}
|
||||||
|
|
||||||
message AdditionalLink {
|
message AdditionalLink {
|
||||||
|
|
|
||||||
|
|
@ -3236,6 +3236,7 @@ type InitResponse struct {
|
||||||
ShowDiagnostics bool `protobuf:"varint,21,opt,name=show_diagnostics,json=showDiagnostics,proto3" json:"show_diagnostics,omitempty"`
|
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"`
|
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"`
|
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
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
|
|
@ -3431,6 +3432,13 @@ func (x *InitResponse) GetLoginRequired() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *InitResponse) GetAvailableThemes() []string {
|
||||||
|
if x != nil {
|
||||||
|
return x.AvailableThemes
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type AdditionalLink struct {
|
type AdditionalLink struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
|
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" +
|
"\x16GetDiagnosticsResponse\x12 \n" +
|
||||||
"\vSshFoundKey\x18\x01 \x01(\tR\vSshFoundKey\x12&\n" +
|
"\vSshFoundKey\x18\x01 \x01(\tR\vSshFoundKey\x12&\n" +
|
||||||
"\x0eSshFoundConfig\x18\x02 \x01(\tR\x0eSshFoundConfig\"\r\n" +
|
"\x0eSshFoundConfig\x18\x02 \x01(\tR\x0eSshFoundConfig\"\r\n" +
|
||||||
"\vInitRequest\"\xa2\b\n" +
|
"\vInitRequest\"\xcd\b\n" +
|
||||||
"\fInitResponse\x12\x1e\n" +
|
"\fInitResponse\x12\x1e\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"showFooter\x18\x01 \x01(\bR\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" +
|
"banner_css\x18\x14 \x01(\tR\tbannerCss\x12)\n" +
|
||||||
"\x10show_diagnostics\x18\x15 \x01(\bR\x0fshowDiagnostics\x12\"\n" +
|
"\x10show_diagnostics\x18\x15 \x01(\bR\x0fshowDiagnostics\x12\"\n" +
|
||||||
"\rshow_log_list\x18\x16 \x01(\bR\vshowLogList\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" +
|
"\x0eAdditionalLink\x12\x14\n" +
|
||||||
"\x05title\x18\x01 \x01(\tR\x05title\x12\x10\n" +
|
"\x05title\x18\x01 \x01(\tR\x05title\x12\x10\n" +
|
||||||
"\x03url\x18\x02 \x01(\tR\x03url\"L\n" +
|
"\x03url\x18\x02 \x01(\tR\x03url\"L\n" +
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ package api
|
||||||
import (
|
import (
|
||||||
ctx "context"
|
ctx "context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"connectrpc.com/connect"
|
"connectrpc.com/connect"
|
||||||
|
|
@ -902,11 +904,62 @@ func (api *oliveTinAPI) Init(ctx ctx.Context, req *connect.Request[apiv1.InitReq
|
||||||
ShowDiagnostics: user.EffectivePolicy.ShowDiagnostics,
|
ShowDiagnostics: user.EffectivePolicy.ShowDiagnostics,
|
||||||
ShowLogList: user.EffectivePolicy.ShowLogList,
|
ShowLogList: user.EffectivePolicy.ShowLogList,
|
||||||
LoginRequired: loginRequired,
|
LoginRequired: loginRequired,
|
||||||
|
AvailableThemes: discoverAvailableThemes(api.cfg),
|
||||||
}
|
}
|
||||||
|
|
||||||
return connect.NewResponse(res), nil
|
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 {
|
func (api *oliveTinAPI) buildRootDashboards(user *authpublic.AuthenticatedUser, dashboards []*config.DashboardComponent) []string {
|
||||||
var rootDashboards []string
|
var rootDashboards []string
|
||||||
dashboardRenderRequest := api.createDashboardRenderRequest(user, "", "")
|
dashboardRenderRequest := api.createDashboardRenderRequest(user, "", "")
|
||||||
|
|
|
||||||
|
|
@ -58,5 +58,8 @@ func (cfg *Config) SetDir(dir string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *Config) GetDir() string {
|
func (cfg *Config) GetDir() string {
|
||||||
|
if len(cfg.sourceFiles) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
return cfg.sourceFiles[len(cfg.sourceFiles)-1]
|
return cfg.sourceFiles[len(cfg.sourceFiles)-1]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -224,58 +224,14 @@ func (e *Executor) GetLogTrackingIds(startOffset int64, pageCount int64) ([]*Int
|
||||||
return trackingIds, pagingResult
|
return trackingIds, pagingResult
|
||||||
}
|
}
|
||||||
|
|
||||||
// isValidLogEntryForACL checks if a log entry has all required fields for ACL checking.
|
|
||||||
func isValidLogEntryForACL(entry *InternalLogEntry) bool {
|
func isValidLogEntryForACL(entry *InternalLogEntry) bool {
|
||||||
return entry != nil && entry.Binding != nil && entry.Binding.Action != nil
|
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 {
|
func isLogEntryAllowedByACL(cfg *config.Config, user *authpublic.AuthenticatedUser, entry *InternalLogEntry) bool {
|
||||||
return acl.IsAllowedLogs(cfg, user, entry.Binding.Action)
|
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 {
|
func (e *Executor) filterLogsByACL(cfg *config.Config, user *authpublic.AuthenticatedUser, dateFilter string) []*InternalLogEntry {
|
||||||
e.logmutex.RLock()
|
e.logmutex.RLock()
|
||||||
defer e.logmutex.RUnlock()
|
defer e.logmutex.RUnlock()
|
||||||
|
|
@ -294,6 +250,48 @@ func (e *Executor) filterLogsByACL(cfg *config.Config, user *authpublic.Authenti
|
||||||
return filtered
|
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
|
// paginateFilteredLogs applies pagination to a filtered list of logs and returns
|
||||||
// the paginated results along with pagination metadata.
|
// the paginated results along with pagination metadata.
|
||||||
func paginateFilteredLogs(filtered []*InternalLogEntry, startOffset int64, pageCount int64) ([]*InternalLogEntry, *PagingResult) {
|
func paginateFilteredLogs(filtered []*InternalLogEntry, startOffset int64, pageCount int64) ([]*InternalLogEntry, *PagingResult) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue