Skip to content
Merged
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
4 changes: 2 additions & 2 deletions css/logreader-main.css

Large diffs are not rendered by default.

80 changes: 40 additions & 40 deletions js/logreader-main.mjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/logreader-main.mjs.map

Large diffs are not rendered by default.

23 changes: 21 additions & 2 deletions lib/Constants.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,41 @@

// !! Keep in sync with src/constants.ts
class Constants {
// Used config Keys

/**
* Used AppConfig Keys
* Logging levels to show, used for filtering
*/
public const CONFIG_KEY_SHOWNLEVELS = 'shownLevels';
/**
* The backend logging level
*/
public const CONFIG_KEY_LOGLEVEL = 'logLevel';
/**
* Display format of the timestamp
*/
public const CONFIG_KEY_DATETIMEFORMAT = 'dateTimeFormat';
/**
* If relative dates should be shown for the timestamp (e.g. '3 hours ago')
*/
public const CONFIG_KEY_RELATIVEDATES = 'relativedates';
/**
* If automatic updates of the UI are enabled (polling for new entries)
*/
public const CONFIG_KEY_LIVELOG = 'liveLog';

/**
* All valid config keys
*/
public const CONFIG_KEYS = [
self::CONFIG_KEY_SHOWNLEVELS,
self::CONFIG_KEY_LOGLEVEL,
self::CONFIG_KEY_DATETIMEFORMAT,
self::CONFIG_KEY_RELATIVEDATES,
self::CONFIG_KEY_LIVELOG
self::CONFIG_KEY_LIVELOG,
];

// other constants
public const LOGGING_LEVELS = [0, 1, 2, 3, 4];
public const LOGGING_LEVEL_NAMES = [
'debug',
Expand Down
17 changes: 17 additions & 0 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,23 @@ const onShowServerLog = () => {
loggingStore.loadMore()
}

/**
* Handle paste events with log entries
* @param event The keyboard event
*/
const onHandlePaste = (event: ClipboardEvent) => {
event.preventDefault()

if (event.clipboardData) {
const paste = event.clipboardData.getData('text')
loggingStore.loadText(paste)
}

}
// Add / remove event listeners
onMounted(() => window.addEventListener('paste', onHandlePaste))
onUnmounted(() => window.removeEventListener('paste', onHandlePaste))

/**
* Toggle polling if live log is dis- / enabled
*/
Expand Down
13 changes: 13 additions & 0 deletions src/components/settings/SettingsActions.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
<template>
<div>
<NcNoteCard type="info" class="info-note">
<!-- eslint-disable-next-line vue/no-v-html -->
<p v-html="t('logreader', 'You can also show log entries copied from your clipboard by pasting them on the log view using: {keyboardShortcut}', { keyboardShortcut: keyboardShortcutText }, undefined, { escape: false })" />
</NcNoteCard>
<NcButton :href="settingsStore.enabled ? downloadURL : null" :disabled="!settingsStore.enabled" download="nextcloud.log">
<template #icon>
<IconDownload :size="20" />
Expand Down Expand Up @@ -31,6 +35,7 @@ import { useLogStore } from '../../store/logging'
import { useSettingsStore } from '../../store/settings.js'

import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
import IconDownload from 'vue-material-design-icons/Download.vue'
import IconUpload from 'vue-material-design-icons/Upload.vue'
import { logger } from '../../utils/logger'
Expand All @@ -39,6 +44,9 @@ import { showError } from '@nextcloud/dialogs'
const settingsStore = useSettingsStore()
const logStore = useLogStore()

// TRANSLATORS The control key abbreviation
const keyboardShortcutText = `<kbd>${t('logreader', 'Ctrl')}</kbd> + <kbd>v</kbd>`

/**
* Logfile download URL
*/
Expand Down Expand Up @@ -71,6 +79,11 @@ const onFileSelected = async () => {
<style lang="scss" scoped>
div {
display: flex;
flex-wrap: wrap;
gap: 12px;
padding-inline-end: 12px;
}
.info-note {
justify-self: stretch;
}
</style>
113 changes: 89 additions & 24 deletions src/store/logging.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,30 @@ import { POLLING_INTERVAL } from '../constants'
const mocks = vi.hoisted(() => {
return {
parseLogFile: vi.fn(),
parseLogString: vi.fn(),
logger: {
debug: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
},
getLog: vi.fn(),
pollLog: vi.fn(),
showError: vi.fn(),
}
})

vi.mock('@nextcloud/dialogs', () => ({
showError: mocks.showError
}))

vi.mock('../utils/logfile.ts', () => {
return {
parseLogFile: mocks.parseLogFile,
parseLogString: mocks.parseLogString,
parseRawLogEntry: vi.fn((v) => v),
}
})

class ServerError extends Error {

public status = 500
Expand Down Expand Up @@ -162,13 +176,6 @@ describe('store:logging', () => {
})

it('loads entries from file', async () => {
vi.mock('../utils/logfile.ts', () => {
return {
parseLogFile: mocks.parseLogFile,
parseRawLogEntry: vi.fn((v) => v),
}
})

vi.mocked(mocks.parseLogFile).mockImplementation(async () => {
return [{ message: 'hello' }]
})
Expand Down Expand Up @@ -197,13 +204,6 @@ describe('store:logging', () => {
})

it('does not load file if no file was selected', async () => {
vi.mock('../utils/logfile.ts', () => {
return {
parseLogFile: mocks.parseLogFile,
parseRawLogEntry: vi.fn((v) => v),
}
})

vi.mock('../utils/logger.ts', () => {
return {
logger: mocks.logger,
Expand All @@ -227,6 +227,81 @@ describe('store:logging', () => {
expect(mocks.parseLogFile).not.toBeCalled()
})

it('loads entries from clipboard', async () => {
mocks.parseLogString.mockImplementationOnce(() => [{ message: 'hello' }])

// clean pinia
createTestingPinia({
fakeApp: true,
createSpy: vi.fn,
stubActions: false,
})

const clipboard = '{message: "hello"}'

const store = useLogStore()
const settings = useSettingsStore()

store.hasRemainingEntries = true
expect(store.hasRemainingEntries).toBe(true)

await store.loadText(clipboard)

// File parsed, so there are no remaining entries
expect(store.hasRemainingEntries).toBe(false)
expect(settings.localFileName).toBe('Clipboard')
expect(mocks.parseLogString).toBeCalledWith(clipboard)
expect(store.allEntries).toEqual([{ message: 'hello' }])
})

it('handles empty clipboard paste', async () => {
// clean pinia
createTestingPinia({
fakeApp: true,
createSpy: vi.fn,
stubActions: false,
})

const store = useLogStore()
const settings = useSettingsStore()

store.hasRemainingEntries = true
expect(store.hasRemainingEntries).toBe(true)

await store.loadText('')

// File parsed, so there are no remaining entries
expect(store.hasRemainingEntries).toBe(true)
expect(settings.localFile).toBe(undefined)
expect(settings.localFileName).toBe('')
})

it('handles invalid clipboard paste', async () => {
// clean pinia
createTestingPinia({
fakeApp: true,
createSpy: vi.fn,
stubActions: false,
})

// throw an error
mocks.parseLogString.mockImplementationOnce(() => { throw new Error() })

const store = useLogStore()
const settings = useSettingsStore()

store.hasRemainingEntries = true
expect(store.hasRemainingEntries).toBe(true)

await store.loadText('invalid')

// File parsed, so there are no remaining entries
expect(store.hasRemainingEntries).toBe(true)
expect(mocks.showError).toBeCalled()
expect(settings.localFile).toBe(undefined)
expect(settings.localFileName).toBe('')
})

it('loads more from server', async () => {
vi.mock('../api.ts', () => {
return {
Expand Down Expand Up @@ -547,11 +622,6 @@ describe('store:logging', () => {
logger: mocks.logger,
}
})
vi.mock('@nextcloud/dialogs', () => {
return {
showError: mocks.showError,
}
})
vi.mocked(mocks.pollLog).mockImplementationOnce(() => { throw Error() })

// clean pinia
Expand Down Expand Up @@ -581,11 +651,6 @@ describe('store:logging', () => {
logger: mocks.logger,
}
})
vi.mock('@nextcloud/dialogs', () => {
return {
showError: mocks.showError,
}
})
vi.mocked(mocks.pollLog).mockImplementationOnce(() => { throw new ServerError() })

// clean pinia
Expand Down
26 changes: 24 additions & 2 deletions src/store/logging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { POLLING_INTERVAL } from '../constants'
import { showError } from '@nextcloud/dialogs'
import { translate as t } from '@nextcloud/l10n'
import { useSettingsStore } from './settings'
import { parseLogFile, parseRawLogEntry } from '../utils/logfile'
import { parseLogFile, parseLogString, parseRawLogEntry } from '../utils/logfile'
import { logger } from '../utils/logger'

/**
Expand Down Expand Up @@ -101,6 +101,28 @@ export const useLogStore = defineStore('logreader-logs', () => {
hasRemainingEntries.value = false
}

/**
* Load entries from string
*/
async function loadText(text: string) {
// Skip if aborted
if (text === '') {
return
}

try {
allEntries.value = await parseLogString(text)
// TRANSLATORS The clipboard used to paste stuff
_settings.localFile = new File([], t('logreader', 'Clipboard'))
// From clipboard so no more entries
hasRemainingEntries.value = false
} catch (e) {
// TRANSLATORS Error when the pasted content from the clipboard could not be parsed
showError(t('logreader', 'Could not parse clipboard content'))
logger.error(e as Error)
}
}

/**
* Stop polling entries
*/
Expand Down Expand Up @@ -169,5 +191,5 @@ export const useLogStore = defineStore('logreader-logs', () => {
}
}

return { allEntries, entries, hasRemainingEntries, query, loadMore, loadFile, startPolling, stopPolling, searchLogs }
return { allEntries, entries, hasRemainingEntries, query, loadMore, loadText, loadFile, startPolling, stopPolling, searchLogs }
})