mirror of https://github.com/Budibase/budibase.git
committed by
GitHub
280 changed files with 9844 additions and 6350 deletions
@ -0,0 +1,214 @@ |
|||
<p align="center"> |
|||
<a href="https://www.budibase.com"> |
|||
<img alt="Budibase" src="https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg" width="60" /> |
|||
</a> |
|||
</p> |
|||
<h1 align="center"> |
|||
Budibase |
|||
</h1> |
|||
|
|||
<h3 align="center"> |
|||
使って楽しいローコードプラットフォーム |
|||
</h3> |
|||
<p align="center"> |
|||
Budibaseはオープンソースのローコードプラットフォームで、生産性を向上させるツールを簡単に構築することができます。 |
|||
</p> |
|||
|
|||
<h3 align="center"> |
|||
🤖 🎨 🚀 |
|||
</h3> |
|||
<br> |
|||
|
|||
<p align="center"> |
|||
<img alt="Budibase design ui" src="https://res.cloudinary.com/daog6scxm/image/upload/v1633524049/ui/design-ui-wide-mobile_gdaveq.jpg"> |
|||
</p> |
|||
|
|||
<p align="center"> |
|||
<a href="https://github.com/Budibase/budibase/releases"> |
|||
<img alt="GitHub all releases" src="https://img.shields.io/github/downloads/Budibase/budibase/total"> |
|||
</a> |
|||
<a href="https://github.com/Budibase/budibase/releases"> |
|||
<img alt="GitHub release (latest by date)" src="https://img.shields.io/github/v/release/Budibase/budibase"> |
|||
</a> |
|||
<a href="https://twitter.com/intent/follow?screen_name=budibase"> |
|||
<img src="https://img.shields.io/twitter/follow/budibase?style=social" alt="Follow @budibase" /> |
|||
</a> |
|||
<img src="https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg" alt="Code of conduct" /> |
|||
<a href="https://codecov.io/gh/Budibase/budibase"> |
|||
<img src="https://codecov.io/gh/Budibase/budibase/graph/badge.svg?token=E8W2ZFXQOH"/> |
|||
</a> |
|||
</p> |
|||
|
|||
<h3 align="center"> |
|||
<a href="https://docs.budibase.com/getting-started">はじめに</a> |
|||
<span> · </span> |
|||
<a href="https://docs.budibase.com">ドキュメント</a> |
|||
<span> · </span> |
|||
<a href="https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas">機能リクエスト</a> |
|||
<span> · </span> |
|||
<a href="https://github.com/Budibase/budibase/issues">バグ報告</a> |
|||
<span> · </span> |
|||
サポート: <a href="https://github.com/Budibase/budibase/discussions">ディスカッション</a> |
|||
</h3> |
|||
|
|||
<br /><br /> |
|||
## ✨ 特徴 |
|||
|
|||
### "本物"のソフトウェアを構築できます |
|||
ほかのプラットフォームとは違い、Budibaseだけでシングルページのアプリケーションを制作し完成させることができます。Budibaseで作られたアプリケーションは素晴らしいパフォーマンスを持っており、レスポンシブデザインにも対応しています。ユーザー達にいい印象を与えること間違いなしでしょう! |
|||
<br /><br /> |
|||
|
|||
### 拡張性が高くオープンソース |
|||
Budibaseはオープンソースで、GPL v3ライセンスの下に公開されています。このことは、Budibaseが常にあなたのそばにいるという安心感を与えてくれることでしょう。そして、私たちは開発者に優しい環境を提供しているので、あなたは好きなだけにソースコードをフォークして改造、もしくは直接Budibaseにコントリビュートすることができます。 |
|||
<br /><br /> |
|||
|
|||
### 既存のデータ、もしくは一から始める |
|||
Budibaseはいろんなツールから既存のデータを使用できます。たとえばMongoDB、CouchDB、 PostgreSQL、MySQL、Airtable、S3、DynamoDB、REST APIなど。ほかのプラットフォームにない特徴として、Budibaseはデータなしの状態でビジネスアプリケーションの構築を一から始めることができます。 [新しいデータリソースをリクエスト](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas)。 |
|||
|
|||
<p align="center"> |
|||
<img alt="Budibase data" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970242/Out%20of%20beta%20launch/data_n1tlhf.png"> |
|||
</p> |
|||
<br /><br /> |
|||
|
|||
### パワフルな内蔵コンポーネントでアプリケーションを設計し構築 |
|||
|
|||
Budibaseには、美しくデザインされた強力なコンポーネントが付属しており、それら使用しUIを簡単に構築することができます。また、CSSによるスタイリングオプションも豊富に用意されているので、よりクリエイティブな表現もも可能です。 |
|||
[Request new component](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas)。 |
|||
|
|||
<p align="center"> |
|||
<img alt="Budibase design" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970243/Out%20of%20beta%20launch/design-like-a-pro_qhlfeu.gif"> |
|||
</p> |
|||
<br /><br /> |
|||
|
|||
### プロセスを自動化し、ほかのツールと連携し、Webhookをでつながる! |
|||
定型化した作業を自動化して時間を節約しましょう。Webhookに接続、Eメールの自動送信など、すべてBudibaseに任せましょう。 こちらで簡単に [新しいオートメーションを作る](https://github.com/Budibase/automations)または[新しいオートメーションをリクエストすることができます](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas)。 |
|||
|
|||
<p align="center"> |
|||
<img alt="Budibase automations" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970486/Out%20of%20beta%20launch/automation_riro7u.png"> |
|||
</p> |
|||
<br /><br /> |
|||
|
|||
### 使い親しんだツールとの統合 |
|||
Budibaseは多くの人気ツールと統合されており、あなたのニーズに合わせたパーフェクトなアプリケーションを構築することができます。 |
|||
|
|||
<p align="center"> |
|||
<img alt="Budibase integrations" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970242/Out%20of%20beta%20launch/integrations_kc7dqt.png"> |
|||
</p> |
|||
<br /><br /> |
|||
|
|||
### 管理者のパラダイス |
|||
Budibaseはどんな規模のプロジェクトにも柔軟に対応できます。Budibaseを使えば、個人または組織のサーバーでセルフホスティングし、ユーザー、オンボーディング、SMTP、アプリ、グループ、テーマなどをひとまとめに管理することが可能です。また、ユーザーやグループにアプリポータルを提供し、グループ管理者にユーザー管理を委ねることも可能です。 |
|||
- プロモーションビデオを視聴する: https://youtu.be/xoljVpty_Kw |
|||
|
|||
<br /><br /><br /> |
|||
|
|||
## 🏁 始めましょう |
|||
|
|||
<a href="https://docs.budibase.com/self-hosting/self-host"><img src="https://res.cloudinary.com/daog6scxm/image/upload/v1634808888/logo/deploy_npl9za.png" /></a> |
|||
|
|||
Docker、KubernetesもしくはDegital Oceanを使用しセルフホスティングするか、セルフホスティングに困難がある、もしくは今すぐ開始したい場合はBudibase Cloudを使用しすぐに始めましょう。 |
|||
|
|||
### [Budibaseをセルフホスティングする](https://docs.budibase.com/self-hosting/self-host) |
|||
|
|||
### [Budibase Cloudを使用する](https://budibase.com) |
|||
|
|||
|
|||
<br /><br /> |
|||
|
|||
## 🎓 Budibaseを学ぶ |
|||
|
|||
Budibaseのドキュメント[はここです](https://docs.budibase.com)。 |
|||
<br /> |
|||
|
|||
|
|||
<br /><br /> |
|||
|
|||
## 💬 コミュニティ |
|||
|
|||
もし何か問題がある、もしくはBudibaseコミュニティのほかのユーザーと交流したいのであれば私たちの[Github discussions](https://github.com/Budibase/budibase/discussions)までお越しください。 |
|||
|
|||
<br /><br /><br /> |
|||
|
|||
|
|||
## ❗ 行動規範 |
|||
|
|||
Budibase は、すべての人を歓迎し、多様で、ハラスメントのない環境を提供することに尽力しています。Budibase コミュニティに参加するすべての人たちが私たちの[**行動規範**](https://github.com/Budibase/budibase/blob/HEAD/.github/CODE_OF_CONDUCT.md)を遵守していただくことお願いします。必ず読んでください。 |
|||
<br /> |
|||
|
|||
|
|||
<br /><br /> |
|||
|
|||
|
|||
## 🙌 Budibaseにコントリビュート |
|||
|
|||
|
|||
バグレポートからプルリクエストの作成まで、すべての貢献は感謝、そして歓迎されております。新しい新機能の実装やAPIの変更を計画している場合は、まずIssueを作成してください。これであなたの貴重な考えは私たちにも伝わり、無駄とはなりません。 |
|||
|
|||
### どこから始めるか混乱していますか? |
|||
ここはコントリビュートをはじめるための最適な場所です! [First time issues project](https://github.com/Budibase/budibase/projects/22). |
|||
|
|||
### リポジトリの構成 |
|||
Budibaseは、lernaによってmonorepo方式で管理されています。budibase パッケージのビルドと公開はlernaによって管理されています。Budibaseを構成するパッケージは以下の通り: |
|||
|
|||
- [packages/builder](https://github.com/Budibase/budibase/tree/HEAD/packages/builder) - budibase builder クライアントサイドのsvelteアプリケーションのコードが含まれています。 |
|||
|
|||
- [packages/client](https://github.com/Budibase/budibase/tree/HEAD/packages/client) - ブラウザ上で動作するモジュールで、JSONの定義を読み取り、そこから"生きている"Webアプリケーションを作成します。 |
|||
|
|||
- [packages/server](https://github.com/Budibase/budibase/tree/HEAD/packages/server) - budibaseのサーバーです。この Koa アプリは、builder アプリと budibase アプリの JS を提供し、データベースとファイル システムと対話するための API を提供する役割を担っています。 |
|||
|
|||
詳しくは[CONTRIBUTING.md](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md)をご覧ください。 |
|||
|
|||
<br /><br /> |
|||
|
|||
|
|||
## 📝 ライセンス |
|||
|
|||
Budibase はオープンソースであり、[GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html)ライセンスの下に公開されています。クライアントとコンポーネントライブラリは [MPL](https://directory.fsf.org/wiki/License:MPL-2.0)で公開されています - ですから、あなたが制作したアプリケーションはどのようなライセンスでも公開することができます。 |
|||
|
|||
<br /><br /> |
|||
|
|||
## ⭐ スター数の履歴 |
|||
|
|||
[](https://starchart.cc/Budibase/budibase) |
|||
|
|||
ビルダーのアップデートの間に問題が発生する場合は[ここ](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md#troubleshooting)を参考に環境をクリアにしてください。 |
|||
|
|||
<br /><br /> |
|||
|
|||
## Contributors ✨ |
|||
|
|||
すばらしい皆さまに感謝しかありません。([emoji key](https://allcontributors.org/docs/en/emoji-key)): |
|||
|
|||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section --> |
|||
<!-- prettier-ignore-start --> |
|||
<!-- markdownlint-disable --> |
|||
<table> |
|||
<tr> |
|||
<td align="center"><a href="http://martinmck.com"><img src="https://avatars1.githubusercontent.com/u/11256663?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Martin McKeaveney</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=shogunpurple" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=shogunpurple" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=shogunpurple" title="Tests">⚠️</a> <a href="#infra-shogunpurple" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td> |
|||
<td align="center"><a href="http://www.michaeldrury.co.uk/"><img src="https://avatars2.githubusercontent.com/u/4407001?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Michael Drury</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=mike12345567" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=mike12345567" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=mike12345567" title="Tests">⚠️</a> <a href="#infra-mike12345567" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td> |
|||
<td align="center"><a href="https://github.com/aptkingston"><img src="https://avatars3.githubusercontent.com/u/9075550?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Andrew Kingston</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=aptkingston" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=aptkingston" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=aptkingston" title="Tests">⚠️</a> <a href="#design-aptkingston" title="Design">🎨</a></td> |
|||
<td align="center"><a href="https://budibase.com/"><img src="https://avatars3.githubusercontent.com/u/3524181?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Michael Shanks</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=mjashanks" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=mjashanks" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=mjashanks" title="Tests">⚠️</a></td> |
|||
<td align="center"><a href="https://github.com/kevmodrome"><img src="https://avatars3.githubusercontent.com/u/534488?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Kevin Åberg Kultalahti</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=kevmodrome" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=kevmodrome" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=kevmodrome" title="Tests">⚠️</a></td> |
|||
<td align="center"><a href="https://www.budibase.com/"><img src="https://avatars2.githubusercontent.com/u/49767913?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Joe</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=joebudi" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=joebudi" title="Code">💻</a> <a href="#content-joebudi" title="Content">🖋</a> <a href="#design-joebudi" title="Design">🎨</a></td> |
|||
<td align="center"><a href="https://github.com/Rory-Powell"><img src="https://avatars.githubusercontent.com/u/8755148?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Rory Powell</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Tests">⚠️</a></td> |
|||
</tr> |
|||
<tr> |
|||
<td align="center"><a href="https://github.com/PClmnt"><img src="https://avatars.githubusercontent.com/u/5665926?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Peter Clement</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=PClmnt" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=PClmnt" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=PClmnt" title="Tests">⚠️</a></td> |
|||
<td align="center"><a href="https://github.com/Conor-Mack"><img src="https://avatars1.githubusercontent.com/u/36074859?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Conor_Mack</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=Conor-Mack" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=Conor-Mack" title="Tests">⚠️</a></td> |
|||
<td align="center"><a href="https://github.com/pngwn"><img src="https://avatars1.githubusercontent.com/u/12937446?v=4?s=100" width="100px;" alt=""/><br /><sub><b>pngwn</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=pngwn" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=pngwn" title="Tests">⚠️</a></td> |
|||
<td align="center"><a href="https://github.com/HugoLd"><img src="https://avatars0.githubusercontent.com/u/26521848?v=4?s=100" width="100px;" alt=""/><br /><sub><b>HugoLd</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=HugoLd" title="Code">💻</a></td> |
|||
<td align="center"><a href="https://github.com/victoriasloan"><img src="https://avatars.githubusercontent.com/u/9913651?v=4?s=100" width="100px;" alt=""/><br /><sub><b>victoriasloan</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=victoriasloan" title="Code">💻</a></td> |
|||
<td align="center"><a href="https://github.com/yashank09"><img src="https://avatars.githubusercontent.com/u/37672190?v=4?s=100" width="100px;" alt=""/><br /><sub><b>yashank09</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=yashank09" title="Code">💻</a></td> |
|||
<td align="center"><a href="https://github.com/SOVLOOKUP"><img src="https://avatars.githubusercontent.com/u/53158137?v=4?s=100" width="100px;" alt=""/><br /><sub><b>SOVLOOKUP</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=SOVLOOKUP" title="Code">💻</a></td> |
|||
<td align="center"><a href="https://github.com/seoulaja"><img src="https://avatars.githubusercontent.com/u/15101654?v=4?s=100" width="100px;" alt=""/><br /><sub><b>seoulaja</b></sub></a><br /><a href="#translation-seoulaja" title="Translation">🌍</a></td> |
|||
<td align="center"><a href="https://github.com/mslourens"><img src="https://avatars.githubusercontent.com/u/1907152?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Maurits Lourens</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=mslourens" title="Tests">⚠️</a> <a href="https://github.com/Budibase/budibase/commits?author=mslourens" title="Code">💻</a></td> |
|||
</tr> |
|||
</table> |
|||
|
|||
<!-- markdownlint-restore --> |
|||
<!-- prettier-ignore-end --> |
|||
|
|||
<!-- ALL-CONTRIBUTORS-LIST:END --> |
|||
|
|||
このプロジェクトは、[all-contributors](https://github.com/all-contributors/all-contributors)仕様に準拠しています。どのような貢献でも歓迎します。 |
|||
|
|||
@ -0,0 +1,17 @@ |
|||
const { |
|||
getAppDB, |
|||
getDevAppDB, |
|||
getProdAppDB, |
|||
getAppId, |
|||
updateAppId, |
|||
doInAppContext, |
|||
} = require("./src/context") |
|||
|
|||
module.exports = { |
|||
getAppDB, |
|||
getDevAppDB, |
|||
getProdAppDB, |
|||
getAppId, |
|||
updateAppId, |
|||
doInAppContext, |
|||
} |
|||
@ -1,4 +1,6 @@ |
|||
module.exports = { |
|||
...require("./src/db/utils"), |
|||
...require("./src/db/constants"), |
|||
...require("./src/db"), |
|||
...require("./src/db/views"), |
|||
} |
|||
|
|||
@ -1 +1 @@ |
|||
module.exports = require("./src/tenancy/deprovision") |
|||
module.exports = require("./src/context/deprovision") |
|||
|
|||
@ -1,6 +1,6 @@ |
|||
const { getGlobalUserParams, getAllApps } = require("../db/utils") |
|||
const { getDB, getCouch } = require("../db") |
|||
const { getGlobalDB } = require("./tenancy") |
|||
const { getGlobalDB } = require("../tenancy") |
|||
const { StaticDatabases } = require("../db/constants") |
|||
|
|||
const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants |
|||
@ -0,0 +1,195 @@ |
|||
const env = require("../environment") |
|||
const { Headers } = require("../../constants") |
|||
const cls = require("./FunctionContext") |
|||
const { getCouch } = require("../db") |
|||
const { getProdAppID, getDevelopmentAppID } = require("../db/conversions") |
|||
const { isEqual } = require("lodash") |
|||
|
|||
// some test cases call functions directly, need to
|
|||
// store an app ID to pretend there is a context
|
|||
let TEST_APP_ID = null |
|||
|
|||
const ContextKeys = { |
|||
TENANT_ID: "tenantId", |
|||
APP_ID: "appId", |
|||
// whatever the request app DB was
|
|||
CURRENT_DB: "currentDb", |
|||
// get the prod app DB from the request
|
|||
PROD_DB: "prodDb", |
|||
// get the dev app DB from the request
|
|||
DEV_DB: "devDb", |
|||
DB_OPTS: "dbOpts", |
|||
} |
|||
|
|||
exports.DEFAULT_TENANT_ID = "default" |
|||
|
|||
exports.isDefaultTenant = () => { |
|||
return exports.getTenantId() === exports.DEFAULT_TENANT_ID |
|||
} |
|||
|
|||
exports.isMultiTenant = () => { |
|||
return env.MULTI_TENANCY |
|||
} |
|||
|
|||
// used for automations, API endpoints should always be in context already
|
|||
exports.doInTenant = (tenantId, task) => { |
|||
return cls.run(() => { |
|||
// set the tenant id
|
|||
cls.setOnContext(ContextKeys.TENANT_ID, tenantId) |
|||
|
|||
// invoke the task
|
|||
return task() |
|||
}) |
|||
} |
|||
|
|||
exports.doInAppContext = (appId, task) => { |
|||
return cls.run(() => { |
|||
// set the app ID
|
|||
cls.setOnContext(ContextKeys.APP_ID, appId) |
|||
|
|||
// invoke the task
|
|||
return task() |
|||
}) |
|||
} |
|||
|
|||
exports.updateTenantId = tenantId => { |
|||
cls.setOnContext(ContextKeys.TENANT_ID, tenantId) |
|||
} |
|||
|
|||
exports.updateAppId = appId => { |
|||
try { |
|||
cls.setOnContext(ContextKeys.APP_ID, appId) |
|||
cls.setOnContext(ContextKeys.PROD_DB, null) |
|||
cls.setOnContext(ContextKeys.DEV_DB, null) |
|||
cls.setOnContext(ContextKeys.CURRENT_DB, null) |
|||
cls.setOnContext(ContextKeys.DB_OPTS, null) |
|||
} catch (err) { |
|||
if (env.isTest()) { |
|||
TEST_APP_ID = appId |
|||
} else { |
|||
throw err |
|||
} |
|||
} |
|||
} |
|||
|
|||
exports.setTenantId = ( |
|||
ctx, |
|||
opts = { allowQs: false, allowNoTenant: false } |
|||
) => { |
|||
let tenantId |
|||
// exit early if not multi-tenant
|
|||
if (!exports.isMultiTenant()) { |
|||
cls.setOnContext(ContextKeys.TENANT_ID, this.DEFAULT_TENANT_ID) |
|||
return |
|||
} |
|||
|
|||
const allowQs = opts && opts.allowQs |
|||
const allowNoTenant = opts && opts.allowNoTenant |
|||
const header = ctx.request.headers[Headers.TENANT_ID] |
|||
const user = ctx.user || {} |
|||
if (allowQs) { |
|||
const query = ctx.request.query || {} |
|||
tenantId = query.tenantId |
|||
} |
|||
// override query string (if allowed) by user, or header
|
|||
// URL params cannot be used in a middleware, as they are
|
|||
// processed later in the chain
|
|||
tenantId = user.tenantId || header || tenantId |
|||
|
|||
// Set the tenantId from the subdomain
|
|||
if (!tenantId) { |
|||
tenantId = ctx.subdomains && ctx.subdomains[0] |
|||
} |
|||
|
|||
if (!tenantId && !allowNoTenant) { |
|||
ctx.throw(403, "Tenant id not set") |
|||
} |
|||
// check tenant ID just incase no tenant was allowed
|
|||
if (tenantId) { |
|||
cls.setOnContext(ContextKeys.TENANT_ID, tenantId) |
|||
} |
|||
} |
|||
|
|||
exports.isTenantIdSet = () => { |
|||
const tenantId = cls.getFromContext(ContextKeys.TENANT_ID) |
|||
return !!tenantId |
|||
} |
|||
|
|||
exports.getTenantId = () => { |
|||
if (!exports.isMultiTenant()) { |
|||
return exports.DEFAULT_TENANT_ID |
|||
} |
|||
const tenantId = cls.getFromContext(ContextKeys.TENANT_ID) |
|||
if (!tenantId) { |
|||
throw Error("Tenant id not found") |
|||
} |
|||
return tenantId |
|||
} |
|||
|
|||
exports.getAppId = () => { |
|||
const foundId = cls.getFromContext(ContextKeys.APP_ID) |
|||
if (!foundId && env.isTest() && TEST_APP_ID) { |
|||
return TEST_APP_ID |
|||
} else { |
|||
return foundId |
|||
} |
|||
} |
|||
|
|||
function getDB(key, opts) { |
|||
const dbOptsKey = `${key}${ContextKeys.DB_OPTS}` |
|||
let storedOpts = cls.getFromContext(dbOptsKey) |
|||
let db = cls.getFromContext(key) |
|||
if (db && isEqual(opts, storedOpts)) { |
|||
return db |
|||
} |
|||
const appId = exports.getAppId() |
|||
const CouchDB = getCouch() |
|||
let toUseAppId |
|||
switch (key) { |
|||
case ContextKeys.CURRENT_DB: |
|||
toUseAppId = appId |
|||
break |
|||
case ContextKeys.PROD_DB: |
|||
toUseAppId = getProdAppID(appId) |
|||
break |
|||
case ContextKeys.DEV_DB: |
|||
toUseAppId = getDevelopmentAppID(appId) |
|||
break |
|||
} |
|||
db = new CouchDB(toUseAppId, opts) |
|||
try { |
|||
cls.setOnContext(key, db) |
|||
if (opts) { |
|||
cls.setOnContext(dbOptsKey, opts) |
|||
} |
|||
} catch (err) { |
|||
if (!env.isTest()) { |
|||
throw err |
|||
} |
|||
} |
|||
return db |
|||
} |
|||
|
|||
/** |
|||
* Opens the app database based on whatever the request |
|||
* contained, dev or prod. |
|||
*/ |
|||
exports.getAppDB = opts => { |
|||
return getDB(ContextKeys.CURRENT_DB, opts) |
|||
} |
|||
|
|||
/** |
|||
* This specifically gets the prod app ID, if the request |
|||
* contained a development app ID, this will open the prod one. |
|||
*/ |
|||
exports.getProdAppDB = opts => { |
|||
return getDB(ContextKeys.PROD_DB, opts) |
|||
} |
|||
|
|||
/** |
|||
* This specifically gets the dev app ID, if the request |
|||
* contained a prod app ID, this will open the dev one. |
|||
*/ |
|||
exports.getDevAppDB = opts => { |
|||
return getDB(ContextKeys.DEV_DB, opts) |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
const NO_APP_ERROR = "No app provided" |
|||
const { APP_DEV_PREFIX, APP_PREFIX } = require("./constants") |
|||
|
|||
exports.isDevAppID = appId => { |
|||
if (!appId) { |
|||
throw NO_APP_ERROR |
|||
} |
|||
return appId.startsWith(APP_DEV_PREFIX) |
|||
} |
|||
|
|||
exports.isProdAppID = appId => { |
|||
if (!appId) { |
|||
throw NO_APP_ERROR |
|||
} |
|||
return appId.startsWith(APP_PREFIX) && !exports.isDevAppID(appId) |
|||
} |
|||
|
|||
exports.isDevApp = app => { |
|||
if (!app) { |
|||
throw NO_APP_ERROR |
|||
} |
|||
return exports.isDevAppID(app.appId) |
|||
} |
|||
|
|||
/** |
|||
* Convert a development app ID to a deployed app ID. |
|||
*/ |
|||
exports.getProdAppID = appId => { |
|||
// if dev, convert it
|
|||
if (appId.startsWith(APP_DEV_PREFIX)) { |
|||
const id = appId.split(APP_DEV_PREFIX)[1] |
|||
return `${APP_PREFIX}${id}` |
|||
} |
|||
return appId |
|||
} |
|||
|
|||
/** |
|||
* Convert a deployed app ID to a development app ID. |
|||
*/ |
|||
exports.getDevelopmentAppID = appId => { |
|||
if (!appId.startsWith(APP_DEV_PREFIX)) { |
|||
const id = appId.split(APP_PREFIX)[1] |
|||
return `${APP_DEV_PREFIX}${id}` |
|||
} |
|||
return appId |
|||
} |
|||
@ -0,0 +1,78 @@ |
|||
const { Headers } = require("../constants") |
|||
const { buildMatcherRegex, matches } = require("./matchers") |
|||
|
|||
/** |
|||
* GET, HEAD and OPTIONS methods are considered safe operations |
|||
* |
|||
* POST, PUT, PATCH, and DELETE methods, being state changing verbs, |
|||
* should have a CSRF token attached to the request |
|||
*/ |
|||
const EXCLUDED_METHODS = ["GET", "HEAD", "OPTIONS"] |
|||
|
|||
/** |
|||
* There are only three content type values that can be used in cross domain requests. |
|||
* If any other value is used, e.g. application/json, the browser will first make a OPTIONS |
|||
* request which will be protected by CORS. |
|||
*/ |
|||
const INCLUDED_CONTENT_TYPES = [ |
|||
"application/x-www-form-urlencoded", |
|||
"multipart/form-data", |
|||
"text/plain", |
|||
] |
|||
|
|||
/** |
|||
* Validate the CSRF token generated aganst the user session. |
|||
* Compare the token with the x-csrf-token header. |
|||
* |
|||
* If the token is not found within the request or the value provided |
|||
* does not match the value within the user session, the request is rejected. |
|||
* |
|||
* CSRF protection provided using the 'Synchronizer Token Pattern' |
|||
* https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#synchronizer-token-pattern
|
|||
* |
|||
*/ |
|||
module.exports = (opts = { noCsrfPatterns: [] }) => { |
|||
const noCsrfOptions = buildMatcherRegex(opts.noCsrfPatterns) |
|||
return async (ctx, next) => { |
|||
// don't apply for excluded paths
|
|||
const found = matches(ctx, noCsrfOptions) |
|||
if (found) { |
|||
return next() |
|||
} |
|||
|
|||
// don't apply for the excluded http methods
|
|||
if (EXCLUDED_METHODS.indexOf(ctx.method) !== -1) { |
|||
return next() |
|||
} |
|||
|
|||
// don't apply when the content type isn't supported
|
|||
let contentType = ctx.get("content-type") |
|||
? ctx.get("content-type").toLowerCase() |
|||
: "" |
|||
if ( |
|||
!INCLUDED_CONTENT_TYPES.filter(type => contentType.includes(type)).length |
|||
) { |
|||
return next() |
|||
} |
|||
|
|||
// don't apply csrf when the internal api key has been used
|
|||
if (ctx.internal) { |
|||
return next() |
|||
} |
|||
|
|||
// apply csrf when there is a token in the session (new logins)
|
|||
// in future there should be a hard requirement that the token is present
|
|||
const userToken = ctx.user.csrfToken |
|||
if (!userToken) { |
|||
return next() |
|||
} |
|||
|
|||
// reject if no token in request or mismatch
|
|||
const requestToken = ctx.get(Headers.CSRF_TOKEN) |
|||
if (!requestToken || requestToken !== userToken) { |
|||
ctx.throw(403, "Invalid CSRF token") |
|||
} |
|||
|
|||
return next() |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
const env = require("../environment") |
|||
const { Headers } = require("../constants") |
|||
|
|||
/** |
|||
* API Key only endpoint. |
|||
*/ |
|||
module.exports = async (ctx, next) => { |
|||
const apiKey = ctx.request.headers[Headers.API_KEY] |
|||
if (apiKey !== env.INTERNAL_API_KEY) { |
|||
ctx.throw(403, "Unauthorized") |
|||
} |
|||
|
|||
return next() |
|||
} |
|||
@ -0,0 +1,76 @@ |
|||
const { getScopedConfig } = require("../../../db/utils") |
|||
const { getGlobalDB } = require("../../../tenancy") |
|||
const google = require("../google") |
|||
const { Configs, Cookies } = require("../../../constants") |
|||
const { clearCookie, getCookie } = require("../../../utils") |
|||
const { getDB } = require("../../../db") |
|||
|
|||
async function preAuth(passport, ctx, next) { |
|||
const db = getGlobalDB() |
|||
// get the relevant config
|
|||
const config = await getScopedConfig(db, { |
|||
type: Configs.GOOGLE, |
|||
workspace: ctx.query.workspace, |
|||
}) |
|||
const publicConfig = await getScopedConfig(db, { |
|||
type: Configs.SETTINGS, |
|||
}) |
|||
let callbackUrl = `${publicConfig.platformUrl}/api/global/auth/datasource/google/callback` |
|||
const strategy = await google.strategyFactory(config, callbackUrl) |
|||
|
|||
if (!ctx.query.appId || !ctx.query.datasourceId) { |
|||
ctx.throw(400, "appId and datasourceId query params not present.") |
|||
} |
|||
|
|||
return passport.authenticate(strategy, { |
|||
scope: ["profile", "email", "https://www.googleapis.com/auth/spreadsheets"], |
|||
accessType: "offline", |
|||
prompt: "consent", |
|||
})(ctx, next) |
|||
} |
|||
|
|||
async function postAuth(passport, ctx, next) { |
|||
const db = getGlobalDB() |
|||
|
|||
const config = await getScopedConfig(db, { |
|||
type: Configs.GOOGLE, |
|||
workspace: ctx.query.workspace, |
|||
}) |
|||
|
|||
const publicConfig = await getScopedConfig(db, { |
|||
type: Configs.SETTINGS, |
|||
}) |
|||
|
|||
let callbackUrl = `${publicConfig.platformUrl}/api/global/auth/datasource/google/callback` |
|||
const strategy = await google.strategyFactory( |
|||
config, |
|||
callbackUrl, |
|||
(accessToken, refreshToken, profile, done) => { |
|||
clearCookie(ctx, Cookies.DatasourceAuth) |
|||
done(null, { accessToken, refreshToken }) |
|||
} |
|||
) |
|||
|
|||
const authStateCookie = getCookie(ctx, Cookies.DatasourceAuth) |
|||
|
|||
return passport.authenticate( |
|||
strategy, |
|||
{ successRedirect: "/", failureRedirect: "/error" }, |
|||
async (err, tokens) => { |
|||
// update the DB for the datasource with all the user info
|
|||
const db = getDB(authStateCookie.appId) |
|||
const datasource = await db.get(authStateCookie.datasourceId) |
|||
if (!datasource.config) { |
|||
datasource.config = {} |
|||
} |
|||
datasource.config.auth = { type: "google", ...tokens } |
|||
await db.put(datasource) |
|||
ctx.redirect( |
|||
`/builder/app/${authStateCookie.appId}/data/datasource/${authStateCookie.datasourceId}` |
|||
) |
|||
} |
|||
)(ctx, next) |
|||
} |
|||
|
|||
exports.preAuth = preAuth |
|||
exports.postAuth = postAuth |
|||
@ -1,84 +0,0 @@ |
|||
const env = require("../environment") |
|||
const { Headers } = require("../../constants") |
|||
const cls = require("./FunctionContext") |
|||
|
|||
exports.DEFAULT_TENANT_ID = "default" |
|||
|
|||
exports.isDefaultTenant = () => { |
|||
return exports.getTenantId() === exports.DEFAULT_TENANT_ID |
|||
} |
|||
|
|||
exports.isMultiTenant = () => { |
|||
return env.MULTI_TENANCY |
|||
} |
|||
|
|||
const TENANT_ID = "tenantId" |
|||
|
|||
// used for automations, API endpoints should always be in context already
|
|||
exports.doInTenant = (tenantId, task) => { |
|||
return cls.run(() => { |
|||
// set the tenant id
|
|||
cls.setOnContext(TENANT_ID, tenantId) |
|||
|
|||
// invoke the task
|
|||
return task() |
|||
}) |
|||
} |
|||
|
|||
exports.updateTenantId = tenantId => { |
|||
cls.setOnContext(TENANT_ID, tenantId) |
|||
} |
|||
|
|||
exports.setTenantId = ( |
|||
ctx, |
|||
opts = { allowQs: false, allowNoTenant: false } |
|||
) => { |
|||
let tenantId |
|||
// exit early if not multi-tenant
|
|||
if (!exports.isMultiTenant()) { |
|||
cls.setOnContext(TENANT_ID, this.DEFAULT_TENANT_ID) |
|||
return |
|||
} |
|||
|
|||
const allowQs = opts && opts.allowQs |
|||
const allowNoTenant = opts && opts.allowNoTenant |
|||
const header = ctx.request.headers[Headers.TENANT_ID] |
|||
const user = ctx.user || {} |
|||
if (allowQs) { |
|||
const query = ctx.request.query || {} |
|||
tenantId = query.tenantId |
|||
} |
|||
// override query string (if allowed) by user, or header
|
|||
// URL params cannot be used in a middleware, as they are
|
|||
// processed later in the chain
|
|||
tenantId = user.tenantId || header || tenantId |
|||
|
|||
// Set the tenantId from the subdomain
|
|||
if (!tenantId) { |
|||
tenantId = ctx.subdomains && ctx.subdomains[0] |
|||
} |
|||
|
|||
if (!tenantId && !allowNoTenant) { |
|||
ctx.throw(403, "Tenant id not set") |
|||
} |
|||
// check tenant ID just incase no tenant was allowed
|
|||
if (tenantId) { |
|||
cls.setOnContext(TENANT_ID, tenantId) |
|||
} |
|||
} |
|||
|
|||
exports.isTenantIdSet = () => { |
|||
const tenantId = cls.getFromContext(TENANT_ID) |
|||
return !!tenantId |
|||
} |
|||
|
|||
exports.getTenantId = () => { |
|||
if (!exports.isMultiTenant()) { |
|||
return exports.DEFAULT_TENANT_ID |
|||
} |
|||
const tenantId = cls.getFromContext(TENANT_ID) |
|||
if (!tenantId) { |
|||
throw Error("Tenant id not found") |
|||
} |
|||
return tenantId |
|||
} |
|||
@ -1,4 +1,4 @@ |
|||
module.exports = { |
|||
...require("./context"), |
|||
...require("../context"), |
|||
...require("./tenancy"), |
|||
} |
|||
|
|||
@ -0,0 +1,42 @@ |
|||
<script> |
|||
import MarkdownEditor from "../../Markdown/MarkdownEditor.svelte" |
|||
|
|||
export let value = "" |
|||
export let placeholder = null |
|||
export let disabled = false |
|||
export let error = null |
|||
export let height = null |
|||
export let id = null |
|||
export let fullScreenOffset = null |
|||
export let easyMDEOptions = null |
|||
</script> |
|||
|
|||
<div class:error> |
|||
<MarkdownEditor |
|||
{value} |
|||
{placeholder} |
|||
{height} |
|||
{id} |
|||
{fullScreenOffset} |
|||
{disabled} |
|||
{easyMDEOptions} |
|||
on:change |
|||
/> |
|||
</div> |
|||
|
|||
<style> |
|||
.error :global(.EasyMDEContainer .editor-toolbar) { |
|||
border-top-color: var(--spectrum-semantic-negative-color-default); |
|||
border-left-color: var(--spectrum-semantic-negative-color-default); |
|||
border-right-color: var(--spectrum-semantic-negative-color-default); |
|||
} |
|||
.error :global(.EasyMDEContainer .CodeMirror) { |
|||
border-bottom-color: var(--spectrum-semantic-negative-color-default); |
|||
border-left-color: var(--spectrum-semantic-negative-color-default); |
|||
border-right-color: var(--spectrum-semantic-negative-color-default); |
|||
} |
|||
.error :global(.EasyMDEContainer .editor-preview-side) { |
|||
border-bottom-color: var(--spectrum-semantic-negative-color-default); |
|||
border-right-color: var(--spectrum-semantic-negative-color-default); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,36 @@ |
|||
<script> |
|||
import Field from "./Field.svelte" |
|||
import RichTextField from "./Core/RichTextField.svelte" |
|||
import { createEventDispatcher } from "svelte" |
|||
|
|||
export let value = null |
|||
export let label = null |
|||
export let labelPosition = "above" |
|||
export let placeholder = null |
|||
export let disabled = false |
|||
export let error = null |
|||
export let height = null |
|||
export let id = null |
|||
export let fullScreenOffset = null |
|||
export let easyMDEOptions = null |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
const onChange = e => { |
|||
value = e.detail |
|||
dispatch("change", e.detail) |
|||
} |
|||
</script> |
|||
|
|||
<Field {label} {labelPosition} {error}> |
|||
<RichTextField |
|||
{error} |
|||
{disabled} |
|||
{value} |
|||
{placeholder} |
|||
{height} |
|||
{id} |
|||
{fullScreenOffset} |
|||
{easyMDEOptions} |
|||
on:change={onChange} |
|||
/> |
|||
</Field> |
|||
@ -1,73 +1,20 @@ |
|||
<script> |
|||
import "@spectrum-css/fieldlabel/dist/index-vars.css" |
|||
import Tooltip from "../Tooltip/Tooltip.svelte" |
|||
import Icon from "../Icon/Icon.svelte" |
|||
import TooltipWrapper from "../Tooltip/TooltipWrapper.svelte" |
|||
|
|||
export let size = "M" |
|||
export let tooltip = "" |
|||
export let showTooltip = false |
|||
</script> |
|||
|
|||
{#if tooltip} |
|||
<div class="container"> |
|||
<label |
|||
for="" |
|||
class={`spectrum-FieldLabel spectrum-FieldLabel--size${size}`} |
|||
> |
|||
<slot /> |
|||
</label> |
|||
<div class="icon-container"> |
|||
<div |
|||
class="icon" |
|||
class:icon-small={size === "M" || size === "S"} |
|||
on:mouseover={() => (showTooltip = true)} |
|||
on:mouseleave={() => (showTooltip = false)} |
|||
> |
|||
<Icon name="InfoOutline" size="S" disabled={true} /> |
|||
</div> |
|||
{#if showTooltip} |
|||
<div class="tooltip"> |
|||
<Tooltip textWrapping={true} direction={"bottom"} text={tooltip} /> |
|||
</div> |
|||
{/if} |
|||
</div> |
|||
</div> |
|||
{:else} |
|||
<TooltipWrapper {tooltip} {size}> |
|||
<label for="" class={`spectrum-FieldLabel spectrum-FieldLabel--size${size}`}> |
|||
<slot /> |
|||
</label> |
|||
{/if} |
|||
</TooltipWrapper> |
|||
|
|||
<style> |
|||
label { |
|||
padding: 0; |
|||
white-space: nowrap; |
|||
} |
|||
.container { |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
.icon-container { |
|||
position: relative; |
|||
display: flex; |
|||
justify-content: center; |
|||
margin-top: 1px; |
|||
margin-left: 5px; |
|||
margin-right: 5px; |
|||
} |
|||
.tooltip { |
|||
position: absolute; |
|||
display: flex; |
|||
justify-content: center; |
|||
top: 15px; |
|||
z-index: 1; |
|||
width: 160px; |
|||
} |
|||
.icon { |
|||
transform: scale(0.75); |
|||
} |
|||
.icon-small { |
|||
margin-top: -2px; |
|||
margin-bottom: -5px; |
|||
} |
|||
</style> |
|||
|
|||
@ -0,0 +1,60 @@ |
|||
<script> |
|||
import SpectrumMDE from "./SpectrumMDE.svelte" |
|||
import { createEventDispatcher } from "svelte" |
|||
|
|||
export let value = null |
|||
export let height = null |
|||
export let placeholder = null |
|||
export let id = null |
|||
export let fullScreenOffset = 0 |
|||
export let disabled = false |
|||
export let easyMDEOptions |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
|
|||
let latestValue |
|||
let mde |
|||
|
|||
// Ensure the value is updated if the value prop changes outside the editor's |
|||
// control |
|||
$: checkValue(value) |
|||
$: mde?.codemirror.on("change", debouncedUpdate) |
|||
|
|||
const checkValue = val => { |
|||
if (mde && val !== latestValue) { |
|||
mde.value(val) |
|||
} |
|||
} |
|||
|
|||
const debounce = (fn, interval) => { |
|||
let timeout |
|||
return () => { |
|||
clearTimeout(timeout) |
|||
timeout = setTimeout(fn, interval) |
|||
} |
|||
} |
|||
|
|||
const update = () => { |
|||
latestValue = mde.value() |
|||
dispatch("change", latestValue) |
|||
} |
|||
|
|||
// Debounce the update function to avoid spamming it constantly |
|||
const debouncedUpdate = debounce(update, 250) |
|||
</script> |
|||
|
|||
{#key height} |
|||
<SpectrumMDE |
|||
bind:mde |
|||
scroll={true} |
|||
{height} |
|||
{id} |
|||
{fullScreenOffset} |
|||
{disabled} |
|||
easyMDEOptions={{ |
|||
initialValue: value, |
|||
placeholder, |
|||
...easyMDEOptions, |
|||
}} |
|||
/> |
|||
{/key} |
|||
@ -0,0 +1,70 @@ |
|||
<script> |
|||
import SpectrumMDE from "./SpectrumMDE.svelte" |
|||
|
|||
export let value |
|||
export let height |
|||
|
|||
let mde |
|||
|
|||
// Keep the value up to date |
|||
$: mde && mde.value(value || "") |
|||
$: { |
|||
if (mde && !mde.isPreviewActive()) { |
|||
mde.togglePreview() |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<div class="markdown-viewer" style="height:{height};"> |
|||
<SpectrumMDE |
|||
bind:mde |
|||
scroll={false} |
|||
easyMDEOptions={{ |
|||
initialValue: value, |
|||
toolbar: false, |
|||
}} |
|||
/> |
|||
</div> |
|||
|
|||
<style> |
|||
.markdown-viewer { |
|||
overflow: auto; |
|||
} |
|||
/* Remove padding, borders and background colors */ |
|||
.markdown-viewer :global(.editor-preview) { |
|||
padding: 0; |
|||
border: none; |
|||
background: transparent; |
|||
} |
|||
.markdown-viewer :global(.CodeMirror) { |
|||
border: none; |
|||
background: transparent; |
|||
padding: 0; |
|||
} |
|||
.markdown-viewer :global(.EasyMDEContainer) { |
|||
background: transparent; |
|||
} |
|||
/* Hide the actual code editor */ |
|||
.markdown-viewer :global(.CodeMirror-scroll) { |
|||
display: none; |
|||
} |
|||
/*Hide the scrollbar*/ |
|||
.markdown-viewer :global(.CodeMirror-vscrollbar) { |
|||
display: none !important; |
|||
} |
|||
/*Position relatively so we only consume whatever space we need */ |
|||
.markdown-viewer :global(.editor-preview-full) { |
|||
position: relative; |
|||
} |
|||
/* Remove margin on the first and last components to fully trim the preview */ |
|||
.markdown-viewer :global(.editor-preview-full > :first-child) { |
|||
margin-top: 0; |
|||
} |
|||
.markdown-viewer :global(.editor-preview-full > :last-child) { |
|||
margin-bottom: 0; |
|||
} |
|||
/* Code blocks in preview */ |
|||
.markdown-viewer :global(.editor-preview-full pre) { |
|||
background: var(--spectrum-global-color-gray-200); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,184 @@ |
|||
<script> |
|||
import EasyMDE from "easymde" |
|||
import "easymde/dist/easymde.min.css" |
|||
import { onMount } from "svelte" |
|||
|
|||
export let height = null |
|||
export let scroll = true |
|||
export let easyMDEOptions = null |
|||
export let mde = null |
|||
export let id = null |
|||
export let fullScreenOffset = null |
|||
export let disabled = false |
|||
|
|||
let element |
|||
|
|||
onMount(() => { |
|||
height = height || "200px" |
|||
mde = new EasyMDE({ |
|||
element, |
|||
spellChecker: false, |
|||
status: false, |
|||
unorderedListStyle: "-", |
|||
maxHeight: scroll ? height : undefined, |
|||
minHeight: scroll ? undefined : height, |
|||
...easyMDEOptions, |
|||
}) |
|||
|
|||
// Revert the editor when we unmount |
|||
return () => { |
|||
mde.toTextArea() |
|||
} |
|||
}) |
|||
|
|||
$: styleString = getStyleString(fullScreenOffset) |
|||
|
|||
const getStyleString = offset => { |
|||
let string = "" |
|||
string += `--fullscreen-offset-x:${offset?.x || "0px"};` |
|||
string += `--fullscreen-offset-y:${offset?.y || "0px"};` |
|||
return string |
|||
} |
|||
</script> |
|||
|
|||
<div class:disabled style={styleString}> |
|||
<textarea disabled {id} bind:this={element} /> |
|||
</div> |
|||
|
|||
<style> |
|||
/* Disabled styles */ |
|||
.disabled :global(textarea) { |
|||
display: none; |
|||
} |
|||
.disabled :global(.CodeMirror-cursor) { |
|||
display: none; |
|||
} |
|||
.disabled :global(.EasyMDEContainer) { |
|||
pointer-events: none; |
|||
} |
|||
.disabled :global(.editor-toolbar button i) { |
|||
color: var(--spectrum-global-color-gray-400); |
|||
} |
|||
.disabled :global(.CodeMirror) { |
|||
color: var(--spectrum-global-color-gray-600); |
|||
} |
|||
|
|||
/* Toolbar container */ |
|||
:global(.EasyMDEContainer .editor-toolbar) { |
|||
background: var(--spectrum-global-color-gray-50); |
|||
border-top: 1px solid var(--spectrum-alias-border-color); |
|||
border-left: 1px solid var(--spectrum-alias-border-color); |
|||
border-right: 1px solid var(--spectrum-alias-border-color); |
|||
} |
|||
/* Main code mirror instance and default color */ |
|||
:global(.EasyMDEContainer .CodeMirror) { |
|||
border: 1px solid var(--spectrum-alias-border-color); |
|||
background: var(--spectrum-global-color-gray-50); |
|||
color: var(--spectrum-alias-text-color); |
|||
} |
|||
/* Toolbar button active state */ |
|||
:global(.EasyMDEContainer .editor-toolbar button.active) { |
|||
background: var(--spectrum-global-color-gray-200); |
|||
border-color: var(--spectrum-global-color-gray-400); |
|||
} |
|||
/* Toolbar button hover state */ |
|||
:global(.EasyMDEContainer .editor-toolbar button:hover) { |
|||
background: var(--spectrum-global-color-gray-200); |
|||
border-color: var(--spectrum-global-color-gray-400); |
|||
} |
|||
/* Toolbar button color */ |
|||
:global(.EasyMDEContainer .editor-toolbar button i) { |
|||
color: var(--spectrum-global-color-gray-800); |
|||
} |
|||
/* Separator between toolbar buttons*/ |
|||
:global(.EasyMDEContainer .editor-toolbar i.separator) { |
|||
border-color: var(--spectrum-global-color-gray-300); |
|||
} |
|||
/* Cursor */ |
|||
:global(.EasyMDEContainer .CodeMirror-cursor) { |
|||
border-color: var(--spectrum-alias-text-color); |
|||
} |
|||
/* Text selections */ |
|||
:global(.EasyMDEContainer .CodeMirror-selectedtext) { |
|||
background: var(--spectrum-global-color-gray-400) !important; |
|||
} |
|||
/* Background of lines containing selected text */ |
|||
:global(.EasyMDEContainer .CodeMirror-selected) { |
|||
background: var(--spectrum-global-color-gray-400) !important; |
|||
} |
|||
/* Color of text for images and links */ |
|||
:global(.EasyMDEContainer .cm-s-easymde .cm-link) { |
|||
color: var(--spectrum-global-color-gray-600); |
|||
} |
|||
/* Color of URL for images and links */ |
|||
:global(.EasyMDEContainer .cm-s-easymde .cm-url) { |
|||
color: var(--spectrum-global-color-gray-500); |
|||
} |
|||
/* Full preview window */ |
|||
:global(.EasyMDEContainer .editor-preview) { |
|||
background: var(--spectrum-global-color-gray-50); |
|||
} |
|||
/* Side by side preview window */ |
|||
:global(.EasyMDEContainer .editor-preview) { |
|||
border: 1px solid var(--spectrum-alias-border-color); |
|||
} |
|||
/* Code blocks in editor */ |
|||
:global(.EasyMDEContainer .cm-s-easymde .cm-comment) { |
|||
background: var(--spectrum-global-color-gray-100); |
|||
} |
|||
/* Code blocks in preview */ |
|||
:global(.EasyMDEContainer pre) { |
|||
background: var(--spectrum-global-color-gray-100); |
|||
padding: 4px; |
|||
border-radius: 4px; |
|||
} |
|||
:global(.EasyMDEContainer code) { |
|||
color: #e83e8c; |
|||
} |
|||
:global(.EasyMDEContainer pre code) { |
|||
color: var(--spectrum-alias-text-color); |
|||
} |
|||
/* Block quotes */ |
|||
:global(.EasyMDEContainer blockquote) { |
|||
border-left: 4px solid var(--spectrum-global-color-gray-400); |
|||
color: var(--spectrum-global-color-gray-700); |
|||
margin-left: 0; |
|||
padding-left: 20px; |
|||
} |
|||
/* HR's */ |
|||
:global(.EasyMDEContainer hr) { |
|||
background-color: var(--spectrum-global-color-gray-300); |
|||
border: none; |
|||
height: 2px; |
|||
} |
|||
/* Tables */ |
|||
:global(.EasyMDEContainer td, .EasyMDEContainer th) { |
|||
border-color: var(--spectrum-alias-border-color) !important; |
|||
} |
|||
/* Links */ |
|||
:global(.EasyMDEContainer a) { |
|||
color: var(--primaryColor); |
|||
} |
|||
:global(.EasyMDEContainer a:hover) { |
|||
color: var(--primaryColorHover); |
|||
} |
|||
/* Allow full screen offset */ |
|||
:global(.EasyMDEContainer .editor-toolbar.fullscreen) { |
|||
left: var(--fullscreen-offset-x); |
|||
top: var(--fullscreen-offset-y); |
|||
} |
|||
:global(.EasyMDEContainer .CodeMirror-fullscreen) { |
|||
left: var(--fullscreen-offset-x); |
|||
top: calc(50px + var(--fullscreen-offset-y)); |
|||
} |
|||
|
|||
:global(.EasyMDEContainer .CodeMirror-fullscreen.CodeMirror-sided) { |
|||
width: calc((100% - var(--fullscreen-offset-x)) / 2) !important; |
|||
} |
|||
|
|||
:global(.EasyMDEContainer .editor-preview-side) { |
|||
left: calc(50% + (var(--fullscreen-offset-x) / 2)); |
|||
top: calc(50px + var(--fullscreen-offset-y)); |
|||
width: calc((100% - var(--fullscreen-offset-x)) / 2) !important; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,60 @@ |
|||
<script> |
|||
import Tooltip from "./Tooltip.svelte" |
|||
import Icon from "../Icon/Icon.svelte" |
|||
|
|||
export let tooltip = "" |
|||
export let size = "M" |
|||
|
|||
let showTooltip = false |
|||
</script> |
|||
|
|||
<div class:container={!!tooltip}> |
|||
<slot /> |
|||
{#if tooltip} |
|||
<div class="icon-container"> |
|||
<div |
|||
class="icon" |
|||
class:icon-small={size === "M" || size === "S"} |
|||
on:mouseover={() => (showTooltip = true)} |
|||
on:mouseleave={() => (showTooltip = false)} |
|||
> |
|||
<Icon name="InfoOutline" size="S" disabled={true} /> |
|||
</div> |
|||
{#if showTooltip} |
|||
<div class="tooltip"> |
|||
<Tooltip textWrapping={true} direction={"bottom"} text={tooltip} /> |
|||
</div> |
|||
{/if} |
|||
</div> |
|||
{/if} |
|||
</div> |
|||
|
|||
<style> |
|||
.container { |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
.icon-container { |
|||
position: relative; |
|||
display: flex; |
|||
justify-content: center; |
|||
margin-top: 1px; |
|||
margin-left: 5px; |
|||
margin-right: 5px; |
|||
} |
|||
.tooltip { |
|||
position: absolute; |
|||
display: flex; |
|||
justify-content: center; |
|||
top: 15px; |
|||
z-index: 1; |
|||
width: 160px; |
|||
} |
|||
.icon { |
|||
transform: scale(0.75); |
|||
} |
|||
.icon-small { |
|||
margin-top: -2px; |
|||
margin-bottom: -5px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,4 @@ |
|||
// @ts-ignore
|
|||
import { run } from "../setup" |
|||
|
|||
run("../../server/src/index", "../../worker/src/index") |
|||
@ -1,34 +0,0 @@ |
|||
import { writable } from "svelte/store" |
|||
import api, { get } from "../api" |
|||
|
|||
const INITIAL_HOSTING_UI_STATE = { |
|||
appUrl: "", |
|||
deployedApps: {}, |
|||
deployedAppNames: [], |
|||
deployedAppUrls: [], |
|||
} |
|||
|
|||
export const getHostingStore = () => { |
|||
const store = writable({ ...INITIAL_HOSTING_UI_STATE }) |
|||
store.actions = { |
|||
fetch: async () => { |
|||
const response = await api.get("/api/hosting/urls") |
|||
const urls = await response.json() |
|||
store.update(state => { |
|||
state.appUrl = urls.app |
|||
return state |
|||
}) |
|||
}, |
|||
fetchDeployedApps: async () => { |
|||
let deployments = await (await get("/api/hosting/apps")).json() |
|||
store.update(state => { |
|||
state.deployedApps = deployments |
|||
state.deployedAppNames = Object.values(deployments).map(app => app.name) |
|||
state.deployedAppUrls = Object.values(deployments).map(app => app.url) |
|||
return state |
|||
}) |
|||
return deployments |
|||
}, |
|||
} |
|||
return store |
|||
} |
|||
@ -0,0 +1,47 @@ |
|||
<script> |
|||
import { ActionButton } from "@budibase/bbui" |
|||
import GoogleLogo from "assets/google-logo.png" |
|||
import { store } from "builderStore" |
|||
import { auth } from "stores/portal" |
|||
|
|||
export let preAuthStep |
|||
export let datasource |
|||
|
|||
$: tenantId = $auth.tenantId |
|||
</script> |
|||
|
|||
<ActionButton |
|||
on:click={async () => { |
|||
let ds = datasource |
|||
if (!ds) { |
|||
ds = await preAuthStep() |
|||
} |
|||
window.open( |
|||
`/api/global/auth/${tenantId}/datasource/google?datasourceId=${datasource._id}&appId=${$store.appId}`, |
|||
"_blank" |
|||
) |
|||
}} |
|||
> |
|||
<div class="inner"> |
|||
<img src={GoogleLogo} alt="google icon" /> |
|||
<p>Sign in with Google</p> |
|||
</div> |
|||
</ActionButton> |
|||
|
|||
<style> |
|||
.inner { |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: center; |
|||
justify-content: center; |
|||
padding-top: var(--spacing-xs); |
|||
padding-bottom: var(--spacing-xs); |
|||
} |
|||
.inner img { |
|||
width: 18px; |
|||
margin: 3px 10px 3px 3px; |
|||
} |
|||
.inner p { |
|||
margin: 0; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,184 @@ |
|||
<script> |
|||
export let width = "100" |
|||
export let height = "100" |
|||
</script> |
|||
|
|||
<svg |
|||
{width} |
|||
{height} |
|||
version="1.0" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
viewBox="0 0 50 80" |
|||
preserveAspectRatio="xMidYMid meet" |
|||
> |
|||
<defs> |
|||
<path |
|||
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" |
|||
id="path-1" |
|||
/> |
|||
<path |
|||
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" |
|||
id="path-3" |
|||
/> |
|||
<path |
|||
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" |
|||
id="path-5" |
|||
/> |
|||
<linearGradient |
|||
x1="50.0053945%" |
|||
y1="8.58610612%" |
|||
x2="50.0053945%" |
|||
y2="100.013939%" |
|||
id="linearGradient-7" |
|||
> |
|||
<stop stop-color="#263238" stop-opacity="0.2" offset="0%" /> |
|||
<stop stop-color="#263238" stop-opacity="0.02" offset="100%" /> |
|||
</linearGradient> |
|||
<path |
|||
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" |
|||
id="path-8" |
|||
/> |
|||
<path |
|||
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" |
|||
id="path-10" |
|||
/> |
|||
<path |
|||
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" |
|||
id="path-12" |
|||
/> |
|||
<path |
|||
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" |
|||
id="path-14" |
|||
/> |
|||
<radialGradient |
|||
cx="3.16804688%" |
|||
cy="2.71744318%" |
|||
fx="3.16804688%" |
|||
fy="2.71744318%" |
|||
r="161.248516%" |
|||
gradientTransform="translate(0.031680,0.027174),scale(1.000000,0.727273),translate(-0.031680,-0.027174)" |
|||
id="radialGradient-16" |
|||
> |
|||
<stop stop-color="#FFFFFF" stop-opacity="0.1" offset="0%" /> |
|||
<stop stop-color="#FFFFFF" stop-opacity="0" offset="100%" /> |
|||
</radialGradient> |
|||
</defs> |
|||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> |
|||
<g |
|||
id="Consumer-Apps-Sheets-Large-VD-R8-" |
|||
transform="translate(-451.000000, -451.000000)" |
|||
> |
|||
<g id="Hero" transform="translate(0.000000, 63.000000)"> |
|||
<g id="Personal" transform="translate(277.000000, 299.000000)"> |
|||
<g id="Sheets-icon" transform="translate(174.833333, 89.958333)"> |
|||
<g id="Group"> |
|||
<g id="Clipped"> |
|||
<mask id="mask-2" fill="white"> |
|||
<use xlink:href="#path-1" /> |
|||
</mask> |
|||
<g id="SVGID_1_" /> |
|||
<path |
|||
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L36.9791667,10.3541667 L29.5833333,0 Z" |
|||
id="Path" |
|||
fill="#0F9D58" |
|||
fill-rule="nonzero" |
|||
mask="url(#mask-2)" |
|||
/> |
|||
</g> |
|||
<g id="Clipped"> |
|||
<mask id="mask-4" fill="white"> |
|||
<use xlink:href="#path-3" /> |
|||
</mask> |
|||
<g id="SVGID_1_" /> |
|||
<path |
|||
d="M11.8333333,31.8020833 L11.8333333,53.25 L35.5,53.25 L35.5,31.8020833 L11.8333333,31.8020833 Z M22.1875,50.2916667 L14.7916667,50.2916667 L14.7916667,46.59375 L22.1875,46.59375 L22.1875,50.2916667 Z M22.1875,44.375 L14.7916667,44.375 L14.7916667,40.6770833 L22.1875,40.6770833 L22.1875,44.375 Z M22.1875,38.4583333 L14.7916667,38.4583333 L14.7916667,34.7604167 L22.1875,34.7604167 L22.1875,38.4583333 Z M32.5416667,50.2916667 L25.1458333,50.2916667 L25.1458333,46.59375 L32.5416667,46.59375 L32.5416667,50.2916667 Z M32.5416667,44.375 L25.1458333,44.375 L25.1458333,40.6770833 L32.5416667,40.6770833 L32.5416667,44.375 Z M32.5416667,38.4583333 L25.1458333,38.4583333 L25.1458333,34.7604167 L32.5416667,34.7604167 L32.5416667,38.4583333 Z" |
|||
id="Shape" |
|||
fill="#F1F1F1" |
|||
fill-rule="nonzero" |
|||
mask="url(#mask-4)" |
|||
/> |
|||
</g> |
|||
<g id="Clipped"> |
|||
<mask id="mask-6" fill="white"> |
|||
<use xlink:href="#path-5" /> |
|||
</mask> |
|||
<g id="SVGID_1_" /> |
|||
<polygon |
|||
id="Path" |
|||
fill="url(#linearGradient-7)" |
|||
fill-rule="nonzero" |
|||
mask="url(#mask-6)" |
|||
points="30.8813021 16.4520313 47.3333333 32.9003646 47.3333333 17.75" |
|||
/> |
|||
</g> |
|||
<g id="Clipped"> |
|||
<mask id="mask-9" fill="white"> |
|||
<use xlink:href="#path-8" /> |
|||
</mask> |
|||
<g id="SVGID_1_" /> |
|||
<g id="Group" mask="url(#mask-9)"> |
|||
<g transform="translate(26.625000, -2.958333)"> |
|||
<path |
|||
d="M2.95833333,2.95833333 L2.95833333,16.2708333 C2.95833333,18.7225521 4.94411458,20.7083333 7.39583333,20.7083333 L20.7083333,20.7083333 L2.95833333,2.95833333 Z" |
|||
id="Path" |
|||
fill="#87CEAC" |
|||
fill-rule="nonzero" |
|||
/> |
|||
</g> |
|||
</g> |
|||
</g> |
|||
<g id="Clipped"> |
|||
<mask id="mask-11" fill="white"> |
|||
<use xlink:href="#path-10" /> |
|||
</mask> |
|||
<g id="SVGID_1_" /> |
|||
<path |
|||
d="M4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,4.80729167 C0,2.36666667 1.996875,0.369791667 4.4375,0.369791667 L29.5833333,0.369791667 L29.5833333,0 L4.4375,0 Z" |
|||
id="Path" |
|||
fill-opacity="0.2" |
|||
fill="#FFFFFF" |
|||
fill-rule="nonzero" |
|||
mask="url(#mask-11)" |
|||
/> |
|||
</g> |
|||
<g id="Clipped"> |
|||
<mask id="mask-13" fill="white"> |
|||
<use xlink:href="#path-12" /> |
|||
</mask> |
|||
<g id="SVGID_1_" /> |
|||
<path |
|||
d="M42.8958333,64.7135417 L4.4375,64.7135417 C1.996875,64.7135417 0,62.7166667 0,60.2760417 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,60.2760417 C47.3333333,62.7166667 45.3364583,64.7135417 42.8958333,64.7135417 Z" |
|||
id="Path" |
|||
fill-opacity="0.2" |
|||
fill="#263238" |
|||
fill-rule="nonzero" |
|||
mask="url(#mask-13)" |
|||
/> |
|||
</g> |
|||
<g id="Clipped"> |
|||
<mask id="mask-15" fill="white"> |
|||
<use xlink:href="#path-14" /> |
|||
</mask> |
|||
<g id="SVGID_1_" /> |
|||
<path |
|||
d="M34.0208333,17.75 C31.5691146,17.75 29.5833333,15.7642188 29.5833333,13.3125 L29.5833333,13.6822917 C29.5833333,16.1340104 31.5691146,18.1197917 34.0208333,18.1197917 L47.3333333,18.1197917 L47.3333333,17.75 L34.0208333,17.75 Z" |
|||
id="Path" |
|||
fill-opacity="0.1" |
|||
fill="#263238" |
|||
fill-rule="nonzero" |
|||
mask="url(#mask-15)" |
|||
/> |
|||
</g> |
|||
</g> |
|||
<path |
|||
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" |
|||
id="Path" |
|||
fill="url(#radialGradient-16)" |
|||
fill-rule="nonzero" |
|||
/> |
|||
</g> |
|||
</g> |
|||
</g> |
|||
</g> |
|||
</g> |
|||
</svg> |
|||
@ -0,0 +1,29 @@ |
|||
<script> |
|||
import { ModalContent, Body, Layout } from "@budibase/bbui" |
|||
import { IntegrationNames } from "constants/backend" |
|||
import cloneDeep from "lodash/cloneDeepWith" |
|||
import GoogleButton from "../_components/GoogleButton.svelte" |
|||
import { saveDatasource as save } from "builderStore/datasource" |
|||
|
|||
export let integration |
|||
export let modal |
|||
|
|||
// kill the reference so the input isn't saved |
|||
let datasource = cloneDeep(integration) |
|||
</script> |
|||
|
|||
<ModalContent |
|||
title={`Connect to ${IntegrationNames[datasource.type]}`} |
|||
onCancel={() => modal.show()} |
|||
cancelText="Back" |
|||
size="L" |
|||
> |
|||
<Layout noPadding> |
|||
<Body size="XS" |
|||
>Authenticate with your google account to use the {IntegrationNames[ |
|||
datasource.type |
|||
]} integration.</Body |
|||
> |
|||
</Layout> |
|||
<GoogleButton preAuthStep={() => save(datasource, true)} /> |
|||
</ModalContent> |
|||
@ -0,0 +1,33 @@ |
|||
<script> |
|||
import { Select, Label } from "@budibase/bbui" |
|||
import { currentAsset } from "builderStore" |
|||
import { findAllMatchingComponents } from "builderStore/componentUtils" |
|||
|
|||
export let parameters |
|||
|
|||
$: components = findAllMatchingComponents($currentAsset.props, component => |
|||
component._component.endsWith("s3upload") |
|||
) |
|||
</script> |
|||
|
|||
<div class="root"> |
|||
<Label small>S3 Upload Component</Label> |
|||
<Select |
|||
bind:value={parameters.componentId} |
|||
options={components} |
|||
getOptionLabel={x => x._instanceName} |
|||
getOptionValue={x => x._id} |
|||
/> |
|||
</div> |
|||
|
|||
<style> |
|||
.root { |
|||
display: grid; |
|||
column-gap: var(--spacing-l); |
|||
row-gap: var(--spacing-s); |
|||
grid-template-columns: 120px 1fr; |
|||
align-items: center; |
|||
max-width: 400px; |
|||
margin: 0 auto; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,15 @@ |
|||
<script> |
|||
import { Select } from "@budibase/bbui" |
|||
import { datasources } from "stores/backend" |
|||
|
|||
export let value = null |
|||
|
|||
$: dataSources = $datasources.list |
|||
.filter(ds => ds.source === "S3" && !ds.config?.endpoint) |
|||
.map(ds => ({ |
|||
label: ds.name, |
|||
value: ds._id, |
|||
})) |
|||
</script> |
|||
|
|||
<Select options={dataSources} {value} on:change /> |
|||
@ -1,120 +1,75 @@ |
|||
<script> |
|||
import { writable, get as svelteGet } from "svelte/store" |
|||
import { |
|||
notifications, |
|||
Input, |
|||
Modal, |
|||
ModalContent, |
|||
Body, |
|||
} from "@budibase/bbui" |
|||
import { hostingStore } from "builderStore" |
|||
import { notifications, Input, ModalContent, Body } from "@budibase/bbui" |
|||
import { apps } from "stores/portal" |
|||
import { string, object } from "yup" |
|||
import { onMount } from "svelte" |
|||
import { capitalise } from "helpers" |
|||
import { APP_NAME_REGEX } from "constants" |
|||
|
|||
const values = writable({ name: null }) |
|||
const errors = writable({}) |
|||
const touched = writable({}) |
|||
const validator = { |
|||
name: string() |
|||
.trim() |
|||
.required("Your application must have a name") |
|||
.matches( |
|||
APP_NAME_REGEX, |
|||
"App name must be letters, numbers and spaces only" |
|||
), |
|||
} |
|||
import { createValidationStore } from "helpers/validation/yup" |
|||
import * as appValidation from "helpers/validation/yup/app" |
|||
|
|||
export let app |
|||
|
|||
let modal |
|||
let valid = false |
|||
let dirty = false |
|||
$: checkValidity($values, validator) |
|||
$: { |
|||
// prevent validation by setting name to undefined without an app |
|||
if (app) { |
|||
$values.name = app?.name |
|||
} |
|||
} |
|||
const values = writable({ name: "", url: null }) |
|||
const validation = createValidationStore() |
|||
$: validation.check($values) |
|||
|
|||
onMount(async () => { |
|||
await hostingStore.actions.fetchDeployedApps() |
|||
const existingAppNames = svelteGet(hostingStore).deployedAppNames |
|||
validator.name = string() |
|||
.trim() |
|||
.required("Your application must have a name") |
|||
.matches( |
|||
APP_NAME_REGEX, |
|||
"App name must be letters, numbers and spaces only" |
|||
) |
|||
.test( |
|||
"non-existing-app-name", |
|||
"Another app with the same name already exists", |
|||
value => { |
|||
return !existingAppNames.some( |
|||
appName => dirty && appName.toLowerCase() === value.toLowerCase() |
|||
) |
|||
} |
|||
) |
|||
$values.name = app.name |
|||
$values.url = app.url |
|||
setupValidation() |
|||
}) |
|||
|
|||
const checkValidity = async (values, validator) => { |
|||
const obj = object().shape(validator) |
|||
Object.keys(validator).forEach(key => ($errors[key] = null)) |
|||
try { |
|||
await obj.validate(values, { abortEarly: false }) |
|||
} catch (validationErrors) { |
|||
validationErrors.inner.forEach(error => { |
|||
$errors[error.path] = capitalise(error.message) |
|||
}) |
|||
} |
|||
valid = await obj.isValid(values) |
|||
const setupValidation = async () => { |
|||
const applications = svelteGet(apps) |
|||
appValidation.name(validation, { apps: applications, currentApp: app }) |
|||
appValidation.url(validation, { apps: applications, currentApp: app }) |
|||
// init validation |
|||
validation.check($values) |
|||
} |
|||
|
|||
async function updateApp() { |
|||
try { |
|||
// Update App |
|||
await apps.update(app.instance._id, { name: $values.name.trim() }) |
|||
hide() |
|||
const body = { |
|||
name: $values.name.trim(), |
|||
} |
|||
if ($values.url) { |
|||
body.url = $values.url.trim() |
|||
} |
|||
await apps.update(app.instance._id, body) |
|||
} catch (error) { |
|||
console.error(error) |
|||
notifications.error(error) |
|||
} |
|||
} |
|||
|
|||
export const show = () => { |
|||
modal.show() |
|||
} |
|||
export const hide = () => { |
|||
modal.hide() |
|||
} |
|||
|
|||
const onCancel = () => { |
|||
hide() |
|||
} |
|||
|
|||
const onShow = () => { |
|||
dirty = false |
|||
// auto add slash to url |
|||
$: { |
|||
if ($values.url && !$values.url.startsWith("/")) { |
|||
$values.url = `/${$values.url}` |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<Modal bind:this={modal} on:hide={onCancel} on:show={onShow}> |
|||
<ModalContent |
|||
title={"Edit app"} |
|||
confirmText={"Save"} |
|||
onConfirm={updateApp} |
|||
disabled={!(valid && dirty)} |
|||
> |
|||
<Body size="S">Update the name of your app.</Body> |
|||
<Input |
|||
bind:value={$values.name} |
|||
error={$touched.name && $errors.name} |
|||
on:blur={() => ($touched.name = true)} |
|||
on:change={() => (dirty = true)} |
|||
label="Name" |
|||
/> |
|||
</ModalContent> |
|||
</Modal> |
|||
<ModalContent |
|||
title={"Edit app"} |
|||
confirmText={"Save"} |
|||
onConfirm={updateApp} |
|||
disabled={!$validation.valid} |
|||
> |
|||
<Body size="S">Update the name of your app.</Body> |
|||
<Input |
|||
bind:value={$values.name} |
|||
error={$validation.touched.name && $validation.errors.name} |
|||
on:blur={() => ($validation.touched.name = true)} |
|||
label="Name" |
|||
/> |
|||
<Input |
|||
bind:value={$values.url} |
|||
error={$validation.touched.url && $validation.errors.url} |
|||
on:blur={() => ($validation.touched.url = true)} |
|||
label="URL" |
|||
placeholder={$values.name |
|||
? "/" + encodeURIComponent($values.name).toLowerCase() |
|||
: "/"} |
|||
/> |
|||
</ModalContent> |
|||
|
|||
@ -0,0 +1,83 @@ |
|||
import { string, mixed } from "yup" |
|||
import { APP_NAME_REGEX, APP_URL_REGEX } from "constants" |
|||
|
|||
export const name = (validation, { apps, currentApp } = { apps: [] }) => { |
|||
validation.addValidator( |
|||
"name", |
|||
string() |
|||
.trim() |
|||
.required("Your application must have a name") |
|||
.matches( |
|||
APP_NAME_REGEX, |
|||
"App name must be letters, numbers and spaces only" |
|||
) |
|||
.test( |
|||
"non-existing-app-name", |
|||
"Another app with the same name already exists", |
|||
value => { |
|||
if (!value) { |
|||
// exit early, above validator will fail
|
|||
return true |
|||
} |
|||
if (currentApp) { |
|||
// filter out the current app if present
|
|||
apps = apps.filter(app => app.appId !== currentApp.appId) |
|||
} |
|||
return !apps |
|||
.map(app => app.name) |
|||
.some(appName => appName.toLowerCase() === value.toLowerCase()) |
|||
} |
|||
) |
|||
) |
|||
} |
|||
|
|||
export const url = (validation, { apps, currentApp } = { apps: [] }) => { |
|||
validation.addValidator( |
|||
"url", |
|||
string() |
|||
.nullable() |
|||
.matches(APP_URL_REGEX, "App URL must not contain spaces") |
|||
.test( |
|||
"non-existing-app-url", |
|||
"Another app with the same URL already exists", |
|||
value => { |
|||
// url is nullable
|
|||
if (!value) { |
|||
return true |
|||
} |
|||
if (currentApp) { |
|||
// filter out the current app if present
|
|||
apps = apps.filter(app => app.appId !== currentApp.appId) |
|||
} |
|||
return !apps |
|||
.map(app => app.url) |
|||
.some(appUrl => appUrl?.toLowerCase() === value.toLowerCase()) |
|||
} |
|||
) |
|||
.test("valid-url", "Not a valid URL", value => { |
|||
// url is nullable
|
|||
if (!value) { |
|||
return true |
|||
} |
|||
// make it clear that this is a url path and cannot be a full url
|
|||
return ( |
|||
value.startsWith("/") && |
|||
!value.includes("http") && |
|||
!value.includes("www") && |
|||
!value.includes(".") && |
|||
value.length > 1 // just '/' is not valid
|
|||
) |
|||
}) |
|||
) |
|||
} |
|||
|
|||
export const file = (validation, { template } = {}) => { |
|||
const templateToUse = |
|||
template && Object.keys(template).length === 0 ? null : template |
|||
validation.addValidator( |
|||
"file", |
|||
templateToUse?.fromFile |
|||
? mixed().required("Please choose a file to import") |
|||
: null |
|||
) |
|||
} |
|||
@ -0,0 +1,66 @@ |
|||
import { capitalise } from "helpers" |
|||
import { object } from "yup" |
|||
import { writable, get } from "svelte/store" |
|||
import { notifications } from "@budibase/bbui" |
|||
|
|||
export const createValidationStore = () => { |
|||
const DEFAULT = { |
|||
errors: {}, |
|||
touched: {}, |
|||
valid: false, |
|||
} |
|||
|
|||
const validator = {} |
|||
const validation = writable(DEFAULT) |
|||
|
|||
const addValidator = (propertyName, propertyValidator) => { |
|||
if (!propertyValidator || !propertyName) { |
|||
return |
|||
} |
|||
validator[propertyName] = propertyValidator |
|||
} |
|||
|
|||
const check = async values => { |
|||
const obj = object().shape(validator) |
|||
// clear the previous errors
|
|||
const properties = Object.keys(validator) |
|||
properties.forEach(property => (get(validation).errors[property] = null)) |
|||
|
|||
let validationError = false |
|||
try { |
|||
await obj.validate(values, { abortEarly: false }) |
|||
} catch (error) { |
|||
if (!error.inner) { |
|||
notifications.error("Unexpected validation error", error) |
|||
validationError = true |
|||
} else { |
|||
error.inner.forEach(err => { |
|||
validation.update(store => { |
|||
store.errors[err.path] = capitalise(err.message) |
|||
return store |
|||
}) |
|||
}) |
|||
} |
|||
} |
|||
|
|||
let valid |
|||
if (properties.length && !validationError) { |
|||
valid = await obj.isValid(values) |
|||
} else { |
|||
// don't say valid until validators have been loaded
|
|||
valid = false |
|||
} |
|||
|
|||
validation.update(store => { |
|||
store.valid = valid |
|||
return store |
|||
}) |
|||
} |
|||
|
|||
return { |
|||
subscribe: validation.subscribe, |
|||
set: validation.set, |
|||
check, |
|||
addValidator, |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue