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