finish manage ui for ssh keys
This commit is contained in:
@ -31,36 +31,11 @@
|
||||
showDetails = false;
|
||||
}
|
||||
|
||||
let deleteModal;
|
||||
function conditionalDelete() {
|
||||
if (!record.isNew) {
|
||||
deleteModal.showModal();
|
||||
}
|
||||
else {
|
||||
deleteCredential();
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCredential() {
|
||||
try {
|
||||
if (!record.isNew) {
|
||||
await invoke('delete_credential', {id: record.id});
|
||||
}
|
||||
dispatch('update');
|
||||
}
|
||||
catch (e) {
|
||||
showDetails = true;
|
||||
// wait for showDetails to take effect and the alert to be rendered
|
||||
window.setTimeout(() => alert.setError(e), 0);
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<div
|
||||
transition:slide|local={{duration: record.isNew ? 300 : 0}}
|
||||
class="rounded-box space-y-4 bg-base-200 {record.is_default ? 'border border-accent' : ''}"
|
||||
>
|
||||
<div class="rounded-box space-y-4 bg-base-200 {record.is_default ? 'border border-accent' : ''}">
|
||||
<div class="flex items-center px-6 py-4 gap-x-4">
|
||||
<h3 class="text-lg font-bold">
|
||||
{#if !record?.isNew && showDetails}
|
||||
@ -85,7 +60,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline btn-error join-item"
|
||||
on:click={conditionalDelete}
|
||||
on:click={() => dispatch('delete', record)}
|
||||
>
|
||||
<Icon name="trash" class="size-6" />
|
||||
</button>
|
||||
@ -141,20 +116,4 @@
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
<dialog bind:this={deleteModal} class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="text-lg font-bold">Delete AWS credential "{record.name}"?</h3>
|
||||
<div class="modal-action">
|
||||
<form method="dialog" class="flex gap-x-4">
|
||||
<button class="btn btn-outline">Cancel</button>
|
||||
<button
|
||||
autofocus
|
||||
class="btn btn-error"
|
||||
on:click={deleteCredential}
|
||||
>Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
</div>
|
||||
|
62
src/views/credentials/ConfirmDelete.svelte
Normal file
62
src/views/credentials/ConfirmDelete.svelte
Normal file
@ -0,0 +1,62 @@
|
||||
<script>
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
import ErrorAlert from '../../ui/ErrorAlert.svelte';
|
||||
|
||||
let record;
|
||||
let modal;
|
||||
let alert;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export function confirm(r) {
|
||||
record = r;
|
||||
modal.showModal();
|
||||
}
|
||||
|
||||
async function deleteCredential() {
|
||||
await invoke('delete_credential', {id: record.id})
|
||||
// closing the modal is dependent on the previous step succeeding
|
||||
modal.close();
|
||||
dispatch('confirm');
|
||||
}
|
||||
|
||||
function credentialDescription(record) {
|
||||
if (record.credential.type === 'AwsBase') {
|
||||
return 'AWS credential';
|
||||
}
|
||||
if (record.credential.type === 'Ssh') {
|
||||
return 'SSH key';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<dialog bind:this={modal} class="modal">
|
||||
<div class="modal-box space-y-6">
|
||||
<ErrorAlert bind:this={alert} />
|
||||
<h3 class="text-lg font-bold">
|
||||
{#if record}
|
||||
Delete {credentialDescription(record)} "{record.name}"?
|
||||
{/if}
|
||||
</h3>
|
||||
<div class="modal-action">
|
||||
<form method="dialog" class="flex gap-x-4">
|
||||
<button
|
||||
class="btn btn-outline"
|
||||
on:click={() => alert.setError(null)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
<button
|
||||
autofocus
|
||||
class="btn btn-error"
|
||||
on:click|preventDefault={() => alert.run(deleteCredential)}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
85
src/views/credentials/EditSshKey.svelte
Normal file
85
src/views/credentials/EditSshKey.svelte
Normal file
@ -0,0 +1,85 @@
|
||||
<script>
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
import ErrorAlert from '../../ui/ErrorAlert.svelte';
|
||||
|
||||
|
||||
export let local;
|
||||
export let isModified;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let alert;
|
||||
|
||||
async function saveCredential() {
|
||||
await invoke('save_credential', {record: local});
|
||||
dispatch('save', local);
|
||||
showDetails = false;
|
||||
}
|
||||
|
||||
async function copyText(evt) {
|
||||
const tooltip = event.currentTarget;
|
||||
await navigator.clipboard.writeText(tooltip.dataset.copyText);
|
||||
const prevText = tooltip.dataset.tip;
|
||||
tooltip.dataset.tip = 'Copied!';
|
||||
window.setTimeout(() => tooltip.dataset.tip = prevText, 2000);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style>
|
||||
.grid {
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<form class="space-y-4" on:submit|preventDefault={() => alert.run(saveCredential)}>
|
||||
<ErrorAlert bind:this={alert} />
|
||||
|
||||
<div class="grid items-baseline gap-4">
|
||||
<span class="justify-self-end">Comment</span>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered bg-transparent"
|
||||
bind:value={local.credential.comment}
|
||||
>
|
||||
|
||||
<span class="justify-self-end">Public key</span>
|
||||
<div
|
||||
class="tooltip tooltip-right"
|
||||
data-tip="Click to copy"
|
||||
data-copy-text={local.credential.public_key}
|
||||
on:click={copyText}
|
||||
>
|
||||
<div class="cursor-pointer text-left textarea textarea-bordered bg-transparent font-mono break-all">
|
||||
{local.credential.public_key}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="justify-self-end">Private key</span>
|
||||
<div
|
||||
class="tooltip tooltip-right"
|
||||
data-tip="Click to copy"
|
||||
data-copy-text={local.credential.private_key}
|
||||
on:click={copyText}
|
||||
>
|
||||
<div class="cursor-pointer text-left textarea textarea-bordered bg-transparent font-mono whitespace-pre overflow-x-auto">
|
||||
{local.credential.private_key}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
{#if isModified}
|
||||
<button
|
||||
transition:fade={{duration: 100}}
|
||||
type="submit"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
@ -2,12 +2,12 @@
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { homeDir } from '@tauri-apps/api/path';
|
||||
import { fade, slide } from 'svelte/transition';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
import ErrorAlert from '../../ui/ErrorAlert.svelte';
|
||||
import FileInput from '../../ui/FileInput.svelte';
|
||||
import Icon from '../../ui/Icon.svelte';
|
||||
import PassphraseInput from '../../ui/PassphraseInput.svelte';
|
||||
import Spinner from '../../ui/Spinner.svelte';
|
||||
|
||||
export let record;
|
||||
|
||||
@ -22,71 +22,59 @@
|
||||
homeDir().then(d => defaultPath = `${d}/.ssh`);
|
||||
|
||||
let alert;
|
||||
let saving = false;
|
||||
async function saveCredential() {
|
||||
let key = await invoke('sshkey_from_file', {path: file.path, passphrase});
|
||||
dispatch('update', key);
|
||||
saving = true;
|
||||
try {
|
||||
let key = await invoke('sshkey_from_file', {path: file.path, passphrase});
|
||||
const payload = {
|
||||
id: record.id,
|
||||
name,
|
||||
is_default: false, // ssh keys don't care about defaults
|
||||
credential: {type: 'Ssh', ...key},
|
||||
};
|
||||
await invoke('save_credential', {record: payload});
|
||||
dispatch('save', payload);
|
||||
}
|
||||
finally {
|
||||
saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<div
|
||||
transition:slide|local={{duration: 300}}
|
||||
class="rounded-box space-y-4 bg-base-200"
|
||||
>
|
||||
<div class="flex justify-end px-6 py-4 gap-x-4">
|
||||
<div class="join ml-auto">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline join-item"
|
||||
on:click={() => showDetails = !showDetails}
|
||||
>
|
||||
<Icon name="pencil" class="size-6" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline btn-error join-item"
|
||||
on:click={() => dispatch('update')}
|
||||
>
|
||||
<Icon name="trash" class="size-6" />
|
||||
</button>
|
||||
</div>
|
||||
<form class="space-y-4" on:submit|preventDefault={alert.run(saveCredential)}>
|
||||
<ErrorAlert bind:this={alert} />
|
||||
|
||||
<div class="grid grid-cols-[auto_1fr] items-center gap-4">
|
||||
<span class="justify-self-end">Name</span>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered bg-transparent"
|
||||
bind:value={name}
|
||||
>
|
||||
|
||||
<span class="justify-self-end">File</span>
|
||||
<FileInput params={{defaultPath}} bind:value={file} on:update={() => name = file.name} />
|
||||
|
||||
<span class="justify-self-end">Passphrase</span>
|
||||
<PassphraseInput class="bg-transparent" bind:value={passphrase} />
|
||||
</div>
|
||||
|
||||
{#if showDetails}
|
||||
<form
|
||||
transition:slide|local={{duration: 200}}
|
||||
class=" px-6 pb-4 space-y-4"
|
||||
on:submit|preventDefault={() => alert.run(saveCredential)}
|
||||
>
|
||||
<ErrorAlert bind:this={alert} />
|
||||
|
||||
<div class="grid grid-cols-[auto_1fr] items-center gap-4">
|
||||
<span class="justify-self-end">Name</span>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered bg-transparent"
|
||||
vind:value={name}
|
||||
>
|
||||
|
||||
<span class="justify-self-end">File</span>
|
||||
<FileInput bind:value={file} params={{defaultPath}} />
|
||||
|
||||
<span class="justify-self-end">Passphrase</span>
|
||||
<PassphraseInput class="bg-transparent" bind:value={passphrase} />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
{#if file?.path}
|
||||
<button
|
||||
transition:fade={{duration: 100}}
|
||||
type="submit"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<div class="flex justify-end">
|
||||
{#if file?.path}
|
||||
<button
|
||||
transition:fade={{duration: 100}}
|
||||
type="submit"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
{#if saving}
|
||||
<Spinner class="size-5 min-w-16" thickness="12" />
|
||||
{:else}
|
||||
<span class="min-w-16">Save</span>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
@ -1,46 +1,45 @@
|
||||
<script>
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { homeDir } from '@tauri-apps/api/path';
|
||||
import { open } from '@tauri-apps/plugin-dialog';
|
||||
import { fade, slide } from 'svelte/transition';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
|
||||
import ErrorAlert from '../../ui/ErrorAlert.svelte';
|
||||
import FileInput from '../../ui/FileInput.svelte';
|
||||
import NewSshKey from './NewSshKey.svelte';
|
||||
import EditSshKey from './EditSshKey.svelte';
|
||||
import Icon from '../../ui/Icon.svelte';
|
||||
|
||||
export let record;
|
||||
|
||||
let showDetails = record.isNew ? true : false;
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let local = JSON.parse(JSON.stringify(record));
|
||||
function copy(obj) {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
let local = copy(record);
|
||||
$: isModified = JSON.stringify(local) !== JSON.stringify(record);
|
||||
let showDetails = record?.isNew;
|
||||
|
||||
let file;
|
||||
let passphrase;
|
||||
|
||||
let defaultPath = null;
|
||||
homeDir().then(d => defaultPath = `${d}/.ssh`);
|
||||
|
||||
function conditionalDelete() {
|
||||
// todo
|
||||
function handleSave(evt) {
|
||||
local = copy(evt.detail);
|
||||
showDetails = false;
|
||||
}
|
||||
|
||||
let alert;
|
||||
async function saveCredential() {
|
||||
let key = await invoke('sshkey_from_file', {startDir, passphrase});
|
||||
record.credential = {type: 'SshKey', ...key};
|
||||
record.isNew = false; // just for now
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<div
|
||||
transition:slide|local={{duration: record.isNew ? 300 : 0}}
|
||||
class="rounded-box space-y-4 bg-base-200"
|
||||
>
|
||||
<div class="rounded-box space-y-4 bg-base-200">
|
||||
<div class="flex items-center px-6 py-4 gap-x-4">
|
||||
<h3 class="text-lg font-bold">{record.name || ''}</h3>
|
||||
{#if !record.isNew}
|
||||
{#if showDetails}
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered bg-transparent text-lg font-bold"
|
||||
bind:value={local.name}
|
||||
>
|
||||
{:else}
|
||||
<h3 class="text-lg font-bold">
|
||||
{record.name}
|
||||
</h3>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div class="join ml-auto">
|
||||
<button
|
||||
@ -53,58 +52,20 @@
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline btn-error join-item"
|
||||
on:click={conditionalDelete}
|
||||
on:click={() => dispatch('delete', record)}
|
||||
>
|
||||
<Icon name="trash" class="size-6" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showDetails}
|
||||
<form
|
||||
transition:slide|local={{duration: 200}}
|
||||
class=" px-6 pb-4 space-y-4"
|
||||
on:submit|preventDefault={() => alert.run(saveCredential)}
|
||||
>
|
||||
<ErrorAlert bind:this={alert} />
|
||||
|
||||
<div class="grid grid-cols-[auto_1fr] items-center gap-4">
|
||||
{#if record.isNew}
|
||||
<span class="justify-self-end">File</span>
|
||||
<FileInput bind:value={file} params={{defaultPath}} />
|
||||
|
||||
<span class="justify-self-end">Passphrase</span>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered bg-transparent"
|
||||
bind:value={passphrase}
|
||||
>
|
||||
{:else}
|
||||
<span class="justify-self-end">Algorithm</span>
|
||||
<span class="font-mono">{record.credential.algorithm}</span>
|
||||
|
||||
<span class="justify-self-end">Comment</span>
|
||||
<span class="font-mono">{record.credential.comment}</span>
|
||||
|
||||
<span class="justify-self-end">Public key</span>
|
||||
<span class="font-mono">{record.credential.public_key}</span>
|
||||
|
||||
<span class="justify-self-end">Private key</span>
|
||||
<span class="font-mono">{record.credential.private_key}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
{#if isModified}
|
||||
<button
|
||||
transition:fade={{duration: 100}}
|
||||
type="submit"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
{#if record && showDetails}
|
||||
<div transition:slide|local={{duration: 200}} class="px-6 pb-4 space-y-4">
|
||||
{#if record.isNew}
|
||||
<NewSshKey {record} on:save on:save={handleSave} />
|
||||
{:else}
|
||||
<EditSshKey bind:local={local} {isModified} on:save />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user