Compare commits
4 Commits
main
...
feat/users
Author | SHA1 | Date |
---|---|---|
Tomáš Mládek | 6ae20f9171 | |
Tomáš Mládek | 5771f32736 | |
Tomáš Mládek | b480cf2e64 | |
Tomáš Mládek | 07c76423ac |
|
@ -6,7 +6,7 @@ pipeline:
|
|||
environment:
|
||||
- FORCE_COLOR=1
|
||||
- EARTHLY_EXEC_CMD="/bin/sh"
|
||||
secrets: [ EARTHLY_CONFIGURATION ]
|
||||
secrets: [EARTHLY_CONFIGURATION]
|
||||
commands:
|
||||
- mkdir ~/.earthly && echo "$EARTHLY_CONFIGURATION" > ~/.earthly/config.yaml
|
||||
- earthly bootstrap
|
||||
|
@ -19,7 +19,7 @@ pipeline:
|
|||
environment:
|
||||
- FORCE_COLOR=1
|
||||
- EARTHLY_EXEC_CMD="/bin/sh"
|
||||
secrets: [ EARTHLY_CONFIGURATION ]
|
||||
secrets: [EARTHLY_CONFIGURATION]
|
||||
commands:
|
||||
- mkdir ~/.earthly && echo "$EARTHLY_CONFIGURATION" > ~/.earthly/config.yaml
|
||||
- earthly bootstrap
|
||||
|
@ -52,14 +52,13 @@ pipeline:
|
|||
SSH_CONFIG,
|
||||
SSH_UPLOAD_KEY,
|
||||
SSH_KNOWN_HOSTS,
|
||||
SENTRY_AUTH_TOKEN
|
||||
]
|
||||
commands:
|
||||
- mkdir ~/.earthly && echo "$EARTHLY_CONFIGURATION" > ~/.earthly/config.yaml
|
||||
- earthly bootstrap
|
||||
- earthly --secret GPG_SIGN_KEY --secret SSH_CONFIG --secret SSH_UPLOAD_KEY --secret SSH_KNOWN_HOSTS +deploy-appimage-nightly
|
||||
when:
|
||||
branch: [ main ]
|
||||
branch: [main]
|
||||
|
||||
docker:nightly:
|
||||
image: earthly/earthly:v0.8.3
|
||||
|
@ -68,7 +67,7 @@ pipeline:
|
|||
environment:
|
||||
- FORCE_COLOR=1
|
||||
- EARTHLY_EXEC_CMD="/bin/sh"
|
||||
secrets: [ EARTHLY_CONFIGURATION, DOCKER_USER, DOCKER_PASSWORD, SENTRY_AUTH_TOKEN ]
|
||||
secrets: [EARTHLY_CONFIGURATION, DOCKER_USER, DOCKER_PASSWORD]
|
||||
commands:
|
||||
- echo $${DOCKER_PASSWORD}| docker login --username $${DOCKER_USER} --password-stdin
|
||||
- mkdir ~/.earthly && echo "$EARTHLY_CONFIGURATION" > ~/.earthly/config.yaml
|
||||
|
@ -76,7 +75,7 @@ pipeline:
|
|||
- earthly --push +docker-minimal
|
||||
- earthly --push +docker
|
||||
when:
|
||||
branch: [ main ]
|
||||
branch: [main]
|
||||
|
||||
docker:release:
|
||||
image: earthly/earthly:v0.8.3
|
||||
|
@ -85,7 +84,7 @@ pipeline:
|
|||
environment:
|
||||
- FORCE_COLOR=1
|
||||
- EARTHLY_EXEC_CMD="/bin/sh"
|
||||
secrets: [ EARTHLY_CONFIGURATION, DOCKER_USER, DOCKER_PASSWORD, SENTRY_AUTH_TOKEN ]
|
||||
secrets: [EARTHLY_CONFIGURATION, DOCKER_USER, DOCKER_PASSWORD]
|
||||
commands:
|
||||
- echo $${DOCKER_PASSWORD}| docker login --username $${DOCKER_USER} --password-stdin
|
||||
- mkdir ~/.earthly && echo "$EARTHLY_CONFIGURATION" > ~/.earthly/config.yaml
|
||||
|
@ -95,7 +94,7 @@ pipeline:
|
|||
- earthly --strict --push +docker --tag=latest
|
||||
- earthly --strict --push +docker --tag=$CI_COMMIT_TAG
|
||||
when:
|
||||
event: [ tag ]
|
||||
event: [tag]
|
||||
|
||||
jslib:publish:
|
||||
image: earthly/earthly:v0.8.3
|
||||
|
@ -104,13 +103,13 @@ pipeline:
|
|||
environment:
|
||||
- FORCE_COLOR=1
|
||||
- EARTHLY_EXEC_CMD="/bin/sh"
|
||||
secrets: [ EARTHLY_CONFIGURATION, NPM_TOKEN ]
|
||||
secrets: [EARTHLY_CONFIGURATION, NPM_TOKEN]
|
||||
commands:
|
||||
- mkdir ~/.earthly && echo "$EARTHLY_CONFIGURATION" > ~/.earthly/config.yaml
|
||||
- earthly bootstrap
|
||||
- earthly --strict --push --secret NPM_TOKEN +publish-js-all
|
||||
when:
|
||||
branch: [ main ]
|
||||
branch: [main]
|
||||
|
||||
gitea:prerelease:
|
||||
image: earthly/earthly:v0.8.3
|
||||
|
@ -127,7 +126,7 @@ pipeline:
|
|||
- rm -rf dist
|
||||
when:
|
||||
event: [ tag ]
|
||||
|
||||
|
||||
appimage:release:
|
||||
image: earthly/earthly:v0.8.3
|
||||
volumes:
|
||||
|
@ -135,15 +134,15 @@ pipeline:
|
|||
environment:
|
||||
- FORCE_COLOR=1
|
||||
- EARTHLY_EXEC_CMD="/bin/sh"
|
||||
secrets: [ EARTHLY_CONFIGURATION, REGISTRY, REGISTRY_USER, REGISTRY_PASSWORD, SENTRY_AUTH_TOKEN ]
|
||||
secrets: [ EARTHLY_CONFIGURATION, REGISTRY, REGISTRY_USER, REGISTRY_PASSWORD ]
|
||||
commands:
|
||||
- mkdir ~/.earthly && echo "$EARTHLY_CONFIGURATION" > ~/.earthly/config.yaml
|
||||
- earthly bootstrap
|
||||
- mkdir -p dist/
|
||||
- earthly --strict -a '+appimage-signed/*' dist/
|
||||
when:
|
||||
event: [ tag ]
|
||||
|
||||
event: [tag]
|
||||
|
||||
# todo: webext
|
||||
|
||||
gitea:release:
|
||||
|
@ -158,4 +157,4 @@ pipeline:
|
|||
target: main
|
||||
note: CHANGELOG_CURRENT.md
|
||||
when:
|
||||
event: [ tag ]
|
||||
event: [tag]
|
|
@ -96,7 +96,7 @@ pub async fn login(
|
|||
_ => Ok(HttpResponse::Ok().json(json!({ "key": token }))),
|
||||
}
|
||||
}
|
||||
Err(_) => Err(ErrorUnauthorized("Invalid credentials.")),
|
||||
Err(e) => Err(ErrorUnauthorized(e)),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -138,32 +138,22 @@ fn check_auth(req: &HttpRequest, state: &State) -> Result<Option<String>, actix_
|
|||
return Ok(None);
|
||||
}
|
||||
|
||||
let header_key = req.headers().get("Authorization").and_then(|value| {
|
||||
value.to_str().ok().and_then(|value| {
|
||||
if value.starts_with("Bearer ") {
|
||||
Some(value.trim_start_matches("Bearer ").to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let cookie_key = req.cookies().ok().and_then(|cookies| {
|
||||
let key = if let Some(value) = req.headers().get("Authorization") {
|
||||
let value = value.to_str().map_err(|err| {
|
||||
ErrorBadRequest(format!("Invalid value in Authorization header: {err:?}"))
|
||||
})?;
|
||||
if !value.starts_with("Bearer ") {
|
||||
return Err(ErrorUnauthorized("Invalid token type."));
|
||||
}
|
||||
Some(value.trim_start_matches("Bearer ").to_string())
|
||||
} else if let Ok(cookies) = req.cookies() {
|
||||
cookies
|
||||
.iter()
|
||||
.find(|c| c.name() == "key")
|
||||
.map(|cookie| cookie.value().to_string())
|
||||
});
|
||||
|
||||
let query_key = req.query_string().split('&').find_map(|pair| {
|
||||
let parts = pair.split('=').collect::<Vec<&str>>();
|
||||
match parts[..] {
|
||||
["auth_key", value] => Some(value.to_string()),
|
||||
_ => None,
|
||||
}
|
||||
});
|
||||
|
||||
let key = header_key.or(cookie_key).or(query_key);
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(key) = key {
|
||||
let token = jsonwebtoken::decode::<JwtClaims>(
|
||||
|
|
|
@ -314,21 +314,13 @@ impl UpEndConnection {
|
|||
.filter(dsl::username.eq(username))
|
||||
.load::<models::UserValue>(&conn)?;
|
||||
|
||||
match user_result.first() {
|
||||
Some(user) => {
|
||||
let parsed_hash = PasswordHash::new(&user.password).map_err(|e| anyhow!(e))?;
|
||||
let argon2 = Argon2::default();
|
||||
argon2
|
||||
.verify_password(password.as_ref(), &parsed_hash)
|
||||
.map_err(|e| anyhow!(e))
|
||||
}
|
||||
None => {
|
||||
let argon2 = Argon2::default();
|
||||
let _ = argon2
|
||||
.verify_password(password.as_ref(), &PasswordHash::new(&DUMMY_HASH).unwrap());
|
||||
Err(anyhow!("user not found"))
|
||||
}
|
||||
}
|
||||
let user = user_result.first().ok_or(anyhow!("User not found"))?;
|
||||
|
||||
let parsed_hash = PasswordHash::new(&user.password).map_err(|e| anyhow!(e))?;
|
||||
let argon2 = Argon2::default();
|
||||
argon2
|
||||
.verify_password(password.as_ref(), &parsed_hash)
|
||||
.map_err(|e| anyhow!(e))
|
||||
}
|
||||
|
||||
pub fn retrieve_entry(&self, hash: &UpMultihash) -> Result<Option<Entry>> {
|
||||
|
@ -546,16 +538,6 @@ impl UpEndConnection {
|
|||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref DUMMY_HASH: String = Argon2::default()
|
||||
.hash_password(
|
||||
"password".as_ref(),
|
||||
&password_hash::SaltString::generate(&mut password_hash::rand_core::OsRng)
|
||||
)
|
||||
.unwrap()
|
||||
.to_string();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use upend_base::constants::{ATTR_IN, ATTR_LABEL};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@upnd/upend",
|
||||
"version": "0.5.5",
|
||||
"version": "0.5.0",
|
||||
"description": "Client library to interact with the UpEnd system.",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
|
|
@ -24,7 +24,6 @@ export type { AddressComponents };
|
|||
|
||||
export type UpendApiError = {
|
||||
kind: "Unauthorized" | "HttpError" | "FetchError" | "Unknown";
|
||||
message?: string;
|
||||
error?: Error;
|
||||
};
|
||||
|
||||
|
@ -38,18 +37,18 @@ export class UpEndApi {
|
|||
private key: string | undefined;
|
||||
private readonly onError: ((error: UpendApiError) => void) | undefined;
|
||||
|
||||
constructor(config?: {
|
||||
constructor(config: {
|
||||
instanceUrl?: string;
|
||||
wasmExtensions?: UpEndWasmExtensions;
|
||||
timeout?: number;
|
||||
authKey?: string;
|
||||
onError?: (error: UpendApiError) => void;
|
||||
}) {
|
||||
this.setInstanceUrl(config?.instanceUrl || "http://localhost:8093");
|
||||
this.wasmExtensions = config?.wasmExtensions;
|
||||
this.timeout = config?.timeout || 30_000;
|
||||
this.key = config?.authKey;
|
||||
this.onError = config?.onError;
|
||||
this.setInstanceUrl(config.instanceUrl || "http://localhost:8093");
|
||||
this.wasmExtensions = config.wasmExtensions;
|
||||
this.timeout = config.timeout || 30_000;
|
||||
this.key = config.authKey;
|
||||
this.onError = config.onError;
|
||||
}
|
||||
|
||||
public setInstanceUrl(apiUrl: string) {
|
||||
|
@ -231,15 +230,8 @@ export class UpEndApi {
|
|||
});
|
||||
}
|
||||
|
||||
public getRaw(
|
||||
address: Address,
|
||||
config?: { preview?: boolean; authenticated?: boolean },
|
||||
) {
|
||||
let result = `${this.apiUrl}/${config?.preview ? "thumb" : "raw"}/${address}`;
|
||||
if (config?.authenticated) {
|
||||
result += `?auth_key=${this.key}`;
|
||||
}
|
||||
return result;
|
||||
public getRaw(address: Address, preview = false) {
|
||||
return `${this.apiUrl}/${preview ? "thumb" : "raw"}/${address}`;
|
||||
}
|
||||
|
||||
public async fetchRaw(
|
||||
|
@ -248,7 +240,7 @@ export class UpEndApi {
|
|||
options?: ApiFetchOptions,
|
||||
) {
|
||||
dbg("Getting %s raw (preview = %s)", address, preview);
|
||||
return await this.fetch(this.getRaw(address, { preview }), options);
|
||||
return await this.fetch(this.getRaw(address, preview), options);
|
||||
}
|
||||
|
||||
public async refreshVault(options?: ApiFetchOptions) {
|
||||
|
@ -441,17 +433,6 @@ export class UpEndApi {
|
|||
}
|
||||
}
|
||||
|
||||
public async register(credentials: {
|
||||
username: string;
|
||||
password: string;
|
||||
}): Promise<void> {
|
||||
await this.fetch(`${this.apiUrl}/auth/register`, undefined, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(credentials),
|
||||
});
|
||||
}
|
||||
|
||||
public async authStatus(
|
||||
options?: ApiFetchOptions,
|
||||
): Promise<{ user: string } | undefined> {
|
||||
|
@ -481,10 +462,7 @@ export class UpEndApi {
|
|||
const controller = options?.abortController || new AbortController();
|
||||
const timeout = options?.timeout || this.timeout;
|
||||
if (timeout > 0) {
|
||||
setTimeout(() => {
|
||||
dbg("Aborting request after %d ms", timeout);
|
||||
controller.abort();
|
||||
}, timeout);
|
||||
setTimeout(() => controller.abort(), timeout);
|
||||
}
|
||||
return controller.signal;
|
||||
}
|
||||
|
@ -510,11 +488,13 @@ export class UpEndApi {
|
|||
});
|
||||
if (!result.ok) {
|
||||
if (result.status === 401) {
|
||||
error = { kind: "Unauthorized", message: await result.text() };
|
||||
error = { kind: "Unauthorized" };
|
||||
} else {
|
||||
error = {
|
||||
kind: "HttpError",
|
||||
message: `HTTP Error ${result.status}: ${result.statusText}`,
|
||||
error: new Error(
|
||||
`HTTP Error ${result.status}: ${result.statusText}`,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ Sentry.init({
|
|||
replaysOnErrorSampleRate: 1.0,
|
||||
|
||||
// If you don't want to use Session Replay, just remove the line below:
|
||||
integrations: [replayIntegration(), Sentry.feedbackIntegration({ colorScheme: 'dark' })],
|
||||
integrations: [replayIntegration()],
|
||||
|
||||
enabled: process.env.NODE_ENV !== 'development'
|
||||
});
|
||||
|
|
|
@ -2,7 +2,6 @@ import { UpEndApi } from '@upnd/upend';
|
|||
import { UpEndWasmExtensionsWeb } from '@upnd/upend/wasm/web';
|
||||
import wasmURL from '@upnd/wasm-web/upend_wasm_bg.wasm?url';
|
||||
import { type StartStopNotifier, writable, type Writable } from 'svelte/store';
|
||||
import * as Sentry from '@sentry/sveltekit';
|
||||
|
||||
const wasm = new UpEndWasmExtensionsWeb(wasmURL);
|
||||
const api = new UpEndApi({ instanceUrl: '/', wasmExtensions: wasm });
|
||||
|
@ -11,10 +10,7 @@ export default api;
|
|||
export const currentUser: Writable<string | undefined> = writable(
|
||||
undefined as string | undefined,
|
||||
((set) => {
|
||||
api.authStatus().then((result) => {
|
||||
set(result?.user);
|
||||
Sentry.setUser({ id: result?.user });
|
||||
});
|
||||
api.authStatus().then((result) => set(result?.user));
|
||||
}) as StartStopNotifier<string | undefined>
|
||||
);
|
||||
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
<script context="module" lang="ts">
|
||||
import mitt from 'mitt';
|
||||
import type { Address } from '@upnd/upend/types';
|
||||
|
||||
export type AddEvents = {
|
||||
choose: void;
|
||||
files: File[];
|
||||
urls: string[];
|
||||
destination: Address;
|
||||
};
|
||||
export const addEmitter = mitt<AddEvents>();
|
||||
</script>
|
||||
|
@ -19,23 +17,18 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import { i18n } from '$lib/i18n';
|
||||
import { selected } from '$lib/components/EntitySelect.svelte';
|
||||
import Modal from '$lib/components/layout/Modal.svelte';
|
||||
import Selector, { type SelectorValue } from '$lib/components/utils/Selector.svelte';
|
||||
import { ATTR_IN } from '@upnd/upend/constants';
|
||||
|
||||
let files: File[] = [];
|
||||
let URLs: string[] = [];
|
||||
let uploading = false;
|
||||
let abortController: AbortController | undefined;
|
||||
|
||||
let destination: Address | undefined;
|
||||
|
||||
let progress: Record<string, number> = {};
|
||||
let totalProgress: number | undefined;
|
||||
|
||||
let filesElement: HTMLDivElement;
|
||||
|
||||
$: visible = files.length + URLs.length > 0 || destination;
|
||||
$: visible = files.length + URLs.length > 0;
|
||||
|
||||
addEmitter.on('files', (ev) => {
|
||||
ev.forEach((file) => {
|
||||
|
@ -46,18 +39,6 @@
|
|||
});
|
||||
});
|
||||
|
||||
addEmitter.on('destination', (ev) => {
|
||||
destination = ev;
|
||||
});
|
||||
|
||||
function onDestinationSelected(ev: CustomEvent<SelectorValue | undefined>) {
|
||||
if (ev.detail?.t === 'Address') {
|
||||
destination = ev.detail.c;
|
||||
} else {
|
||||
destination = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function upload() {
|
||||
uploading = true;
|
||||
|
||||
|
@ -77,16 +58,6 @@
|
|||
},
|
||||
timeout: -1
|
||||
});
|
||||
if (destination) {
|
||||
await api.putEntry({
|
||||
entity: address,
|
||||
attribute: ATTR_IN,
|
||||
value: {
|
||||
t: 'Address',
|
||||
c: destination
|
||||
}
|
||||
});
|
||||
}
|
||||
addresses.push(address);
|
||||
|
||||
if (!uploading) {
|
||||
|
@ -119,7 +90,6 @@
|
|||
URLs = [];
|
||||
progress = {};
|
||||
uploading = false;
|
||||
destination = undefined;
|
||||
}
|
||||
|
||||
function onKeydown(event: KeyboardEvent) {
|
||||
|
@ -144,8 +114,9 @@
|
|||
<svelte:window on:beforeunload={onBeforeUnload} />
|
||||
<svelte:body on:keydown={onKeydown} />
|
||||
|
||||
{#if visible}
|
||||
<Modal on:close={reset}>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions -->
|
||||
<div class="addmodal-container" class:visible class:uploading on:click={reset}>
|
||||
<div class="addmodal" on:click|stopPropagation>
|
||||
<div class="files" bind:this={filesElement}>
|
||||
{#each files as file}
|
||||
<div class="entry">
|
||||
|
@ -183,28 +154,17 @@
|
|||
{/if}
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="controls-destination">
|
||||
<div class="label"><Icon plain name="download" /> {$i18n.t('Destination')}</div>
|
||||
<Selector
|
||||
initial={destination ? { t: 'Address', c: destination } : undefined}
|
||||
types={['Address', 'NewAddress']}
|
||||
placeholder={$i18n.t('Choose automatically') || ''}
|
||||
on:input={onDestinationSelected}
|
||||
/>
|
||||
</div>
|
||||
<div class="controls-submit">
|
||||
<IconButton small disabled={uploading} name="upload" on:click={upload}>
|
||||
{$i18n.t('Upload')}
|
||||
</IconButton>
|
||||
</div>
|
||||
<IconButton small disabled={uploading} name="upload" on:click={upload}>
|
||||
{$i18n.t('Upload')}
|
||||
</IconButton>
|
||||
</div>
|
||||
{#if uploading}
|
||||
<div class="progress">
|
||||
<ProgressBar value={totalProgress} />
|
||||
</div>
|
||||
{/if}
|
||||
</Modal>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.addmodal-container {
|
||||
|
@ -300,23 +260,8 @@
|
|||
.controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 3em;
|
||||
margin-top: 0.5rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.controls-destination {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
font-size: 1rem;
|
||||
flex-grow: 3;
|
||||
}
|
||||
|
||||
.controls-submit {
|
||||
margin: 0 1rem;
|
||||
}
|
||||
|
||||
.progress {
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
import LabelBorder from './utils/LabelBorder.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { type Address } from '@upnd/upend/types';
|
||||
import Icon from '$lib/components/utils/Icon.svelte';
|
||||
const dispatch = createEventDispatcher<{
|
||||
highlighted: string | undefined;
|
||||
add: Address;
|
||||
|
@ -17,7 +16,6 @@
|
|||
export let hide = false;
|
||||
|
||||
export let header = '';
|
||||
export let icon: string | null = null;
|
||||
export let confirmRemoveMessage: string | null = $i18n.t('Are you sure you want to remove this?');
|
||||
export let emptyMessage = $i18n.t('Nothing to show.');
|
||||
|
||||
|
@ -26,8 +24,8 @@
|
|||
|
||||
$: if (adding && selector) selector.focus();
|
||||
|
||||
async function add(ev: CustomEvent<SelectorValue | undefined>) {
|
||||
if (ev.detail?.t !== 'Address') {
|
||||
async function add(ev: CustomEvent<SelectorValue>) {
|
||||
if (ev.detail.t !== 'Address') {
|
||||
return;
|
||||
}
|
||||
dispatch('add', ev.detail.c);
|
||||
|
@ -41,10 +39,7 @@
|
|||
</script>
|
||||
|
||||
<LabelBorder {hide}>
|
||||
<span slot="header"
|
||||
>{#if icon}<Icon plain name={icon} />
|
||||
{/if}{header}</span
|
||||
>
|
||||
<span slot="header">{header}</span>
|
||||
|
||||
{#if adding}
|
||||
<div class="selector">
|
||||
|
|
|
@ -90,14 +90,14 @@
|
|||
{#if group}
|
||||
{#if icon}
|
||||
<div class="icon">
|
||||
<Icon plain name={icon} />
|
||||
<Icon name={icon} />
|
||||
</div>
|
||||
{/if}
|
||||
<UpObject link address={group} labels={title ? [title] : undefined} />
|
||||
{:else}
|
||||
{#if icon}
|
||||
<div class="icon">
|
||||
<Icon plain name={icon} />
|
||||
<Icon name={icon} />
|
||||
</div>
|
||||
{/if}
|
||||
{title || ''}
|
||||
|
|
|
@ -22,7 +22,6 @@
|
|||
import { debug } from 'debug';
|
||||
import { Any } from '@upnd/upend/query';
|
||||
import { isDefined } from '$lib/util/werk';
|
||||
import Icon from '$lib/components/utils/Icon.svelte';
|
||||
const dbg = debug('kestrel:Inspect');
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
|
@ -193,7 +192,7 @@
|
|||
const allAttributes = (
|
||||
await Promise.all(
|
||||
($entity?.attr[`~${ATTR_OF}`] ?? []).map(async (e) => {
|
||||
return { address: e.address, components: await api.addressToComponents(e.entity) };
|
||||
return { address: e.entity, components: await api.addressToComponents(e.entity) };
|
||||
})
|
||||
)
|
||||
)
|
||||
|
@ -444,7 +443,6 @@
|
|||
{#if currentUntypedProperties.length > 0}
|
||||
<EntryView
|
||||
title={$i18n.t('Other Properties') || ''}
|
||||
icon="shape-triangle"
|
||||
widgets={attributeWidgets}
|
||||
entries={currentUntypedProperties}
|
||||
on:change={onChange}
|
||||
|
@ -455,7 +453,6 @@
|
|||
{#if currentUntypedLinks.length > 0}
|
||||
<EntryView
|
||||
title={$i18n.t('Links') || ''}
|
||||
icon="right-arrow-circle"
|
||||
widgets={linkWidgets}
|
||||
entries={currentUntypedLinks}
|
||||
on:change={onChange}
|
||||
|
@ -466,7 +463,6 @@
|
|||
{#if !correctlyTagged || !incorrectlyTagged}
|
||||
<EntryView
|
||||
title={`${$i18n.t('Members')}`}
|
||||
icon="link"
|
||||
widgets={taggedWidgets}
|
||||
entries={tagged}
|
||||
on:change={onChange}
|
||||
|
@ -475,7 +471,6 @@
|
|||
{:else}
|
||||
<EntryView
|
||||
title={`${$i18n.t('Typed Members')} (${correctlyTagged.length})`}
|
||||
icon="link"
|
||||
widgets={taggedWidgets}
|
||||
entries={tagged.filter((e) => correctlyTagged?.includes(e.entity))}
|
||||
on:change={onChange}
|
||||
|
@ -483,7 +478,6 @@
|
|||
/>
|
||||
<EntryView
|
||||
title={`${$i18n.t('Untyped members')} (${incorrectlyTagged.length})`}
|
||||
icon="unlink"
|
||||
widgets={taggedWidgets}
|
||||
entries={tagged.filter((e) => incorrectlyTagged?.includes(e.entity))}
|
||||
on:change={onChange}
|
||||
|
@ -494,7 +488,6 @@
|
|||
{#if currentBacklinks.length > 0}
|
||||
<EntryView
|
||||
title={`${$i18n.t('Referred to')} (${currentBacklinks.length})`}
|
||||
icon="left-arrow-circle"
|
||||
entries={currentBacklinks}
|
||||
on:change={onChange}
|
||||
{address}
|
||||
|
@ -503,10 +496,7 @@
|
|||
|
||||
{#if $entityInfo?.t === 'Attribute'}
|
||||
<LabelBorder>
|
||||
<span slot="header">
|
||||
<Icon plain name="color" />
|
||||
{$i18n.t('Used')} ({attributesUsed.length})
|
||||
</span>
|
||||
<span slot="header">{$i18n.t('Used')} ({attributesUsed.length})</span>
|
||||
<EntryList columns="entity,value" entries={attributesUsed} orderByValue />
|
||||
</LabelBorder>
|
||||
{/if}
|
||||
|
|
|
@ -39,7 +39,6 @@
|
|||
<EntitySetEditor
|
||||
entities={Object.keys(groups)}
|
||||
header={$i18n.t('Groups') || ''}
|
||||
icon="link-alt"
|
||||
hide={Object.keys(groups).length === 0}
|
||||
on:add={(e) => addGroup(e.detail)}
|
||||
on:remove={(e) => removeGroup(e.detail)}
|
||||
|
|
|
@ -10,8 +10,6 @@
|
|||
import { ATTR_OF } from '@upnd/upend/constants';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import LabelBorder from './utils/LabelBorder.svelte';
|
||||
import Icon from '$lib/components/utils/Icon.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let entity: Readable<UpObject | undefined>;
|
||||
|
@ -23,7 +21,6 @@
|
|||
|
||||
let types: Array<{ address: string; entry: UpEntry; required: UpEntry | undefined }> = [];
|
||||
$: updateTypes($entity?.attr[`~${ATTR_OF}`] || []);
|
||||
|
||||
async function updateTypes(entries: UpEntry[]) {
|
||||
types = [];
|
||||
const query = await api.query(
|
||||
|
@ -50,8 +47,8 @@
|
|||
});
|
||||
}
|
||||
|
||||
async function add(ev: CustomEvent<SelectorValue | undefined>) {
|
||||
if (!$entity || ev.detail?.t !== 'Attribute') {
|
||||
async function add(ev: CustomEvent<SelectorValue>) {
|
||||
if (!$entity || ev.detail.t !== 'Attribute') {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -101,7 +98,7 @@
|
|||
|
||||
{#if types.length || $entity?.attr['~IN']?.length}
|
||||
<LabelBorder hide={types.length === 0}>
|
||||
<span slot="header"><Icon plain name="list-check" /> {$i18n.t('Type Attributes')}</span>
|
||||
<span slot="header">{$i18n.t('Type Attributes')}</span>
|
||||
{#if adding}
|
||||
<div class="selector">
|
||||
<Selector
|
||||
|
@ -125,11 +122,7 @@
|
|||
class:required={type.required}
|
||||
on:click={() => setRequired(type.entry, !type.required)}
|
||||
>
|
||||
{#if type.required}
|
||||
<Icon plain name="lock" /> {$i18n.t('Required')}
|
||||
{:else}
|
||||
<Icon plain name="lock-open" /> {$i18n.t('Optional')}
|
||||
{/if}
|
||||
{type.required ? $i18n.t('Required') : $i18n.t('Optional')}
|
||||
</button>
|
||||
<div class="controls">
|
||||
<IconButton name="x-circle" on:click={() => remove(type.entry)} />
|
||||
|
|
|
@ -2,8 +2,6 @@
|
|||
import { i18n } from '$lib/i18n';
|
||||
import Icon from '$lib/components/utils/Icon.svelte';
|
||||
import { login } from '$lib/api';
|
||||
import Modal from '$lib/components/layout/Modal.svelte';
|
||||
import { type UpendApiError } from '@upnd/upend/api';
|
||||
|
||||
let username = '';
|
||||
let password = '';
|
||||
|
@ -16,43 +14,66 @@
|
|||
authenticating = true;
|
||||
await login({ username, password });
|
||||
} catch (e) {
|
||||
error = (e as UpendApiError).message || (e as UpendApiError).kind;
|
||||
error = (e as object).toString();
|
||||
} finally {
|
||||
authenticating = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal disabled={authenticating}>
|
||||
<h2>
|
||||
<Icon name="lock" />
|
||||
{$i18n.t('Authorization required')}
|
||||
</h2>
|
||||
<form on:submit|preventDefault={submit}>
|
||||
<input
|
||||
name="username"
|
||||
placeholder={$i18n.t('Username')}
|
||||
type="text"
|
||||
bind:value={username}
|
||||
required
|
||||
/>
|
||||
<input
|
||||
name="password"
|
||||
placeholder={$i18n.t('Password')}
|
||||
type="password"
|
||||
bind:value={password}
|
||||
required
|
||||
/>
|
||||
<button type="submit"> <Icon plain name="log-in" /> {$i18n.t('Login')}</button>
|
||||
</form>
|
||||
{#if error}
|
||||
<div class="error">{error}</div>
|
||||
{/if}
|
||||
</Modal>
|
||||
<div class="modal-container">
|
||||
<div class="modal" class:authenticating>
|
||||
<h2>
|
||||
<Icon name="lock" />
|
||||
{$i18n.t('Authorization required')}
|
||||
</h2>
|
||||
<form on:submit|preventDefault={submit}>
|
||||
<input placeholder={$i18n.t('Username')} type="text" bind:value={username} required />
|
||||
<input placeholder={$i18n.t('Password')} type="password" bind:value={password} required />
|
||||
<button type="submit"> <Icon plain name="log-in" /> {$i18n.t('Login')}</button>
|
||||
</form>
|
||||
{#if error}
|
||||
<div class="error">{error}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use '$lib/styles/colors';
|
||||
|
||||
.modal-container {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(0, 0, 0, 0.66);
|
||||
color: var(--foreground);
|
||||
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--foreground);
|
||||
padding: 2rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
&.authenticating {
|
||||
filter: brightness(0.66);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
margin: 0 0 1rem 0;
|
||||
|
|
|
@ -229,9 +229,9 @@
|
|||
resizeObserver.observe(viewEl as any);
|
||||
});
|
||||
|
||||
async function onSelectorInput(ev: CustomEvent<SelectorValue | undefined>) {
|
||||
async function onSelectorInput(ev: CustomEvent<SelectorValue>) {
|
||||
const value = ev.detail;
|
||||
if (value?.t !== 'Address') return;
|
||||
if (value.t !== 'Address') return;
|
||||
const address = value.c;
|
||||
|
||||
const [xValue, yValue] = selectorCoords as any;
|
||||
|
@ -261,7 +261,7 @@
|
|||
types={['Attribute', 'NewAttribute']}
|
||||
initial={x ? { t: 'Attribute', name: x } : undefined}
|
||||
on:input={(ev) => {
|
||||
if (ev.detail?.t === 'Attribute') x = ev.detail.name;
|
||||
if (ev.detail.t === 'Attribute') x = ev.detail.name;
|
||||
}}
|
||||
/>
|
||||
<div class="value">
|
||||
|
@ -277,7 +277,7 @@
|
|||
types={['Attribute', 'NewAttribute']}
|
||||
initial={y ? { t: 'Attribute', name: y } : undefined}
|
||||
on:input={(ev) => {
|
||||
if (ev.detail?.t === 'Attribute') y = ev.detail.name;
|
||||
if (ev.detail.t === 'Attribute') y = ev.detail.name;
|
||||
}}
|
||||
/>
|
||||
<div class="value">
|
||||
|
|
|
@ -191,17 +191,24 @@
|
|||
{/if}
|
||||
<HashBadge {address} />
|
||||
<div class="label" class:resolving title={displayLabel}>
|
||||
{#if banner && !link}
|
||||
<Editable value={{ t: 'String', c: displayLabel }} types={['String']} on:edit={onLabelEdit}>
|
||||
<UpObjectLabel label={displayLabel} backpath={resolvedBackpath} />
|
||||
</Editable>
|
||||
{:else if link}
|
||||
<UpLink passthrough to={{ entity: address }}>
|
||||
<UpObjectLabel label={displayLabel} backpath={resolvedBackpath} />
|
||||
</UpLink>
|
||||
{:else}
|
||||
<UpObjectLabel label={displayLabel} backpath={resolvedBackpath} />
|
||||
{/if}
|
||||
<Editable
|
||||
value={{ t: 'String', c: displayLabel }}
|
||||
editable={banner}
|
||||
types={['String']}
|
||||
on:edit={onLabelEdit}
|
||||
>
|
||||
<div class="label-inner">
|
||||
{#if banner && hasFile}
|
||||
<UpObjectLabel label={displayLabel} backpath={resolvedBackpath} />
|
||||
{:else if link}
|
||||
<UpLink to={{ entity: address }}>
|
||||
<UpObjectLabel label={displayLabel} backpath={resolvedBackpath} />
|
||||
</UpLink>
|
||||
{:else}
|
||||
<UpObjectLabel label={displayLabel} backpath={resolvedBackpath} />
|
||||
{/if}
|
||||
</div>
|
||||
</Editable>
|
||||
{#if $entity?.get(ATTR_KEY) && !$entity?.get(ATTR_KEY)?.toString()?.startsWith('TYPE_')}
|
||||
<div class="key">{$entity.get(ATTR_KEY)}</div>
|
||||
{/if}
|
||||
|
@ -307,17 +314,10 @@
|
|||
}
|
||||
|
||||
.label {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
margin-left: 0.25em;
|
||||
|
||||
:global(a) {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.label-inner {
|
||||
|
@ -351,8 +351,8 @@
|
|||
}
|
||||
|
||||
&:not(.banner) .key {
|
||||
display: inline-block;
|
||||
margin-left: 0.5em;
|
||||
flex-grow: 1;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&.show-type .secondary,
|
||||
|
@ -384,6 +384,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
|
||||
:global(a) {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin: 0 0.1em;
|
||||
}
|
||||
|
@ -393,7 +402,6 @@
|
|||
}
|
||||
|
||||
.link-button {
|
||||
padding: 0.25em;
|
||||
opacity: 0.66;
|
||||
transition:
|
||||
opacity 0.2s,
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
<div class="label">
|
||||
<UpObject {address} {labels} {banner} {select} link on:resolved />
|
||||
<UpObject {address} {labels} {banner} {select} on:resolved />
|
||||
</div>
|
||||
</div>
|
||||
</UpLink>
|
||||
|
|
|
@ -24,8 +24,7 @@
|
|||
|
||||
<style lang="scss">
|
||||
.upobject-label {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.backpath {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import Icon from '../utils/Icon.svelte';
|
||||
import Jobs from './Jobs.svelte';
|
||||
import Notifications from './Notifications.svelte';
|
||||
import { i18n } from '$lib/i18n';
|
||||
import { i18n } from '../../i18n';
|
||||
|
||||
let hidden = true;
|
||||
let activeJobs: number;
|
||||
|
@ -20,8 +20,6 @@
|
|||
on:keydown={(ev) => {
|
||||
if (['Space', 'Enter'].includes(ev.key)) hidden = !hidden;
|
||||
}}
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div class="info">
|
||||
{#if activeJobs > 0}
|
||||
|
@ -44,7 +42,6 @@
|
|||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
z-index: 9;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -2,13 +2,13 @@
|
|||
import { addEmitter } from '../AddModal.svelte';
|
||||
import Icon from '../utils/Icon.svelte';
|
||||
import { jobsEmitter } from './Jobs.svelte';
|
||||
import api from '$lib/api';
|
||||
import api, { currentUser, logout } from '$lib/api';
|
||||
import Selector, { type SelectorValue } from '../utils/Selector.svelte';
|
||||
import { i18n } from '$lib/i18n';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import { vaultInfo } from '$lib/util/info';
|
||||
import HeaderUserDropdown from '$lib/components/layout/HeaderUserDropdown.svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
|
||||
let selector: Selector;
|
||||
let userDropdown = false;
|
||||
|
@ -28,7 +28,7 @@
|
|||
lastSearched = lastSearched.slice(0, 10);
|
||||
}
|
||||
|
||||
async function onInput(event: CustomEvent<SelectorValue | undefined>) {
|
||||
async function onInput(event: CustomEvent<SelectorValue>) {
|
||||
const value = event.detail;
|
||||
if (!value) return;
|
||||
|
||||
|
@ -77,6 +77,12 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<svelte:body
|
||||
on:click={() => {
|
||||
userDropdown = false;
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="header">
|
||||
<h1>
|
||||
<a href="/">
|
||||
|
@ -109,7 +115,17 @@
|
|||
>
|
||||
<Icon name="user" />
|
||||
</button>
|
||||
<HeaderUserDropdown bind:open={userDropdown} />
|
||||
{#if userDropdown}
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions a11y-click-events-have-key-events -->
|
||||
<div class="user-dropdown" transition:slide on:click|stopPropagation={() => {}}>
|
||||
<div class="user">
|
||||
<Icon plain name="user" />
|
||||
{$currentUser || '???'}
|
||||
</div>
|
||||
<hr />
|
||||
<button on:click={() => logout()}> <Icon name="log-out" />{$i18n.t('Log out')}</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
@ -153,6 +169,18 @@
|
|||
}
|
||||
}
|
||||
|
||||
.user-dropdown {
|
||||
background: var(--background);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--foreground);
|
||||
padding: 0.5em;
|
||||
position: absolute;
|
||||
top: 3.5rem;
|
||||
right: 0.5rem;
|
||||
box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.5);
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.name {
|
||||
display: none;
|
||||
|
|
|
@ -1,140 +0,0 @@
|
|||
<script lang="ts">
|
||||
import api, { currentUser, logout } from '$lib/api';
|
||||
import { type UpendApiError } from '@upnd/upend/api';
|
||||
import { i18n } from '$lib/i18n';
|
||||
import Icon from '$lib/components/utils/Icon.svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import Modal from '$lib/components/layout/Modal.svelte';
|
||||
|
||||
export let open = false;
|
||||
|
||||
let changePasswordModal = false;
|
||||
let currentPassword = '';
|
||||
let newPassword = '';
|
||||
let newPasswordConfirm = '';
|
||||
|
||||
async function changePassword() {
|
||||
try {
|
||||
const result = await api.authenticate(
|
||||
{ username: $currentUser!, password: currentPassword },
|
||||
'key'
|
||||
);
|
||||
if (result) {
|
||||
await api.register({ username: $currentUser!, password: newPassword });
|
||||
alert($i18n.t('Password changed successfully').toString());
|
||||
changePasswordModal = false;
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
alert(
|
||||
$i18n.t('Error authenticating: {error}', { error: (e as UpendApiError).message }).toString()
|
||||
);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:body
|
||||
on:click={() => {
|
||||
open = false;
|
||||
}}
|
||||
/>
|
||||
|
||||
{#if open}
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions a11y-click-events-have-key-events -->
|
||||
<div class="user-dropdown" transition:slide on:click|stopPropagation>
|
||||
<div class="user">
|
||||
<Icon plain name="user" />
|
||||
{$currentUser || '???'}
|
||||
</div>
|
||||
<button on:click={() => (changePasswordModal = true)}>
|
||||
<Icon name="lock" />{$i18n.t('Change password')}</button
|
||||
>
|
||||
<button on:click={() => logout()}> <Icon name="log-out" />{$i18n.t('Log out')}</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if changePasswordModal}
|
||||
<Modal on:close={() => (changePasswordModal = false)}>
|
||||
<h2>
|
||||
<Icon name="lock" />
|
||||
{$i18n.t('Change password')}
|
||||
</h2>
|
||||
<form class="change-password">
|
||||
<label>
|
||||
<span>{$i18n.t('Current password')}</span>
|
||||
<input type="password" bind:value={currentPassword} required />
|
||||
</label>
|
||||
<label>
|
||||
<span>{$i18n.t('New password')}</span>
|
||||
<input type="password" bind:value={newPassword} required />
|
||||
</label>
|
||||
<label>
|
||||
<span>{$i18n.t('Confirm new password')}</span>
|
||||
<input type="password" bind:value={newPasswordConfirm} required />
|
||||
</label>
|
||||
<button
|
||||
type="submit"
|
||||
on:click={() => changePassword()}
|
||||
disabled={newPassword !== newPasswordConfirm || !newPassword}
|
||||
>{$i18n.t('Change password')}</button
|
||||
>
|
||||
</form>
|
||||
</Modal>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.user-dropdown {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
background: var(--background);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--foreground);
|
||||
padding: 0.5em;
|
||||
position: absolute;
|
||||
top: 3.5rem;
|
||||
right: 0.5rem;
|
||||
box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.5);
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.user {
|
||||
font-weight: 500;
|
||||
margin: 0 1rem;
|
||||
}
|
||||
|
||||
button,
|
||||
.user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.change-password {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
align-items: center;
|
||||
gap: 0.5rem 1rem;
|
||||
|
||||
& label {
|
||||
display: contents;
|
||||
|
||||
& span {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
& button {
|
||||
grid-column: span 2;
|
||||
justify-content: center;
|
||||
justify-self: center;
|
||||
font-weight: 500;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,59 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
export let disabled = false;
|
||||
export let dismissable = true;
|
||||
|
||||
function close() {
|
||||
if (!dismissable) return;
|
||||
dispatch('close');
|
||||
}
|
||||
|
||||
function onKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:body on:keydown={onKeydown} />
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions -->
|
||||
<div class="modal-container" on:click={close} transition:fade={{ duration: 200 }}>
|
||||
<div class="modal" class:disabled on:click|stopPropagation>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.modal-container {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(0, 0, 0, 0.66);
|
||||
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--foreground);
|
||||
padding: 2rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
&.disabled {
|
||||
filter: brightness(0.66);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -20,7 +20,7 @@
|
|||
$: if (editing && selector) selector.focus();
|
||||
$: if (!focus && !hover) editing = false;
|
||||
|
||||
function onInput(ev: CustomEvent<SelectorValue | undefined>) {
|
||||
function onInput(ev: CustomEvent<SelectorValue>) {
|
||||
newValue = ev.detail;
|
||||
selector.focus();
|
||||
}
|
||||
|
|
|
@ -35,7 +35,6 @@
|
|||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
padding: 0.25em;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
<script lang="ts">
|
||||
import { slide } from 'svelte/transition';
|
||||
|
||||
export let hide = false;
|
||||
let hidden = true;
|
||||
</script>
|
||||
|
@ -8,35 +6,28 @@
|
|||
<section class="labelborder" class:hide class:hidden>
|
||||
<header
|
||||
on:click={() => {
|
||||
if (hide) hidden = !hidden;
|
||||
if (hide) {
|
||||
hidden = !hidden;
|
||||
}
|
||||
}}
|
||||
on:keydown={(ev) => {
|
||||
if (['Space', 'Enter'].includes(ev.key) && hide) hidden = !hidden;
|
||||
}}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<slot name="header-full">
|
||||
<h3>
|
||||
<slot name="header" />
|
||||
</h3>
|
||||
<h3><slot name="header" /></h3>
|
||||
</slot>
|
||||
</header>
|
||||
{#if !hide || !hidden}
|
||||
<div class="content" transition:slide>
|
||||
<slot />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="hidden-indicator" />
|
||||
{/if}
|
||||
<div class="content">
|
||||
<slot />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
<style lang="scss">
|
||||
section.labelborder {
|
||||
margin-top: 0.66rem;
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
|
||||
& header {
|
||||
header {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
|
@ -45,24 +36,28 @@
|
|||
padding-bottom: 0.33rem;
|
||||
margin-bottom: 0.33rem;
|
||||
|
||||
& h3 {
|
||||
h3 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.hide {
|
||||
& header {
|
||||
header {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&.hide.hidden {
|
||||
opacity: 0.66;
|
||||
}
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
&.hidden {
|
||||
opacity: 0.66;
|
||||
|
||||
& .hidden-indicator {
|
||||
border-top: 1px solid var(--foreground);
|
||||
margin-top: calc(-0.33rem + 2px);
|
||||
header {
|
||||
border-bottom-width: 0.5px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
import LabelBorder from './LabelBorder.svelte';
|
||||
import { i18n } from '$lib/i18n';
|
||||
import { format } from 'date-fns';
|
||||
import Icon from '$lib/components/utils/Icon.svelte';
|
||||
const dispatch = createEventDispatcher<{ change: WidgetChange }>();
|
||||
|
||||
export let address: string;
|
||||
|
@ -35,7 +34,7 @@
|
|||
</script>
|
||||
|
||||
<LabelBorder hide={!notes?.length}>
|
||||
<span slot="header"><Icon plain name="note" /> {$i18n.t('Notes')}</span>
|
||||
<span slot="header">Notes</span>
|
||||
<div class="notes" contenteditable on:input={onInput} bind:this={contentEl}>
|
||||
{#each (notes || '\n').split('\n') as line, idx}
|
||||
{#if idx > 0}<br />{/if}
|
||||
|
|
|
@ -93,10 +93,7 @@
|
|||
import debug from 'debug';
|
||||
import Spinner from './Spinner.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
input: SelectorValue | undefined;
|
||||
focus: boolean;
|
||||
}>();
|
||||
const dispatch = createEventDispatcher();
|
||||
const dbg = debug('kestrel:Selector');
|
||||
let selectorEl: HTMLElement;
|
||||
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
import Selector, { type SelectorValue } from '../utils/Selector.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type { WidgetChange } from '$lib/types/base';
|
||||
import { addEmitter } from '$lib/components/AddModal.svelte';
|
||||
import debug from 'debug';
|
||||
const dispatch = createEventDispatcher();
|
||||
const dbg = debug(`kestrel:EntityList`);
|
||||
|
@ -124,7 +123,7 @@
|
|||
|
||||
$: if (adding && addSelector) addSelector.focus();
|
||||
|
||||
function addEntity(ev: CustomEvent<SelectorValue | undefined>) {
|
||||
function addEntity(ev: CustomEvent<SelectorValue>) {
|
||||
dbg('Adding entity', ev.detail);
|
||||
const addAddress = ev.detail?.t == 'Address' ? ev.detail.c : undefined;
|
||||
if (!addAddress) return;
|
||||
|
@ -203,39 +202,26 @@
|
|||
{#if address}
|
||||
<div class="add">
|
||||
{#if adding}
|
||||
<div class="main">
|
||||
<Selector
|
||||
bind:this={addSelector}
|
||||
placeholder={$i18n.t('Add or create an entry') || ''}
|
||||
types={['Address', 'NewAddress']}
|
||||
on:input={addEntity}
|
||||
on:focus={(ev) => {
|
||||
if (!ev.detail) {
|
||||
adding = false;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Selector
|
||||
bind:this={addSelector}
|
||||
placeholder={$i18n.t('Search database or paste an URL') || ''}
|
||||
types={['Address', 'NewAddress']}
|
||||
on:input={addEntity}
|
||||
on:focus={(ev) => {
|
||||
if (!ev.detail) {
|
||||
adding = false;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<div class="main">
|
||||
<IconButton
|
||||
name="plus-circle"
|
||||
outline
|
||||
subdued
|
||||
on:click={() => {
|
||||
adding = true;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{#if address}
|
||||
<IconButton
|
||||
outline
|
||||
subdued
|
||||
name="upload"
|
||||
title={$i18n.t('Upload a file') || ''}
|
||||
on:click={() => addEmitter.emit('destination', address || '')}
|
||||
/>
|
||||
{/if}
|
||||
<IconButton
|
||||
name="plus-circle"
|
||||
outline
|
||||
subdued
|
||||
on:click={() => {
|
||||
adding = true;
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -332,13 +318,7 @@
|
|||
|
||||
.add {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
|
||||
& .main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.entitylist.style-grid .add {
|
||||
|
|
|
@ -364,8 +364,7 @@
|
|||
<div class="cell mark-attribute">
|
||||
<Selector
|
||||
types={['Attribute', 'NewAttribute']}
|
||||
on:input={(ev) =>
|
||||
(newEntryAttribute = ev.detail?.t === 'Attribute' ? ev.detail?.name : '')}
|
||||
on:input={(ev) => (newEntryAttribute = ev.detail?.name)}
|
||||
on:focus={(ev) => (addFocus = ev.detail)}
|
||||
keepFocusOnSet
|
||||
bind:this={newAttrSelector}
|
||||
|
@ -428,7 +427,7 @@
|
|||
|
||||
.attr-action {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
|
|
@ -27,4 +27,4 @@
|
|||
{#if $vaultInfo && !$vaultInfo.public && !$currentUser}
|
||||
<LoginModal />
|
||||
{/if}
|
||||
<DropPasteHandler />
|
||||
<DropPasteHandler />>
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
let root: HTMLDivElement;
|
||||
let identities: string[] = [];
|
||||
$: addresses = $page.params.addresses.split(',');
|
||||
let backgrounds: Record<string, string | undefined> = {};
|
||||
|
||||
function add(value: SelectorValue) {
|
||||
if (value.t !== 'Address') return;
|
||||
|
@ -97,6 +98,7 @@
|
|||
{only}
|
||||
on:close={() => close(index)}
|
||||
on:detail={(ev) => onDetailChanged(index, ev)}
|
||||
background="var(--background-lightest)"
|
||||
>
|
||||
<CombineColumn spec={address} on:close={() => close(index)} />
|
||||
</BrowseColumn>
|
||||
|
@ -109,6 +111,7 @@
|
|||
close(index);
|
||||
}}
|
||||
on:detail={(ev) => onDetailChanged(index, ev)}
|
||||
background="var(--background-lightest)"
|
||||
>
|
||||
<SelectedColumn />
|
||||
</BrowseColumn>
|
||||
|
@ -145,13 +148,20 @@
|
|||
{address}
|
||||
{index}
|
||||
{only}
|
||||
background={backgrounds[address]}
|
||||
on:close={() => close(index)}
|
||||
on:resolved={(ev) => onIdentified(index, ev)}
|
||||
on:detail={(ev) => onDetailChanged(index, ev)}
|
||||
on:combine={() => addCombine(address)}
|
||||
let:detail
|
||||
>
|
||||
<Inspect {address} {detail} on:resolved on:close />
|
||||
<Inspect
|
||||
{address}
|
||||
{detail}
|
||||
on:resolved
|
||||
on:close
|
||||
on:background={(ev) => (backgrounds[address] = ev.detail)}
|
||||
/>
|
||||
</BrowseColumn>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -42,12 +42,6 @@ export const Link: Story = {
|
|||
}
|
||||
};
|
||||
|
||||
export const Keyed: Story = {
|
||||
args: {
|
||||
address: 'zb2rhmpmTFPxdhaxTQg5Ug3KHFU8DZNUPh8TaPY2v8UQVJbQk'
|
||||
}
|
||||
};
|
||||
|
||||
export const Banner: Story = {
|
||||
args: {
|
||||
banner: true
|
||||
|
@ -61,13 +55,6 @@ export const BannerWithLabels: Story = {
|
|||
}
|
||||
};
|
||||
|
||||
export const KeyedBanner: Story = {
|
||||
args: {
|
||||
address: 'zb2rhmpmTFPxdhaxTQg5Ug3KHFU8DZNUPh8TaPY2v8UQVJbQk',
|
||||
banner: true
|
||||
}
|
||||
};
|
||||
|
||||
export const Overflow: Story = {
|
||||
args: {
|
||||
labels: ['qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnm'.repeat(3)]
|
||||
|
|
|
@ -8,8 +8,7 @@ export default defineConfig({
|
|||
sentrySvelteKit({
|
||||
sourceMapsUploadOptions: {
|
||||
org: 'upend',
|
||||
project: 'upend-kestrel',
|
||||
authToken: process.env.SENTRY_AUTH_TOKEN
|
||||
project: 'upend-kestrel'
|
||||
}
|
||||
}),
|
||||
sveltekit()
|
||||
|
|
Loading…
Reference in New Issue