mirror of
https://github.com/Rolands-Laucis/Socio.git
synced 2026-05-15 14:15:57 -06:00
upd framework demo to 0.8.1
This commit is contained in:
parent
abe79664c0
commit
03f36c4dbc
4 changed files with 79 additions and 100 deletions
123
demos/full-stack_framework/package-lock.json
generated
123
demos/full-stack_framework/package-lock.json
generated
|
|
@ -9,7 +9,7 @@
|
|||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"sequelize": "^6.28.0",
|
||||
"socio": "^0.7.3",
|
||||
"socio": "^0.8.1",
|
||||
"sqlite3": "^5.1.4",
|
||||
"svelte-french-toast": "^1.0.3",
|
||||
"ws": "^8.11.0"
|
||||
|
|
@ -561,25 +561,25 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@sveltejs/kit": {
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.8.3.tgz",
|
||||
"integrity": "sha512-32tiLy5PPpt2lquK2p53/5wR+ghAXw0HymIBEezmwmwtzx7Xf36xw3RG3fDYQ9gyzon89T+JRweXgAv/qhhvSQ==",
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.15.9.tgz",
|
||||
"integrity": "sha512-Og+4WlguPVPS0PmAHefp4KxvTVZfyDN09aORVXIdKSzqzodSJiLs7Fhi/Q0z0YjmcoNLWF24tI0a6mTusL6Yfg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^2.1.1",
|
||||
"@types/cookie": "^0.5.1",
|
||||
"cookie": "^0.5.0",
|
||||
"devalue": "^4.3.0",
|
||||
"esm-env": "^1.0.0",
|
||||
"kleur": "^4.1.5",
|
||||
"magic-string": "^0.29.0",
|
||||
"magic-string": "^0.30.0",
|
||||
"mime": "^3.0.0",
|
||||
"sade": "^1.8.1",
|
||||
"set-cookie-parser": "^2.5.1",
|
||||
"sirv": "^2.0.2",
|
||||
"tiny-glob": "^0.2.9",
|
||||
"undici": "5.20.0"
|
||||
"undici": "~5.22.0"
|
||||
},
|
||||
"bin": {
|
||||
"svelte-kit": "svelte-kit.js"
|
||||
|
|
@ -592,30 +592,18 @@
|
|||
"vite": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/kit/node_modules/magic-string": {
|
||||
"version": "0.29.0",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.29.0.tgz",
|
||||
"integrity": "sha512-WcfidHrDjMY+eLjlU+8OvwREqHwpgCeKVBUpQ3OhYYuvfaYCUgcbuBzappNzZvg/v8onU3oQj+BYpkOJe9Iw4Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.4.13"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/vite-plugin-svelte": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-2.0.2.tgz",
|
||||
"integrity": "sha512-xCEan0/NNpQuL0l5aS42FjwQ6wwskdxC3pW1OeFtEKNZwRg7Evro9lac9HesGP6TdFsTv2xMes5ASQVKbCacxg==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-2.1.1.tgz",
|
||||
"integrity": "sha512-7YeBDt4us0FiIMNsVXxyaP4Hwyn2/v9x3oqStkHU3ZdIc5O22pGwUwH33wUqYo+7Itdmo8zxJ45Qvfm3H7UUjQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4",
|
||||
"deepmerge": "^4.2.2",
|
||||
"deepmerge": "^4.3.1",
|
||||
"kleur": "^4.1.5",
|
||||
"magic-string": "^0.27.0",
|
||||
"magic-string": "^0.30.0",
|
||||
"svelte-hmr": "^0.15.1",
|
||||
"vitefu": "^0.2.3"
|
||||
"vitefu": "^0.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >= 16"
|
||||
|
|
@ -987,9 +975,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/deepmerge": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
|
||||
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
|
|
@ -1510,9 +1498,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz",
|
||||
"integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==",
|
||||
"version": "0.30.0",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz",
|
||||
"integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.4.13"
|
||||
|
|
@ -2399,9 +2387,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/socio": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/socio/-/socio-0.7.3.tgz",
|
||||
"integrity": "sha512-QKo/JuoD786z6TfHPkaUkOCH3QrDQD78xcTurYEdg5k5Sm9hrPbv2mA6/gkp1Dg1AJRpcItcFj9ote3lXc0Lzw==",
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/socio/-/socio-0.8.1.tgz",
|
||||
"integrity": "sha512-O9haZmOW+5lq2GBdhWGMtxr6XIRbKQW4EEeCfcikMfHH/p6gyPjEyUHIDmKkXySe6RVEVifmF/G4kcznZwbbew==",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.5.1",
|
||||
"ws": "^8.9.0"
|
||||
|
|
@ -2800,15 +2788,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "5.20.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-5.20.0.tgz",
|
||||
"integrity": "sha512-J3j60dYzuo6Eevbawwp1sdg16k5Tf768bxYK4TUJRH7cBM4kFCbf3mOnM/0E3vQYXvpxITbbWmBafaDbxLDz3g==",
|
||||
"version": "5.22.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-5.22.0.tgz",
|
||||
"integrity": "sha512-fR9RXCc+6Dxav4P9VV/sp5w3eFiSdOjJYsbtWfd4s5L5C4ogyuVpdKIVHeW0vV1MloM65/f7W45nR9ZxwVdyiA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"busboy": "^1.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.18"
|
||||
"node": ">=14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unique-filename": {
|
||||
|
|
@ -3277,49 +3265,38 @@
|
|||
}
|
||||
},
|
||||
"@sveltejs/kit": {
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.8.3.tgz",
|
||||
"integrity": "sha512-32tiLy5PPpt2lquK2p53/5wR+ghAXw0HymIBEezmwmwtzx7Xf36xw3RG3fDYQ9gyzon89T+JRweXgAv/qhhvSQ==",
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.15.9.tgz",
|
||||
"integrity": "sha512-Og+4WlguPVPS0PmAHefp4KxvTVZfyDN09aORVXIdKSzqzodSJiLs7Fhi/Q0z0YjmcoNLWF24tI0a6mTusL6Yfg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sveltejs/vite-plugin-svelte": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^2.1.1",
|
||||
"@types/cookie": "^0.5.1",
|
||||
"cookie": "^0.5.0",
|
||||
"devalue": "^4.3.0",
|
||||
"esm-env": "^1.0.0",
|
||||
"kleur": "^4.1.5",
|
||||
"magic-string": "^0.29.0",
|
||||
"magic-string": "^0.30.0",
|
||||
"mime": "^3.0.0",
|
||||
"sade": "^1.8.1",
|
||||
"set-cookie-parser": "^2.5.1",
|
||||
"sirv": "^2.0.2",
|
||||
"tiny-glob": "^0.2.9",
|
||||
"undici": "5.20.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"magic-string": {
|
||||
"version": "0.29.0",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.29.0.tgz",
|
||||
"integrity": "sha512-WcfidHrDjMY+eLjlU+8OvwREqHwpgCeKVBUpQ3OhYYuvfaYCUgcbuBzappNzZvg/v8onU3oQj+BYpkOJe9Iw4Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jridgewell/sourcemap-codec": "^1.4.13"
|
||||
}
|
||||
}
|
||||
"undici": "~5.22.0"
|
||||
}
|
||||
},
|
||||
"@sveltejs/vite-plugin-svelte": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-2.0.2.tgz",
|
||||
"integrity": "sha512-xCEan0/NNpQuL0l5aS42FjwQ6wwskdxC3pW1OeFtEKNZwRg7Evro9lac9HesGP6TdFsTv2xMes5ASQVKbCacxg==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-2.1.1.tgz",
|
||||
"integrity": "sha512-7YeBDt4us0FiIMNsVXxyaP4Hwyn2/v9x3oqStkHU3ZdIc5O22pGwUwH33wUqYo+7Itdmo8zxJ45Qvfm3H7UUjQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"debug": "^4.3.4",
|
||||
"deepmerge": "^4.2.2",
|
||||
"deepmerge": "^4.3.1",
|
||||
"kleur": "^4.1.5",
|
||||
"magic-string": "^0.27.0",
|
||||
"magic-string": "^0.30.0",
|
||||
"svelte-hmr": "^0.15.1",
|
||||
"vitefu": "^0.2.3"
|
||||
"vitefu": "^0.2.4"
|
||||
}
|
||||
},
|
||||
"@tootallnate/once": {
|
||||
|
|
@ -3590,9 +3567,9 @@
|
|||
}
|
||||
},
|
||||
"deepmerge": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
|
||||
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||||
"dev": true
|
||||
},
|
||||
"delegates": {
|
||||
|
|
@ -4008,9 +3985,9 @@
|
|||
}
|
||||
},
|
||||
"magic-string": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz",
|
||||
"integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==",
|
||||
"version": "0.30.0",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz",
|
||||
"integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jridgewell/sourcemap-codec": "^1.4.13"
|
||||
|
|
@ -4603,9 +4580,9 @@
|
|||
"optional": true
|
||||
},
|
||||
"socio": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/socio/-/socio-0.7.3.tgz",
|
||||
"integrity": "sha512-QKo/JuoD786z6TfHPkaUkOCH3QrDQD78xcTurYEdg5k5Sm9hrPbv2mA6/gkp1Dg1AJRpcItcFj9ote3lXc0Lzw==",
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/socio/-/socio-0.8.1.tgz",
|
||||
"integrity": "sha512-O9haZmOW+5lq2GBdhWGMtxr6XIRbKQW4EEeCfcikMfHH/p6gyPjEyUHIDmKkXySe6RVEVifmF/G4kcznZwbbew==",
|
||||
"requires": {
|
||||
"base64-js": "^1.5.1",
|
||||
"ws": "^8.9.0"
|
||||
|
|
@ -4866,9 +4843,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"undici": {
|
||||
"version": "5.20.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-5.20.0.tgz",
|
||||
"integrity": "sha512-J3j60dYzuo6Eevbawwp1sdg16k5Tf768bxYK4TUJRH7cBM4kFCbf3mOnM/0E3vQYXvpxITbbWmBafaDbxLDz3g==",
|
||||
"version": "5.22.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-5.22.0.tgz",
|
||||
"integrity": "sha512-fR9RXCc+6Dxav4P9VV/sp5w3eFiSdOjJYsbtWfd4s5L5C4ogyuVpdKIVHeW0vV1MloM65/f7W45nR9ZxwVdyiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"busboy": "^1.6.0"
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
"type": "module",
|
||||
"dependencies": {
|
||||
"sequelize": "^6.28.0",
|
||||
"socio": "^0.7.3",
|
||||
"socio": "^0.8.1",
|
||||
"sqlite3": "^5.1.4",
|
||||
"svelte-french-toast": "^1.0.3",
|
||||
"ws": "^8.11.0"
|
||||
|
|
|
|||
|
|
@ -1,46 +1,47 @@
|
|||
console.log('running hooks.server.ts ...')
|
||||
|
||||
//socio stuff
|
||||
import { log, info, done, soft_error } from 'socio/dist/logging';
|
||||
import { SocioServer } from 'socio/dist/core';
|
||||
import { SocioSecurity } from 'socio/dist/secure';
|
||||
import {perMessageDeflate} from 'socio/dist/utils'; //for auto compressing WS messages. Carefully read documentation before using this! Possible memory leaks!
|
||||
|
||||
//DB stuff
|
||||
import { Sequelize } from 'sequelize';
|
||||
|
||||
//types
|
||||
import type { QueryFunction, QueryFuncParams } from 'socio/dist/core';
|
||||
import type { PropValue, id } from 'socio/dist/types';
|
||||
import type { PropValue, id, PropAssigner } from 'socio/dist/types';
|
||||
import type { SocioSession } from 'socio/dist/core-session';
|
||||
|
||||
try{
|
||||
info('Starting SocioServer...');
|
||||
|
||||
//constants
|
||||
const ws_port = 3000; //can be set up that the websockets run on the same port as the http server
|
||||
const Query = await InitDB_GetQueryFunc();
|
||||
|
||||
//load in the secure_private_key with dotenv or smth. Dont hardcode like this
|
||||
const socsec = new SocioSecurity({ secure_private_key: 'skk#$U#Y$7643GJHKGDHJH#$K#$HLI#H$KBKDBDFKU34534', verbose: true });
|
||||
const socserv = new SocioServer({ port: 3000, perMessageDeflate }, { DB_query_function: Query, verbose: true, socio_security: socsec });
|
||||
|
||||
const validate_color_prop: PropAssigner = (curr_val: PropValue, new_val: PropValue): boolean => {
|
||||
if (typeof new_val != 'string' || new_val.length != 7) return false;
|
||||
if (!new_val.match(/^#[0-9a-f]{6}/mi)) return false;
|
||||
return socserv.SetPropVal('color', new_val);
|
||||
}
|
||||
socserv.RegisterProp('color', '#ffffff', validate_color_prop);
|
||||
} catch (e:any) {
|
||||
soft_error(e);
|
||||
}
|
||||
|
||||
async function InitDB_GetQueryFunc() {
|
||||
const sequelize = new Sequelize('sqlite::memory:');
|
||||
await sequelize.query('CREATE TABLE Users(userid INTEGER PRIMARY KEY AUTOINCREMENT, name varchar(50), num INTEGER NOT NULL DEFAULT 0);', { logging: false });
|
||||
await sequelize.query('INSERT INTO Users (name, num) VALUES("Jane", 42);', { logging: false });
|
||||
await sequelize.query('INSERT INTO Users (name, num) VALUES("John", 69);', { logging: false });
|
||||
|
||||
async function QueryWrap(client: SocioSession, id: id, sql: string, params: object | null = null) {
|
||||
const QueryWrap = async (client: SocioSession, id: id, sql: string, params: object | null = null) => {
|
||||
//@ts-expect-error
|
||||
return (await sequelize.query(sql, { logging: false, raw: true, replacements: params }))[0] as Promise<object>;
|
||||
}
|
||||
|
||||
const socsec = new SocioSecurity({ secure_private_key: 'skk#$U#Y$7643GJHKGDHJH#$K#$HLI#H$KBKDBDFKU34534', verbose: true });
|
||||
const socserv = new SocioServer({ port: ws_port }, { DB_query_function: QueryWrap as QueryFunction, verbose: true, socio_security: socsec });
|
||||
|
||||
socserv.RegisterProp('color', '#ffffff', (curr_val: PropValue, new_val: PropValue): boolean => {
|
||||
if (typeof new_val != 'string' || new_val.length != 7) return false;
|
||||
if (!new_val.match(/^#[0-9a-f]{6}/mi)) return false;
|
||||
return socserv.SetPropVal('color', new_val);
|
||||
});
|
||||
} catch (e:any) {
|
||||
soft_error(e);
|
||||
}
|
||||
|
||||
|
||||
/** @type {import('@sveltejs/kit').Handle} */
|
||||
//@ts-ignore
|
||||
export async function handle({ event, resolve }) {
|
||||
return await resolve(event);
|
||||
return QueryWrap as QueryFunction;
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
import { SocioClient } from "socio/dist/core-client";
|
||||
import type {id} from 'socio/dist/types'
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import {socio} from 'socio/dist/utils'
|
||||
|
||||
import { slide } from "svelte/transition";
|
||||
import toast from 'svelte-french-toast'; //https://github.com/kbrgl/svelte-french-toast
|
||||
|
|
@ -37,13 +38,13 @@
|
|||
ready = await sc.ready();
|
||||
toast.success('Socio Client connected!', {icon:'🥳',position: "bottom-center", duration:1000});
|
||||
|
||||
sc.Subscribe({sql: "SELECT COUNT(*) AS RES FROM users WHERE name = :name;--socio",params: { name: "John" }}, (res) => {
|
||||
sc.Subscribe({sql: socio`SELECT COUNT(*) AS RES FROM users WHERE name = :name;`,params: { name: "John" }}, (res) => {
|
||||
//@ts-ignore
|
||||
user_count = res[0].RES as number; //res is whatever object your particular DB interface lib returns from a raw query
|
||||
}
|
||||
);
|
||||
|
||||
sc.Subscribe({ sql: "SELECT * FROM users;--socio" },(res) => {
|
||||
sc.Subscribe({ sql: socio`SELECT * FROM users;` },(res) => {
|
||||
users = res as { userid: number; name: string; num: number }[]; //res is whatever object your particular DB interface lib returns from a raw query
|
||||
}
|
||||
);
|
||||
|
|
@ -82,7 +83,7 @@
|
|||
<div class="horiz">
|
||||
<h6 class="darker_text bold">single sql query:</h6>
|
||||
<h4>SELECT 42+69 AS RESULT; =</h4>
|
||||
{#await sc.Query("SELECT 42+69 AS RESULT;--socio")}
|
||||
{#await sc.Query(socio`SELECT 42+69 AS RESULT;`)}
|
||||
<Bloom><Spinner style="--h:24px;--t:6px;" /></Bloom>
|
||||
{:then res}
|
||||
<h4 class="bold">{res[0].RESULT}</h4>
|
||||
|
|
@ -112,7 +113,7 @@
|
|||
style="width:100%;"
|
||||
on:click={async () =>
|
||||
await sc.Query(
|
||||
"INSERT INTO users (name, num) VALUES(:name, :num);--socio",
|
||||
socio`INSERT INTO users (name, num) VALUES(:name, :num);`,
|
||||
insert_fields
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue