initial ssh key model and creation ui
This commit is contained in:
@ -5,12 +5,16 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
import AwsCredential from './credentials/AwsCredential.svelte';
|
||||
import NewSshKey from './credentials/NewSshKey.svelte';
|
||||
import Icon from '../ui/Icon.svelte';
|
||||
import Nav from '../ui/Nav.svelte';
|
||||
|
||||
let show = false;
|
||||
|
||||
let records = []
|
||||
let records = null
|
||||
$: awsRecords = (records || []).filter(r => r.credential.type === 'AwsBase');
|
||||
$: sshRecords = (records || []).filter(r => r.credential.type === 'SshKey');
|
||||
|
||||
let defaults = writable({});
|
||||
async function loadCreds() {
|
||||
records = await invoke('list_credentials');
|
||||
@ -24,7 +28,18 @@
|
||||
id: crypto.randomUUID(),
|
||||
name: null,
|
||||
is_default: false,
|
||||
credential: {type: 'AwsBase', AccessKeyId: null, SecretAccessKey: null},
|
||||
credential: {type: 'AwsBase', AccessKeyId: '', SecretAccessKey: ''},
|
||||
isNew: true,
|
||||
});
|
||||
records = records;
|
||||
}
|
||||
|
||||
function newSsh() {
|
||||
records.push({
|
||||
id: crypto.randomUUID(),
|
||||
name: null,
|
||||
is_default: false,
|
||||
credential: {type: 'SshKey', algorithm: '', private_key: '', public_key: '', comment: '',},
|
||||
isNew: true,
|
||||
});
|
||||
records = records;
|
||||
@ -36,27 +51,53 @@
|
||||
<h1 slot="title" class="text-2xl font-bold">Credentials</h1>
|
||||
</Nav>
|
||||
|
||||
<div class="max-w-xl mx-auto mb-12 flex flex-col gap-y-4 justify-center">
|
||||
<div class="divider">
|
||||
<h2 class="text-xl font-bold">AWS Access Keys</h2>
|
||||
</div>
|
||||
<div class="max-w-xl mx-auto mb-12 flex flex-col gap-y-12 justify-center">
|
||||
<div class="flex flex-col gap-y-4">
|
||||
<div class="divider">
|
||||
<h2 class="text-xl font-bold">AWS Access Keys</h2>
|
||||
</div>
|
||||
|
||||
{#if records.length > 0}
|
||||
{#each records as record (record.id)}
|
||||
<AwsCredential {record} {defaults} on:update={loadCreds} />
|
||||
{/each}
|
||||
<button class="btn btn-primary btn-wide mx-auto" on:click={newAws}>
|
||||
<Icon name="plus-circle-mini" class="size-5" />
|
||||
Add
|
||||
</button>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-6 items-center rounded-box border-2 border-dashed border-neutral-content/30 p-6">
|
||||
<div>You have no saved AWS credentials.</div>
|
||||
{#if awsRecords.length > 0}
|
||||
{#each awsRecords as record (record.id)}
|
||||
<AwsCredential {record} {defaults} on:update={loadCreds} />
|
||||
{/each}
|
||||
<button class="btn btn-primary btn-wide mx-auto" on:click={newAws}>
|
||||
<Icon name="plus-circle-mini" class="size-5" />
|
||||
Add
|
||||
</button>
|
||||
{:else if records !== null}
|
||||
<div class="flex flex-col gap-6 items-center rounded-box border-2 border-dashed border-neutral-content/30 p-6">
|
||||
<div>You have no saved AWS credentials.</div>
|
||||
<button class="btn btn-primary btn-wide mx-auto" on:click={newAws}>
|
||||
<Icon name="plus-circle-mini" class="size-5" />
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-y-4">
|
||||
<div class="divider">
|
||||
<h2 class="text-xl font-bold">SSH Keys</h2>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if sshRecords.length > 0}
|
||||
{#each sshRecords as record (record.id)}
|
||||
{#if record.isNew}
|
||||
<NewSshKey {record} on:update={e => console.log(e)} />
|
||||
{:else}
|
||||
<!-- EditSshKey -->
|
||||
{/if}
|
||||
{/each}
|
||||
{:else if records !== null}
|
||||
<div class="flex flex-col gap-6 items-center rounded-box border-2 border-dashed border-neutral-content/30 p-6">
|
||||
<div>You have no saved SSH keys.</div>
|
||||
<button class="btn btn-primary btn-wide mx-auto" on:click={newSsh}>
|
||||
<Icon name="plus-circle-mini" class="size-5" />
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
let showDetails = record.isNew ? true : false;
|
||||
|
||||
let localName = name;
|
||||
let local = JSON.parse(JSON.stringify(record));
|
||||
$: isModified = JSON.stringify(local) !== JSON.stringify(record);
|
||||
|
||||
@ -63,7 +62,13 @@
|
||||
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">{record.name || ''}</h3>
|
||||
<h3 class="text-lg font-bold">
|
||||
{#if !record?.isNew && showDetails}
|
||||
<input type="text" class="input input-bordered bg-transparent" bind:value={local.name}>
|
||||
{:else}
|
||||
{record.name || ''}
|
||||
{/if}
|
||||
</h3>
|
||||
|
||||
{#if record.is_default}
|
||||
<span class="badge badge-accent">Default</span>
|
||||
@ -129,9 +134,9 @@
|
||||
transition:fade={{duration: 100}}
|
||||
type="submit"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
|
92
src/views/credentials/NewSshKey.svelte
Normal file
92
src/views/credentials/NewSshKey.svelte
Normal file
@ -0,0 +1,92 @@
|
||||
<script>
|
||||
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 ErrorAlert from '../../ui/ErrorAlert.svelte';
|
||||
import FileInput from '../../ui/FileInput.svelte';
|
||||
import Icon from '../../ui/Icon.svelte';
|
||||
import PassphraseInput from '../../ui/PassphraseInput.svelte';
|
||||
|
||||
export let record;
|
||||
|
||||
let name;
|
||||
let file;
|
||||
let passphrase = '';
|
||||
let showDetails = true;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let defaultPath = null;
|
||||
homeDir().then(d => defaultPath = `${d}/.ssh`);
|
||||
|
||||
let alert;
|
||||
async function saveCredential() {
|
||||
let key = await invoke('sshkey_from_file', {path: file.path, passphrase});
|
||||
dispatch('update', key);
|
||||
}
|
||||
|
||||
</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>
|
||||
</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>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
</div>
|
110
src/views/credentials/SshKey.svelte
Normal file
110
src/views/credentials/SshKey.svelte
Normal file
@ -0,0 +1,110 @@
|
||||
<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 ErrorAlert from '../../ui/ErrorAlert.svelte';
|
||||
import FileInput from '../../ui/FileInput.svelte';
|
||||
import Icon from '../../ui/Icon.svelte';
|
||||
|
||||
export let record;
|
||||
|
||||
let showDetails = record.isNew ? true : false;
|
||||
|
||||
let local = JSON.parse(JSON.stringify(record));
|
||||
$: isModified = JSON.stringify(local) !== JSON.stringify(record);
|
||||
|
||||
let file;
|
||||
let passphrase;
|
||||
|
||||
let defaultPath = null;
|
||||
homeDir().then(d => defaultPath = `${d}/.ssh`);
|
||||
|
||||
function conditionalDelete() {
|
||||
// todo
|
||||
}
|
||||
|
||||
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="flex items-center px-6 py-4 gap-x-4">
|
||||
<h3 class="text-lg font-bold">{record.name || ''}</h3>
|
||||
|
||||
<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={conditionalDelete}
|
||||
>
|
||||
<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}
|
||||
</div>
|
Reference in New Issue
Block a user