Skip to content

Commit

Permalink
Merge pull request #1040 from nextcloud/fix/ui-vue-8
Browse files Browse the repository at this point in the history
feat: Allow pasting log entries
  • Loading branch information
icewind1991 authored Feb 14, 2024
2 parents 356ebeb + 896731a commit 5b52869
Show file tree
Hide file tree
Showing 8 changed files with 207 additions and 71 deletions.
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 }
})

0 comments on commit 5b52869

Please sign in to comment.