refactor database
This commit is contained in:
		
							parent
							
								
									924e803d16
								
							
						
					
					
						commit
						4b04cc92f1
					
				
					 11 changed files with 171 additions and 69 deletions
				
			
		| 
						 | 
				
			
			@ -20,13 +20,11 @@
 | 
			
		|||
  import LoadingFooter from './LoadingFooter.html'
 | 
			
		||||
  import VirtualList from './VirtualList.html'
 | 
			
		||||
  import { splice, push } from 'svelte-extras'
 | 
			
		||||
  import worker from 'workerize-loader!../_utils/database/database'
 | 
			
		||||
  import { mergeStatuses } from '../_utils/statuses'
 | 
			
		||||
  import { mark, stop } from '../_utils/marks'
 | 
			
		||||
  import { timelines } from '../_static/timelines'
 | 
			
		||||
  import { toast } from '../_utils/toast'
 | 
			
		||||
 | 
			
		||||
  const database = worker()
 | 
			
		||||
  import { database } from '../_utils/database/database'
 | 
			
		||||
 | 
			
		||||
  const FETCH_LIMIT = 20
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,15 +1,15 @@
 | 
			
		|||
import keyval from "idb-keyval"
 | 
			
		||||
import { getKnownDbs } from './knownDbs'
 | 
			
		||||
import debounce from 'lodash/debounce'
 | 
			
		||||
import { OBJECT_STORE, getDatabase } from './shared'
 | 
			
		||||
import { TIMELINE_STORE, getTimelineDatabase } from './timelines'
 | 
			
		||||
 | 
			
		||||
const MAX_NUM_STORED_STATUSES = 1000
 | 
			
		||||
const CLEANUP_INTERVAL = 60000
 | 
			
		||||
 | 
			
		||||
async function cleanup(instanceName, timeline) {
 | 
			
		||||
  const db = await getDatabase(instanceName, timeline)
 | 
			
		||||
  const db = await getTimelineDatabase(instanceName, timeline)
 | 
			
		||||
  return await new Promise((resolve, reject) => {
 | 
			
		||||
    const tx = db.transaction(OBJECT_STORE, 'readwrite')
 | 
			
		||||
    const store = tx.objectStore(OBJECT_STORE)
 | 
			
		||||
    const tx = db.transaction(TIMELINE_STORE, 'readwrite')
 | 
			
		||||
    const store = tx.objectStore(TIMELINE_STORE)
 | 
			
		||||
    const index = store.index('pinafore_id_as_negative_big_int')
 | 
			
		||||
 | 
			
		||||
    store.count().onsuccess = (e) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -37,11 +37,18 @@ export const cleanupOldStatuses = debounce(async () => {
 | 
			
		|||
  if (process.env.NODE_ENV !== 'production') {
 | 
			
		||||
    console.log('cleanupOldStatuses')
 | 
			
		||||
  }
 | 
			
		||||
  let knownDbs = (await keyval.get('known_dbs')) || {}
 | 
			
		||||
  let dbNames = Object.keys(knownDbs)
 | 
			
		||||
  for (let dbName of dbNames) {
 | 
			
		||||
    let [ instanceName, timeline ] = knownDbs[dbName]
 | 
			
		||||
    await cleanup(instanceName, timeline)
 | 
			
		||||
 | 
			
		||||
  let knownDbs = await getKnownDbs()
 | 
			
		||||
  let instanceNames = Object.keys(knownDbs)
 | 
			
		||||
  for (let instanceName of instanceNames) {
 | 
			
		||||
    let knownDbsForInstance = knownDbs[instanceName] || []
 | 
			
		||||
    for (let knownDb of knownDbsForInstance) {
 | 
			
		||||
      let {type, dbName} = knownDb
 | 
			
		||||
      if (type !== 'timeline') {
 | 
			
		||||
        continue
 | 
			
		||||
      }
 | 
			
		||||
      await cleanup(instanceName, dbName)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (process.env.NODE_ENV !== 'production') {
 | 
			
		||||
    console.log('done cleanupOldStatuses')
 | 
			
		||||
| 
						 | 
				
			
			@ -1,36 +1,10 @@
 | 
			
		|||
import { cleanupOldStatuses } from './cleanup'
 | 
			
		||||
import { OBJECT_STORE, getDatabase } from './shared'
 | 
			
		||||
import { toReversePaddedBigInt, transformStatusForStorage } from './utils'
 | 
			
		||||
import worker from 'workerize-loader!./databaseInsideWorker'
 | 
			
		||||
 | 
			
		||||
export async function getTimeline(instanceName, timeline, maxId = null, limit = 20) {
 | 
			
		||||
  const db = await getDatabase(instanceName, timeline)
 | 
			
		||||
  return await new Promise((resolve, reject) => {
 | 
			
		||||
    const tx = db.transaction(OBJECT_STORE, 'readonly')
 | 
			
		||||
    const store = tx.objectStore(OBJECT_STORE)
 | 
			
		||||
    const index = store.index('pinafore_id_as_negative_big_int')
 | 
			
		||||
    let sinceAsNegativeBigInt = maxId ? toReversePaddedBigInt(maxId) : null
 | 
			
		||||
    let query = sinceAsNegativeBigInt ? IDBKeyRange.lowerBound(sinceAsNegativeBigInt, false) : null
 | 
			
		||||
import * as databaseInsideWorker from './databaseInsideWorker'
 | 
			
		||||
 | 
			
		||||
    let res
 | 
			
		||||
    index.getAll(query, limit).onsuccess = (e) => {
 | 
			
		||||
      res = e.target.result
 | 
			
		||||
    }
 | 
			
		||||
// workerize-loader causes weirdness during development
 | 
			
		||||
let database = process.browser && process.env.NODE_ENV === 'production' ? worker() : databaseInsideWorker
 | 
			
		||||
 | 
			
		||||
    tx.oncomplete = () => resolve(res)
 | 
			
		||||
    tx.onerror = () => reject(tx.error.name + ' ' + tx.error.message)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function insertStatuses(instanceName, timeline, statuses) {
 | 
			
		||||
  cleanupOldStatuses()
 | 
			
		||||
  const db = await getDatabase(instanceName, timeline)
 | 
			
		||||
  return await new Promise((resolve, reject) => {
 | 
			
		||||
    const tx = db.transaction(OBJECT_STORE, 'readwrite')
 | 
			
		||||
    const store = tx.objectStore(OBJECT_STORE)
 | 
			
		||||
    for (let status of statuses) {
 | 
			
		||||
      store.put(transformStatusForStorage(status))
 | 
			
		||||
    }
 | 
			
		||||
    tx.oncomplete = () => resolve()
 | 
			
		||||
    tx.onerror = () => reject(tx.error.name + ' ' + tx.error.message)
 | 
			
		||||
  })
 | 
			
		||||
export {
 | 
			
		||||
  database
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										65
									
								
								routes/_utils/database/databaseInsideWorker.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								routes/_utils/database/databaseInsideWorker.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,65 @@
 | 
			
		|||
import { META_STORE, getMetaDatabase } from './meta'
 | 
			
		||||
import { cleanupOldStatuses } from './cleanupTimelines'
 | 
			
		||||
import { TIMELINE_STORE, getTimelineDatabase } from './timelines'
 | 
			
		||||
import { toReversePaddedBigInt, transformStatusForStorage } from './utils'
 | 
			
		||||
 | 
			
		||||
export async function getTimeline(instanceName, timeline, maxId = null, limit = 20) {
 | 
			
		||||
  const db = await getTimelineDatabase(instanceName, timeline)
 | 
			
		||||
  return await new Promise((resolve, reject) => {
 | 
			
		||||
    const tx = db.transaction(TIMELINE_STORE, 'readonly')
 | 
			
		||||
    const store = tx.objectStore(TIMELINE_STORE)
 | 
			
		||||
    const index = store.index('pinafore_id_as_negative_big_int')
 | 
			
		||||
    let sinceAsNegativeBigInt = maxId ? toReversePaddedBigInt(maxId) : null
 | 
			
		||||
    let query = sinceAsNegativeBigInt ? IDBKeyRange.lowerBound(sinceAsNegativeBigInt, false) : null
 | 
			
		||||
 | 
			
		||||
    let res
 | 
			
		||||
    index.getAll(query, limit).onsuccess = (e) => {
 | 
			
		||||
      res = e.target.result
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    tx.oncomplete = () => resolve(res)
 | 
			
		||||
    tx.onerror = () => reject(tx.error.name + ' ' + tx.error.message)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function insertStatuses(instanceName, timeline, statuses) {
 | 
			
		||||
  const db = await getTimelineDatabase(instanceName, timeline)
 | 
			
		||||
  await new Promise((resolve, reject) => {
 | 
			
		||||
    const tx = db.transaction(TIMELINE_STORE, 'readwrite')
 | 
			
		||||
    const store = tx.objectStore(TIMELINE_STORE)
 | 
			
		||||
    for (let status of statuses) {
 | 
			
		||||
      store.put(transformStatusForStorage(status))
 | 
			
		||||
    }
 | 
			
		||||
    tx.oncomplete = () => resolve()
 | 
			
		||||
    tx.onerror = () => reject(tx.error.name + ' ' + tx.error.message)
 | 
			
		||||
  })
 | 
			
		||||
  /* no await */ cleanupOldStatuses()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function setInstanceVerifyCredentials(instanceName, verifyCredentials) {
 | 
			
		||||
  const db = await getMetaDatabase(instanceName)
 | 
			
		||||
  return await new Promise((resolve, reject) => {
 | 
			
		||||
    const tx = db.transaction(META_STORE, 'readwrite')
 | 
			
		||||
    const store = tx.objectStore(META_STORE)
 | 
			
		||||
    store.put({
 | 
			
		||||
      key: 'verifyCredentials',
 | 
			
		||||
      value: verifyCredentials
 | 
			
		||||
    })
 | 
			
		||||
    tx.oncomplete = () => resolve()
 | 
			
		||||
    tx.onerror = () => reject(tx.error.name + ' ' + tx.error.message)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getInstanceVerifyCredentials(instanceName, verifyCredentials) {
 | 
			
		||||
  const db = await getMetaDatabase(instanceName)
 | 
			
		||||
  return await new Promise((resolve, reject) => {
 | 
			
		||||
    const tx = db.transaction(META_STORE, 'readwrite')
 | 
			
		||||
    const store = tx.objectStore(META_STORE)
 | 
			
		||||
    let res
 | 
			
		||||
    store.get('verifyCredentials').onsuccess = (e) => {
 | 
			
		||||
      res = e.target.result && e.target.result.value
 | 
			
		||||
    }
 | 
			
		||||
    tx.oncomplete = () => resolve(res)
 | 
			
		||||
    tx.onerror = () => reject(tx.error.name + ' ' + tx.error.message)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								routes/_utils/database/knownDbs.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								routes/_utils/database/knownDbs.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
import keyval from "idb-keyval"
 | 
			
		||||
 | 
			
		||||
export async function addKnownDb(instanceName, type, dbName) {
 | 
			
		||||
  let knownDbs = (await keyval.get('known_dbs')) || {}
 | 
			
		||||
  if (!knownDbs[instanceName]) {
 | 
			
		||||
    knownDbs[instanceName] = []
 | 
			
		||||
  }
 | 
			
		||||
  if (!knownDbs[instanceName].some(db => db.type === type && db.dbName === dbName)) {
 | 
			
		||||
    knownDbs[instanceName].push({type, dbName})
 | 
			
		||||
  }
 | 
			
		||||
  await keyval.set('known_dbs', knownDbs)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getKnownDbs() {
 | 
			
		||||
  return (await keyval.get('known_dbs')) || {}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getKnownDbsForInstance(instanceName) {
 | 
			
		||||
  let knownDbs = await getKnownDbs()
 | 
			
		||||
  return knownDbs[instanceName] || []
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										29
									
								
								routes/_utils/database/meta.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								routes/_utils/database/meta.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,29 @@
 | 
			
		|||
import { addKnownDb } from './knownDbs'
 | 
			
		||||
 | 
			
		||||
const databaseCache = {}
 | 
			
		||||
export const META_STORE = 'meta'
 | 
			
		||||
 | 
			
		||||
export function getMetaDatabase(instanceName) {
 | 
			
		||||
  const key = `${instanceName}_${META_STORE}`
 | 
			
		||||
  if (databaseCache[key]) {
 | 
			
		||||
    return Promise.resolve(databaseCache[key])
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let dbName = key
 | 
			
		||||
 | 
			
		||||
  addKnownDb(instanceName, 'meta', dbName)
 | 
			
		||||
 | 
			
		||||
  databaseCache[key] = new Promise((resolve, reject) => {
 | 
			
		||||
    let req = indexedDB.open(dbName, 1)
 | 
			
		||||
    req.onerror = reject
 | 
			
		||||
    req.onblocked = () => {
 | 
			
		||||
      console.log('idb blocked')
 | 
			
		||||
    }
 | 
			
		||||
    req.onupgradeneeded = () => {
 | 
			
		||||
      let db = req.result;
 | 
			
		||||
      db.createObjectStore(META_STORE, {keyPath: 'key'})
 | 
			
		||||
    }
 | 
			
		||||
    req.onsuccess = () => resolve(req.result)
 | 
			
		||||
  })
 | 
			
		||||
  return databaseCache[key]
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,25 +1,21 @@
 | 
			
		|||
import keyval from "idb-keyval"
 | 
			
		||||
import { addKnownDb } from './knownDbs'
 | 
			
		||||
 | 
			
		||||
const databaseCache = {}
 | 
			
		||||
export const OBJECT_STORE = 'statuses'
 | 
			
		||||
export const TIMELINE_STORE = 'statuses'
 | 
			
		||||
 | 
			
		||||
export function createDbName(instanceName, timeline) {
 | 
			
		||||
  return `${OBJECT_STORE}_${instanceName}_${timeline}`
 | 
			
		||||
export function createTimelineDbName(instanceName, timeline) {
 | 
			
		||||
  return `${instanceName}_timeline_${timeline}`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getDatabase(instanceName, timeline) {
 | 
			
		||||
export function getTimelineDatabase(instanceName, timeline) {
 | 
			
		||||
  const key = `${instanceName}_${timeline}`
 | 
			
		||||
  if (databaseCache[key]) {
 | 
			
		||||
    return Promise.resolve(databaseCache[key])
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let dbName = createDbName(instanceName, timeline)
 | 
			
		||||
  let dbName = createTimelineDbName(instanceName, timeline)
 | 
			
		||||
 | 
			
		||||
  keyval.get('known_dbs').then(knownDbs => {
 | 
			
		||||
    knownDbs = knownDbs || {}
 | 
			
		||||
    knownDbs[dbName] = [instanceName, timeline]
 | 
			
		||||
    keyval.set('known_dbs', knownDbs)
 | 
			
		||||
  })
 | 
			
		||||
  addKnownDb(instanceName, 'timeline', dbName)
 | 
			
		||||
 | 
			
		||||
  databaseCache[key] = new Promise((resolve, reject) => {
 | 
			
		||||
    let req = indexedDB.open(dbName, 1)
 | 
			
		||||
| 
						 | 
				
			
			@ -29,7 +25,7 @@ export function getDatabase(instanceName, timeline) {
 | 
			
		|||
    }
 | 
			
		||||
    req.onupgradeneeded = () => {
 | 
			
		||||
      let db = req.result;
 | 
			
		||||
      let oStore = db.createObjectStore(OBJECT_STORE, {
 | 
			
		||||
      let oStore = db.createObjectStore(TIMELINE_STORE, {
 | 
			
		||||
        keyPath: 'id'
 | 
			
		||||
      })
 | 
			
		||||
      oStore.createIndex('pinafore_id_as_negative_big_int', 'pinafore_id_as_negative_big_int')
 | 
			
		||||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
import { get } from '../ajax'
 | 
			
		||||
import { basename } from './utils'
 | 
			
		||||
 | 
			
		||||
export function getThisUserAccount(instanceName, accessToken) {
 | 
			
		||||
export function getVerifyCredentials(instanceName, accessToken) {
 | 
			
		||||
  let url = `${basename(instanceName)}/api/v1/accounts/verify_credentials`
 | 
			
		||||
  return get(url, {
 | 
			
		||||
    'Authorization': `Bearer ${accessToken}`
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,14 +6,14 @@
 | 
			
		|||
  <SettingsLayout page='settings/instances/{{params.instanceName}}' label="{{params.instanceName}}">
 | 
			
		||||
    <h1 class="instance-name-h1">{{params.instanceName}}</h1>
 | 
			
		||||
 | 
			
		||||
    {{#if instanceUserAccount}}
 | 
			
		||||
    {{#if verifyCredentials}}
 | 
			
		||||
      <h2>Logged in as:</h2>
 | 
			
		||||
      <div class="acct-current-user">
 | 
			
		||||
        <img alt="Profile picture for {{'@' + instanceUserAccount.acct}}"
 | 
			
		||||
             class="acct-avatar" src="{{instanceUserAccount.avatar}}" />
 | 
			
		||||
        <img alt="Profile picture for {{'@' + verifyCredentials.acct}}"
 | 
			
		||||
             class="acct-avatar" src="{{verifyCredentials.avatar}}" />
 | 
			
		||||
        <a class="acct-handle" rel="noopener" target="_blank"
 | 
			
		||||
           href="{{instanceUserAccount.url}}">{{'@' + instanceUserAccount.acct}}</a>
 | 
			
		||||
        <span class="acct-display-name">{{instanceUserAccount.display_name}}</span>
 | 
			
		||||
           href="{{verifyCredentials.url}}">{{'@' + verifyCredentials.acct}}</a>
 | 
			
		||||
        <span class="acct-display-name">{{verifyCredentials.display_name}}</span>
 | 
			
		||||
      </div>
 | 
			
		||||
      <h2>Theme:</h2>
 | 
			
		||||
      <form class="theme-chooser" aria-label="Choose a theme">
 | 
			
		||||
| 
						 | 
				
			
			@ -97,7 +97,8 @@
 | 
			
		|||
  import { store } from '../../_utils/store'
 | 
			
		||||
  import Layout from '../../_components/Layout.html'
 | 
			
		||||
  import SettingsLayout from '../_components/SettingsLayout.html'
 | 
			
		||||
  import { getThisUserAccount } from '../../_utils/mastodon/user'
 | 
			
		||||
  import { getVerifyCredentials } from '../../_utils/mastodon/user'
 | 
			
		||||
  import { database } from '../../_utils/database/database'
 | 
			
		||||
  import { themes } from '../../_static/themes'
 | 
			
		||||
  import { switchToTheme } from '../../_utils/themeEngine'
 | 
			
		||||
  import { goto } from 'sapper/runtime.js'
 | 
			
		||||
| 
						 | 
				
			
			@ -117,9 +118,18 @@
 | 
			
		|||
      let loggedInInstances = this.store.get('loggedInInstances')
 | 
			
		||||
      let instanceThemes = this.store.get('instanceThemes')
 | 
			
		||||
      let instanceData = loggedInInstances[instanceName]
 | 
			
		||||
      let instanceUserAccount = await getThisUserAccount(instanceName, instanceData.access_token)
 | 
			
		||||
      let verifyCredentials = await database.getInstanceVerifyCredentials(instanceName)
 | 
			
		||||
      if (verifyCredentials) { // update
 | 
			
		||||
        getVerifyCredentials(instanceName, instanceData.access_token).then(verifyCredentials => {
 | 
			
		||||
          database.setInstanceVerifyCredentials(instanceName, verifyCredentials)
 | 
			
		||||
        })
 | 
			
		||||
      } else {
 | 
			
		||||
        verifyCredentials = await getVerifyCredentials(instanceName, instanceData.access_token)
 | 
			
		||||
        database.setInstanceVerifyCredentials(instanceName, verifyCredentials)
 | 
			
		||||
        verifyCredentials = verifyCredentials
 | 
			
		||||
      }
 | 
			
		||||
      this.set({
 | 
			
		||||
        instanceUserAccount: instanceUserAccount,
 | 
			
		||||
        verifyCredentials: verifyCredentials,
 | 
			
		||||
        selectedTheme: instanceThemes[instanceName]
 | 
			
		||||
      })
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -72,11 +72,12 @@
 | 
			
		|||
  import Layout from '../../_components/Layout.html';
 | 
			
		||||
  import SettingsLayout from '../_components/SettingsLayout.html'
 | 
			
		||||
  import { registerApplication, generateAuthLink, getAccessTokenFromAuthCode } from '../../_utils/mastodon/oauth'
 | 
			
		||||
  import { getThisUserAccount } from '../../_utils/mastodon/user'
 | 
			
		||||
  import { getVerifyCredentials } from '../../_utils/mastodon/user'
 | 
			
		||||
  import { store } from '../../_utils/store'
 | 
			
		||||
  import { goto } from 'sapper/runtime.js'
 | 
			
		||||
  import { switchToTheme } from '../../_utils/themeEngine'
 | 
			
		||||
  import LoadingMask from '../../_components/LoadingMask'
 | 
			
		||||
  import { database } from '../../_utils/database/database'
 | 
			
		||||
 | 
			
		||||
  const REDIRECT_URI = (typeof location !== 'undefined' ?
 | 
			
		||||
      location.origin : 'https://pinafore.social') + '/settings/instances/add'
 | 
			
		||||
| 
						 | 
				
			
			@ -177,7 +178,9 @@
 | 
			
		|||
        this.store.save()
 | 
			
		||||
        switchToTheme('default')
 | 
			
		||||
        // fire off request for account so it's cached in the SW
 | 
			
		||||
        getThisUserAccount(currentRegisteredInstanceName, instanceData.access_token)
 | 
			
		||||
        getVerifyCredentials(currentRegisteredInstanceName, instanceData.access_token).then(verifyCredentials => {
 | 
			
		||||
          database.setInstanceVerifyCredentials(currentRegisteredInstanceName, verifyCredentials)
 | 
			
		||||
        })
 | 
			
		||||
        goto('/')
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -42,7 +42,6 @@ const NETWORK_ONLY = [
 | 
			
		|||
]
 | 
			
		||||
 | 
			
		||||
const CACHE_FIRST = [
 | 
			
		||||
  '/api/v1/accounts/verify_credentials',
 | 
			
		||||
  '/system/accounts/avatars'
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue