Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allow pasting log entries #1040

Merged
merged 3 commits into from
Feb 14, 2024
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 @@
loggingStore.loadMore()
}

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

Check warning on line 89 in src/App.vue

View check run for this annotation

Codecov / codecov/patch

src/App.vue#L88-L89

Added lines #L88 - L89 were not covered by tests

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

Check warning on line 93 in src/App.vue

View check run for this annotation

Codecov / codecov/patch

src/App.vue#L92-L93

Added lines #L92 - L93 were not covered by tests
}

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

Check warning on line 99 in src/App.vue

View check run for this annotation

Codecov / codecov/patch

src/App.vue#L98-L99

Added lines #L98 - L99 were not covered by tests

/**
* 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 { 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 @@
const settingsStore = useSettingsStore()
const logStore = useLogStore()

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

Check warning on line 48 in src/components/settings/SettingsActions.vue

View check run for this annotation

Codecov / codecov/patch

src/components/settings/SettingsActions.vue#L48

Added line #L48 was not covered by tests

/**
* Logfile download URL
*/
Expand Down Expand Up @@ -71,6 +79,11 @@
<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 @@
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

Check warning on line 30 in src/store/logging.spec.ts

View workflow job for this annotation

GitHub Actions / NPM lint

Missing trailing comma
}))

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 @@
})

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 @@
})

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 @@
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 @@
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 @@
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 { 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 @@
hasRemainingEntries.value = false
}

/**

Check warning on line 104 in src/store/logging.ts

View workflow job for this annotation

GitHub Actions / NPM lint

Missing JSDoc @param "text" declaration
* 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 @@
}
}

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