diff --git a/packages/api/src/utility/crypting.js b/packages/api/src/utility/crypting.js index f24ede6a7..95220e6d6 100644 --- a/packages/api/src/utility/crypting.js +++ b/packages/api/src/utility/crypting.js @@ -33,6 +33,26 @@ function loadEncryptionKey() { return _encryptionKey; } +async function loadEncryptionKeyFromExternal(storedValue, setStoredValue) { + const encryptor = simpleEncryptor.createEncryptor(defaultEncryptionKey); + + if (!storedValue) { + const generatedKey = crypto.randomBytes(32); + const newKey = generatedKey.toString('hex'); + const result = { + encryptionKey: newKey, + }; + await setStoredValue(encryptor.encrypt(result)); + + setEncryptionKey(newKey); + + return; + } + + const data = encryptor.decrypt(storedValue); + setEncryptionKey(data['encryptionKey']); +} + let _encryptor = null; function getEncryptor() { @@ -43,35 +63,32 @@ function getEncryptor() { return _encryptor; } -function encryptPasswordField(connection, field) { - if ( - connection && - connection[field] && - !connection[field].startsWith('crypt:') && - connection.passwordMode != 'saveRaw' - ) { +function encryptObjectPasswordField(obj, field) { + if (obj && obj[field] && !obj[field].startsWith('crypt:')) { return { - ...connection, - [field]: 'crypt:' + getEncryptor().encrypt(connection[field]), + ...obj, + [field]: 'crypt:' + getEncryptor().encrypt(obj[field]), }; } - return connection; + return obj; } -function decryptPasswordField(connection, field) { - if (connection && connection[field] && connection[field].startsWith('crypt:')) { +function decryptObjectPasswordField(obj, field) { + if (obj && obj[field] && obj[field].startsWith('crypt:')) { return { - ...connection, - [field]: getEncryptor().decrypt(connection[field].substring('crypt:'.length)), + ...obj, + [field]: getEncryptor().decrypt(obj[field].substring('crypt:'.length)), }; } - return connection; + return obj; } function encryptConnection(connection) { - connection = encryptPasswordField(connection, 'password'); - connection = encryptPasswordField(connection, 'sshPassword'); - connection = encryptPasswordField(connection, 'sshKeyfilePassword'); + if (connection.passwordMode != 'saveRaw') { + connection = encryptObjectPasswordField(connection, 'password'); + connection = encryptObjectPasswordField(connection, 'sshPassword'); + connection = encryptObjectPasswordField(connection, 'sshKeyfilePassword'); + } return connection; } @@ -81,12 +98,24 @@ function maskConnection(connection) { } function decryptConnection(connection) { - connection = decryptPasswordField(connection, 'password'); - connection = decryptPasswordField(connection, 'sshPassword'); - connection = decryptPasswordField(connection, 'sshKeyfilePassword'); + connection = decryptObjectPasswordField(connection, 'password'); + connection = decryptObjectPasswordField(connection, 'sshPassword'); + connection = decryptObjectPasswordField(connection, 'sshKeyfilePassword'); return connection; } +function encryptUser(user) { + if (user.encryptPassword) { + user = encryptObjectPasswordField(user, 'password'); + } + return user; +} + +function decryptUser(user) { + user = decryptObjectPasswordField(user, 'password'); + return user; +} + function pickSafeConnectionInfo(connection) { if (process.env.LOG_CONNECTION_SENSITIVE_VALUES) { return connection; @@ -99,10 +128,18 @@ function pickSafeConnectionInfo(connection) { }); } +function setEncryptionKey(encryptionKey) { + _encryptionKey = encryptionKey; + _encryptor = null; +} + module.exports = { loadEncryptionKey, encryptConnection, + encryptUser, + decryptUser, decryptConnection, maskConnection, pickSafeConnectionInfo, + loadEncryptionKeyFromExternal, }; diff --git a/packages/web/public/global.css b/packages/web/public/global.css index f4f427d87..d39a035f9 100644 --- a/packages/web/public/global.css +++ b/packages/web/public/global.css @@ -58,6 +58,24 @@ body { .relative { position: relative; } +.scroll { + overflow: scroll; +} +.bg-0 { + background-color: var(--theme-bg-0); +} +.bg-1 { + background-color: var(--theme-bg-1); +} +.bg-2 { + background-color: var(--theme-bg-2); +} +.bg-3 { + background-color: var(--theme-bg-3); +} +.bg-4 { + background-color: var(--theme-bg-4); +} .col-10 { flex-basis: 83.3333%; diff --git a/packages/web/src/buttons/ToolStripContainer.svelte b/packages/web/src/buttons/ToolStripContainer.svelte index e0a36466d..496af1729 100644 --- a/packages/web/src/buttons/ToolStripContainer.svelte +++ b/packages/web/src/buttons/ToolStripContainer.svelte @@ -16,7 +16,7 @@
-
+
@@ -41,6 +41,10 @@ max-height: 100%; } + .content.isComponentActive { + max-height: calc(100% - 30px); + } + .toolstrip { display: flex; flex-wrap: wrap; diff --git a/packages/web/src/commands/stdCommands.ts b/packages/web/src/commands/stdCommands.ts index ba6d3e43d..8b99b6584 100644 --- a/packages/web/src/commands/stdCommands.ts +++ b/packages/web/src/commands/stdCommands.ts @@ -619,6 +619,24 @@ registerCommand({ onClick: doLogout, }); +registerCommand({ + id: 'app.loggedUserCommands', + category: 'App', + name: 'Logged user', + getSubCommands: () => { + const config = getCurrentConfig(); + if (!config) return []; + return [ + { + text: 'Logout', + onClick: () => { + doLogout(); + }, + }, + ]; + }, +}); + registerCommand({ id: 'app.disconnect', category: 'App', diff --git a/packages/web/src/elements/TabControl.svelte b/packages/web/src/elements/TabControl.svelte index 4ca49ea36..a89d6bfa3 100644 --- a/packages/web/src/elements/TabControl.svelte +++ b/packages/web/src/elements/TabControl.svelte @@ -17,6 +17,7 @@ export let containerMaxWidth = undefined; export let flex1 = true; export let contentTestId = undefined; + export let inlineTabs = false; export function setValue(index) { value = index; @@ -27,7 +28,7 @@
-
+
{#each _.compact(tabs) as tab, index}
(value = index)} data-testid={tab.testid}> @@ -78,17 +79,27 @@ height: var(--dim-tabs-height); min-height: var(--dim-tabs-height); right: 0; - background-color: var(--theme-bg-2); overflow-x: auto; max-width: 100%; } + .tabs:not(.inlineTabs) { + background-color: var(--theme-bg-2); + } + + .tabs.inlineTabs { + border-bottom: 1px solid var(--theme-border); + text-transform: uppercase; + } + + .tabs.inlineTabs .tab-item.selected { + border-bottom: 2px solid var(--theme-font-link); + } .tabs::-webkit-scrollbar { height: 7px; } .tab-item { - border-right: 1px solid var(--theme-border); padding-left: 15px; padding-right: 15px; display: flex; @@ -96,6 +107,10 @@ cursor: pointer; } + .tabs:not(.inlineTabs) .tab-item { + border-right: 1px solid var(--theme-border); + } + /* .tab-item:hover { color: ${props => props.theme.tabs_font_hover}; } */ @@ -124,4 +139,5 @@ .container.isInline:not(.tabVisible) { display: none; } + diff --git a/packages/web/src/elements/TableControl.svelte b/packages/web/src/elements/TableControl.svelte index e8eb1051b..835902349 100644 --- a/packages/web/src/elements/TableControl.svelte +++ b/packages/web/src/elements/TableControl.svelte @@ -31,6 +31,11 @@ export let noCellPadding = false; export let domTable = undefined; + export let stickyHeader = false; + + export let checkedKeys = null; + export let onSetCheckedKeys = null; + export let extractCheckedKey = x => x.id; const dispatch = createEventDispatcher(); @@ -63,11 +68,15 @@ on:keydown tabindex={selectable ? -1 : undefined} on:keydown={handleKeyDown} + class:stickyHeader > - + + {#if checkedKeys} + + {/if} {#each columnList as col} - { if (col.sortable) { @@ -89,7 +98,7 @@ {#if sortedByField == col.fieldName} {/if} - + {/each} @@ -108,6 +117,18 @@ } }} > + {#if checkedKeys} + + { + if (e.target['checked']) onSetCheckedKeys(_.uniq([...checkedKeys, extractCheckedKey(row)])); + else onSetCheckedKeys(checkedKeys.filter(x => x != extractCheckedKey(row))); + }} + /> + + {/if} {#each columnList as col} {@const rowProps = { ...col.props, ...(col.getProps ? col.getProps(row) : null) }} @@ -164,7 +185,7 @@ background: var(--theme-bg-hover); } - thead td { + thead th { border: 1px solid var(--theme-border); background-color: var(--theme-bg-1); padding: 5px; @@ -184,4 +205,31 @@ td.clickable { cursor: pointer; } + + thead.stickyHeader { + position: sticky; + top: 0; + z-index: 1; + border-top: 1px solid var(--theme-border); + } + + table.stickyHeader th { + border-left: none; + } + + thead.stickyHeader :global(tr:first-child) :global(th) { + border-top: 1px solid var(--theme-border); + } + + table.stickyHeader td { + border: 0px; + border-bottom: 1px solid var(--theme-border); + border-right: 1px solid var(--theme-border); + } + + table.stickyHeader { + border-spacing: 0; + border-collapse: separate; + border-left: 1px solid var(--theme-border); + } diff --git a/packages/web/src/forms/ExtendedCheckBoxField.svelte b/packages/web/src/forms/ExtendedCheckBoxField.svelte new file mode 100644 index 000000000..e5c88f1a2 --- /dev/null +++ b/packages/web/src/forms/ExtendedCheckBoxField.svelte @@ -0,0 +1,79 @@ + + +
{ + onChange(getNextValue()); + }} +> +
+
+ {label} +
+
+ + diff --git a/packages/web/src/modals/ConfirmModal.svelte b/packages/web/src/modals/ConfirmModal.svelte index dceb19281..49a70c5d7 100644 --- a/packages/web/src/modals/ConfirmModal.svelte +++ b/packages/web/src/modals/ConfirmModal.svelte @@ -8,15 +8,23 @@ export let message; export let onConfirm; + export let confirmLabel = 'OK'; + export let header = null; + + {#if header} + {header} + {/if} + + {message} { closeCurrentModal(); onConfirm(); diff --git a/packages/web/src/tabs/ConnectionTab.svelte b/packages/web/src/tabs/ConnectionTab.svelte index d58e6f343..8c1da2f5c 100644 --- a/packages/web/src/tabs/ConnectionTab.svelte +++ b/packages/web/src/tabs/ConnectionTab.svelte @@ -41,6 +41,7 @@ export let tabid; export let conid; export let connectionStore = undefined; + export let inlineTabs = false; export let onlyTestButton; @@ -237,6 +238,7 @@
{/each} + {#if $config?.isUserLoggedIn && $config?.login} +
visibleCommandPalette.set(findCommand('app.loggedUserCommands'))}> + + {$config?.login} +
+ {/if} + {#if $appUpdateStatus}