198 lines
7.4 KiB
Vue
198 lines
7.4 KiB
Vue
<template>
|
|
<BaseLayout hide-search>
|
|
<main class="content">
|
|
<div class="container-fluid p-0">
|
|
<div v-if="qualified" class="card">
|
|
<div class="card-header">Domain</div>
|
|
<div class="card-body">{{ qualified.domain }}</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-header">Decoded</div>
|
|
<div class="card-body">
|
|
<ul>
|
|
<li v-for="(value, index) in decoded" :key="index">{{ value }}</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-header">Deserialized</div>
|
|
<div class="card-body">
|
|
<p>kind: {{ deserialized.kind }}</p>
|
|
<ul>
|
|
<li v-for="(value, name) in fields" :key="name">{{ name }}: {{ value }}</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-header">Expanded</div>
|
|
<div class="card-body">
|
|
<router-link v-if="expandedRoute" :to="expandedRoute">{{ expandedRoute }}</router-link>
|
|
<span v-else-if="error" class="text-danger">{{ error }}</span>
|
|
<span v-else>No page exists for kind '{{ deserialized.kind }}'</span>
|
|
</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-header">Examples</div>
|
|
<table class="table table-striped mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th>Kind</th>
|
|
<th>Fields</th>
|
|
<th>Serialized</th>
|
|
<th>Bits</th>
|
|
<th>Token</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="example in examples" :key="example.token">
|
|
<td>{{ example.kind }}</td>
|
|
<td>
|
|
<span v-for="(value, name) in example.fields" :key="name" class="me-2">
|
|
{{ name }}: {{ value }}
|
|
</span>
|
|
</td>
|
|
<td>[{{ example.ints.join(', ') }}]</td>
|
|
<td><code>{{ example.bits }}</code></td>
|
|
<td>
|
|
<router-link :to="'/' + example.token">{{ example.token }}</router-link>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</BaseLayout>
|
|
</template>
|
|
|
|
<script>
|
|
import {mapActions} from 'vuex';
|
|
import BaseLayout from '@/components/BaseLayout.vue';
|
|
import {
|
|
decodeShortId, deserializeShortId, encodeShortId, serializeShortId,
|
|
isDomainQualifiedShortId, decodeDomainQualifiedShortId
|
|
} from '@/lib/short-id';
|
|
import {expandedRoute as buildExpandedRoute, NEEDS_IDMAP, domainQualifiedRoute} from '@/router';
|
|
|
|
const EXAMPLES = [
|
|
{kind: 'item', owner_identity_id: 7, item_local_id: 42},
|
|
{kind: 'group', group_id: 11},
|
|
{kind: 'group_item', owner_group_id: 5, item_local_id: 42},
|
|
{kind: 'file', file_id: 123},
|
|
{kind: 'storage_location', owner_identity_id: 3, storage_location_id: 1000},
|
|
{kind: 'group_storage_location', owner_group_id: 5, storage_location_id: 1000},
|
|
{kind: 'category', category_id: 5},
|
|
{kind: 'workflow', owner_identity_id: 2, workflow_id: 9},
|
|
];
|
|
|
|
// Debug-only mirror of short-id.js's bit-packing (2-bit kind tag + 4-bit chunks, see docs/handles-and-shortids.md), recomputed from `ints` for display only.
|
|
const KIND_TAG_BITS = 2;
|
|
const CHUNK_BITS = 4;
|
|
const DIRECT_KIND_COUNT = 2 ** KIND_TAG_BITS - 1;
|
|
|
|
function chunkedIntBits(value, chunkBits) {
|
|
value = BigInt(value);
|
|
const mask = (1n << BigInt(chunkBits)) - 1n;
|
|
const chunks = [];
|
|
let remaining = value;
|
|
do {
|
|
chunks.unshift(remaining & mask);
|
|
remaining >>= BigInt(chunkBits);
|
|
} while (remaining > 0n);
|
|
return chunks.map((chunk, i) => {
|
|
const continuation = i === chunks.length - 1 ? '0' : '1';
|
|
return continuation + chunk.toString(2).padStart(chunkBits, '0');
|
|
}).join('');
|
|
}
|
|
|
|
function shortIdBitSegments(ints) {
|
|
const [kindIndex, ...fieldValues] = ints;
|
|
const segments = [];
|
|
if (kindIndex < DIRECT_KIND_COUNT) {
|
|
segments.push(kindIndex.toString(2).padStart(KIND_TAG_BITS, '0'));
|
|
} else {
|
|
segments.push('1'.repeat(KIND_TAG_BITS));
|
|
segments.push(chunkedIntBits(kindIndex - DIRECT_KIND_COUNT, CHUNK_BITS));
|
|
}
|
|
fieldValues.forEach(value => segments.push(chunkedIntBits(value, CHUNK_BITS)));
|
|
|
|
const totalBits = segments.join('').length;
|
|
const paddingBits = (6 - (totalBits % 6)) % 6;
|
|
if (paddingBits > 0) {
|
|
segments.push('0'.repeat(paddingBits));
|
|
}
|
|
return segments.join(' ');
|
|
}
|
|
|
|
export default {
|
|
name: 'ShortId',
|
|
components: {
|
|
BaseLayout
|
|
},
|
|
props: {
|
|
short_id: {
|
|
type: String,
|
|
required: true
|
|
}
|
|
},
|
|
data() {
|
|
return {
|
|
error: null,
|
|
foreignRoute: undefined,
|
|
}
|
|
},
|
|
computed: {
|
|
// '<domain>:~<token>' vs a bare '~<token>' - see docs/handles-and-shortids.md#domain-qualified-short-id.
|
|
qualified() {
|
|
return isDomainQualifiedShortId(this.short_id) ? decodeDomainQualifiedShortId(this.short_id) : null;
|
|
},
|
|
decoded() {
|
|
return this.qualified ? this.qualified.ints : decodeShortId(this.short_id);
|
|
},
|
|
deserialized() {
|
|
return deserializeShortId(this.decoded);
|
|
},
|
|
fields() {
|
|
const {kind, ...fields} = this.deserialized;
|
|
return fields;
|
|
},
|
|
expandedRoute() {
|
|
return this.qualified ? (this.foreignRoute || null) : buildExpandedRoute(this.deserialized);
|
|
},
|
|
examples() {
|
|
return EXAMPLES.map(named => {
|
|
const {kind, ...fields} = named;
|
|
const ints = serializeShortId(named);
|
|
const token = encodeShortId(ints);
|
|
return {kind, fields, ints, token, bits: shortIdBitSegments(ints)};
|
|
});
|
|
}
|
|
},
|
|
watch: {
|
|
// expandedRoute re-evaluates once fetchIdMap resolves (see buildExpandedRoute in router.js); finishes the redirect the router's synchronous guard couldn't.
|
|
expandedRoute(url) {
|
|
if (url) {
|
|
this.$router.replace(url);
|
|
}
|
|
}
|
|
},
|
|
methods: {
|
|
...mapActions(['fetchIdMap']),
|
|
async resolveIfQualified() {
|
|
if (this.qualified) {
|
|
this.foreignRoute = await domainQualifiedRoute(this.qualified.domain, this.decoded) || null;
|
|
}
|
|
}
|
|
},
|
|
mounted() {
|
|
this.resolveIfQualified();
|
|
if (NEEDS_IDMAP.has(this.deserialized.kind) && !this.$store.state.idmapLoaded) {
|
|
this.fetchIdMap()
|
|
.then(() => this.resolveIfQualified())
|
|
.catch(e => {
|
|
this.error = e.message;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
</script>
|