Skip to content

Commit

Permalink
Merge pull request #90 from streamsync-cloud/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
ramedina86 authored Sep 3, 2023
2 parents 9c1e55e + 6a825d6 commit 54e6e68
Show file tree
Hide file tree
Showing 68 changed files with 4,072 additions and 2,113 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
docs/docs/.vitepress/cache/
docs/docs/.vitepress/temp/
ui/dist/
ui/custom_components_dist/
build/
dist/
src/streamsync.egg-info/
Expand Down
2 changes: 1 addition & 1 deletion apps/hello/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ RUN apt-get update -y && mkdir /app && python3 -m venv /app/venv
ENV PATH="/app/venv/bin:$PATH"
COPY . /app
WORKDIR /app
RUN pip3 install streamsync && pip3 install -r requirements.txt
RUN pip3 install 'streamsync[ds]' && pip3 install -r requirements.txt

FROM python:3-slim AS run-image
RUN apt-get update -y && mkdir /app
Expand Down
28 changes: 20 additions & 8 deletions docs/docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ export default {
text: "Backend-initiated actions",
link: "/backend-initiated-actions",
},
{ text: "Page routes", link: "/page-routes" },
{ text: "Sessions", link: "/sessions" },
{ text: "Custom server", link: "/custom-server" },
],
},
{
text: "Extending it",
items: [
{
text: "Stylesheets",
link: "/stylesheets",
Expand All @@ -41,9 +49,10 @@ export default {
text: "Frontend scripts",
link: "/frontend-scripts",
},
{ text: "Page routes", link: "/page-routes" },
{ text: "Sessions", link: "/sessions" },
{ text: "Custom server", link: "/custom-server" },
{
text: "Custom components",
link: "/custom-components",
},
],
},
{
Expand All @@ -56,9 +65,12 @@ export default {
},
],
},
// vite: defineConfig({
// ssr: {
// external: ["plotly.js-dist", "vega-embed"],
// },
// }),
vite: {
build: {
target: "esnext",
},
define: {
STREAMSYNC_LIVE_CCT: JSON.stringify("no"),
},
},
};
175 changes: 175 additions & 0 deletions docs/docs/custom-components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# Custom components

It's possible to extend Streamsync with custom component templates.

They're developed using Vue 3 and TypeScript. Once transpiled, they can be used by copying them to the `extensions/` folder of any project.

::: tip Custom components behave exactly like built-in ones
They are just as performant, can contain other components, and offer the same Builder experience. They only differ from built-in components in the way that they're bundled and imported.
:::

## Architecture

Streamsync frontend compiles to a collection of static assets that is distributed in the Python package. These static assets are then served via FastAPI.

During initialisation time, the server scans the `extensions/` folder in the project folder and looks for `.css` and `.js` files. This folder is also served, similarly to `static/`. If it finds any valid files in `extensions/`, it shares the list with clients and tells them to dynamically import these files during runtime.

_Extensions_ and _custom templates_ are currently synonyms, but this might change in order to accommodate other extension capabilities.

![Custom Components - Architecture](./images/custom-components.architecture.png)

Dependencies are [_provided_](https://vuejs.org/api/composition-api-dependency-injection.html) using injection symbols and can be _injected_ to be used by the component template. These include `evaluatedFields`, which contain the current values of the editable fields. Injected dependencies are fully typed, making development easier.

[Rollup's `external` feature](https://rollupjs.org/configuration-options/#external), invoked via Vite, allows for extensions to be compiled without dependencies and link those during runtime. Therefore, extensions aren't bundled to be standalone, but rather to work as a piece of a puzzle.

![Custom Components - External](./images/custom-components.external.png)

## Anatomy of a template

A template defines how a certain component is rendered. For example, `corebutton` defines how _Button_ components are rendered.

Streamsync component templates are purely frontend-based. **They are Vue 3 templates that extend the Vue specification** via a [custom option](https://vuejs.org/api/utility-types.html#componentcustomoptions), `streamsync`. This _custom option_ defines all the Streamsync-specific behaviour of the component. For example, its `fields` property establishes which fields will be editable via Builder.

### Simple example

This example shows a template for _Bubble Message_, a simple demo component with one editable field, `text`.

```vue
<template>
<div class="BubbleMessage">
<div class="triangle"></div>
<div class="message">
<!-- Shows the current value of the field "text" -->
{{ fields.text.value }}
</div>
</div>
</template>
<script lang="ts">
export default {
streamsync: {
name: "Bubble Message",
description: "Shows a message in the shape of a speech bubble.",
category: "Content",
// Fields will be editable via Streamsync Builder
fields: {
text: {
name: "Text",
type: FieldType.Text,
},
},
// Preview field is used in the Component Tree
previewField: "text",
},
};
</script>
<script setup lang="ts">
import { FieldType } from "../streamsyncTypes";
import injectionKeys from "../injectionKeys";
import { inject } from "vue";
/*
The values for the fields defined earlier in the custom option
will be available using the evaluatedFields injection symbol.
*/
const fields = inject(injectionKeys.evaluatedFields);
</script>
<style scoped>
/* ... */
</style>
```

The code above will make _Bubble Message_ available in Builder.

![Custom Components - Bubble Message](./images/custom-components.bubble-message.png)

## Developing templates

### Run a local server

To get started, clone the [Streamsync repository](https://github.com/streamsync-cloud/streamsync) from GitHub.

To develop custom templates, at least in a developer-friendly way, you'll need a frontend development server with instant reloads.

The frontend code for Streamsync can be found in the folder `ui`. With Node and npm in your system, run `npm install` to install dependencies, and start the server with support for custom component templates using `npm run custom.dev`.

```sh
cd ui
npm install

# "custom.dev" links templates in "custom_components/"
# "dev" runs the server without them

npm run custom.dev
```

The command above will start a frontend server, but won't be of much use by itself —a backend is needed. The frontend development server proxies backend requests to port 5000.

Start Streamsync via command line, specifying the option `--port 5000`, to provide a backend in that port. It's recommended to create a new app for testing the template you're developing.

```sh
streamsync create customtester
streamsync edit customtester --port 5000
```

You should now be able to access Streamsync via the URL provided by Vite, e.g. `http://localhost:5174`. In Builder's _Toolkit_, you should see the sample component, _Balloon Message_. Add it to your tester application.

### Create a new component

Go to `ui/src/custom_components/` and open the Vue single-file components, i.e. the `.vue` files. These files contain comments that will help you get started. Try editing the provided templates, you should see changes reflected.

::: tip
You can also have a look at the built-in component templates, since their syntax is equivalent. They can be found in the `ui/src/core_components/` folder.
:::

You can get started by duplicating one of these examples. Make sure you add the new template to the entrypoint, as discussed below.

### Define entrypoint

For custom component templates to be taken into account, they need to be accessible from the entrypoint.

Edit `ui/src/custom_components/index.ts` to define which templates you wish to export and under which identifiers.

```ts

// Import the templates

import BubbleMessage from './BubbleMessage.vue';
import BubbleMessageAdvanced from './BubbleMessageAdvanced.vue';

// Export an object with the ids and the templates as default

export default {
"bubblemessage": BubbleMessage,
"bubblemessageadvanced": BubbleMessageAdvanced
}
```

A single or multiple templates can be specified. Take into account that they will all be exported, and later imported, together.

## Bundling templates

### Pack and collect

Execute `npm run custom.build`, this will generate the output `.js` and `.css` files into `ui/custom_components_dist`.

```sh
# "build" builds the entire frontend
# "custom.build" only builds the custom templates

npm run custom.build
```

Collect the files from `ui/custom_components_dist` and pack them in a folder such as `my_custom_bubbles`.

### Try them

The folder containing the generated files, e.g. `my_custom_bubbles`, can now be placed in the `extensions/` folder of any Streamsync project. It'll be automatically detected during server startup.
21 changes: 16 additions & 5 deletions docs/docs/event-handlers.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def evaluate(state, payload):
state["is_a_cat"] = result
```

Take into account that globals are indeed global. If you need to store data that's only relevant to a particular user, use state.
Take into account that globals apply to all users. If you need to store data that's only relevant to a particular user, use application state.

## Standard output

Expand All @@ -115,14 +115,25 @@ def payload_inspector(state, payload):

## Execution flow

Event handlers run in a thread pool and are non-blocking. Mutations are sent to the frontend after the function has finished executing.
Event handlers run in a thread pool and are non-blocking. Each event is processed independently from each other.

State mutations are sent to the frontend after the function has finished executing. The code below will accumulate all mutations and send to the frontend after the function returns.

```py
def handle_fast(state):
state["text"] = "Hello"
state["x"] += 3
state["y"] += 2
```

However, for long-running tasks, Streamsync will periodically check state and provide partial updates to the user.

```py
def handle_slowly(state):
state["message"] = "Loading..."
import time
state["counter"] += 1
time.sleep(5)
print("Five seconds have passed")
state["message"] = "Completed"
```

The code above will sleep for five seconds, but without blocking your program. After sleeping, it'll notify the frontend that the variable of `counter` has changed.
The code above will set `message` to "Loading...", then to "Completed".
9 changes: 7 additions & 2 deletions docs/docs/handling-inputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,14 @@ def recalculate(state):

## Handling inputs safely

Streamsync automatically sanitises the payloads it provides. For example, if a _Dropdown Input_ component lists options `high` and `low`, you're guaranteed you won't get a value like `"Robert'); DROP TABLE students;--"`. You'll get `"high"`, `"low"` or `None`.
Streamsync automatically sanitises the payloads it provides for its built-in events, those that start with `ss-`.

Nevertheless, inputs that use the generic `ss-change` event, such as _Text Input_, can contain any string. Therefore, as with any application, it's important to be familiar with the risks associated with handling user input, especially SQL injections.
For example, if a _Dropdown Input_ component lists options `high` and `low`, you're guaranteed you won't get a value like `"Robert'); DROP TABLE students;--"` when handling `ss-option-change`. You'll get `"high"`, `"low"` or `None`.

::: warning Inputs are sanitised, but you should still be careful

As with any application, it's important to be familiar with the risks associated with handling user input, especially SQL injections. If you're using any custom HTML and mixing it with user generated content, make sure you understand XSS.
:::

## Creating forms

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/docs/images/custom-components.external.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/docs/sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import streamsync as ss

@ss.session_verifier
def check_headers(headers):
if headers.get("x-success") is not None:
if headers.get("x-success") is None:
return False
return True
```
Loading

0 comments on commit 54e6e68

Please sign in to comment.